listen() 函数在网络编程中是服务器端建立连接的关键步骤。它将套接字设置为被动监听模式,等待客户端的连接请求。本文将深入剖析 listen() 函数,从其定义、参数、返回值,到工作原理、使用示例和常见问题。


一、listen() 函数概述

1.1 定义与作用

listen() 函数用于将一个绑定(bind())了地址和端口的套接字设置为 被动监听模式。这意味着套接字将准备接受客户端的连接请求,是构建服务器程序的重要一步。

1.2 函数原型

1
2
3
4
#include <sys/types.h>
#include <sys/socket.h>

int listen(int sockfd, int backlog);

二、listen() 函数的参数与返回值

2.1 参数解析

  • sockfd:套接字文件描述符,由 socket() 函数创建并返回,并已通过 bind() 绑定了本地地址和端口。
  • backlog:未完成连接队列的最大长度,即内核允许在监听套接字上排队的未处理连接的数量。

2.2 返回值

  • 成功:返回 0,表示套接字已成功进入监听状态。
  • 失败:返回 -1,并设置 errno,指示具体的错误原因。

三、listen() 函数的工作原理

3.1 连接队列的概念

在服务器端,TCP 协议使用两个队列来管理连接:

  1. 未完成连接队列:存放已收到客户端的 SYN 请求,但服务器尚未发送 ACK 的连接(半连接)。
  2. 已完成连接队列:存放已完成三次握手的连接,等待 accept() 函数处理。

backlog 参数影响的是已完成连接队列的大小。

3.2 套接字状态的转换

调用 listen() 后,套接字从主动模式(默认)转换为被动模式,准备接受连接请求。此时,套接字可以接收来自客户端的 SYN 包,并按照 TCP 的三次握手流程建立连接。


四、listen() 的使用示例

4.1 服务器端监听示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define PORT 8080
#define BACKLOG 5

int main() {
    int server_fd;
    struct sockaddr_in address;
    int opt = 1;
    int addrlen = sizeof(address);

    // 创建套接字
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
        perror("socket failed");
        return -1;
    }

    // 设置端口重用
    if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) {
        perror("setsockopt failed");
        close(server_fd);
        return -1;
    }

    // 绑定地址
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(PORT);

    if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
        perror("bind failed");
        close(server_fd);
        return -1;
    }

    // 开始监听
    if (listen(server_fd, BACKLOG) < 0) {
        perror("listen failed");
        close(server_fd);
        return -1;
    }

    printf("Server is listening on port %d\n", PORT);

    // 接受连接(省略 accept() 部分)

    close(server_fd);
    return 0;
}

五、注意事项

5.1 backlog 参数的影响

  • 含义backlog 指定了内核为此套接字排队的最大连接数量。
  • 实际效果:不同操作系统对 backlog 的处理可能不同,有的会对其进行限制或调整。
  • 建议:设置合适的 backlog 值,根据服务器的负载能力和预期的并发连接数进行调整。

5.2 套接字的状态检查

  • 问题:调用 listen() 之前,必须确保套接字已成功创建并绑定。
  • 解决方案:在调用 listen() 之前,检查 socket()bind() 的返回值,确保没有错误。

5.3 多次调用 listen()

  • 问题:对同一个套接字多次调用 listen() 会导致错误。
  • 解决方案listen() 只需调用一次,后续操作由 accept() 处理。

5.4 权限问题

  • 问题:绑定到低于 1024 的端口需要超级用户权限,否则 bind() 会失败,进而无法调用 listen()
  • 解决方案:以适当的权限运行程序,或选择高于 1024 的端口。

六、常见陷阱

6.1 忘记调用 listen()

  • 问题:创建并绑定套接字后,未调用 listen(),直接调用 accept(),会导致程序阻塞或出错。
  • 解决方案:在服务器程序中,确保在 bind() 之后调用 listen(),然后再调用 accept()

6.2 backlog 设置过小

  • 问题backlog 设置过小,可能导致高并发情况下,新的连接请求被拒绝。
  • 解决方案:根据服务器的处理能力,适当增大 backlog 值,避免连接被拒绝。

6.3 未正确处理返回值

  • 问题:未检查 listen() 的返回值,无法及时发现监听失败的问题。
  • 解决方案:始终检查 listen() 的返回值,处理可能的错误。

6.4 忽略 errno 的信息

  • 问题:在 listen() 失败时,未使用 errno 提供的错误信息,导致调试困难。
  • 解决方案:在错误处理时,使用 perror()strerror(errno) 获取详细的错误描述。

七、listen() 函数的高级用法

7.1 调整系统限制

  • 问题:操作系统可能对半连接队列和已完成连接队列的大小有上限。
  • 解决方案:通过调整系统参数,增大连接队列的最大值。

7.1.1 Linux 系统参数调整

  • /proc/sys/net/core/somaxconn:定义了监听队列的最大长度,默认值通常为 128。
  • 修改方法
1
echo 1024 > /proc/sys/net/core/somaxconn

或修改 /etc/sysctl.conf,添加:

1
net.core.somaxconn = 1024

然后执行 sysctl -p 使配置生效。

7.2 高并发服务器的优化

  • 方法:结合 epollkqueue 等 I/O 多路复用机制,提升服务器的并发处理能力。
  • 示例:使用 accept4() 函数,可以在接受连接时直接设置套接字为非阻塞模式,减少系统调用次数。

八、拓展资料:关键概念解释

8.1 TCP 三次握手

  • 定义:TCP 协议中,建立连接需要经过的三个步骤,确保双方建立可靠的连接。
  • 步骤
    1. SYN:客户端发送 SYN 包,请求建立连接。
    2. SYN-ACK:服务器收到后,回复 SYN-ACK 包,表示同意连接。
    3. ACK:客户端收到 SYN-ACK 后,发送 ACK 包,连接建立成功。

8.2 半连接队列与全连接队列

  • 半连接队列:存放已收到 SYN 包,但未完成三次握手的连接请求。
  • 全连接队列:存放已完成三次握手,等待 accept() 处理的连接。

8.3 accept() 函数

  • 作用:从已完成连接队列中取出一个已建立的连接,返回新的套接字文件描述符,用于与客户端进行通信。
  • 注意accept() 会阻塞,直到有新的连接可用,或设置为非阻塞模式。

8.4 I/O 多路复用

  • 概念:使用单个线程或进程监控多个文件描述符的状态,提高资源利用率。
  • 常用机制select()poll()epoll()kqueue() 等。
  • 应用场景:高并发网络服务器,实时性要求高的应用。

九、示例代码:完整的服务器程序

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <signal.h>

#define PORT 8080
#define BACKLOG 10

int main() {
    int server_fd, new_socket;
    struct sockaddr_in address;
    int opt = 1;
    int addrlen = sizeof(address);

    // 忽略 SIGPIPE 信号,防止因为客户端断开连接导致程序终止
    signal(SIGPIPE, SIG_IGN);

    // 创建套接字
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
        perror("socket failed");
        return -1;
    }

    // 设置端口重用
    if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) {
        perror("setsockopt failed");
        close(server_fd);
        return -1;
    }

    // 绑定地址
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(PORT);

    if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
        perror("bind failed");
        close(server_fd);
        return -1;
    }

    // 开始监听
    if (listen(server_fd, BACKLOG) < 0) {
        perror("listen failed");
        close(server_fd);
        return -1;
    }

    printf("Server is listening on port %d\n", PORT);

    // 接受连接
    while (1) {
        new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t *)&addrlen);
        if (new_socket < 0) {
            perror("accept failed");
            continue;
        }

        printf("Accepted a connection from %s:%d\n",
               inet_ntoa(address.sin_addr), ntohs(address.sin_port));

        // 简单地向客户端发送一条消息
        const char *message = "Hello, Client!\n";
        send(new_socket, message, strlen(message), 0);

        // 关闭与客户端的连接
        close(new_socket);
    }

    close(server_fd);
    return 0;
}