服务端程序工作流程: 创建socket → \rightarrow → 绑定监听的IP地址和端口 → \rightarrow → 监听客户端连接 → \rightarrow → 接受/发送数据。对应到系统API的调用就是socket() → \rightarrow → bind() → \rightarrow → listen() → \rightarrow → accept() → \rightarrow → recv()/send()
socket()创建socket,返回一个文件描述符,这个文件描述符只用于监听客户端的连接,不负责与客户端通信。调用accept()函数后,会返回一个与当前客户端通信的文件描述符。
TCP建立连接的过程: 调用listen()后,服务器监听指定的端口。收到客户端发来的SYN报文后(第一次握手),将这个连接请求放入半连接队列,并返回SYN+ACK报文(第二次握手)。当收到客户端发来的ACK报文后(第三次握手),服务器将这个连接请求从半连接队列中拿出,放入全连接队列中,等待accpet()取出。
listen()和accpet()的区别: 调用listen()后就已经可以与客户端建立连接了。accpet()只是从全连接队列中拿出一个已经建立的连接
#include
#include
#include
#include
#include
#include
#include
int open_socket() {
int sockfd = 0;
if ((sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) == -1) {
perror("socket wrong\n");
}
// 在 struct sockaddr_in 中设置监听信息,三个属性:
// 1. sin_family:协议类型
// 2. sin_port:端口号
// 3. sin_addr:一个struct,其中只包含一个 unsigned long 字段 s_addr,存储 ip 地址
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
// INADDR_ANY:监听所有网卡的 ip 地址
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
// htons: 将主机字节序(通常为小端)转换为网络字节序(大端)
server_addr.sin_port = htons(5005);
if (bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) != 0) {
perror("bind wrong\n");
}
if (listen(sockfd, 4096) != 0) {
perror("listen wrong\n");
}
return sockfd;
}
int main() {
int sockfd = open_socket();
int clientfd = 0;
struct sockaddr_in client_addr;
int size = sizeof(struct sockaddr_in);
clientfd = accept(sockfd, (struct sockaddr*)&client_addr, (socklen_t*)&size);
char buffer[1024];
int ret = 0;
while (true) {
memset(buffer, 0, sizeof(buffer));
ret = recv(clientfd, buffer, sizeof(buffer), 0);
if (ret == 0) {
printf("client disconnect\n");
break;
} else if (ret < 0) {
perror("recv error\n");
} else {
printf("msg size = %d, msg = %s\n", ret, buffer);
}
}
close(sockfd);
close(clientfd);
return 0;
}
全连接队列满了会怎样: listen()的第二个参数由用户指定全连接队列最大长度,但实际长度由这个参数和内核参数共同决定。实际长度=min(backlog, /proc/sys/net/core/somaxconn)。backlog约等于listen()的第二个参数,但略有不同。比如我指定backlog = 2,实际的全连接队列最大长度为3。
当全连接队列满后,服务端不会再接受第三次握手的ACK报文,一定时间后会重发第二次握手的SYN+ACK报文,因此没有进入全连接队列的客户端会回到SYN_SENT状态并重新发送ACK报文,直到超时。比如服务端程序不进行accept(),启动10个客户端连接,用ss -nt命令查看连接状态,发现只有三个客户端可以进入ESTABLISH状态,其它客户端处于SYN_SENT状态。
客户端工作流程: 创建socket → \rightarrow → 连接服务端 → \rightarrow → 接受/发送数据
#include
#include
#include
#include
#include
#include
#include
#include
int open_connect(const char* ip, uint16_t port) {
int sockfd;
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
perror("socket wrong\n");
return sockfd;
}
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
// struct hostent:保存 ip 地址,以网络字节序存储
struct hostent* h = NULL;
// gethostbyname:将 ip 地址或域名转化为网络字节序,返回一个 struct hostent*
if ((h = gethostbyname(ip)) == NULL) {
printf("wrong host name\n");
}
memcpy(&server_addr.sin_addr, h->h_addr, h->h_length);
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(port);
if (connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) != 0) {
perror("connect wrong\n");
}
return sockfd;
}
int main() {
int sockfd = open_connect("127.0.0.1", 5005);
char buffer[1024];
int ret;
for (int i = 0; i < 1000; i++) {
memset(buffer, 0, sizeof(buffer));
sprintf(buffer, "this is message %d", i);
// strlen(buffer) + 1 是额外计算'\0'所占的内存
if ((ret = send(sockfd, buffer, strlen(buffer) + 1, 0)) <= 0) {
perror("send error\n");
}
printf("send message: %s\n", buffer);
}
close(sockfd);
return 0;
}
send()函数: 调用send()函数后,会将数据拷贝到内核缓冲区中(只拷贝,不负责真的发送)。如果要发送的数据超过了缓冲区大小,则返回错误。如果发送数据超过了缓冲区当前剩余大小,则只拷贝缓冲区能容下的部分数据。
上述程序运行后,服务端接收到的数据为:
客户端每次发送18或19个字节的数据,即字符串“this is message xx”,但服务端每次接受到的数据长度不一样,说明有多条数据拼接在一起(实际打印的数据会截断到’\0’的位置),这就是TCP粘包问题。
TCP粘包: TCP是面向字节流的协议,一个TCP报文中携带的数据并不由应用程序决定。上面的客户端程序每次send一个字符串到内核缓冲区,但每次send的数据不一定是一个TCP报文,一个报文中可能包含多个send过来的数据。服务端在读取时,会在buffer大小范围内,将缓冲区的数据全都读进来。
解决TCP粘包问题: 每次发送数据之前,先发送一个固定长度的自定义包头,包头中定义了这一次数据的长度。服务端先按照包头长度接受包头数据,再按照包头数据中指定的数据长度接受这一次通信的数据。
在下面代码中,我们使用一个int类型作为“包头”,代表发送数据的长度。而int类型固定4字节,因此服务端每次先接受4字节的数据x,再接受x字节的字符串数据。
// 替代 recv() 函数
bool receive_message(int clientfd, char* buf) {
int one_size = 0;
int msg_size = 0;
// 先接受 msg_size
one_size = recv(clientfd, &msg_size, sizeof(int), 0);
if (one_size == 0) {
printf("client disconnect\n");
return false;
}
if (one_size < 0) {
perror("recv wrong\n");
return false;
}
int pos = 0;
while (msg_size > 0) {
one_size = recv(clientfd, buf + pos, msg_size, 0);
if (one_size == 0) {
printf("client disconnect\n");
return false;
}
if (one_size < 0) {
perror("recv wrong\n");
return false;
}
pos += one_size;
msg_size -= one_size;
}
printf("message size = %d, message: %s\n", pos, buf);
return true;
}
客户端调用send()函数时,由于可能当前缓冲区剩余空间不足以放下所有要发送的数据,因此也需要用send()函数的返回值来判断是否完成发送
// 替代 send() 函数
void send_message(int sockfd, const char* msg) {
int msg_size = strlen(msg);
int send_size = 0;
send_size = send(sockfd, &msg_size, sizeof(int), 0);
if (send_size < 0) {
perror("send wrong\n");
return;
}
int pos = 0;
while (msg_size > 0) {
send_size = send(sockfd, msg + pos, strlen(msg) - pos, 0);
if (send_size < 0) {
perror("send wrong\n");
return;
}
pos += send_size;
msg_size -= send_size;
}
printf("send message: %s\n", msg);
}