TCP是一种面向连接的通信方式,一个TCP服务器难免会遇到同时处理多个用户的连接请求的问题,本文用一个简化的实例说明如何在一个TCP服务器程序中,使用select处理同时出现的多个客户连接,文章给出了程序源代码,阅读本文应该具备了基本的socket编程知识,熟悉基本的服务器/客户端模型架构;本文对初学者难度不大。

1. 基本思路

  • TCP服务器端程序,对于每一个连接请求,可以使用多线程的方式为每一个连接启动一个线程处理该连接的通信,但使用多线程的方式,通常认为有如下缺点:
    1. 多线程编程和调试相对都比较难,而且有时会出现无法预知的问题;
    2. 上下文切换的开销较大;
    3. 对于巨大量的连接,可扩展性不足;
    4. 可能发生死锁。
  • 使用select处理多连接的基本思路
    1. 建立一个用于侦听的socket,叫做master_socket;
    1
    
    int master_socket = socket(AF_INET , SOCK_STREAM , 0);
    
    1. 建立一个sockets数组,用于存储已经与master_socket建立连接的socket,叫做client_socket,初始化时全部清0,数组的长度即为程序允许的最大连接数;
    1
    2
    3
    4
    5
    6
    7
    
    #define MAX_CLIENTS     30
    
    int client_socket[MAX_CLIENTS];
    int i;
    for (i = 0; i < MAX_CLIENTS; i++) {
        client_socket[i] = 0;
    }
    
    1. 绑定服务器地址并在master_socket上启动侦听;
    1
    2
    3
    4
    5
    6
    7
    
    #define PORT          8888
    struct sockaddr_in address;
    
    address.sin_family      = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port        = htons(PORT);
    bind(master_socket, (struct sockaddr *)&address, sizeof(address));
    
    1. 在master_socket上侦听
    1
    
    listen(master_socket, 3);
    
    1. 将master_socket、client_socket中不为0的项加入到readfds中,启动select;
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    
    fd_set readfds;
    int max_fd, client_count;
    
    FD_ZERO(&readfds);
    FD_SET(master_socket, &readfds);
    max_fd = master_socket;
    
    client_count = 0;
    for (i = 0 ; i < MAX_CLIENTS; i++) {
        if (client_socket[i] > 0) {
            FD_SET(client_socket[i], &readfds);
            client_count++;
        }
        if (client_socket[i] > max_fd) max_fd = client_socket[i];
    }
    activity = select(max_fd + 1, &readfds, NULL, NULL, NULL);
    
    1. 有活动socket返回时,如果是master_socket则调用accept接受连接,生成新的socket并加入到client_socket中,发送欢迎信息后回到步骤4;
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    
    int new_socket;
    char *message = "ECHO Daemon v1.0 \n\n";
    
    addrlen = sizeof(address);
    if (FD_ISSET(master_socket, &readfds)) {
        new_socket = accept(master_socket, (struct sockaddr *)&address, (socklen_t *)&addrlen);
        send(new_socket, message, strlen(message), 0);
        if (client_count < MAX_CLIENTS) {
            for (i = 0; i < MAX_CLIENTS; i++) {
                if (client_socket[i] == 0) {
                    client_socket[i] = new_socket;
                    client_count++;
                    break;
                }
            }
        } else {
            close(new_socket);
        }
    }
    
    1. 如果不是master_socket,为client_socket中的一员,则调用read从socket中读出数据,处理并做出回应,回到步骤4;
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    for (i = 0; i < MAX_CLIENTS; i++) {
        if (client_socket[i] == 0) continue;
    
        if (FD_ISSET(client_socket[i], &readfds)) {
            if ((nread = read(client_socket[i], buffer, 1024)) > 0) {
                buffer[nread] = '\0';
                send(client_socket[i], buffer, strlen(buffer), 0 );
            }
        }
    }
    
    1. 如果从client_socket读出数据长度为0,表示socket已经关闭,关闭socket,并从client_socket中清除该socket,回到步骤4;
    1
    2
    3
    4
    
    if (nread == 0) {
        close(client_socket[i]);
        client_socket[i] = 0;
    }
    
    1. 如果从client_socket读出数据长度小于0,如果errno=EINTR,则直接返回步骤4;
    2. 如果从client_socket读出数据长度小于0,如果errno不是EINTR,则关闭socket,并从client_socket中清除该socket,回到步骤4;
    1
    2
    3
    4
    
    if (errno != EINTR) {
        close(client_socket[i]);
        client_socket[i] = 0;
    }
    

2. 主要函数、宏和数据结构

3. 实例

  • 该实例最多可以同时处理30个连接,理论上可以更多,与机器的资源有关;

  • 该实例收到客户端的信息后没有做处理,将收到的信息发回给了客户端;

  • 服务器源程序:select-server.c(点击文件名下载源程序)

  • 编译

    1
    
    gcc -Wall select-server.c -o select-server
    
  • 运行

    1
    
    ./select-server
    
  • 测试,使用nc模拟客户端,有关nc命令的使用方法,可以参考另一篇文章《如何在Linux命令行下发送和接收UDP数据包》

    • 服务器ip:192.168.2.114

    • 在另一台机器(或者多台机器)开三个终端窗口,分别输入下面命令:

      1
      2
      3
      
      nc -n 192.168.2.114 8888
      
      hello from client 1
      
      1
      2
      3
      
      nc -n 192.168.2.114 8888
      
      hello from client 2
      
      1
      2
      3
      
      nc -n 192.168.2.114 8888
      
      hello from client 3
      
    • 分别在第2、第3、第1终端窗口中按下ctrl+c退出nc命令

    • 服务器端运行截屏

      Screenshot of server


    • 客户端第一个窗口运行截屏

      Screenshot of client

欢迎订阅 『网络编程专栏』


欢迎访问我的博客:https://whowin.cn

email: hengch@163.com

donation