📜  TCP 和 UDP 服务器使用 select(1)

📅  最后修改于: 2023-12-03 15:05:30.378000             🧑  作者: Mango

TCP 和 UDP 服务器使用 select

简介

在网络编程中,常见的传输层协议有TCP和UDP。TCP是一种面向连接的协议,提供可靠的数据传输,而UDP则是无连接的协议,不保证数据传输的可靠性。在编写TCP和UDP服务器时,可以使用select函数来实现多路复用,提高服务器的效率。

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);

参数说明:

  • nfds:待监听的文件描述符的数量,应该是待监听的文件描述符中最大的文件描述符编号加1。
  • readfds、writefds、exceptfds:分别表示可读、可写、异常的文件描述符集合,可以传入NULL表示不关心该集合。
  • timeout:表示select函数等待的时间,可以传入NULL表示永远等待,也可以传入一个时间结构体,表示等待的最大时间。

返回值:

  • 成功:返回文件描述符状态变化的数量,也就是就绪文件描述符的数量。同时,传入的文件描述符集合将被修改,只保留就绪的文件描述符。
  • 失败:返回-1,并设置errno变量为错误码。
TCP服务器

以下是使用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函数来实现对客户端的连接与消息的处理。

UDP服务器

以下是使用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是无连接的协议,因此在接收和发送消息时,需要同时传入客户端的地址和端口号。