基于 TCP 的网络编程开发分为服务器端和客户端两部分,常见的核心步骤和流程如下:
connect()函数:
对于客户端的 connect() 函数,该函数的功能为客户端主动连接服务器,建立连接是通过三次握手,而这个连接的过程是由内核完成,不是这个函数完成的,这个函数的作用仅仅是通知 Linux 内核,让 Linux 内核自动完成 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()(注意listen函数并不会阻塞),客户端就可以通过 connect() (这里connect是阻塞的)和服务器建立连接,而这个连接的过程是由内核完成。
下面为测试的服务器和客户端代码,运行程序时,要先运行服务器,再运行客户端:
客户端:
1 /************************************************************************* 2 > File Name: clientTest1.c 3 > Summary: TCP编程 验证在server端调用listen之后,3次握手完成 客户端 4 > Author: xuelisheng 5 > Created Time: 2018年12月19日 6 ************************************************************************/ 7 8 #include <stdio.h> 9 #include <unistd.h> 10 #include <string.h> 11 #include <stdlib.h> 12 #include <arpa/inet.h> 13 #include <sys/socket.h> 14 #include <netinet/in.h> 15 int main(int argc, char *argv[]) 16 { 17 unsigned short port = 8000; // 服务器的端口号 18 char *server_ip = "127.0.0.1"; // 服务器ip地址 19 20 int sockfd; 21 sockfd = socket(AF_INET, SOCK_STREAM, 0);// 创建通信端点:套接字 22 if(sockfd < 0) 23 { 24 perror("socket"); 25 exit(-1); 26 } 27 28 struct sockaddr_in server_addr; 29 bzero(&server_addr,sizeof(server_addr)); // 初始化服务器地址 30 server_addr.sin_family = AF_INET; 31 server_addr.sin_port = htons(port); 32 inet_pton(AF_INET, server_ip, &server_addr.sin_addr); 33 34 // 阻塞等待server端调用listen,如果此函数执行完,则3次握手完成 35 int err_log = connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)); // 主动连接服务器 36 if(err_log != 0) 37 { 38 perror("connect"); 39 close(sockfd); 40 exit(-1); 41 } 42 43 system("netstat -an | grep 8000"); // 查看连接状态 44 while(1); 45 return 0; 46 }
服务端:
1 /************************************************************************* 2 > File Name: serverTest1.c 3 > Summary: TCP编程 服务端 4 > Author: xuelisheng 5 > Created Time: 2018年12月19日 6 ************************************************************************/ 7 8 #include <stdio.h> 9 #include <stdlib.h> 10 #include <string.h> 11 #include <unistd.h> 12 #include <sys/socket.h> 13 #include <netinet/in.h> 14 #include <arpa/inet.h> 15 int main(int argc, char *argv[]) 16 { 17 unsigned short port = 8000; 18 19 int sockfd; 20 // 创建套接字 21 sockfd = socket(AF_INET, SOCK_STREAM, 0);// 创建通信端点:套接字 22 if(sockfd < 0) 23 { 24 perror("socket"); 25 exit(-1); 26 } 27 28 struct sockaddr_in my_addr; 29 bzero(&my_addr, sizeof(my_addr)); 30 my_addr.sin_family = AF_INET; 31 my_addr.sin_port = htons(port); 32 my_addr.sin_addr.s_addr = htonl(INADDR_ANY); 33 34 // 绑定 ip+port 35 int err_log = bind(sockfd, (struct sockaddr*)&my_addr, sizeof(my_addr)); 36 if( err_log != 0) 37 { 38 perror("binding"); 39 close(sockfd); 40 exit(-1); 41 } 42 43 err_log = listen(sockfd, 10); 44 if(err_log != 0) 45 { 46 perror("listen"); 47 close(sockfd); 48 exit(-1); 49 } 50 51 printf("listen client @port=%d... ",port); 52 53 sleep(10); // 延时10s 54 55 system("netstat -an | grep 8000"); // 查看连接状态 56 57 return 0; 58 }
这里服务端先执行,执行完listen函数,接下来休眠10s...... 此时起客户端服务,connect后,3次握手完成。值得注意的是,服务端的函数并不会阻塞等到client端的connect的到来。
服务端输出:
mi@mi-OptiPlex-7050:~/codeSelf/netWorkProgramme/tcpCode$ ./serverTest1 listen client @port=8000... tcp 0 0 0.0.0.0:8000 0.0.0.0:* LISTEN tcp 0 0 127.0.0.1:51702 127.0.0.1:8000 ESTABLISHED tcp 0 0 127.0.0.1:8000 127.0.0.1:51702 ESTABLISHED
客户端输出:
mi@mi-OptiPlex-7050:~/codeSelf/netWorkProgramme/tcpCode$ ./clientTest1 tcp 0 0 0.0.0.0:8000 0.0.0.0:* LISTEN tcp 0 0 127.0.0.1:51702 127.0.0.1:8000 ESTABLISHED tcp 0 0 127.0.0.1:8000 127.0.0.1:51702 ESTABLISHED
三次握手的连接队列
这里详细的介绍一下 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() 才开始调用。