为什么 listen() (在调用 accept() 之前)足以让应用程序完成三次握手?

为什么 listen() (在调用 accept() 之前)足以让应用程序完成三次握手?

我正在 Linux 上用 C 调试一个非常基本的 tcp 服务器。我在调用 accept() 的那一行之前停止了执行。令我惊讶的是,当客户端发送 SYN 时,tcpdump 显示服务器以 SYN-ACK 响应(客户端会立即用最终 ACK 回复)。

ss命令确实表明应用程序已经在监听绑定的端口。

我知道我已经调用了 listen(),因此应用程序将监听绑定的端口。但是,按照相同的语义,应该在服务器接受连接之前调用 accept()。

在 listen() 手册页中,它写道(斜体是我的):

listen() 将 sockfd 引用的套接字标记为被动套接字,即用于使用 accept(2) 接受传入连接请求的套接字

而 accept() 手册页则指出:

它提取第一个待处理连接队列上的连接请求用于监听套接字

由此我们可以理解,在建立连接之前应该调用 accept()。

我在这里遗漏了什么?如果这是标准行为,可以指出我的主要来源吗?还是只是特定于实现的?

下面是我使用的代码。如果我在调用 listen() 之前停止执行,使用 netcat 将显示发送的 SYN 以 RST 进行回复。但如果我在执行 listen() 之后执行相同操作,tcpdump 将显示服务器以 SYN-ACK 进行回复。

#include <unistd.h>
#include <stdio.h>
#include <sys/wait.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;
}

答案1

listen() 的“backlog”整数参数(你指定的参数5)决定了可以接受多少个连接,例如:达到该限制后,新的 SYN将要被忽略或拒绝。

它允许将这些连接排队,例如在单线程服务器中,当 BSD 套接字 API 最初设计时(如果我没有记错的话,它早于线程),这很可能已经找到了。服务器可能必须先 fork() 才能 accept() 下一个客户端,或者它甚至可能决定在到达下一个客户端之前立即在同一线程中处理请求(如果是简单协议的话)(例如基本 Whois 或 QotD 服务器)。

如果服务器根本没有响应,客户端很快就会达到连接超时;但是一旦连接建立,超时时间可能会更长(例如 SMTP 定义的超时时间为5分钟直到问候语)。话虽如此,我不知道在创建此 API 时什么样的延迟才是“正常的”,因此对此有一些猜测。

答案2

您未能区分用户空间应用程序的工作和内核的支持工作,在这种情况下,这也是应用程序层和传输层(及更低层)之间的分离。内核负责 TCP 握手,更一般地说,负责建立 TCP 级连接。应用程序的调用要求listen()它开始为入站传输层连接请求执行此操作。在用户空间端,应用程序所连接的套接字accepts()位于应用程序层并为应用程序层提供服务,至少就应用程序需要知道的而言。

而 accept() 手册页则指出:

它提取监听套接字的待处理连接队列中的第一个连接请求

由此我们可以理解,在建立连接之前应该调用 accept()。

不,你的意思不是“在TCP应该建立连接”。 accept()提供处理应用程序级连接请求。用户空间通常看不到低于该值的任何内容。

此外,推迟握手完成还有什么目的呢?

相关内容