You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

为何调用listen()后未执行accept(),TCP服务器仍能完成三次握手?

关于Linux下TCP服务器listen()与三次握手的疑问解答

问题描述

我在调试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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.25 18:13:13