使用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
1
int tcp_fd = socket(AF_INET, SOCK_STREAM, 0);
- 建立一个用于接收UDP数据的UDP socket
1
int 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上侦听
1
listen(tcp_fd, 5);
- 将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);
- 调用select()直至其中一个socket有可读数据
1 2
int 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
1
sockfd = 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 2
char *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,退出
1
close(sockfd);
-
udp客户端程序流程
- 建立一个UDP socket
1
int 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,退出
1
close(sockfd);
2. 主要函数、宏和数据结构
-
select()函数
select()
函数用于监视文件描述符的变化情况——可读、可写或是异常。- 函数定义
1
int 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 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-63
,fd_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 模拟客户端,测试 TCP1 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数据包》
-
在服务器端的运行截屏
-
TCP 测试客户端的截屏
-
UDP 测试客户端的截屏
-
-
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
-
服务器端的运行截图
-
客户端的运行截图
-
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