socket套接字记录

发布时间 2023-05-09 00:26:15作者: LX2020

socket在日常工作中用的比较多,但是之前接触的比较少,需要总结一下,这里做一下记录:

套接字是用来数据交互的,也是一种IO操作,Unix/linux系统中,为了统一各种硬件的操作,简化接口,不同的硬件设备看成一个文件,对这些文件的操作,等同对磁盘上普通文件的操作。为了表示和区分已经打开的文件,给每个文件分配一个id,这个id是一个整数,称为文件描述符,程序在执行任何形式的io操作的时候,都是在读取或者写入一个文件描述符,一个文件描述符只是一个和打开的文件相关联的整数,他的背后可能是一个硬盘上的普通文件,终端或者是网络连接。

1、套接字的种类

套接字有很多种,例如unix套接字,internet套接字,目前用到的就这两种,主要记录这两种。

2、internet套接字

根据数据的传输方式,可以将internet套接字分成两种类型,在通过socket()函数创建创建连接的时候,必须告诉它使用哪种数据传输方式。

  • 流格式套接字(SOCK_STREAM)也叫面向连接的套接字,是一种可靠的。双向的通信数据流,数据可以准确的到达另一台计算机,损坏或者丢失,可以重新发送。(里面有自己的内部纠错机制),使用的是TCP协议,TCP协议会控制你的数据按照顺序到达并且没有错误。
  • 数据报格式套接字(SOCK_DGRAM)也叫无连接的套接字,计算机只管传输数据,如果数据在传输中损坏,或者没有到达另一台计算机,是无法补救的,数据错了无法重传!

socket() 函数用来创建套接字,确定套接字的各种属性,然后服务器端要用 bind() 函数将套接字与特定的 IP 地址和端口绑定起来,只有这样,流经该 IP 地址和端口的数据才能交给套接字处理。类似地,客户端也要用 connect() 函数建立连接。

函数用法示例如下:

int serv_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

之后就是设置参数了,发送方和接收方要设置不同的参数:

下面是接收方设置
image
下面是发送方设置
image

可以看到这里都是用sockaddr_in结构体,之后在强制转为sock_addr的类型,因为sock_addr是一种统一的标准,而sock_addr_in是专门针对ipv4的,相关的结构体说明如下所示:

struct sockaddr_in{
    sa_family_t     sin_family;   //地址族(Address Family),也就是地址类型
    uint16_t        sin_port;     //16位的端口号
    struct in_addr  sin_addr;     //32位IP地址
    char            sin_zero[8];  //不使用,一般用0填充
};
  • 对于服务器端程序,使用 bind() 绑定套接字后,还需要使用 listen() 函数让套接字进入被动监听状态,再调用 accept() 函数,就可以随时响应客户端的请求了。

  • 通过listen()函数使套接字进入被动监听状态(没有客户端连接时,套接字处于睡眠状态,只有当接收到客户端请求的时候,才会被唤醒起来响应请求)

  • 当套接字处于监听状态时,可以通过 accept() 函数来接收客户端请求。它的参数与 listen()connect() 是相同的,sock 为服务器端套接字,addr 为 sockaddr_in 结构体变量,addrlen 为参数 addr 的长度,可由 sizeof() 求得。

接收消息示例如下所示

image

accept() 返回一个新的套接字来和客户端通信,addr 保存了客户端的IP地址和端口号,而 sock 是服务器端的套接字,后面和客户端通信时,要使用这个新生成的套接字,而不是原来服务器端的套接字。
listen() 只是让套接字进入监听状态,并没有真正接收客户端请求,listen() 后面的代码会继续执行,直到遇到 accept()accept() 会阻塞程序执行(后面代码不能被执行),直到有新的请求到来。

3、主机字节序和网络字节序

前面给结构体设置参数的时候涉及到了这个部分内容,因此这里做一下补充:

主机字节序和网络字节序是两种不同的字节序。主机字节序是指在计算机内部处理数据时采用的字节序,而网络字节序是指在计算机网络中传输数据时采用的字节序。

在网络传输数据时,不同的计算机可能采用不同的主机字节序,因此需要一种公共的字节序来保证数据的正确传输。因此,网络字节序通常采用大端字节序(也称为“网络序”),即高位字节在前,低位字节在后。

比如上面用到的htons()htons() 用来将当前主机字节序转换为网络字节序,其中h代表主机(host)字节序,n代表网络(network)字节序,s代表short,htons 是 h、to、n、s 的组合,可以理解为”将 short 型数据从当前主机字节序转换为网络字节序。

常见的网络字节转换函数有:

  • htons():host to network short,将 short 类型数据从主机字节序转换为网络字节序。
  • ntohs():network to host short,将 short 类型数据从网络字节序转换为主机字节序。
  • htonl():host to network long,将 long 类型数据从主机字节序转换为网络字节序。
  • ntohl():network to host long,将 long 类型数据从网络字节序转换为主机字节序。

是基于internet套接字的完整示例代码

server.c

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>

int main(){
    int serv_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

    struct sockaddr_in serv_addr;
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    serv_addr.sin_port = htons(1234);
    bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));

    listen(serv_sock, 20);

    struct sockaddr_in clnt_addr;
    socklen_t clnt_addr_size = sizeof(clnt_addr);
    int clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_addr_size);

    char str[] = "send message";
    write(clnt_sock, str, sizeof(str));

    close(clnt_sock);
    close(serv_sock);

    return 0;
}

client.c

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>

int main(){
    int sock = socket(AF_INET, SOCK_STREAM, 0);

    struct sockaddr_in serv_addr;
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    serv_addr.sin_port = htons(1234);
    connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));

    char buffer[40];
    read(sock, buffer, sizeof(buffer) - 1);
    printf("message form server %s\n", buffer);

    close(sock);
    return 0;
}

可以改进一点实现循环收发:
server.c


#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>


int main(){
    int serv_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

    struct sockaddr_in serv_addr;
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    serv_addr.sin_port = htons(1234);
    bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));

    listen(serv_sock, 20);

    struct sockaddr_in clnt_addr;
    socklen_t clnt_addr_size = sizeof(clnt_addr);
    char buffer[100] = {0};
    while (1)
    {
        int clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_addr, &clnt_addr_size);
        int strlen = recv(clnt_sock, buffer, 100, 0);
        send(clnt_sock, buffer, strlen, 0);

        close(clnt_sock);
        memset(buffer, 0 ,100);
    }
    close(serv_sock);

    return 0;    
}

client.c

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>


int main(){
    struct sockaddr_in serv_addr;
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    serv_addr.sin_port = htons(1234);

    char bufsend[100] = {0};
    char bufrecv[100] = {0};

    while (1)
    {
        int sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
        connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr));

        printf("input send message:");
        scanf("%s", bufsend);
        send(sock, bufsend, strlen(bufsend), 0);
        recv(sock, bufrecv, 100, 0);
        printf("message form server %s\n", bufrecv);

        memset(bufsend, 0, 100);
        memset(bufrecv, 0, 100);
        close(sock);
    }

    return 0;
}

4、unix套接字

一般流程为: ocket函数创建了一个Unix socket,并使用bind函数将其绑定到指定的文件路径上。然后通过listen函数监听客户端连接请求,并使用accept函数接受客户端连接。最后,通过recv函数从客户端接收数据,并在控制台输出。整个过程完成后,需要使用close函数关闭套接字,释放资源。

注意,Unix socket只能用于本地进程间通信,不能用于网络通信。此外,在使用Unix socket进行进程间通信时,需要确保各进程所使用的文件路径相同,否则无法建立连接。

下面是完整示例:

client.c

#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#define SOCKET_PATH "/tmp/socket"

int main(void)
{
    int sockfd;
    struct sockaddr_un servaddr;

    sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
    if (sockfd == -1) {
        perror("socket error");
        exit(EXIT_FAILURE);
    }

    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sun_family = AF_UNIX;
    strncpy(servaddr.sun_path, SOCKET_PATH, sizeof(servaddr.sun_path)-1);

    if (connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1) {
        perror("connect error");
        exit(EXIT_FAILURE);
    }

    // 发送数据
    char buf[] = "Hello, Unix Socket!";
    ssize_t n = send(sockfd, buf, sizeof(buf), 0);
    if (n == -1) {
        perror("send error");
        exit(EXIT_FAILURE);
    }

    close(sockfd);

    return 0;
}

server.c


#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#define SOCKET_PATH "/tmp/socket"

int main(void)
{
    int sockfd;
    struct sockaddr_un servaddr;
    char buf[1024];

    sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
    if (sockfd == -1) {
        perror("socket error");
        exit(EXIT_FAILURE);
    }

    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sun_family = AF_UNIX;
    strncpy(servaddr.sun_path, SOCKET_PATH, sizeof(servaddr.sun_path)-1);

    unlink(SOCKET_PATH);

    if (bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1) {
        perror("bind error");
        exit(EXIT_FAILURE);
    }

    if (listen(sockfd, 5) == -1) {
        perror("listen error");
        exit(EXIT_FAILURE);
    }

    for (;;) {
        int connfd = accept(sockfd, NULL, NULL);
        if (connfd == -1) {
            perror("accept error");
            continue;
        }
        // 处理连接请求
        int nbytes = recv(connfd, buf, sizeof(buf), 0);
        if (nbytes == -1)
        {
            perror("recv");
            exit(EXIT_FAILURE);
        }
        printf("Received message: %s\n", buf);
        close(connfd);
    }

    close(sockfd);
    unlink(SOCKET_PATH);

    return 0;
}

5、netlink套接字

netlink常用于用户态和内核态之间进行通信,下面是一个netlink套接字的示例

内核态:

#include <linux/init.h>
#include <linux/module.h>
#include <linux/types.h>
#include <net/sock.h>
#include <linux/netlink.h>
 
#define NETLINK_TEST     30
#define MSG_LEN            125
#define USER_PORT        100
 
MODULE_LICENSE("GPL");
MODULE_AUTHOR("zhangwj");
MODULE_DESCRIPTION("netlink example");
 
struct sock *nlsk = NULL;
extern struct net init_net;
 
int send_usrmsg(char *pbuf, uint16_t len)
{
    struct sk_buff *nl_skb;
    struct nlmsghdr *nlh;
 
    int ret;
 
    /* 创建sk_buff 空间 */
    nl_skb = nlmsg_new(len, GFP_ATOMIC);
    if(!nl_skb)
    {
        printk("netlink alloc failure\n");
        return -1;
    }
 
    /* 设置netlink消息头部 */
    nlh = nlmsg_put(nl_skb, 0, 0, NETLINK_TEST, len, 0);
    if(nlh == NULL)
    {
        printk("nlmsg_put failaure \n");
        nlmsg_free(nl_skb);
        return -1;
    }
 
    /* 拷贝数据发送 */
    memcpy(nlmsg_data(nlh), pbuf, len);
    ret = netlink_unicast(nlsk, nl_skb, USER_PORT, MSG_DONTWAIT);
 
    return ret;
}
 
static void netlink_rcv_msg(struct sk_buff *skb)
{
    struct nlmsghdr *nlh = NULL;
    char *umsg = NULL;
    char *kmsg = "hello users!!!";
 
    if(skb->len >= nlmsg_total_size(0))
    {
        nlh = nlmsg_hdr(skb);
        umsg = NLMSG_DATA(nlh);
        if(umsg)
        {
            printk("kernel recv from user: %s\n", umsg);
            send_usrmsg(kmsg, strlen(kmsg));
        }
    }
}
 
struct netlink_kernel_cfg cfg = { 
        .input  = netlink_rcv_msg, /* set recv callback */
};  
 
int test_netlink_init(void)
{
    /* create netlink socket */
    nlsk = (struct sock *)netlink_kernel_create(&init_net, NETLINK_TEST, &cfg);
    if(nlsk == NULL)
    {   
        printk("netlink_kernel_create error !\n");
        return -1; 
    }   
    printk("test_netlink_init\n");
    
    return 0;
}
 
void test_netlink_exit(void)
{
    if (nlsk){
        netlink_kernel_release(nlsk); /* release ..*/
        nlsk = NULL;
    }   
    printk("test_netlink_exit!\n");
}
 
module_init(test_netlink_init);
module_exit(test_netlink_exit);

用户态

#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <string.h>
#include <linux/netlink.h>
#include <stdint.h>
#include <unistd.h>
#include <errno.h>
 
#define NETLINK_TEST    30
#define MSG_LEN            125
#define MAX_PLOAD        125
 
typedef struct _user_msg_info
{
    struct nlmsghdr hdr;
    char  msg[MSG_LEN];
} user_msg_info;
 
int main(int argc, char **argv)
{
    int skfd;
    int ret;
    user_msg_info u_info;
    socklen_t len;
    struct nlmsghdr *nlh = NULL;
    struct sockaddr_nl saddr, daddr;
    char *umsg = "hello netlink!!";
 
    /* 创建NETLINK socket */
    skfd = socket(AF_NETLINK, SOCK_RAW, NETLINK_TEST);
    if(skfd == -1)
    {
        perror("create socket error\n");
        return -1;
    }
 
    memset(&saddr, 0, sizeof(saddr));
    saddr.nl_family = AF_NETLINK; //AF_NETLINK
    saddr.nl_pid = 100;  //端口号(port ID) 
    saddr.nl_groups = 0;
    if(bind(skfd, (struct sockaddr *)&saddr, sizeof(saddr)) != 0)
    {
        perror("bind() error\n");
        close(skfd);
        return -1;
    }
 
    memset(&daddr, 0, sizeof(daddr));
    daddr.nl_family = AF_NETLINK;
    daddr.nl_pid = 0; // to kernel 
    daddr.nl_groups = 0;
 
    nlh = (struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_PLOAD));
    memset(nlh, 0, sizeof(struct nlmsghdr));
    nlh->nlmsg_len = NLMSG_SPACE(MAX_PLOAD);
    nlh->nlmsg_flags = 0;
    nlh->nlmsg_type = 0;
    nlh->nlmsg_seq = 0;
    nlh->nlmsg_pid = saddr.nl_pid; //self port
 
    memcpy(NLMSG_DATA(nlh), umsg, strlen(umsg));
    ret = sendto(skfd, nlh, nlh->nlmsg_len, 0, (struct sockaddr *)&daddr, sizeof(struct sockaddr_nl));
    if(!ret)
    {
        perror("sendto error\n");
        close(skfd);
        exit(-1);
    }
    printf("send kernel:%s\n", umsg);
 
    memset(&u_info, 0, sizeof(u_info));
    len = sizeof(struct sockaddr_nl);
    ret = recvfrom(skfd, &u_info, sizeof(user_msg_info), 0, (struct sockaddr *)&daddr, &len);
    if(!ret)
    {
        perror("recv form kernel error\n");
        close(skfd);
        exit(-1);
    }
 
    printf("from kernel:%s\n", u_info.msg);
    close(skfd);
 
    free((void *)nlh);
    return 0;
}

6、套接字监听

当有很多套接字同时工作的时候,由于接收函数是阻塞的,这样是很不方便的,因此最好是能有一种类似中断的机制,于是可以使用监听的方案来提高效率,常用的有select,poll,和epoll这几种。select、poll和epoll都是Linux系统中的I/O多路复用机制,可以同时监听多个文件描述符,当其中有可读或可写事件时进行处理。

它们之间的主要区别如下:

  • select和poll的缺点:当需要监听大量的文件描述符时,每次调用select或poll都需要将所有的文件描述符从用户空间复制到内核空间,这个操作的开销比较大。此外,在处理大量文件描述符时,每次调用select或poll都需要遍历整个文件描述符集合,导致效率低下。

  • epoll的优点:epoll使用了更高效的数据结构来存储文件描述符,因此能够支持成千上万个文件描述符的监听,而不会出现性能问题。此外,epoll提供了三种工作模式(水平触发、边缘触发和信号驱动)以及更细粒度的控制选项,使其比select和poll更加灵活和强大。

  • 工作方式:select和poll采用轮询方式来检查是否有事件发生,而epoll通过回调函数来通知应用程序有哪些事件已经准备好了。

  • 使用场景:对于少量的连接,select和poll可能是比较适合的选择;而当需要同时处理大量的连接时,epoll是更好的选择。

下面是具体的例子:

epoll.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/epoll.h>

#define MAX_CLIENTS 10

int main(int argc, char *argv[])
{
    int server_sockfd, client_sockfd[MAX_CLIENTS];
    struct sockaddr_in server_addr, client_addr;
    int client_len = sizeof(client_addr);
    int max_fd, fd_num, i, j;
    struct epoll_event ev, events[MAX_CLIENTS + 1];
    int epollfd;
    // 创建TCP套接字
    server_sockfd = socket(AF_INET, SOCK_STREAM, 0);

    // 初始化服务器地址结构体
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    server_addr.sin_port = htons(atoi(argv[1]));

    // 绑定套接字到服务器地址
    bind(server_sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));

    // 监听客户端请求
    listen(server_sockfd, MAX_CLIENTS);

    // 创建epoll文件描述符
    epollfd = epoll_create(MAX_CLIENTS + 1);
    if (epollfd == -1)
    {
        perror("epoll_create");
        exit(EXIT_FAILURE);
    }

    // 添加server_sockfd到epoll事件表
    ev.events = EPOLLIN;
    ev.data.fd = server_sockfd;
    if (epoll_ctl(epollfd, EPOLL_CTL_ADD, server_sockfd, &ev) == -1)
    {
        perror("epoll_ctl: server_sockfd");
        exit(EXIT_FAILURE);
    }

    while (1)
    {
        // 监听epoll事件表
        fd_num = epoll_wait(epollfd, events, MAX_CLIENTS + 1, -1);
        if (fd_num == -1)
        {
            perror("epoll_wait");
            exit(EXIT_FAILURE);
        }
        for (i = 0; i < fd_num; i++)
        {
            if (events[i].data.fd == server_sockfd)
            {
                // 接受客户端连接请求
                client_sockfd[i] = accept(server_sockfd, (struct sockaddr *)&client_addr, &client_len);
                if (client_sockfd[i] == -1)
                {
                    perror("accept");
                    exit(EXIT_FAILURE);
                }

                // 添加client_sockfd到epoll事件表
                ev.events = EPOLLIN;
                ev.data.fd = client_sockfd[i];
                if (epoll_ctl(epollfd, EPOLL_CTL_ADD, client_sockfd[i], &ev) == -1)
                {
                    perror("epoll_ctl: client_sockfd");
                    exit(EXIT_FAILURE);
                }
            }
            else
            {
                // 处理客户端数据
                // ...
            }
        }
    }

    // 关闭套接字
    close(server_sockfd);
    for (j = 0; j < MAX_CLIENTS; j++)
    {
        if (client_sockfd[j] > 0)
        {
            close(client_sockfd[j]);
        }
    }

    return 0;
}

poll.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <poll.h>

#define MAX_CLIENTS 10

int main(int argc, char *argv[])
{
    int server_sockfd, client_sockfd[MAX_CLIENTS];
    struct sockaddr_in server_addr, client_addr;
    int client_len = sizeof(client_addr);
    int max_fd, fd_num, i, j;
    struct pollfd fds[MAX_CLIENTS + 1];

    // 创建TCP套接字
    server_sockfd = socket(AF_INET, SOCK_STREAM, 0);

    // 初始化服务器地址结构体
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    server_addr.sin_port = htons(atoi(argv[1]));

    // 绑绑定套接字到服务器地址
    bind(server_sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
    // 监听客户端请求
    listen(server_sockfd, MAX_CLIENTS);

    // 初始化pollfd数组
    fds[0].fd = server_sockfd;
    fds[0].events = POLLIN;
    for (i = 1; i <= MAX_CLIENTS; i++)
    {
        fds[i].fd = -1;
    }

    while (1)
    {
        // 监听文件描述符
        fd_num = poll(fds, MAX_CLIENTS + 1, -1);

        if (fds[0].revents & POLLIN)
        {
            // 接受客户端连接请求
            client_sockfd[i] = accept(server_sockfd, (struct sockaddr *)&client_addr, &client_len);
            for (j = 1; j <= MAX_CLIENTS; j++)
            {
                if (fds[j].fd < 0)
                {
                    fds[j].fd = client_sockfd[i];
                    fds[j].events = POLLIN;
                    break;
                }
            }
            i++;
        }

        for (j = 1; j <= MAX_CLIENTS; j++)
        {
            if (fds[j].fd < 0)
            {
                continue;
            }
            if (fds[j].revents & POLLIN)
            {
                // 处理客户端数据
                // ...
            }
        }
    }

    // 关闭套接字
    close(server_sockfd);
    for (j = 1; j <= MAX_CLIENTS; j++)
    {
        if (fds[j].fd > 0)
        {
            close(fds[j].fd);
        }
    }

    return 0;
}

select.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/select.h>

#define MAX_CLIENTS 10

int main(int argc, char *argv[])
{
    int server_sockfd, client_sockfd[MAX_CLIENTS];
    struct sockaddr_in server_addr, client_addr;
    int client_len = sizeof(client_addr);
    int max_fd, fd_num, i, j;
    fd_set read_fds, all_fds;

    // 创建TCP套接字
    server_sockfd = socket(AF_INET, SOCK_STREAM, 0);

    // 初始化服务器地址结构体
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    server_addr.sin_port = htons(atoi(argv[1]));

    // 绑定套接字到服务器地址
    bind(server_sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));

    // 监听客户端请求
    listen(server_sockfd, MAX_CLIENTS);

    // 初始化文件描述符集合
    FD_ZERO(&all_fds);
    FD_SET(server_sockfd, &all_fds);
    max_fd = server_sockfd;

    while (1) {
        // 复制文件描述符集合
        read_fds = all_fds;

        // 监听文件描述符
        fd_num = select(max_fd + 1, &read_fds, NULL, NULL, NULL);

        if (FD_ISSET(server_sockfd, &read_fds)) {
            // 接受客户端连接请求
            client_sockfd[i] = accept(server_sockfd, (struct sockaddr *)&client_addr, &client_len);
            FD_SET(client_sockfd[i], &all_fds);
            if (client_sockfd[i] > max_fd) {
                max_fd = client_sockfd[i];
            }
            i++;
        }

        for (j = 0; j < i; j++) {
            if (FD_ISSET(client_sockfd[j], &read_fds)) {
                // 处理客户端数据
                // ...
            }
        }
    }

    // 关闭套接字
    close(server_sockfd);
    for (j = 0; j < i; j++) {
        close(client_sockfd[j]);
    }

    return 0;
}