Skip to main content

Command Palette

Search for a command to run...

TCP服务器如何使用select处理多客户连接

Updated
2 min read
TCP服务器如何使用select处理多客户连接

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

1. 基本思路

  • TCP服务器端程序,对于每一个连接请求,可以使用多线程的方式为每一个连接启动一个线程处理该连接的通信,但使用多线程的方式,通常认为有如下缺点:
    1. 多线程编程和调试相对都比较难,而且有时会出现无法预知的问题;
    2. 上下文切换的开销较大;
    3. 对于巨大量的连接,可扩展性不足;
    4. 可能发生死锁。
  • 使用select处理多连接的基本思路

    1. 建立一个用于侦听的socket,叫做master_socket;
      int master_socket = socket(AF_INET , SOCK_STREAM , 0);
      
    2. 建立一个sockets数组,用于存储已经与master_socket建立连接的socket,叫做client_socket,初始化时全部清0,数组的长度即为程序允许的最大连接数;

      #define MAX_CLIENTS     30
      
      int client_socket[MAX_CLIENTS];
      int i;
      for (i = 0; i < MAX_CLIENTS; i++) {
         client_socket[i] = 0;
      }
      
    3. 绑定服务器地址并在master_socket上启动侦听;

      #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));
      
    4. 在master_socket上侦听
      listen(master_socket, 3);
      
    5. 将master_socket、client_socket中不为0的项加入到readfds中,启动select;

      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);
      
    6. 有活动socket返回时,如果是master_socket则调用accept接受连接,生成新的socket并加入到client_socket中,发送欢迎信息后回到步骤4;

      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);
         }
      }
      
    7. 如果不是master_socket,为client_socket中的一员,则调用read从socket中读出数据,处理并做出回应,回到步骤4;

      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 );
             }
         }
      }
      
    8. 如果从client_socket读出数据长度为0,表示socket已经关闭,关闭socket,并从client_socket中清除该socket,回到步骤4;
      if (nread == 0) {
         close(client_socket[i]);
         client_socket[i] = 0;
      }
      
    9. 如果从client_socket读出数据长度小于0,如果errno=EINTR,则直接返回步骤4;
    10. 如果从client_socket读出数据长度小于0,如果errno不是EINTR,则关闭socket,并从client_socket中清除该socket,回到步骤4;
      if (errno != EINTR) {
        close(client_socket[i]);
        client_socket[i] = 0;
      }
      

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

3. 实例

  • 该实例最多可以同时处理30个连接,理论上可以更多,与机器的资源有关;
  • 该实例收到客户端的信息后没有做处理,将收到的信息发回给了客户端;
  • 服务器源程序:select-server.c(点击文件名下载源程序)
  • 编译
    gcc -Wall select-server.c -o select-server
    
  • 运行
    ./select-server
    
  • 测试,使用nc模拟客户端,有关nc命令的使用方法,可以参考另一篇文章《如何在Linux命令行下发送和接收UDP数据包》

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

      nc -n 192.168.2.114 8888
      
      hello from client 1
      
      nc -n 192.168.2.114 8888
      
      hello from client 2
      
      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

More from this blog

双向链表及如何使用GLib的GList实现双向链表

双向链表是一种比单向链表更为灵活的数据结构,与单向链表相比可以有更多的应用场景,本文讨论双向链表的基本概念及实现方法,并着重介绍使用GLib的GList实现单向链表的方法及步骤,本文给出了多个实际范例源代码,旨在帮助学习基于GLib编程的读者较快地掌握GList的使用方法,本文程序在 ubuntu 20.04 下编译测试完成,gcc 版本号 9.4.0;本文适合初学者阅读。 1 双向链表及其实现 在文章《单向链表以及如何使用GLib中的GSList实现单向链表》中,介绍了单向链表以及基于 G...

Oct 29, 20245 min read24
双向链表及如何使用GLib的GList实现双向链表

C程序员应该知道的最好的8个c编程框架

C 编程框架是开发人员必不可少的工具,编程框架可以为构建强大且性能优异的应用程序提供结构化的基础,本文将对 8 个最佳 C 编程框架和库做出简要的介绍,如果您正在寻找适合初学者的 C 编程框架或旨在进行 C 编程框架比较,相信本文可以给您一定的帮助。 顶级 C 编程框架 – 概述 本文将介绍以下 8 个 C 语言编程框架: 序号框架名称主要特点易于集成下载链接 1GTK全面的小部件集,跨平台支持中等的下载 2Qt跨平台支持,集成开发环境中等的下载 3CMocka轻量级,模...

Oct 19, 20244 min read36
C程序员应该知道的最好的8个c编程框架

单向链表以及如何使用GLib中的GSList实现单向链表

单向链表是一种基础的数据结构,也是一种简单而灵活的数据结构,本文讨论单向链表的基本概念及实现方法,并着重介绍使用GLib的GSList实现单向链表的方法及步骤,本文给出了多个实际范例源代码,旨在帮助学习基于GLib编程的读者较快地掌握GSList的使用方法,本文程序在 ubuntu 20.04 下编译测试完成,gcc 版本号 9.4.0;本文适合初学者阅读。 1 单向链表及其实现 在文章《使用GLib进行C语言编程的实例》中,简单介绍了 GLib,建议阅读本文前先阅读这篇文章; 单向链表是一...

Aug 19, 20246 min read23
单向链表以及如何使用GLib中的GSList实现单向链表

使用GLib进行C语言编程的实例

本文将讨论使用GLib进行编程的基本步骤,GLib是一个跨平台的,用C语言编写的3个底层库(以前是5个)的集合,GLib提供了多种高级的数据结构,如内存块、双向和单向链表、哈希表等,GLib还实现了线程相关的函数、多线程编程以及相关的工具,例如原始变量访问、互斥锁、异步队列等,GLib主要由GNOME开发;本文是使用GLib编程的入门文章,旨在通过实例帮助希望学习GLib编程的读者较快地入门,本文将给出多个使用GLib库编程范例的源代码,本文程序在 ubuntu 20.04 下编译测试完成,gc...

Aug 9, 20245 min read9
使用GLib进行C语言编程的实例

Linux下使用libiw进行无线信号扫描的实例

打开电脑连接wifi是一件很平常的事情,但这些事情通常都是操作系统下的wifi管理程序替我们完成的,如何在程序中扫描wifi信号其实资料并不多,前面已经有两篇文章介绍了如何使用ioctl()扫描wifi信号,但其实在Linux下有一个简单的库对这些ioctl()的操作进行了封装,这个库就是libiw,使用libiw可以简化编程,本文介绍了如果使用libiw对wifi信号进行扫描的基本方法,本文将给出完整的源代码,本文程序在 ubuntu 20.04 下编译测试完成,gcc 版本号 9.4.0;尽...

Jul 4, 20244 min read21
Linux下使用libiw进行无线信号扫描的实例

whowin - 开源和分享是技术发展的源泉和动力

42 posts

一个从业30多年的退休程序员,主要从事嵌入式软件开发。