📅  最后修改于: 2023-12-03 15:05:30.378000             🧑  作者: Mango
在网络编程中,常见的传输层协议有TCP和UDP。TCP是一种面向连接的协议,提供可靠的数据传输,而UDP则是无连接的协议,不保证数据传输的可靠性。在编写TCP和UDP服务器时,可以使用select函数来实现多路复用,提高服务器的效率。
select函数是一种等待多个文件描述符状态改变的函数,可以实现I/O复用。它可以同时等待多个文件描述符,当其中一个文件描述符的状态发生变化时,就会返回。select函数具有超时和信号处理的功能。在使用TCP和UDP服务器时,我们可以将所有连接和监听的文件描述符都加入到select函数的fd_set中,然后使用select函数等待监听客户端的连接和接收客户端的请求。当其中一个文件描述符的状态发生变化时,我们就可以对其进行处理。
以下是select函数的原型:
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
参数说明:
返回值:
以下是使用select函数实现的TCP服务器。该服务器将监听一个端口,并接受客户端的请求。当客户端发送数据时,服务器将回复一个echo消息,即将客户端发送的数据原样返回。
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/select.h>
#define MAXSOCKETS 1024
#define BUF_SIZE 1024
static int sockets[MAXSOCKETS];
static int num_sockets = 0;
void handle_new_connection(int listen_socket) {
struct sockaddr_in client_addr;
int client_sock;
socklen_t addrlen = sizeof(client_addr);
char buffer[BUF_SIZE];
client_sock = accept(listen_socket, (struct sockaddr*)&client_addr, &addrlen);
if (client_sock == -1) {
perror("accept");
return;
}
printf("Accepted new connection from %s:%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
// 将新的客户端连接的文件描述符加入到sockets数组中,以便后续使用select函数监听
if (num_sockets >= MAXSOCKETS) {
printf("Too many sockets\n");
close(client_sock);
return;
}
sockets[num_sockets++] = client_sock;
// 发送欢迎消息
char *welcome_message = "Welcome to my server!\n";
if (send(client_sock, welcome_message, strlen(welcome_message), 0) == -1) {
perror("send");
}
}
void handle_client_data(int client_sock) {
char buffer[BUF_SIZE];
int nbytes = recv(client_sock, buffer, sizeof(buffer), 0);
if (nbytes == -1) {
perror("recv");
return;
}
if (nbytes == 0) {
// 客户端关闭了连接,移除文件描述符并关闭连接
close(client_sock);
for (int i = 0; i < num_sockets; i++) {
if (client_sock == sockets[i]) {
num_sockets--;
sockets[i] = sockets[num_sockets];
break;
}
}
printf("Disconnected from client\n");
return;
}
// 将客户端发送的消息原样返回
if (send(client_sock, buffer, nbytes, 0) == -1) {
perror("send");
}
}
int main(int argc, char *argv[]) {
int server_sock, client_sock, max_socket_fd, nfds, ready;
struct sockaddr_in server_addr;
fd_set readfds;
// 创建监听socket
server_sock = socket(AF_INET, SOCK_STREAM, 0);
if (server_sock == -1) {
perror("socket");
exit(1);
}
// 绑定端口号
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080);
server_addr.sin_addr.s_addr = INADDR_ANY;
if (bind(server_sock, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
perror("bind");
exit(1);
}
// 开始监听
if (listen(server_sock, SOMAXCONN) == -1) {
perror("listen");
exit(1);
}
// 将服务器的文件描述符加入到sockets数组中
sockets[num_sockets++] = server_sock;
// 使用select函数监听所有文件描述符的状态
while (1) {
// 构建文件描述符集合
FD_ZERO(&readfds);
max_socket_fd = -1;
for (int i = 0; i < num_sockets; i++) {
if (sockets[i] > max_socket_fd) {
max_socket_fd = sockets[i];
}
FD_SET(sockets[i], &readfds);
}
// 使用select函数等待文件描述符状态变化
nfds = select(max_socket_fd+1, &readfds, NULL, NULL, NULL);
if (nfds == -1) {
perror("select");
exit(1);
}
// 遍历所有文件描述符,做相应的处理
for (int i = 0; i < num_sockets; i++) {
if (FD_ISSET(sockets[i], &readfds)) {
if (sockets[i] == server_sock) {
// 处理新的客户端连接
handle_new_connection(server_sock);
} else {
// 处理已连接的客户端发送的消息
handle_client_data(sockets[i]);
}
}
}
}
return 0;
}
该服务器使用了数组来存储所有的文件描述符,使用select函数来等待文件描述符状态变化,并通过handle_new_connection和handle_client_data函数来实现对客户端的连接与消息的处理。
以下是使用select函数实现的UDP服务器。该服务器将接收客户端发送的消息,并将客户端的地址和端口号原样返回。
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/select.h>
#define BUF_SIZE 1024
int main(int argc, char *argv[]) {
int server_sock, client_sock, max_socket_fd, nfds, ready;
struct sockaddr_in server_addr, client_addr;
socklen_t addrlen = sizeof(client_addr);
char buffer[BUF_SIZE];
fd_set readfds;
// 创建socket
server_sock = socket(AF_INET, SOCK_DGRAM, 0);
if (server_sock == -1) {
perror("socket");
exit(1);
}
// 绑定端口号
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080);
server_addr.sin_addr.s_addr = INADDR_ANY;
if (bind(server_sock, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
perror("bind");
exit(1);
}
// 使用select函数监听socket的状态
while (1) {
// 构建文件描述符集合
FD_ZERO(&readfds);
FD_SET(server_sock, &readfds);
// 使用select函数等待socket状态变化
nfds = select(server_sock+1, &readfds, NULL, NULL, NULL);
if (nfds == -1) {
perror("select");
exit(1);
}
// 处理socket的状态变化
if (FD_ISSET(server_sock, &readfds)) {
int nbytes = recvfrom(server_sock, buffer, sizeof(buffer), 0, (struct sockaddr*)&client_addr, &addrlen);
if (nbytes == -1) {
perror("recvfrom");
exit(1);
}
// 将客户端发送的消息原样返回
if (sendto(server_sock, buffer, nbytes, 0, (struct sockaddr*)&client_addr, addrlen) == -1) {
perror("sendto");
}
}
}
return 0;
}
该UDP服务器只需要监听一个文件描述符,即socket,使用select函数等待该文件描述符的状态变化,并在收到客户端发送的消息后将其原样返回。由于UDP是无连接的协议,因此在接收和发送消息时,需要同时传入客户端的地址和端口号。