本文记录使用C++在Linux和Windows下进行Socket通信的方式。
socket是什么?套接字是什么? (biancheng.net)
基本概念
socket提供两种通信机制:
stream
:流式传输,基于TCP
,有序、可靠、双向字节流。datagram
:数据报传输,基于UDP
,不可靠,可能丢失和乱序。对数据长度有限制,效率高。音视频聊天
可以采用。应用场景越来越少。
socket通信流程:

socket()
int socket(int af, int type, int protocol);//Linux
SOCKET socket(int af, int type, int protocol);//Windows
af
:协议族(Address Family),即IP地址类型。也可以写作PF(Protocol Family),因此所有AF_XX
等价于PF_XX
。AF_INET
:IPv4地址,如127.0.0.1。AF_INET6
:IPv6地址,如1030::C9B4:FF12:48AA:1A2B。
type
:数据传输方式,即socket类型。SOCK_STREAM
:流式,基于TCPSOCK_DGRAM
:数据报,基于UDP
protocol
:传输协议。- 0:根据
af
和type
自动推导。当两种不同协议支持同一种af
和type
时,无法自动推导。 IPPROTO_TCP
:TCP传输协议IPPROTO_UDP
:UDP传输协议
- 0:根据
SOCKET sock = socket(AF_INET, SOCK_STREAM, 0);
bind()
将具体的地址和端口绑定到socket
注意绑定前要转为
网络字节序
。
htons()
:host to network shorthtonl()
:host to network longntohl()
:network to host longntohs()
:network to host short
int bind(int sock, struct sockaddr *addr, socklen_t addrlen); //Linux
int bind(SOCKET sock, const struct sockaddr *addr, int addrlen); //Windows
sock
:由socket()产生的socket描述符。addrlen
:addr
的大小,由sizeof
计算。返回值:
addr
:sockaddr
结构体指针,将sockaddr_in
转换得到struct sockaddr_in{ sa_family_t sin_family; //地址类型 uint16_t sin_port; //16位的端口号 struct in_addr sin_addr; //32位IP地址 char sin_zero[8]; //不使用,一般用0填充 };
sin_family
:和socket()
的af
取值一致。sin_port
:端口号,取值范围为1024~65536
,需要使用htons()
进行转换。sin_addr
:in_addr
结构体。struct in_addr{ in_addr_t s_addr; //32位的IP地址,等价于unsigned long,是一个整数 }; //因为s_addr是一个整数,而IP地址一般是字符串,所以要使用inet_addr()进行转换 unsigned long ip = inet_addr("127.0.0.1");
为什么要转换为
sockaddr
:- 两者长度都是16B,只是
sockaddr
将地址和端口合并到一起。要给sa_data
赋值必须同时指定,如"127.0.0.1:80"
,但没有相关函数将字符串转成需要的形式,所以很难给sockaddr
赋值,因此使用sockaddr_in
代替。 sockaddr
更通用,而sockaddr_in
和sockaddr_in6
分别保存IPv4和IPv6地址。
示例:
sockaddr_in sockAddr;
memset(&sockAddr, 0, sizeof(sockaddr_in)); //内存初始化为0
sockAddr.sin_family = AF_INET;
sockAddr.sin_port = htons(PORT);
sockAddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
bind(servSock, (sockaddr*)&servAddr, sizeof (servAddr));
connect()
int connect(int sock, struct sockaddr *serv_addr, socklen_t addrlen); //Linux
int connect(SOCKET sock, const struct sockaddr *serv_addr, int addrlen); //Windows
用来建立连接,参数和bind()
相同,但connect()
用于客户端。
listen()
使socket进入被动监听。即没有客户端请求时,socket处于“睡眠”,当接收到请求时,才会“唤醒”。
int listen(int sock, int backlog); //Linux
int listen(SOCKET sock, int backlog); //Windows
sock
:进入监听的socketbacklog
:请求队列
的最大长度,如果为SOMAXCONN
表示由系统决定,一般比较大。
请求队列
- socket正在处理客户端请求时,有新的请求进来,就将新请求放入缓冲区,即请求队列。
- 当请求队列满时,不再接受新请求,Linux的客户端会收到
ECONNREFUSED
,Windows会收到WSAECONNRESFUSED
。
accept()
listen()只是监听请求,accept()才真正接受并处理。accept会阻塞
程序,直到有新的请求到来。
int accept(int sock, struct sockaddr *addr, socklen_t *addrlen); //Linux
SOCKET accept(SOCKET sock, struct sockaddr *addr, int *addrlen); //Window
sock
:服务器端
的socketaddr
:保存客户端
的IP地址和端口号- 返回值:一个新的socket,专门用来与该次请求的客户端通信
发送和接收数据
Linux下:
万物皆可为文件,因此socket和普通文件一样,可以直接使用write()/read()
发送和接受数据
ssize_t write(int fd, const void *buf, size_t nbytes);
fd
:要写入的文件描述符,即socketbuf
:要写入的数据的地址nbytes
:要写入的字节数- 返回值:写入成功返回字节数,失败返回-1
ssize_t read(int fd, void *buf, size_t nbytes);
fd
:要读取的文件描述符,即socketbuf
:存放读取出来的数据的地址nbytes
:要读取的字节数- 返回值:读取成功返回字节数,如果读取到文件结尾返回0,失败返回-1
Windows
需要用专门的send()/recv()
int send(SOCKET sock, const char *buf, int len, int flags);
- flags:发送数据时的选项。一般设置为0。
int recv(SOCKET sock, char *buf, int len, int flags);
socket缓冲区和阻塞模式
缓冲区
每个socket创建后都会有两个缓冲区,输入缓冲区
和输出缓冲区
。
write和send并不立即向网络中传输数据,而是先将数据写入输出缓冲区
,再通过TCP协议将数据从缓冲区发送到目标机器。一旦数据写入缓冲区,函数就返回,不管发送。
read和recv同理。
缓冲区特性:
- 在每个TCP socket中单独存在
- 创建socket时自动生成
- 关闭socket后TCP仍会发送输出缓冲区的数据
- 关闭socket后输入缓冲区数据丢失
- 大小一般为8KB,通过
getsockopt
获取 - 发送方在接收到ACK后才会清空输出缓冲区
阻塞模式
对于TCP socket,默认是阻塞的,也可以修改为非阻塞。
使用write和send发送数据时:
- 首先检查输出缓冲区,如果空间不够则阻塞,直到发送足够数据后空间足够才唤醒
- 如果TCP正在发送数据,阻塞write和send,直到发送完毕。
- 如果写入数据大于缓冲区,分批写入。
- 直到所有数据写入缓冲区,write和send才返回。
使用read和recv读取时:
- 首先检查输入缓冲区,如果有数据则读取,否则阻塞直到有数据
- 如果要读取的长度小于缓冲区中数据长度,则剩余数据会积压,直到再次读取。
- 直到读取到数据后才会返回,否则一直阻塞。
TCP的粘包(数据无边界性)
因为read时缓冲区数据可能是多次write的结果,所以无法区分每一次write的数据边界。例如两次分别写入1和3,读取时会读出13。