一、前言

上一篇我们学习了 select 监听键盘输入,理解了 IO 多路复用的基础用法。

本篇我们进入网络编程实战: 使用 select 实现一个高并发 TCP 服务器,可以同时接收多个客户端连接、接收消息、回复消息

这是 Linux 后端开发最经典、最基础的高并发模型


二、实现功能

  1. 服务器监听 127.0.0.1:6000
  2. 支持多个客户端同时连接
  3. 使用 select 同时监听所有客户端
  4. 接收客户端消息并打印
  5. 给客户端回复 ok
  6. 客户端断开自动清理资源
  7. 5 秒无事件超时打印

三、核心知识点

1. select 作用

  • 同时监听多个文件描述符(服务端 socket + 多个客户端 socket)
  • 阻塞等待,直到有事件发生(新连接 / 消息 / 超时)
  • 单线程实现高并发,不需要创建大量线程

2. 文件描述符数组

我们用一个数组 fds[MAXFD] 管理所有需要监听的描述符:

  • -1 表示空闲
  • 有效值表示需要监听的 fd

3. 四个 fd_set 操作宏

FD_ZERO(&fdset);    // 清空集合
FD_SET(fd, &fdset); // 添加 fd 到集合
FD_ISSET(fd, &fdset);// 判断 fd 是否有事件
FD_CLR(fd, &fdset); // 删除 fd(本例没用到)

4. select 返回值

  • -1:出错
  • 0:超时
  • >0:有事件发生(新连接 / 新消息)

四、完整代码

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

#define MAXFD 10

int socket_init();
void fds_init(int fds[])
{
    for (int i = 0; i < MAXFD; i++)
    {
        fds[i] = -1; // 数组中每个元素置为-1,代表空闲
    }
}

void fds_add(int fds[], int fd)
{
    for (int i = 0; i < MAXFD; i++)
    {
        if (fds[i] == -1)
        {
            fds[i] = fd;
            break;
        }
    }
}

void fds_del(int fds[], int fd)
{
    for (int i = 0; i < MAXFD; i++)
    {
        if (fds[i] == fd)
        {
            fds[i] = -1; // 空闲
            break;
        }
    }
}

void accept_client(int sockfd, int fds[])
{
    int c = accept(sockfd,NULL,NULL);
    if( c < 0 )
    {
        return;
    }

    printf("accept c=%d\n",c);

    fds_add(fds,c);//将新产生的描述符c,添加到数组中
}
void recv_data(int c, int fds[])
{
    char buff[128] = {0};
    int n = recv(c,buff,1,0);
    if( n <= 0 )
    {
        fds_del(fds,c);
        close(c);
        printf("close(%d)\n",c);
        return;
    }

    printf("recv(%d)=%s\n",c,buff);
    send(c,"ok",2,0);
}
int main()
{
    int sockfd = socket_init();
    if (sockfd == -1)
    {
        exit(1);
    }

    int fds[MAXFD];       // 收集描述符 fd_set
    fds_init(fds);        // 数组置空
    fds_add(fds, sockfd); // 将sockfd,添加到数组,在此时数组只有一个有效描述符

    fd_set fdset; // select 集合,读事件

    while (1)
    {
        FD_ZERO(&fdset); // 清空集合
        int maxfd = -1;  // 记录描述符最大值
        for (int i = 0; i < MAXFD; i++)
        {
            if( fds[i] == -1 )
            {
                continue;//跳过无效描述符 -1
            }

            FD_SET(fds[i],&fdset);//将描述符添加到集合
            if( fds[i] > maxfd)
            {
                maxfd = fds[i];
            }
        }

        struct timeval tv;
        tv.tv_sec = 5;//5秒
        tv.tv_usec = 0;//微秒
        int n = select(maxfd +1,&fdset,NULL,NULL,&tv);
        printf("--------\n");
        if( n == -1)
        {
            printf("select err\n");
        }
        else if ( n == 0 )
        {
            printf("time out\n");
        }
        else
        {
            for(int i = 0; i < MAXFD; i++)
            {
                if( fds[i] == -1 )
                {
                    continue;
                }

                if( FD_ISSET(fds[i],&fdset))
                {
                    if( fds[i] == sockfd)//接受连接accept
                    {
                        accept_client(sockfd,fds);
                    }
                    else//接收数据,recv
                    {
                        recv_data(fds[i],fds);
                    }
                }
            }
        }
    }
}
int socket_init()
{
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd == -1)
    {
        return -1;
    }

    struct sockaddr_in saddr;
    memset(&saddr, 0, sizeof(saddr));
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(6000);
    saddr.sin_addr.s_addr = inet_addr("127.0.0.1");
    int res = bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr));
    if (res == -1)
    {
        printf("bind err\n");
        return -1;
    }

    res = listen(sockfd, 5);
    if (res == -1)
    {
        return -1;
    }

    return sockfd;
}

五、代码结构讲解

1. fds 数组管理

  • fds_init:初始化数组为 -1
  • fds_add:添加新 fd
  • fds_del:删除断开的 fd

2. socket_init()

服务器初始化四件套:

  • socket()
  • bind()
  • listen()
  • 返回监听 fd

3. accept_client()

接收新客户端连接,把新客户端 fd 加入数组。

4. recv_data()

接收客户端消息,回复 ok,客户端断开则清理资源。

5. main 函数(select 核心逻辑)

  1. 循环清空 fd 集合
  2. 将所有有效 fd 加入 select 监听
  3. select 阻塞等待事件
  4. 判断事件类型:
    • 监听 fd → 新连接
    • 客户端 fd → 接收消息

六、学习收获总结

    本代码基于 select IO 多路复用 实现高并发 TCP 服务器,核心知识点涵盖:使用数组管理所有文件描述符,实现监听套接字与客户端套接字的统一维护;通过 FD_ZERO、FD_SET、FD_ISSET 完成描述符集合操作,每次循环重新构建集合以保证监听有效性;利用 select 单线程阻塞监听多路 IO 事件,区分新连接与数据接收事件,分别调用 acceptrecv 处理;通过超时机制避免无限阻塞,对客户端断开做自动关闭与资源清理。整体实现了单线程处理多客户端连接、数据交互、异常处理的完整服务端逻辑,是理解网络 IO 多路复用最经典的入门案例。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐