独立的 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
    1
    
    int tcp_fd = socket(AF_INET, SOCK_STREAM, 0);
    
    1. 建立一个用于接收UDP数据的UDP socket
    1
    
    int udp_fd = socket(AF_INET, SOCK_DGRAM, 0);
    
    1. 将这两个socket均绑定到服务器的地址上
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    
    #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));
    
    1. 在TCP socket上侦听
    1
    
    listen(tcp_fd, 5);
    
    1. 将TCP socket和UDP socket均加入到一个空的描述符集中
    1
    2
    3
    4
    5
    
    fd_set rset;
    
    FD_ZERO(&rset);
    FD_SET(tcp_fd, &rset);
    FD_SET(udp_fd, &rset);
    
    1. 调用select()直至其中一个socket有可读数据
    1
    2
    
    int max_fd = (tcp_fd > udp_fd) ? tcp_fd : udp_fd + 1;
    select(max_fd, &rset, NULL, NULL, NULL);
    
    1. 处理TCP客户端发出的请求

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

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    
    #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);
    }
    
    1. 处理UDP客户端发来的消息

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

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    
    #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
    1
    
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    
    1. 向服务器发出连接请求,等待服务器接受
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    
    #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));
    
    1. 向服务器发送信息,并等待服务器的回应
    1
    2
    
    char *message = "Hello Server";
    write(sockfd, message, strlen(message));
    
    1. 接收到服务器回应
    1
    2
    3
    4
    5
    6
    7
    8
    
    #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);
    
    1. 关闭socket,退出
    1
    
    close(sockfd);
    
  • udp客户端程序流程

    1. 建立一个UDP socket
    1
    
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    
    1. 向服务器发送信息,并等待服务器回应
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    
    #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));
    
    1. 收到服务器回应
    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    #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);
    
    1. 关闭socket,退出
    1
    
    close(sockfd);
    

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

  • select()函数

    • select() 函数用于监视文件描述符的变化情况——可读、可写或是异常。
    • 函数定义
      1
      
      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 函数的超时时间
    • 定义
      1
      2
      3
      4
      
      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 清空
      1
      2
      
      fd_set fds;
      FD_ZERO(fds);
      
  • 宏FD_SET

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

    • 该宏定义在头文件 sys/select.h
    • 检查一个文件描述符集 fds 中是否有文件描述符 fd,下面例子中检查文件描述符集 fds 中是否存在文件描述符 fd;
      1
      2
      3
      4
      5
      6
      7
      8
      9
      
      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(点击文件名下载源程序)

  • 服务器端程序的编译

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

    • 在一台机器上启动服务器端程序

      1
      
      ./tu-server
      
    • 假定服务器 IP 为 192.168.2.112,在另一台机器上启动 nc 模拟客户端,测试 TCP

      1
      2
      
      nc -n 192.168.2.112 5000
      hello server
      
    • 退出 TCP 测试,重新启动 nc,测试 UDP

      1
      
      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(点击文件名下载源程序)

  • 客户端程序编译

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

    • 在服务器上(192.168.2.112)上运行服务器端程序

      1
      
      ./tu-server
      
    • 在另一台机器上运行客户端程序

      1
      2
      
      ./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