TCP网络编程中connect()、listen()和accept()三者之间的关系




版权声明:本博客文章,大多是本人整理编写,或在网络中收集,转载请注明出处! https://blog.csdn.net/tennysonsky/article/details/45621341



基于 TCP 的网络编程开发分为服务器端和客户端两部分,常见的核心步骤和流程如下:


connect()函数

对于客户端的 connect() 函数,该函数的功能为客户端主动连接服务器,建立连接是通过三次握手,而这个连接的过程是由内核完成,不是这个函数完成的,这个函数的作用仅仅是通知 Linux 内核,让 Linux 内核自动完成 TCP 三次握手连接(三次握手详情,请看《浅谈 TCP 三次握手》),最后把连接的结果返回给这个函数的返回值(成功连接为0, 失败为-1)。


通常的情况,客户端的 connect() 函数默认会一直阻塞,直到三次握手成功或超时失败才返回(正常的情况,这个过程很快完成)。


listen()函数

对于服务器,它是被动连接的。举一个生活中的例子,通常的情况下,移动的客服(相当于服务器)是等待着客户(相当于客户端)电话的到来。而这个过程,需要调用listen()函数。


    
    
  1. #include<sys/socket.h>
  2. int listen(int sockfd, int backlog);


listen() 函数的主要作用就是将套接字( sockfd )变成被动的连接监听套接字(被动等待客户端的连接),至于参数 backlog 的作用是设置内核中连接队列的长度(这个长度有什么用,后面做详细的解释),TCP 三次握手也不是由这个函数完成,listen()的作用仅仅告诉内核一些信息。


这里需要注意的是,listen()函数不会阻塞,它主要做的事情为,将该套接字和套接字对应的连接队列长度告诉 Linux 内核,然后,listen()函数就结束。


这样的话,当有一个客户端主动连接(connect()),Linux 内核就自动完成TCP 三次握手,将建立好的链接自动存储到队列中,如此重复。


所以,只要 TCP 服务器调用了 listen(),客户端就可以通过 connect() 和服务器建立连接,而这个连接的过程是由内核完成



下面为测试的服务器和客户端代码,运行程序时,要先运行服务器,再运行客户端:

服务器:


    
    
  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <string.h>
  4. #include <unistd.h>
  5. #include <sys/socket.h>
  6. #include <netinet/in.h>
  7. #include <arpa/inet.h>
  8. int main(int argc, char argv[])
  9. {
  10. unsigned short port = 8000;
  11. int sockfd;
  12. sockfd = socket(AF_INET, SOCK_STREAM, 0); // 创建通信端点:套接字
  13. if(sockfd < 0)
  14. {
  15. perror( "socket");
  16. exit( -1);
  17. }
  18. struct sockaddr_in my_addr;
  19. bzero(&my_addr, sizeof(my_addr));
  20. my_addr.sin_family = AF_INET;
  21. my_addr.sin_port = htons(port);
  22. my_addr.sin_addr.s_addr = htonl(INADDR_ANY);
  23. int err_log = bind(sockfd, (struct sockaddr)&my_addr, sizeof(my_addr));
  24. if( err_log != 0)
  25. {
  26. perror( "binding");
  27. close(sockfd);
  28. exit( -1);
  29. }
  30. err_log = listen(sockfd, 10);
  31. if(err_log != 0)
  32. {
  33. perror( "listen");
  34. close(sockfd);
  35. exit( -1);
  36. }
  37. printf( "listen client @port=%d...\n",port);
  38. sleep( 10); // 延时10s
  39. system( "netstat -an | grep 8000"); // 查看连接状态
  40. return 0;
  41. }


客户端:


    
    
  1. #include <stdio.h>
  2. #include <unistd.h>
  3. #include <string.h>
  4. #include <stdlib.h>
  5. #include <arpa/inet.h>
  6. #include <sys/socket.h>
  7. #include <netinet/in.h>
  8. int main(int argc, char argv[])
  9. {
  10. unsigned short port = 8000; // 服务器的端口号
  11. char *server_ip = "10.221.20.12"; // 服务器ip地址
  12. int sockfd;
  13. sockfd = socket(AF_INET, SOCK_STREAM, 0); // 创建通信端点:套接字
  14. if(sockfd < 0)
  15. {
  16. perror( "socket");
  17. exit( -1);
  18. }
  19. struct sockaddr_in server_addr;
  20. bzero(&server_addr, sizeof(server_addr)); // 初始化服务器地址
  21. server_addr.sin_family = AF_INET;
  22. server_addr.sin_port = htons(port);
  23. inet_pton(AF_INET, server_ip, &server_addr.sin_addr);
  24. int err_log = connect(sockfd, (struct sockaddr)&server_addr, sizeof(server_addr)); // 主动连接服务器
  25. if(err_log != 0)
  26. {
  27. perror( "connect");
  28. close(sockfd);
  29. exit( -1);
  30. }
  31. system( "netstat -an | grep 8000"); // 查看连接状态
  32. while( 1);
  33. return 0;
  34. }

运行程序时,要先运行服务器,再运行客户端,运行结果如下:


三次握手的连接队列

这里详细的介绍一下 listen() 函数的第二个参数( backlog)的作用:告诉内核连接队列的长度。


为了更好的理解 backlog 参数,我们必须认识到内核为任何一个给定的监听套接口维护两个队列:

1、未完成连接队列(incomplete connection queue),每个这样的 SYN 分节对应其中一项:已由某个客户发出并到达服务器,而服务器正在等待完成相应的 TCP 三次握手过程。这些套接口处于 SYN_RCVD 状态。


2、已完成连接队列(completed connection queue),每个已完成 TCP 三次握手过程的客户对应其中一项。这些套接口处于 ESTABLISHED 状态。


 

当来自客户的 SYN 到达时,TCP 在未完成连接队列中创建一个新项,然后响应以三次握手的第二个分节:服务器的 SYN 响应,其中稍带对客户 SYN 的 ACK(即SYN+ACK),这一项一直保留在未完成连接队列中,直到三次握手的第三个分节(客户对服务器 SYN 的 ACK )到达或者该项超时为止(曾经源自Berkeley的实现为这些未完成连接的项设置的超时值为75秒)。


如果三次握手正常完成,该项就从未完成连接队列移到已完成连接队列的队尾。


backlog 参数历史上被定义为上面两个队列的大小之和,大多数实现默认值为 5,当服务器把这个完成连接队列的某个连接取走后,这个队列的位置又空出一个,这样来回实现动态平衡,但在高并发 web 服务器中此值显然不够。


accept()函数

accept()函数功能是,从处于 established 状态的连接队列头部取出一个已经完成的连接,如果这个队列没有已经完成的连接,accept()函数就会阻塞,直到取出队列中已完成的用户连接为止。


如果,服务器不能及时调用 accept() 取走队列中已完成的连接,队列满掉后会怎样呢?UNP(《unix网络编程》)告诉我们,服务器的连接队列满掉后,服务器不会对再对建立新连接的syn进行应答,所以客户端的 connect 就会返回 ETIMEDOUT。但实际上Linux的并不是这样的!


下面为测试代码,服务器 listen() 函数只指定队列长度为 2,客户端有 6 个不同的套接字主动连接服务器,同时,保证客户端的 6 个 connect()函数都先调用完毕,服务器的 accpet() 才开始调用。


服务器:


    
    
  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <string.h>
  4. #include <unistd.h>
  5. #include <sys/socket.h>
  6. #include <netinet/in.h>
  7. #include <arpa/inet.h>
  8. int main(int argc, char argv[])
  9. {
  10. unsigned short port = 8000;
  11. int sockfd = socket(AF_INET, SOCK_STREAM, 0);
  12. if(sockfd < 0)
  13. {
  14. perror( "socket");
  15. exit( -1);
  16. }
  17. struct sockaddr_in my_addr;
  18. bzero(&my_addr, sizeof(my_addr));
  19. my_addr.sin_family = AF_INET;
  20. my_addr.sin_port = htons(port);
  21. my_addr.sin_addr.s_addr = htonl(INADDR_ANY);
  22. int err_log = bind(sockfd, (struct sockaddr)&my_addr, sizeof(my_addr));
  23. if( err_log != 0)
  24. {
  25. perror( "binding");
  26. close(sockfd);
  27. exit( -1);
  28. }
  29. err_log = listen(sockfd, 2); // 等待队列为2
  30. if(err_log != 0)
  31. {
  32. perror( "listen");
  33. close(sockfd);
  34. exit( -1);
  35. }
  36. printf( "after listen\n");
  37. sleep( 20); //延时 20秒
  38. printf( "listen client @port=%d...\n",port);
  39. int i = 0;
  40. while( 1)
  41. {
  42. struct sockaddr_in client_addr;
  43. char cli_ip[INET_ADDRSTRLEN] = "";
  44. socklen_t cliaddr_len = sizeof(client_addr);
  45. int connfd;
  46. connfd = accept(sockfd, (struct sockaddr*)&client_addr, &cliaddr_len);
  47. if(connfd < 0)
  48. {
  49. perror( "accept");
  50. continue;
  51. }
  52. inet_ntop(AF_INET, &client_addr.sin_addr, cli_ip, INET_ADDRSTRLEN);
  53. printf( "-----------%d------\n", ++i);
  54. printf( "client ip=%s,port=%d\n", cli_ip,ntohs(client_addr.sin_port));
  55. char recv_buf[ 512] = { 0};
  56. while( recv(connfd, recv_buf, sizeof(recv_buf), 0) > 0 )
  57. {
  58. printf( "recv data ==%s\n",recv_buf);
  59. break;
  60. }
  61. close(connfd); //关闭已连接套接字
  62. //printf("client closed!\n");
  63. }
  64. close(sockfd); //关闭监听套接字
  65. return 0;
  66. }

客户端:


    
    
  1. #include <stdio.h>
  2. #include <unistd.h>
  3. #include <string.h>
  4. #include <stdlib.h>
  5. #include <arpa/inet.h>
  6. #include <sys/socket.h>
  7. #include <netinet/in.h>
  8. void test_connect()
  9. {
  10. unsigned short port = 8000; // 服务器的端口号
  11. char server_ip = "10.221.20.12"; // 服务器ip地址
  12. int sockfd;
  13. sockfd = socket(AF_INET, SOCK_STREAM, 0); // 创建通信端点:套接字
  14. if(sockfd < 0)
  15. {
  16. perror( "socket");
  17. exit( -1);
  18. }
  19. struct sockaddr_in server_addr;
  20. bzero(&server_addr, sizeof(server_addr)); // 初始化服务器地址
  21. server_addr.sin_family = AF_INET;
  22. server_addr.sin_port = htons(port);
  23. inet_pton(AF_INET, server_ip, &server_addr.sin_addr);
  24. int err_log = connect(sockfd, (struct sockaddr)&server_addr, sizeof(server_addr)); // 主动连接服务器
  25. if(err_log != 0)
  26. {
  27. perror( "connect");
  28. close(sockfd);
  29. exit( -1);
  30. }
  31. printf( "err_log ========= %d\n", err_log);
  32. char send_buf[ 100]= "this is for test";
  33. send(sockfd, send_buf, strlen(send_buf), 0); // 向服务器发送信息
  34. system( "netstat -an | grep 8000"); // 查看连接状态
  35. //close(sockfd);
  36. }
  37. int main(int argc, char *argv[])
  38. {
  39. pid_t pid;
  40. pid = fork();
  41. if( 0 == pid){
  42. test_connect(); // 1
  43. pid_t pid = fork();
  44. if( 0 == pid){
  45. test_connect(); // 2
  46. } else if(pid > 0){
  47. test_connect(); // 3
  48. }
  49. } else if(pid > 0){
  50. test_connect(); // 4
  51. pid_t pid = fork();
  52. if( 0 == pid){
  53. test_connect(); // 5
  54. } else if(pid > 0){
  55. test_connect(); // 6
  56. }
  57. }
  58. while( 1);
  59. return 0;
  60. }

同样是先运行服务器,在运行客户端,服务器 accept()函数前延时了 20 秒, 保证了客户端的 connect() 全部调用完毕后再调用 accept(),运行结果如下:

服务器运行效果图:




客户端运行效果图:



按照 UNP 的说法,连接队列满后(这里设置长度为 2,发了 6 个连接),以后再调用 connect() 应该统统超时失败,但实际上测试结果是:有的 connect()立刻成功返回了,有的经过明显延迟后成功返回了。对于服务器 accpet() 函数也是这样的结果:有的立马成功返回,有的延迟后成功返回。


对于上面服务器的代码,我们把lisen()的第二个参数改为 0 的数,重新运行程序,发现:

客户端 connect() 全部返回连接成功(有些会延时):



服务器 accpet() 函数却不能把连接队列的所有连接都取出来:



对于上面服务器的代码,我们把lisen()的第二个参数改为大于 6 的数(如 10),重新运行程序,发现,客户端 connect() 立马返回连接成功, 服务器 accpet() 函数也立马返回成功。


TCP 的连接队列满后,Linux 不会如书中所说的拒绝连接,只是有些会延时连接,而且accept()未必能把已经建立好的连接全部取出来(如:当队列的长度指定为 0 ),写程序时服务器的 listen() 的第二个参数最好还是根据需要填写,写太大不好(具体可以看cat /proc/sys/net/core/somaxconn,默认最大值限制是 128),浪费资源,写太小也不好,延时建立连接。


测试代码下载请点此处。





  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值