使用select实现的UDP/TCP组合服务器
文章目录
独立的 TCP 服务器和UDP服务器,可以找到很多例子,但如果一个服务希望在同一个端口上既提供 TCP 服务,也提供 UDP 服务,写两个服务端显然不是一个好的办法,也不利于以后的维护,本文将把UDP服务器和 TCP 服务器合并成一个服务器,该服务器既可以提供 UDP 服务也可以提供 TCP 服务,本文将给出完整的源代码,阅读本文需要掌握基本的 socket 编程方法,本文对初学者难度不大。
1. 基本流程
-
本示例一共有三个程序,
tcp/udp服务器:tu-server.c,tcp客户端:t-client.c和udp客户端: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”;
-
服务器端程序流程
- 建立一个用于侦听TCP连接请求的TCP socket
1int tcp_fd = socket(AF_INET, SOCK_STREAM, 0);- 建立一个用于接收UDP数据的UDP socket
1int udp_fd = socket(AF_INET, SOCK_DGRAM, 0);- 将这两个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));- 在TCP socket上侦听
1listen(tcp_fd, 5);- 将TCP socket和UDP socket均加入到一个空的描述符集中
1 2 3 4 5fd_set rset; FD_ZERO(&rset); FD_SET(tcp_fd, &rset); FD_SET(udp_fd, &rset);- 调用select()直至其中一个socket有可读数据
1 2int max_fd = (tcp_fd > udp_fd) ? tcp_fd : udp_fd + 1; select(max_fd, &rset, NULL, NULL, NULL);- 处理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); }- 处理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 客户端程序流程
- 建立一个TCP socket
1sockfd = socket(AF_INET, SOCK_STREAM, 0);- 向服务器发出连接请求,等待服务器接受
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 2char *message = "Hello Server"; write(sockfd, message, strlen(message));- 接收到服务器回应
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);- 关闭socket,退出
1close(sockfd); -
udp客户端程序流程
- 建立一个UDP socket
1int sockfd = socket(AF_INET, SOCK_DGRAM, 0);- 向服务器发送信息,并等待服务器回应
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 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);- 关闭socket,退出
1close(sockfd);
2. 主要函数、宏和数据结构
-
select()函数
select()函数用于监视文件描述符的变化情况——可读、可写或是异常。- 函数定义
1int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); - 参数说明
- nfds:最大的文件描述符加1
- readfds:等待可读事件的文件描述符集合,如果不关心读事件,可设置为NULL;
- writefds:等待可写事件(缓冲区中是否有空间)的文件描述符集合,如果不关心写事件,可设置为NULL;
- exceptfds:当相应的文件描述符发生异常时,失败的文件描述符将被放进exceptfds中,如果不关心异常事件,可设置为NULL;
- timeout:等待select返回的事件;如果timeout=NULL,则一直等待,直至select返回;如果timeout=固定值,则等待固定时间后返回;如果timeout=0,则立即返回;
-
struct timeval结构
- 该结构用于指定 select 函数的超时时间
- 定义
1 2 3 4struct 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-63,fd_set[1]可以表示文件描述符fd=64-127; - fd_set是一个文件描述符的集合,当fd_set中的某一位为1,表示这个集合中包含有这个fd
- 文件描述符集,该结构定义在头文件
-
宏FD_ZERO
- 该宏定义在头文件
sys/select.h中 - 该宏可以将一个 fd_set 全部清空,下面的例子将 fds 清空
1 2fd_set fds; FD_ZERO(fds);
- 该宏定义在头文件
-
宏FD_SET
- 该宏定义在头文件
sys/select.h中 - 将指定的文件描述符 fd 加入到某一个文件描述符集 fd_set 中,下面的例子将文件描述符 fd 加入到文件描述符集 fds 中
1 2fd_set fds; FD_SET(FD, fds);
- 该宏定义在头文件
-
宏FD_ISSET
- 该宏定义在头文件
sys/select.h中 - 检查一个文件描述符集 fds 中是否有文件描述符 fd,下面例子中检查文件描述符集 fds 中是否存在文件描述符 fd;
1 2 3 4 5 6 7 8 9fd_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(点击文件名下载源程序)
-
服务器端程序的编译
1gcc -Wall tu-server.c -o tu-server -
服务器端程序的测试
-
在一台机器上启动服务器端程序
1./tu-server -
假定服务器 IP 为
192.168.2.112,在另一台机器上启动 nc 模拟客户端,测试 TCP1 2nc -n 192.168.2.112 5000 hello server -
退出 TCP 测试,重新启动 nc,测试 UDP
1nc -n -u 192.168.2.112 5000 -
有关 nc 命令的使用方法,可以参考另一篇文章《如何在Linux命令行下发送和接收UDP数据包》
-
在服务器端的运行截屏

-
TCP 测试客户端的截屏

-
UDP 测试客户端的截屏

-
-
TCP 客户端程序:t-client.c(点击文件名下载源程序)
-
UDP 客户端程序:u-client.c(点击文件名下载源程序)
-
客户端程序编译
1 2gcc -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 -
服务器端的运行截图

-
客户端的运行截图

-
4. 后记
- 服务器端对 TCP 连接的处理是在是太简陋了,因为 TCP 连接建立后,产生一个新的 socket,本例中为 conn_fd,通常的做法应该是把 conn_fd 也加入到 rset 中,这样就可以处理多个 TCP 连接了,同时在处理 TCP 连接时也不会让程序阻塞;
- 服务器端对 TCP 连接的处理,也可以使用多线程的方式,即 accept 一个连接请求后,生成新的 conn_fd,建立一个线程,专门处理这个 connection,也不失为一个办法,但相对要复杂一些。
欢迎订阅 『网络编程专栏』
欢迎访问我的博客:https://whowin.cn
email: hengch@163.com

文章作者 whowin
上次更新 2023-01-07