Skip to main content

Command Palette

Search for a command to run...

使用select实现的UDP/TCP组合服务器

Updated
4 min read
使用select实现的UDP/TCP组合服务器

独立的 TCP 服务器和UDP服务器,可以找到很多例子,但如果一个服务希望在同一个端口上既提供 TCP 服务,也提供 UDP 服务,写两个服务端显然不是一个好的办法,也不利于以后的维护,本文将把UDP服务器和 TCP 服务器合并成一个服务器,该服务器既可以提供 UDP 服务也可以提供 TCP 服务,本文将给出完整的源代码,阅读本文需要掌握基本的 socket 编程方法,本文对初学者难度不大。

1. 基本流程

  • 本示例一共有三个程序,tcp/udp 服务器:tu-server.ctcp 客户端:t-client.cudp 客户端: u-client.c
  • 服务器端程序的基本思路是:在程序中为 tcp 服务和 udp 服务各建立一个 socket,将这两个 socket 放入 readfds 中,并将参数传递给 select(),当 readfds 中(也就是 tcp 或者 udp socket)的某一个有数据发过来(udp)或者有客户端连接请求时,select() 将返回,程序判断是哪个 socket 需要处理然后根据需要进入 TCP 处理程序或者 UDP 处理程序处理 socket 事件;
  • 本例中,服务器端做了简单化处理,收到客户端信息后,并不作处理,对 TCP 客户端,回应 "Hello TCP Client",对UDP客户端,则回应 "Hello UDP Client";
  • 服务器端程序流程

    1. 建立一个用于侦听TCP连接请求的TCP socket
      int tcp_fd = socket(AF_INET, SOCK_STREAM, 0);
      
    2. 建立一个用于接收UDP数据的UDP socket
      int udp_fd = socket(AF_INET, SOCK_DGRAM, 0);
      
    3. 将这两个socket均绑定到服务器的地址上

      #define PORT            5000
      
      struct sockaddr_in server_addr;
      
      bzero(&server_addr, sizeof(server_addr));
      server_addr.sin_family = AF_INET;
      server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
      server_addr.sin_port = htons(PORT);
      
      bind(tcp_fd, (struct sockaddr *)&server_addr, sizeof(server_addr));
      bind(udp_fd, (struct sockaddr*)&server_addr, sizeof(server_addr));
      
    4. 在TCP socket上侦听
      listen(tcp_fd, 5);
      
    5. 将TCP socket和UDP socket均加入到一个空的描述符集中

      fd_set rset;
      
      FD_ZERO(&rset);
      FD_SET(tcp_fd, &rset);
      FD_SET(udp_fd, &rset);
      
    6. 调用select()直至其中一个socket有可读数据

      int max_fd = (tcp_fd > udp_fd) ? tcp_fd : udp_fd + 1;
      select(max_fd, &rset, NULL, NULL, NULL);
      
    7. 处理TCP客户端发出的请求

      如果是 TCP 客户端发出请求,则接受客户端的连接请求,接收客户端发来的信息,然后回应 "Hello TCP Client",然后退出,回到步骤 5;

      #define BUF_SIZE        1024
      
      struct sockaddr_in client_addr;
      char buffer[BUF_SIZE];
      socklen_t len;
      ssize_t n;
      char *tcp_msg = "Hello TCP Client";
      
      socklen_t len = sizeof(client_addr);
      int conn_fd = accept(tcp_fd, (struct sockaddr*)&client_addr, &len);
      if (conn_fd > 0) {
         bzero(buffer, sizeof(buffer));
         n = 0;
         n = read(conn_fd, buffer, sizeof(buffer));
         if (n > 0) {
             buffer[n] = '\0';
             write(conn_fd, tcp_msg, strlen(tcp_msg));
         }
         close(conn_fd);
      }
      
    8. 处理UDP客户端发来的消息

      如果是 UDP 客户端发来消息,则接收客户端发来的信息,然后回应 "Hello UDP Client",回到步骤5

      #define BUF_SIZE        1024
      
      struct sockaddr_in client_addr;
      char buffer[BUF_SIZE];
      socklen_t len;
      ssize_t n;
      char *udp_msg = "Hello UDP Client";
      
      socklen_t len = sizeof(client_addr);
      bzero(buffer, sizeof(buffer));
      n = 0;
      n = recvfrom(udp_fd, buffer, sizeof(buffer), 0, (struct sockaddr *)&client_addr, &len);
      if (n > 0) {
         buffer[n] = '\0';
         sendto(udp_fd, udp_msg, strlen(udp_msg), 0, (struct sockaddr *)&client_addr, sizeof(client_addr));
      }
      
  • tcp 客户端程序流程

    1. 建立一个TCP socket
      sockfd = socket(AF_INET, SOCK_STREAM, 0);
      
    2. 向服务器发出连接请求,等待服务器接受

      #define SERVER_IP   "192.168.2.112"
      #define PORT        5000
      
      struct sockaddr_in server_addr;
      memset(&server_addr, 0, sizeof(server_addr));
      
      server_addr.sin_family      = AF_INET;
      server_addr.sin_port        = htons(PORT);
      server_addr.sin_addr.s_addr = inet_addr(SERVER_IP);
      
      connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
      
    3. 向服务器发送信息,并等待服务器的回应

      char *message = "Hello Server";
      write(sockfd, message, strlen(message));
      
    4. 接收到服务器回应

      #define BUF_SIZE    1024
      
      char buffer[BUF_SIZE];
      int n = 0
      memset(buffer, 0, sizeof(buffer));
      n = read(sockfd, buffer, sizeof(buffer));
      buffer[n] = '\0';
      printf("Message from server: %s\n", buffer);
      
    5. 关闭socket,退出
      close(sockfd);
      
  • udp客户端程序流程

    1. 建立一个UDP socket
      int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
      
    2. 向服务器发送信息,并等待服务器回应

      #define SERVER_IP   "192.168.2.112"
      #define PORT        5000
      
      char *message = "Hello Server";
      struct sockaddr_in server_addr;
      
      memset(&server_addr, 0, sizeof(server_addr));
      
      // Filling server information
      server_addr.sin_family      = AF_INET;
      server_addr.sin_port        = htons(PORT);
      server_addr.sin_addr.s_addr = inet_addr(SERVER_IP);
      // send hello message to server
      sendto(sockfd, message, strlen(message), 0, (const struct sockaddr *)&server_addr, sizeof(server_addr));
      
    3. 收到服务器回应

      #define BUF_SIZE    1024
      
      char buffer[BUF_SIZE];
      int len = sizeof(struct sockaddr_in);
      int n = 0;
      memset(buffer, 0, BUF_SIZE);
      n = recvfrom(sockfd, buffer, BUF_SIZE, 0, (struct sockaddr *)&server_addr, (socklen_t *)&len);
      buffer[n] = '\0';
      printf("Message from server: %s\n", buffer);
      
    4. 关闭socket,退出
      close(sockfd);
      

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

  • select()函数

    • select() 函数用于监视文件描述符的变化情况——可读、可写或是异常。
    • 函数定义
      int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
      
    • 参数说明
      1. nfds:最大的文件描述符加1
      2. readfds:等待可读事件的文件描述符集合,如果不关心读事件,可设置为NULL;
      3. writefds:等待可写事件(缓冲区中是否有空间)的文件描述符集合,如果不关心写事件,可设置为NULL;
      4. exceptfds:当相应的文件描述符发生异常时,失败的文件描述符将被放进exceptfds中,如果不关心异常事件,可设置为NULL;
      5. timeout:等待select返回的事件;如果timeout=NULL,则一直等待,直至select返回;如果timeout=固定值,则等待固定时间后返回;如果timeout=0,则立即返回;
  • struct timeval结构

    • 该结构用于指定 select 函数的超时时间
    • 定义
      struct timeval {
          long    tv_sec;         /* seconds */
          long    tv_usec;        /* microseconds */
      };
      
    • 如果希望 select() 等待5秒后返回,则要设置 struct timeval timeout={5, 0};
  • fd_set

    • 文件描述符集,该结构定义在头文件 sys/select.h
    • 本质上,fd_set 是一个 long int 的数组,其中的每一位表示一个文件描述符,在 x86-64 中,long int 长度为 8 个字节,64 位,所以 fd_set[0] 可以表示文件描述符 fd=0-63fd_set[1] 可以表示文件描述符 fd=64-127
    • fd_set是一个文件描述符的集合,当fd_set中的某一位为1,表示这个集合中包含有这个fd
  • 宏FD_ZERO

    • 该宏定义在头文件 sys/select.h
    • 该宏可以将一个 fd_set 全部清空,下面的例子将 fds 清空
      fd_set fds;
      FD_ZERO(fds);
      
  • 宏FD_SET

    • 该宏定义在头文件 sys/select.h
    • 将指定的文件描述符 fd 加入到某一个文件描述符集 fd_set 中,下面的例子将文件描述符 fd 加入到文件描述符集 fds 中
      fd_set fds;
      FD_SET(FD, fds);
      
  • 宏FD_ISSET

    • 该宏定义在头文件 sys/select.h
    • 检查一个文件描述符集 fds 中是否有文件描述符 fd,下面例子中检查文件描述符集 fds 中是否存在文件描述符 fd;
      fd_set fds
      ......
      if (FD_ISSET(fd, &fds)) {
          // fd is part of the set fds.
          some codes
      } else {
          // fd is not in the fds
          some codes
      }
      
  • 其它函数和数据结构的介绍,请参考另两篇文章《使用C语言实现服务器/客户端的UDP通信》《使用C语言实现服务器/客户端的TCP通信》

3. 实例

  • 本示例一共有三个程序,tcp/udp 服务器:tu-server.c,tcp 客户端:t-client.c 和 udp 客户端:u-client.c
  • 本示例演示了如何使用 select 机制在一个服务器程序里既提供 TCP 服务,又提供 UDP 服务;有些服务(比如聊天),可以既允许 UDP 接入,也允许 TCP 接入的,这种情况下,这样一种机制就显得比较实用;
  • 服务器端程序:tu-server.c(点击文件名下载源程序)

  • 服务器端程序的编译

    gcc -Wall tu-server.c -o tu-server
    
  • 服务器端程序的测试

    • 在一台机器上启动服务器端程序
      ./tu-server
      
    • 假定服务器 IP 为 192.168.2.112,在另一台机器上启动 nc 模拟客户端,测试 TCP
      nc -n 192.168.2.112 5000
      hello server
      
    • 退出 TCP 测试,重新启动 nc,测试 UDP
      nc -n -u 192.168.2.112 5000
      
    • 有关 nc 命令的使用方法,可以参考另一篇文章《如何在Linux命令行下发送和接收UDP数据包》
    • 在服务器端的运行截屏

      screenshot of tcp/udp server test


    • TCP 测试客户端的截屏

      screenshot of tcp client test


    • UDP 测试客户端的截屏

      screenshot of udp client test


  • TCP 客户端程序:t-client.c(点击文件名下载源程序)

  • UDP 客户端程序:u-client.c(点击文件名下载源程序)

  • 客户端程序编译

    gcc -Wall t-client.c -o t-client
    gcc -Wall u-client.c -o u-client
    
  • 程序运行

    • 在服务器上(192.168.2.112)上运行服务器端程序
      ./tu-server
      
    • 在另一台机器上运行客户端程序
      ./t-client
      ./u-client
      
    • 服务器端的运行截图

      Screenshot of server


    • 客户端的运行截图

      Screenshot of client

4. 后记

  • 服务器端对 TCP 连接的处理是在是太简陋了,因为 TCP 连接建立后,产生一个新的 socket,本例中为 conn_fd,通常的做法应该是把 conn_fd 也加入到 rset 中,这样就可以处理多个 TCP 连接了,同时在处理 TCP 连接时也不会让程序阻塞;
  • 服务器端对 TCP 连接的处理,也可以使用多线程的方式,即 accept 一个连接请求后,生成新的 conn_fd,建立一个线程,专门处理这个 connection,也不失为一个办法,但相对要复杂一些。

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


欢迎访问我的博客: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多年的退休程序员,主要从事嵌入式软件开发。