为何调用listen()后未执行accept(),TCP服务器仍能完成三次握手?
问题描述
我在调试Linux下一个基础的C语言TCP服务器时,在调用accept()的代码行之前暂停了程序执行,意外发现当客户端发送SYN包时,tcpdump显示服务器回复了SYN-ACK包,客户端随即回复最终的ACK包,完成了三次握手。
ss命令显示应用确实已在绑定端口上处于监听状态。
我知道已经调用了listen(),应用会监听绑定端口,但按语义来说,似乎应该先调用accept(),服务器才能接受连接。
listen()的手册页提到(斜体为我标注):
listen() marks the socket referred to by sockfd as a passive socket, that is, as a socket that will be used to accept incoming connection requests using accept(2).
而accept()的手册页说明:
It extracts the first connection request on the queue of pending connections for the listening socket
由此似乎可以理解为,应先调用accept()才能建立连接。
我忽略了什么?如果这是标准行为,能否给出权威来源?还是这只是特定实现的行为?
测试情况:如果在调用listen()前暂停执行,使用netcat测试会看到SYN包被回复RST包;但如果在listen()执行后暂停,tcpdump会显示服务器回复SYN-ACK包。
服务器代码如下:
#include <unistd.h> #include <stdio.h> #include <sys/wait.h> #include <string.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <errno.h> void error(const char* message) { printf("%s %s\n", message, strerror(errno)); } int main(int argc, char** argv) { const int sockfd = socket(AF_INET, SOCK_STREAM, 0); if ( sockfd == -1 ) { error("Socket error:"); return 1; } struct sockaddr_in servaddr; memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(12345); servaddr.sin_addr.s_addr = htonl(INADDR_ANY); if ( bind(sockfd, (struct sockaddr*) &servaddr, sizeof(servaddr)) == - 1 ) { error("Bind error:"); return 1; } if ( listen(sockfd, 5) == -1 ) { error("Listen error: "); return 1; } printf("Ready.\n"); struct sockaddr_in cliaddr; socklen_t cliaddrlen = sizeof(cliaddr); char response[512]; while (1) { const int connfd = accept(sockfd, (struct sockaddr*) &cliaddr, &cliaddrlen); if ( connfd == -1 ) { printf("Accept error: %s\n", strerror(errno)); return 1; } const pid_t pid = fork(); if ( pid == -1 ) { printf("Fork error: %s\n", strerror(errno)); continue; } if ( pid == 0 ) { close(sockfd); char buffer[16]; inet_ntop(AF_INET, &cliaddr.sin_addr, buffer, 16); printf("Connection from %s accepted.\n", buffer); while ( 1 ) { int nread = read(connfd, response, 512); if ( nread == -1 ) { printf("%s\n", strerror(errno)); } if (nread == 1 && response[0] == '\n') { break; } write(connfd, response, nread); //write(STDIN_FILENO, response, nread); } printf("Good bye!\n"); close(connfd); return 0; } close(connfd); wait(NULL); } return 0; }
解答
你忽略的核心点是:TCP的三次握手是由操作系统内核完成的,而非应用层的accept()调用。
1. listen()的实际作用
调用listen()后,内核会为该监听套接字维护两个队列:
- 半连接队列(SYN队列):存放已收到客户端SYN包但还未完成三次握手的连接
- 已完成连接队列(Accept队列):存放已经完成三次握手、等待应用层调用
accept()提取的连接
此时内核已经做好了处理客户端连接请求的准备,一旦收到客户端的SYN包,就会自动回复SYN-ACK,等收到客户端的ACK后,将连接从半连接队列移到已完成连接队列,整个过程完全不需要应用层介入。
2. accept()的角色
accept()的作用仅仅是从已完成连接队列中取出一个连接,创建新的套接字描述符返回给应用层,让应用可以和客户端进行数据交互。它并不参与三次握手的过程,只是“提取”已经建立好的连接。
3. 手册页内容的正确理解
listen()手册页的描述是说该套接字会被用来通过accept()接受连接,但这并不意味着accept()要先于三次握手执行,而是指这个套接字的用途是被动接受连接,内核会先处理好连接建立的流程,再交给accept()提取。accept()手册页提到的“pending connections”正是指已完成三次握手、等待应用提取的连接队列,这也印证了三次握手在accept()之前就已经完成。
4. 这是标准行为
这是POSIX标准规定的TCP套接字行为,并非Linux特有的实现。POSIX标准中对listen()的定义明确说明,调用该函数后,套接字进入被动监听状态,内核开始接受连接请求;而accept()只是从已完成的连接队列中取出连接。
你测试中看到的现象完全符合标准:
listen()调用前,内核未将该套接字标记为被动监听,收到SYN包后会回复RST包表示端口未监听listen()调用后,内核开始处理连接请求,自动完成三次握手,直到应用调用accept()提取连接
内容的提问来源于stack exchange,提问作者Lady Idiot

