套接字编程
所谓套接字(Socket),就是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。一个套接字就是网络上进程通信的一端,提供了应用层进程利用网络协议交换数据的机制。
Sockaddr结构
在linux下,根据所使用的不同协议,又分为以下三种结构,在使用时,我们可以选择自己所需要的结构,通信时再将我们所使用的结构强转为sockaddr,这样就能保证数据格式的一致
- IPv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示,包括16位地址类型, 16 位端口号和32位IP地址.
- IPv4、IPv6地址类型分别定义为常数AF_INET、AF_INET6. 这样,只要取得某种sockaddr结构体的首地址, 不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容.
- socket API可以都用struct sockaddr *类型表示, 在使用的时候需要强制转化成sockaddr_in; 这样的好 处是程序的通用性, 可以接收IPv4, IPv6, 以及UNIX Domain Socket各种类型的sockaddr结构体指针做为 参数;
通常情况下,因为我们使用的都是IPV4,所以一般用的都是sockaddr_in结构,这个结构中用来描述通信双方的主要信息就是端口号和ip地址
sockaddr_in结构
sin_family:指代协议族,在socket编程中只能是AF_INET
sin_port:存储端口号(使用网络字节顺序)
sin_addr:存储IP地址,使用in_addr这个数据结构
sin_zero:是为了让sockaddr与sockaddr_in两个数据结构保持大小相同而保留的空字节。
in_addr结构
这个结构中存储的其实就是一个32位的整型ip地址。
字节序
字节序就是CPU对数据再内存中以字节为单位的存取顺序,也就是我们通常所说的大端小端问题。
关于大小端的问题我之前有写过一篇博客
大端小端存储解析
这里就简要说一下
大端存储模式:是指数据的低位保存在内存的高地址中,而数据的高位,保存在内存的低地址中。
小端存储模式:是指数据的低位保存在内存的低地址中,而数据的高位,保存在内存的高地址中。
在网络通信中,网络字节序采用大端的存储模式,而主机字节序根据主机不同也不一样,我们现在的家用机一般都是小端,但网络上的通信不能确保主机字节序的唯一性,因为受众是整个网络,而一旦通信的双方主机字节序不同,就会造成通信时的数据二义,所以需要确保字节序相同,就需要在通信时将主机字节序转换为通用的网络字节序。
在arpa/inet.h
这个头文件中,也为我们提供了一套字节序的转换接口。
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);//将无符号长整型的主机字节序转换为网络字节序
uint16_t htons(uint16_t hostshort);//将无符号短整型的主机字节序转换为网络字节序
uint32_t ntohl(uint32_t netlong);//将无符号长整型的网络字节序转换为主机字节序
uint16_t ntohs(uint16_t netshort);//将无符号短整型的网络字节序转换为主机字节序
//h代表主机字节序,n代表网络字节序,l代表长整型,s代表短整型
地址转换
同样的,我们输入进去的ip地址一般都是点分十进制的ip地址,而通信时需要的是网络字节序的整数ip地址。
#include <arpa/inet.h>
char *inet_ntoa(struct in_addr in);//将网络字节序的整数ip地址转换为点分十进制的字符串ip地址
in_addr_t inet_addr(const char *cp);//将点分十进制的字符串ip地址转换为网络字节序的整数ip地址
int inet_aton(const char *cp, struct in_addr *inp);//将点分十进制的字符串ip地址转换为网络字节序的整数ip地址(与addr的区别它会认为如255.255.255.255这类特殊地址有效)
in_addr_t inet_network(const char *cp); //将点分十进制的字符串ip地址转换为主机字节序的整数ip地址
struct in_addr inet_makeaddr(in_addr_t net, in_addr_t host);
in_addr_t inet_lnaof(struct in_addr in);
in_addr_t inet_netof(struct in_addr in);
常用套接字接口
// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);
// 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr *address, socklen_t address_len);
// 开始监听socket (TCP, 服务器)
int listen(int socket, int backlog);
// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address, socklen_t* address_len);
// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
//断开连接
int close(int sockfd);
//发送数据
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
//接收数据
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
UDP的通信流程
计算机网络 (三) 传输层 :一文搞懂UDP与TCP协议
在这篇博客中,我描述了UDP与TCP的特性以及通信流程,下面就根据特性来规划该如何通过Socket来实现UDP通信。
这里我就简单的画一个图。
因为UDP是无连接的,所以只需要再创建套接字后绑定地址信息,就可以直接进行通信。
这里有一点需要注意,就是客户端一般不会主动绑定地址信息。
原因是客户端用什么地址和端口接收数据都无所谓,只需要确保能够将数据发送出去即可。如果不绑定地址信息,系统会自动选择合适的地址端口进行绑定,而如果手动绑定,很可能会绑定到已使用或者将要使用的端口,此时就会产生端口的冲突,所以为了减少端口冲突,客户端一般不会主动绑定。
UDPSocket的封装
为了能够更方便的使用,一般都会根据不同协议和使用情景,来封装一套Socket接口,使用时就只需要根据协议特性来传递参数即可。
具体的思路我都写在了注释里面
#include<iostream>
#include<sys/socket.h>
#include<string>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<cstdio>
#include<unistd.h>
//内联函数,用来检测当前操作是否出错
inline void CheckSafe(bool ret)
{
if(ret == false)
{
std::cerr << "Socket发生错误" << std::endl;
exit(0);
}
}
class UdpSocket
{
public:
UdpSocket() : _socket_fd(-1)
{}
//创建socket
bool Socket()
{
_socket_fd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if(_socket_fd == -1)
{
perror("socket create error");
return false;
}
return true;
}
//绑定地址信息
bool Bind(const std::string& ip, uint16_t port)
{
struct sockaddr_in addr;
addr.sin_family = AF_INET;
//主机字节序转换成网络字节序,方便统一
addr.sin_port = htons(port);
//将字符串的ip地址转为网络字节序的二进制数据
addr.sin_addr.s_addr = inet_addr(ip.c_str());
socklen_t len = sizeof(struct sockaddr_in);
//强转地址结构,使接口统一
int ret = bind(_socket_fd, (struct sockaddr*)& addr, len);
if(ret == -1)
{
perror("socket bind error");
return false;
}
return true;
}
bool Recv(std::string& buff, std::string* ip = NULL, uint16_t* port = NULL)
{
//对端地址信息
struct sockaddr_in peer_addr;
socklen_t len = sizeof(struct sockaddr_in);
//接收缓冲区
char temp[1024] = {0};
int ret = recvfrom(_socket_fd, temp, 1024, 0, (struct sockaddr*)&peer_addr, &len);
if(ret == -1)
{
perror("receive error");
return false;
}
//将数据从缓冲区取出
buff.assign(temp, ret);
//获取对端地址信息
if(port != NULL)
{
*port = htons(peer_addr.sin_port);
}
if(ip != NULL)
{
*ip = inet_ntoa(peer_addr.sin_addr);
}
return true;
}
bool Send(const std::string& data, const std::string& ip, const uint16_t& port)
{
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = inet_addr(ip.c_str());
socklen_t len = sizeof(struct sockaddr_in);
int ret = sendto(_socket_fd, data.c_str(), data.size(), 0, (sockaddr*)& addr, len);
if(ret == -1)
{
perror("send error");
return false;
}
return true;
}
void Close()
{
close(_socket_fd);
_socket_fd = -1;
}
private:
int _socket_fd;
};
按照前面所化的流程以及封装的对应接口,来实现服务端
UDP服务器
#include<iostream>
#include"UdpSocket.hpp"
using namespace std;
int main (int argc, char *argv[])
{
if(argc != 3)
{
cerr << "正确输入方式: ./udp_srv.cpp ip port\n" << endl;
return -1;
}
//获取命令行输入的ip地址和端口
string ip = argv[1];
uint16_t port = stoi(argv[2]);
UdpSocket Socket;
//创建套接字
CheckSafe(Socket.Socket());
//绑定地址信息
CheckSafe(Socket.Bind(ip, port));
while(1)
{
string cli_ip;
uint16_t cli_port;
string message;
//接受数据
CheckSafe(Socket.Recv(message, &cli_ip, &cli_port));
cout << "cli[" << cli_ip << ":" << cli_port << "]:send message: " << message << endl;
message.clear();
cout << "srv send reply message: ";
getline(cin, message);
//给客户端回复数据
CheckSafe(Socket.Send(message, cli_ip, cli_port));
}
//关闭套接字
Socket.Close();
return 0;
}
按照前面所化的流程以及封装的对应接口,来实现客户端
UDP客户端
#include<iostream>
#include"UdpSocket.hpp"
using namespace std;
int main (int argc, char *argv[])
{
if(argc != 3)
{
cerr << "正确输入方式: ./udp_cli.cpp ip port\n" << endl;
return -1;
}
//获取命令行输入的ip地址和端口
string ip = argv[1];
uint16_t port = stoi(argv[2]);
UdpSocket Socket;
//创建套接字
CheckSafe(Socket.Socket());
//发送方不需要主动绑定地址信息,让系统自动选取即可,因为只需要保证能够发送数据,并且接收到数据即可,哪个地址端口都无所谓,这样还能减少端口冲突的概率
//发送数据
while(1)
{
cout << "cli send message: ";
string message;
getline(cin, message);
//如果输入quit则退出
if(message == "quit")
break;
//发送数据
CheckSafe(Socket.Send(message, ip, port));
message.clear();
//接受数据
CheckSafe(Socket.Recv(message));
cout << "srv reply message: " << message << endl;
}
//关闭套接字
Socket.Close();
return 0;
}
服务端
客户端