TCP网络编程中connect、listen、accept的关系函数accrint
01. TCP服务器和客户端流程
02. connect 函数
对于客户端的 connect() 函数来说,该函数的作用是客户端主动去连接服务端。连接是通过三次握手建立的,而这个连接过程是内核完成的,而不是这个函数完成的。这个函数的作用只是通知 Linux 内核,让 Linux 内核自动完成 TCP 三次握手连接,最终将连接结果返回到这个函数的返回值(连接成功为 0,失败为 -1)。
正常情况下,客户端的connect()函数默认会阻塞,直到三次握手成功或者超时才返回(正常情况下这个过程很快就完成)。
03. listen 函数
对于服务器来说,是被动连接。举个生活中的例子,通常情况下,移动客服(相当于服务器)在等待客户(相当于客户端)的来电。而这个过程需要调用 listen() 函数。
listen()函数的主要作用是将套接字(sockfd)变成被动连接监听套接字(被动等待客户端的连接)。至于参数backlog,其作用是设置内核中连接队列的长度。TCP三次握手并不是通过此函数完成的。listen()的作用只是告诉内核一些信息。
这里需要注意的是,listen()函数不会阻塞,它的主要作用是告诉Linux内核这个socket以及这个socket对应的连接队列的长度,然后listen()函数结束。
这种情况下,当有客户端主动连接(connect())时,Linux内核会自动完成TCP三次握手,并自动将建立好的链接存放到队列中,并重复这个过程。
因此,只要TCP服务器调用listen(),客户端就可以通过connect()与服务器建立连接,连接过程由内核完成。
以下是测试的服务器和客户端代码,运行程序时先运行服务器,再运行客户端:
服务器:
#包括
#包括
#包括
#包括
#包括
#包括
#包括
int main(int argc,char *argv[])
无符号短端口 = 8000;
int sockfd;
sockfd = socket(AF_INET, SOCK_STREAM, 0); // 创建通信端点:socket
如果(sockfd < 0)
perror(“插座”);
退出(-1);
结构sockaddr_in my_addr;
bzero(&my_addr,sizeof(my_addr));
我的地址.sin_family = AF_INET;
my_addr.sin_port = htons(端口);
复制代码
int err_log = bind(sockfd,(struct sockaddr*)&my_addr,sizeof(my_addr));
如果(err_log!= 0)
perror(“绑定”);
关闭(sockfd);
退出(-1);
err_log = 监听(sockfd, 10);
如果 (err_log != 0)
perror(“听”);
关闭(sockfd);
退出(-1);
printf("监听客户端@port=%d…\n",port);
sleep(10); // 延迟 10 秒
system("netstat -an | grep 8000"); //检查连接状态
返回0;
客户:
#包括
#包括
#包括
#包括
#包括
#包括
#包括
int main(int argc,char *argv[])
unsigned short port = 8000; //服务器端口号
char *server_ip = "10.221.20.12"; //服务器IP地址
int sockfd;
sockfd = socket(AF_INET, SOCK_STREAM, 0); // 创建通信端点:socket
如果(sockfd < 0)
perror(“插座”);
退出(-1);
结构sockaddr_in server_addr;
bzero(&server_addr,sizeof(server_addr)); //初始化服务器地址
服务器地址.sin_family = AF_INET;
服务器地址.sin_port = htons(端口);
inet_pton(AF_INET,server_ip,&服务器地址.sin_addr);
int err_log = connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)); //主动连接服务器
如果 (err_log != 0)
perror(“连接”);
关闭(sockfd);
退出(-1);
system("netstat -an | grep 8000"); //检查连接状态
虽然(1);
返回0;
运行程序时,先运行服务端,再运行客户端,运行结果如下:
04.三次握手
这里详细介绍一下listen()函数的第二个参数(backlog)的作用:告诉内核连接队列的长度。
为了更好地理解 backlog 参数,我们必须认识到内核为任何给定的监听套接字维护两个队列:
1、未完成连接队列,每个SYN段对应其中的一个:已经由某个客户端发送,并到达服务器,而服务器正在等待相应的TCP三次握手过程的完成。这些套接字处于SYN_RCVD状态。
2.已完成连接队列,每个完成TCP三次握手过程的客户端都对应其中一个。这些套接字处于ESTABLISHED状态。
当来自客户端的 SYN 到达时,TCP 会在未完成连接队列中创建一个新项,然后用三次握手的第二段进行响应:服务器的 SYN 响应,该响应带有对客户端 SYN 的 ACK(即 SYN+ACK)。此项将保留在未完成连接队列中,直到三次握手的第三段(客户端对服务器 SYN 的 ACK)到达或此项超时(曾经源自伯克利的实现将这些未完成连接项的超时值设置为 75 秒)。
如果三次握手正常完成,则该项目从未完成的连接队列移动到已完成的连接队列的末尾。
backlog 参数历史上一直被定义为上面两个队列大小的总和。大多数实现的默认值是 5。当服务器从已完成的连接队列中取出一个连接时,这个队列中的另一个位置就会空出来,从而实现来回的动态平衡。但在高并发的 Web 服务器中,这个值显然是不够的。
05.接受函数
accept()函数在建立状态时从连接队列的头部取出一个已完成的连接,如果队列中没有已完成的连接,则accept()函数会阻塞,直到取出队列中已完成的用户连接为止。
如果服务器不能及时调用 accept() 来取走队列中已完成的连接,当队列满了时会发生什么? UNP(Unix 网络编程)告诉我们,当服务器的连接队列满了时,服务器将不会响应新的连接的 syn 请求,因此客户端的 connect 将返回 ETIMEDOUT。但事实上,在 Linux 中并非如此!
以下是测试代码,服务端 listen() 函数只指定了队列长度为 2,客户端有 6 个不同的 socket 主动连接服务端,同时保证在服务端的 accpet() 被调用之前,客户端的 6 个 connect() 函数全部被调用。
服务器
#包括
#包括
#包括
#包括
#包括
#包括
#包括
int main(int argc,char *argv[])
无符号短端口 = 8000;
int sockfd = 套接字(AF_INET, SOCK_STREAM, 0);
如果(sockfd < 0)
perror(“插座”);
退出(-1);
结构sockaddr_in my_addr;
bzero(&my_addr,sizeof(my_addr));
我的地址.sin_family = AF_INET;
my_addr.sin_port = htons(端口);
复制代码
int err_log = bind(sockfd,(struct sockaddr*)&my_addr,sizeof(my_addr));
如果(err_log!= 0)
perror(“绑定”);
关闭(sockfd);
退出(-1);
err_log = listen(sockfd, 2); //等待队列为2
如果 (err_log != 0)
perror(“听”);
关闭(sockfd);
退出(-1);
printf("监听结束后\n");
sleep(20); //延迟20秒
printf("监听客户端@port=%d…\n",port);
int i = 0;
while(1)
结构sockaddr_in客户端地址;
char cli_ip[INET_ADDRSTRLEN] = “”;
socklen_t cliaddr_len = sizeof(client_addr);
int confd;
connfd = 接受(sockfd,(struct sockaddr*)&client_addr,&cliaddr_len);
如果 (connfd < 0)
perror(“接受”);
继续;
inet_ntop(AF_INET,&client_addr.sin_addr,cli_ip,INET_ADDRSTRLEN);
printf("----------- %d------\n", ++i);
printf("客户端ip=%s,端口=%d\n",cli_ip,ntohs(client_addr.sin_port));
字符recv_buf[512] = {0};
while( recv(connfd, recv_buf, sizeof(recv_buf), 0) > 0 )
printf("接收数据==%s\n",recv_buf);
休息;
close(connfd); //关闭已连接的套接字
//printf("客户端已关闭!\n");
close(sockfd); //关闭监听套接字
返回0;
客户
#包括
#包括
#包括
#包括
#包括
#包括
#包括
无效测试_连接()
unsigned short port = 8000; //服务器端口号
char *server_ip = "10.221.20.12"; //服务器IP地址
int sockfd;
sockfd = socket(AF_INET, SOCK_STREAM, 0); // 创建通信端点:socket
如果(sockfd < 0)
perror(“插座”);
退出(-1);
结构sockaddr_in server_addr;
bzero(&server_addr,sizeof(server_addr)); //初始化服务器地址
服务器地址.sin_family = AF_INET;
服务器地址.sin_port = htons(端口);
inet_pton(AF_INET,server_ip,&服务器地址.sin_addr);
int err_log = connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)); //主动连接服务器
如果 (err_log != 0)
perror(“连接”);
关闭(sockfd);
退出(-1);
printf("错误日志 ========= %d\n", 错误日志);
char send_buf[100]="这是用于测试";
send(sockfd, send_buf, strlen(send_buf), 0); // 发送信息到服务器
system("netstat -an | grep 8000"); //检查连接状态
//关闭(sockfd);
int main(int argc,char *argv[])
pid_t pid;
进程ID = fork();
如果(0 == pid){
测试连接(); // 1
pid_t pid = fork();
如果(0 == pid){
测试连接(); // 2
}否则,如果(pid> 0){
测试连接(); // 3
}否则,如果(pid> 0){
测试连接(); // 4
pid_t pid = fork();
如果(0 == pid){
测试连接(); // 5
}否则,如果(pid> 0){
测试连接(); // 6
虽然(1);
返回0;
先运行服务器,再运行客户端。服务器延迟20秒后再调用accept()函数,以保证在客户端的所有connect()调用完成后才调用accept()。结果如下:
客户端运行结果
根据 UNP 的说法,当连接队列已满时(此处将长度设置为 2,发送了 6 个连接),所有后续的 connect() 调用都应该超时并失败。但实际测试结果表明,有些 connect() 调用立即成功返回,而有些则在延迟相当长的时间后才成功返回。服务器 accpet() 函数也是如此:有些立即成功返回,而有些则在延迟后成功返回。
对于上面的服务端代码,我们把lisen()的第二个参数改为0,重新运行程序,发现所有客户端connect()都返回连接成功(有的会延迟)
服务器 accpet() 函数无法将所有连接从连接队列中取出:
保存图片并直接上传(img-i12tqYDQ-35)(assets/image-
对于上面的服务端代码,我们将lisen()的第二个参数改为大于6的数字(比如10),重新运行程序,发现客户端connect()函数立刻返回连接成功,服务端accpet()函数也立刻返回连接成功。
当TCP连接队列满了的时候,Linux不会像书上说的那样拒绝连接,但是有些连接会延迟,accept()也可能无法把所有建立的连接都取出来(比如当指定队列长度为0的时候)。在写程序的时候,最好根据需要填写服务器的listen()的第二个参数,不宜写得太大(具体见cat /proc/sys/net/core/somaxconn,默认最大值限制为128),浪费资源,也不宜写得太小,延迟连接的建立。