第五章 IO模型

5.1 I/O模型分类

IO模型是描述计算机如何处理输入/输出操作的方式。在网络编程和系统编程中,了解不同的IO模型对选择合适的编程方法和提高程序性能非常重要。常见的IO模型包括以下几种:

阻塞 与 运行

阻塞IO(Blocking I/O):

在阻塞IO模型中,应用程序调用IO函数时会被阻塞,直到IO操作完成。也就是说,程序在等待数据的传输期间无法执行其他操作。该模型简单易用,但在高并发情况下性能较差,因为每个IO操作都会占用一个线程或进程。

非阻塞IO(Non-blocking I/O):

非阻塞IO模型中,应用程序在调用IO函数时,如果数据未就绪,函数会立即返回,而不会阻塞程序的执行。应用程序需要不断地轮询(poll)IO函数以检查数据是否就绪。这种模型可以提高程序的响应能力,但轮询操作会消耗大量的CPU资源。

IO复用(I/O Multiplexing):

IO复用模型使用selectpollepoll等系统调用来监控多个文件描述符(fd),当其中一个或多个文件描述符就绪时,通知应用程序进行相应的IO操作。IO复用模型比非阻塞IO更加高效,因为它避免了轮询的高CPU占用问题,适用于处理大量并发连接的场景。

信号驱动IO(Signal-driven I/O):

在信号驱动IO模型中,应用程序可以为文件描述符设置信号驱动模式,并注册一个信号处理函数。当IO事件发生时,内核会通过信号通知应用程序。应用程序在收到信号后进行相应的IO操作。此模型减少了CPU轮询的开销,但信号处理需要格外小心,编程难度较大。

异步IO(Asynchronous I/O, AIO):

异步IO模型中,应用程序发起IO请求后立即返回,并且不等待IO操作完成。内核在完成IO操作后,会通知应用程序。异步IO允许程序在等待IO的过程中执行其他任务,因此可以最大化系统资源的利用效率。它适用于高并发的网络服务器或需要高实时性的数据处理场景。

各种IO模型的对比

IO模型阻塞非阻塞IO复用信号驱动异步IO
调用复杂度简单中等较复杂较复杂复杂
系统开销最低
并发性能中等最高
使用场景简单简单多连接高并发高性能

后面两个 信号驱动IO异步IO 都为我们后续的驱动 课程 内容 不在本次课程内讲述

5.2 阻塞I/O

是最基本和最常见的IO模型。它的特点是在进行IO操作时,程序会等待数据的读取或写入完成后才继续执行其他任务。在阻塞IO模型中,调用IO函数的进程或线程会被“阻塞”,即挂起,直到操作完成。这种模型简单易用,但在高并发环境下可能会导致性能问题。

创建三个管道文件 模拟阻塞通讯

创建管道文件的名称
mkfifo 文件名

示例程序如下

#include <head.h>

int main(int argc, char const *argv[])
{
    printf("准备启动\n");
    int fifo_1 = open( "fifo_1" , O_RDONLY);
    printf("文件1启动成功\n");
    int fifo_2 = open( "fifo_2" , O_RDONLY);
    printf("文件2启动成功\n");
    int fifo_3 = open( "fifo_3" , O_RDONLY);
    printf("文件3启动成功\n");

    char buf[128];
    printf("全部文件启动成功\n");
    

    while (1)
    {
        memset(buf , 0 , sizeof(buf));
        read( fifo_1 , buf ,sizeof(buf) );
        printf( "fifo_1 = %s\n" , buf );

        memset(buf , 0 , sizeof(buf));
        read( fifo_2 , buf ,sizeof(buf) );
        printf( "fifo_2 = %s\n" , buf );

        memset(buf , 0 , sizeof(buf));
        read( fifo_3 , buf ,sizeof(buf) );
        printf( "fifo_3 = %s\n" , buf );
    }
    

    return 0;
}


发送程序

#include <head.h>
#define FIFO "fifo_1"

int main(int argc, char const *argv[])
{
    int fd_fifo = open( FIFO , O_WRONLY );

    char buf[128];

    while (1)
    {
        // 从键盘输入
        printf("输入:");
        scanf("%s" , buf);
        // 写入到管道中
        write( fd_fifo ,buf , strlen(buf) );
    }
    
    return 0;
}


5.3 非阻塞I/O

非阻塞模式 I/O 定义:将一个套接字设置为非阻塞模式时,相当于告诉系统内核:如果 I/O 操作不能立即完成,请不要让进程等待,而是马上返回一个错误。

Polling 机制:使用非阻塞模式的套接字时,需要通过一个循环不断测试文件描述符是否有数据可读,这个过程称为 “polling”。

CPU 资源浪费:频繁的 polling 操作会导致 CPU 资源的浪费,因为应用程序不断地占用 CPU 来检查 I/O 操作是否完成。

使用场景:这种模式在实际应用中不太常见。

在一些函数中 是有非阻塞 的选项的 但是大部分函数是没有非阻塞选项,所以需要使用 fcntl函数

函数说明

#include <unistd.h>
#include <fcntl.h>

int fcntl(int fd, int cmd, ... /* arg */ );
功能:用来控制文件描述符的状态
    
参数:	
	fd 要操作的 文件描述符
    cmd 
        F_GETFL 获取文件描述符的状态 arg 被忽略
        F_SETFL 设置文件描述符的状态 agr 是一个 int 类型
    ...: 可变参数
返回值:
	如果cmd 为 F_GETFL 成功返回文件描述符标志位
    如果cmd 为 F_SETFL 成功 返回 0
    失败 返回 -1 并重置错误码

编写思路

1、需要通过 F_GETFL 获取原有的状态 给 flags
2、将 flags 或上 O_NONBLOCK (非阻塞状态) 
3、然后通过 F_SETFL 写入你想要的flags

读端

写端

#include <head.h>

int main(int argc, char const *argv[])
{
    printf("准备启动\n");

    int fifo[3];

    fifo[0] = open("fifo_1", O_RDONLY);
    printf("文件1启动成功\n");
    fifo[1] = open("fifo_2", O_RDONLY);
    printf("文件2启动成功\n");
    fifo[2] = open("fifo_3", O_RDONLY);
    printf("文件3启动成功\n");

    char buf[128];
    printf("全部文件启动成功\n");

    int read_sata;
    int flags;

    for (size_t i = 0; i < 3; i++)
    {
        flags = fcntl(fifo[i], F_GETFL); // 获取原始状态 并 返回给 flag
        flags |= O_NONBLOCK;			 // 将 收到的 flag 增加一个非阻塞态
        fcntl(fifo[i], F_SETFL, flags);  // 将 flag 设定到我们当前的文件描述符中
    }

    while (1)
    {
        for (size_t i = 0; i < 3; i++)
        {
            memset(buf, 0, sizeof(buf));
            read_sata = read(fifo[i], buf, sizeof(buf));
            if (-1 != read_sata)
            {
                printf("fifo_%ld = %s\n",i ,  buf);
            }
        }
    }

    return 0;
}

5.4 多路复用 I/O

5.4.1 概念

IO多路复用也成为 IO 多路转接, 通过IO多路复用的方式可以同时检测多个文件描述符,并且这个检测的过程是阻塞的,一旦检测到某个文件描述符就绪,程序就会自动解除阻塞,之后就可以基于这些就绪的文件进行通讯。

通过这种方式可以在单进程/单线程的场景下面实现服务器并发,而我们常见的转接方式有:selectpollepoll

5.4.2 select()

首先第一个介绍的是我们的 select 函数,select函数是跨平台的函数,我们可以通过这个函数来检测我们的文件描述符状态。

他的原理就是检查我们提供文件描述符的读写缓冲区:

  • 读缓冲区:检测内部有无数据,如果有数据则设定为就绪
  • 写缓冲区:检测内部有无可以写写入的空间,有空间则设定改缓冲区就绪
  • 读写异常:检测读写缓冲区是否有异常,有 则设定为就绪

函数原型

#include <sys/select.h>
struct timeval {
    time_t      tv_sec;         /* seconds */
    suseconds_t tv_usec;        /* microseconds */
};

int select(int nfds, fd_set *readfds, fd_set *writefds,
           fd_set *exceptfds, struct timeval * timeout);
参数:
	nfds:		要监视的的最大文件描述符 + 1

	readfds:	要监视 读 文件描述符 集合
        			不需要则 为 NULL 

	writefds:	要监视 写 文件描述符 集合
        			不需要则 为 NULL 

	exceptfds:	要监视 异常 文件描述符 集合
        			不需要则 为 NULL 

	timeout:	指定超时时间
        			NULL:永久阻塞 直到有文件描述符输入
        			等待固定时长:函数在固定时间没有检测到就绪 则直接接触阻塞 函数返回0

返回值:
	大于0: 成功返回就绪文件描述符的总个数
    等于-1: 函数调用失败
    等于0: 超时 没有就绪的文件描述符

描述符集合

// 将文件描述符在集合中删除
void FD_CLR(int fd, fd_set *set);

0001 0010 1111
10x01 << 1024 - fd1
    0001 0000 0000 
2、取反 
    1110 1111 1111 
3、取与运算
    0001 0010 1111
    1110 1111 1111
    0000 0010 1111
    
// 判断文件描述符 是否在集合中  在集合中返回 非0 不在集合中 返回 0 
int  FD_ISSET(int fd, fd_set *set);
10x01 << 1024 - fd1
    0001 0000 0000
2、取与
    0001 0010 1111
    0001 0000 0000
    
    0001 0000 0000 | > 0 代表 文件描述符 在集合中
    0000 0000 0000 | = 0 代表 文件描述符 不在集合中
 
// 将文件描述符添加到集合中
void FD_SET(int fd, fd_set *set);
10x01 << 1024 - fd1
    0001 0000 0000
2、取或
    0000 1000 0000
    
    原始数据:0001 0010 1111
    操作数据:0000 1000 0000
    结果数据:0001 1010 1111
// 清空集合
void FD_ZERO(fd_set *set);
1、取与
    原始数据:0001 0010 1111
    操作数据:0000 0000 0000
    结果数据:0000 0000 0000

select的本质

// 类型原型
typedef struct
{
    __fd_mask __fds_bits[__FD_SETSIZE / __NFDBITS];
} fd_set;

// 参数拆分
typedef long int __fd_mask;

#define __FD_SETSIZE		1024

#define __NFDBITS	(8 * (int) sizeof (__fd_mask))

long __fds_bits[1024 / 64]

long __fds_bits[16]

1024 长度的比特数组

总共为 1024 比特
    
fd_set fd_s;
    fd_s[1] = 1;

也就是说,在这个集合中 每一个位的位置 , 就代表我们的文件描述符。

当文件描述符所对应的 标志位 为 1 时 则代表 检测这个文件的状态

为 0 则反之

文件操作实例程序

#include <head.h>

int main(int argc, char const *argv[])
{

    int fifo_1 = open("fifo_1", O_RDONLY);

    int fifo_2 = open("fifo_2", O_RDONLY);

    int fifo_3 = open("fifo_3", O_RDONLY);


    char buf[128];


    fd_set read_set;      // 集合
    fd_set read_set_temp; // 副本

    int max_fds; // 最大值

    int fds_num; // 就绪个数

    // 清空集合
    FD_ZERO(&read_set);

    // 添加文件描述符
    FD_SET(fifo_1, &read_set);
    max_fds = (max_fds > fifo_1 ? max_fds : fifo_1);

    FD_SET(fifo_2, &read_set);
    max_fds = (max_fds > fifo_2 ? max_fds : fifo_2);

    FD_SET(fifo_3, &read_set);
    max_fds = (max_fds > fifo_3 ? max_fds : fifo_3);

    while (1)
    {
        read_set_temp = read_set;

        fds_num = select(max_fds + 1, &read_set_temp, NULL, NULL, NULL);
        // 4 5 6 
        // 0 5 0

        for (size_t i = 3; i < max_fds + 1; i++)
        {
            memset(buf, 0, sizeof(buf));
            
            if (0 != FD_ISSET( i , &read_set_temp ))
            {
                read( i , buf, sizeof(buf));
                printf("fifo = %s\n", buf);[]
            }
            
        }
    }

    return 0;
}

关于使用的问题

为啥只能监控 小于 FD_SETSIZE(1024) 的文件描述符

​ 因为,fd_set只有 128 字节 只有 1024 比特

四个宏的本质

​ 位置操作

​ 晚上思考 四个宏 通过位操作如何实现

为什么 nfds 要传 max_fd + 1

​ nfds 获取方式 是通过 文件描述符 而文件描述符是从 0 开始计算

select 返回值 是什么含义 有什么用 怎么用

​ 0 超时

​ -1 错误

​ >0 检测到多少个文件描述符就绪

减少循环遍历 FD_ISSET的 时间

​ 使用select 的返回值 检测返回到多少个 文件描述符 在循环的过程中 执行一个 就 减减一次 当这个值 为0 的时候退出循环

循环中使用 select 为什么要每次重置集合

​ 因为在 select 中 当文件就绪后, 集合中没有就绪的文件描述符 会被重置为0 ,下一次检测 就没有原先的文件描述符集合

在程序中创建一个母本 和 一个副本

5.4.3 poll()

==原理相同,需要同学们自行查阅资料学习==

5.4.4 epoll()

==原理相同,需要同学们自行查阅资料学习==

第六章 服务器模型

6.1 多线程并发服务器

需要解决的问题

1、如何做服务器转发

用全局变量做存储
用互斥锁做同步

2、如何检测TCP断开

int recv_size = recv(Sock_fd, buf_recv, sizeof(buf_recv), 0);
if (0 == recv_size) // 判定退出

示例程序

6.2 多进程并发服务器

需要解决的问题

1、如何防止僵尸进程

// 处理SIGCHLD信号,防止僵尸进程
void handle_sigchld(int sig) {
    while (waitpid(-1, NULL, WNOHANG) > 0);  // 非阻塞等待子进程结束
}

// 处理僵尸进程
signal(SIGCHLD, handle_sigchld);

2、如何做服务器转发

很难解决

使用共享内存存储连接的用户列表
使用信号灯做同步
	
    父进程 初始化 tcp 	
        服务器套接字   3
    	
    等待子进程连接	4
    
    
    子进程1  打开 套接字 4 3
    
    子进程2  连接打开套接字  5 4 3 
    
    子进程3  连接打开套接字  6 5 4 3 
    
    实时操作系统
    	
    板载系统
    	32 stm32  ESP32
    	分线程
    		stm32 1M
    64  A7 芯片 

3、第一个连接的进程 无法向 第二个链接的进程发送信息

示例程序

main.c

#include <sys_head.h>
#include "tcp.h"
#include "list.h"

// 多进程 TCP 服务器

// 共享内存用定义
#define SHM_MAX_SIZE 16384
#define SHM_KEY 1234
#define SEM_KEY 12345

User_list *shm_init(void);

// 信号量加锁  进程的信号灯  	P操作 获取 释放资源   进程的同步 或 互斥  
void My_Sem_wait(int semid);

// 信号量解锁				V操作	创建资源
void My_Sem_signal(int semid);

// 处理SIGCHLD信号,防止僵尸进程
void handle_sigchld(int sig);

// 子进程处理函数
void handle_client(int Sock_fd, User_list *list, int semid);

int main(int argc, char const *argv[])
{
    // 判断命令行参数
    if (argc < 2)
    {
        printf("参数错误:%s ip port\n", argv[0]);
        return -1;
    }

    /********************* 初始化TCP 服务器 *********************/

    int Sock_server_fd = TCP_init(atoi(argv[1]));

    /*********************** 初始化共享内存 **********************/
    User_list *User_list_ptr = shm_init();

    /*********************** 初始化信号灯集 **********************/
    int semid = semget(SEM_KEY, 1, IPC_CREAT | 0666);
    semctl(semid, 0, SETVAL, 1); // 初始化信号量值为 1

    /*********************** 注册处理僵尸进程 **********************/
    signal(SIGCHLD, handle_sigchld);

    /*********************** 声明收发缓冲区 **********************/
    char buf_send[1024];
    char buf_recv[1024];

    while (1)
    {
        int Sock_client_fd = TCP_accept(Sock_server_fd);

        // 创建进程
        pid_t client_pid = fork();
        if (0 == client_pid) // 子进程
        {
            // 关闭父进程的套接字  服务器套接字
            close(Sock_server_fd); // 关闭父进程的套接字

            // 子进程操作函数
            handle_client(Sock_client_fd, User_list_ptr, semid);

            shmdt(User_list_ptr);

            exit(-1);
        }
        else if (0 < client_pid) // 父亲进程
        {

            My_Sem_wait(semid); // 加锁

            // 将新的连接添加到链表中
            User_list_ptr->data[User_list_ptr->len].Sock_fd = Sock_client_fd;

            sprintf(buf_send, "%s|fd:%d|id:%d", "新用户", Sock_client_fd, User_list_ptr->len);

            strcpy(User_list_ptr->data[User_list_ptr->len].name, buf_send);

            send(Sock_client_fd, buf_send, strlen(buf_send), 0);

            User_list_ptr->len++;

            // 关闭子进程的文件描述符
            // close(Sock_client_fd);

            My_Sem_signal(semid); // 解锁
        }
    }
    shmdt(User_list_ptr);

    //  7、关闭套接字  close
    close(Sock_server_fd);
    return 0;
}

// 初始化共享内存
User_list *shm_init(void)
{
    int shmid;

    // 创建一个共享内存段,权限为 0666
    shmid = shmget(SHM_KEY, SHM_MAX_SIZE, 0666 | IPC_CREAT);
    if (shmid == -1)
    {
        perror("shmget 创建共享内存段失败");
        exit(EXIT_FAILURE);
    }

    printf("共享内存段创建成功,ID: %d\n", shmid);

    // 附加共享内存
    User_list *user_list_ptr = (User_list *)shmat(shmid, NULL, 0);
    if (user_list_ptr == (User_list *)-1)
    {
        perror("shmat failed");
        exit(1);
    }
    // 打印指针的地址
    printf("附加共享内存成功, 地址: %p\n", user_list_ptr);

    // 清空共享内存
    memset(user_list_ptr, 0, sizeof(User_list));

    // 初始化顺序表
    user_list_ptr->len = 0;

    return user_list_ptr;
}

// 处理SIGCHLD信号,防止僵尸进程
void handle_sigchld(int sig)
{
    while (waitpid(-1, NULL, WNOHANG) > 0)
        ; // 非阻塞等待子进程结束
}

// 信号量加锁
void My_Sem_wait(int semid)
{
    struct sembuf Sem_b = {0, -1, 0};
    semop(semid, &Sem_b, 1);
}

// 信号量解锁
void My_Sem_signal(int semid)
{
    struct sembuf Sem_b = {0, 1, 0};
    semop(semid, &Sem_b, 1);
}

// 子进程处理函数
void handle_client(int Sock_fd, User_list *list, int semid)
{
    // 收发缓冲区
    char buf_send[2048];
    char buf_recv[1024];

    // 计算当前进程所在顺序表的位置
    My_Sem_wait(semid); // 加锁
    int index = 0;
    for (size_t i = 0; i < list->len; i++)
    {
        if (Sock_fd == list->data[i].Sock_fd)
        {
            index = i;
        }
    }
    My_Sem_signal(semid); // 解锁

    while (1)
    {
        memset(buf_send, 0, sizeof(buf_send));
        memset(buf_recv, 0, sizeof(buf_recv));

        int recv_size = recv(Sock_fd, buf_recv, sizeof(buf_recv), 0);
        if (0 == recv_size) // 判定退出
        {
            // 从顺序表中删除
            My_Sem_wait(semid); // 加锁

            for (size_t i = index; i < list->len; i++)
            {
                list->data[index] = list->data[index + 1];
            }

            list->len--;

            printf("退出成功....");

            close(Sock_fd);

            My_Sem_signal(semid); // 解锁

            return;
        }

        // 修改名字
        if (NULL != strstr(buf_recv, "set_name_"))
        {
            My_Sem_wait(semid); // 加锁
            // 写入姓名
            strcpy(list->data[index].name, buf_recv + 9);

            sprintf(buf_send, "修改名称成功:%s", list->data[index].name);

            send(Sock_fd, buf_send, strlen(buf_send), 0);

            My_Sem_signal(semid); // 解锁

            continue;
        }

        // 转发 群发
        else
        {
            My_Sem_wait(semid); // 加锁
            printf("开始群发\n");
            // 组装字符串
            sprintf(buf_send, "%s : %s", list->data[index].name, buf_recv);

            // 遍历整个链表
            printf("len = %d index = %d\n", list->len, index);
            for (size_t i = 0; i < list->len; i++)
            {
                printf("name = [%s] fd = [%d]\n", list->data[i].name, list->data[i].Sock_fd);
            }

            // 发送给其他成员
            for (size_t i = 0; i < list->len; i++)
            {
                if (Sock_fd != list->data[i].Sock_fd)
                {
                    printf("aaaaa\n");
                    printf("发送的接收的fd = %d\n", list->data[i].Sock_fd);
                    int result = send(list->data[i].Sock_fd, buf_send, strlen(buf_send), 0);
                    if (result < 0)
                    {
                        perror("发送失败");
                        // 处理发送失败的情况(比如关闭连接、删除用户等)
                    }
                }
            }

            My_Sem_signal(semid); // 解锁
        }
    }
}

list.h

#ifndef __LIST_H__
#define __LIST_H__

#define List_Max 50


// 声明 用户数据结构体
typedef struct tpc_Sock_fds
{
    int Sock_fd;
    char name[128];
} tpc_Sock_fds;

// 声明 用户顺序表
typedef struct User_list
{
    tpc_Sock_fds data[List_Max];
    int len;
}User_list;



#endif

tcp.c

#include "tcp.h"
#include <sys_head.h>

// 初始化函数

int TCP_init(int port)
{
    int server_fd;
    struct sockaddr_in address;
    int opt = 1;

    // 创建套接字
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0)
    {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }

    // 设置套接字选项
    if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)))
    {
        perror("setsockopt");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    // 配置地址结构
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(port);

    // 绑定套接字
    if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0)
    {
        perror("bind failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    // 监听套接字
    if (listen(server_fd, 3) < 0)
    {
        perror("listen");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    printf("服务器启动成功 端口: %d...\n", port);

    return server_fd;
}

int TCP_accept(int server_fd)
{
    int new_socket;
    struct sockaddr_in client_address;
    socklen_t addrlen = sizeof(client_address);

    // 阻塞等待客户端连接
    if ((new_socket = accept(server_fd, (struct sockaddr *)&client_address, &addrlen)) < 0)
    {
        perror("accept");
        exit(EXIT_FAILURE);
    }

    printf("新连接: socket fd = %d, ip : %s, port: %d\n", new_socket,
           inet_ntoa(client_address.sin_addr), ntohs(client_address.sin_port));

    return new_socket;
}



tcp.h

#ifndef __TCP_H__
#define __TCP_h__

int TCP_init(int port);

int TCP_accept(int server_fd);

#endif

6.3 多路IO复用

#include <sys_head.h>

// 默认IP
#define MY_IP "10.0.12.15"


// 定义一个全局变量的数组
typedef struct tpc_Sock_fds
{
    int Sock_fd;
    char name[128];
} tpc_Sock_fds;

#define MAX_LIST 20
#define SHM_ID 4

typedef struct tpc_sql_list
{
    tpc_Sock_fds data[MAX_LIST];
    int len;
} tpc_sql_list;

// 求集合最大值 宏函数
#define MAX_FDS(fd, Fd_Max) fd > Fd_Max ? fd : Fd_Max;

int main(int argc, char const *argv[])
{
    /****************************************** 获取命令行参数 *****************************************/
    // 判断命令行参数
    if (argc < 2)
    {
        printf("参数错误:%s ip port\n", argv[0]);
        return -1;
    }
    // 给定默认ip

    char ip_str[128];

    if (3 != argc)
    {
		// 写入默认IP
        strcpy(ip_str, MY_IP);
    }
    else
    {

        strcpy(ip_str, argv[2]);
    }
    /****************************************** 获取命令行参数 *****************************************/

    /***************************************** TCP服务器初始化 ****************************************/
    //  1、创建套接字
    int Sock_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (-1 == Sock_fd)
    {
        ERRLOG("socket erron");
        return -1;
    }

    //  2、填充服务器网络信息结构体
    struct sockaddr_in Sock_addr;
    Sock_addr.sin_family = AF_INET;                // 选择 IPV4
    Sock_addr.sin_port = htons(atoi(argv[1]));     // 给定端口 // atoi 将字符串 转换成数字
    Sock_addr.sin_addr.s_addr = inet_addr(ip_str); // IP地址

    socklen_t Sock_size = sizeof(Sock_addr);

    //  3、将套接字与服务器的网络信息结构体绑定
    if (-1 == bind(Sock_fd, (struct sockaddr *)&Sock_addr, Sock_size))
    {
        ERRLOG("bind err");
        return -1;
    }

    //  4、将套接字设置城 监听状态

    if (-1 == listen(Sock_fd, 20))
    {
        ERRLOG("Sock_fd err");
        return -1;
    }

    printf("启动服务器\n");

    /***************************************** TCP服务器初始化 ****************************************/

    /***************************************** 初始化IO复用集合 ***************************************/
    // 创建集合
    fd_set Tcp_client_fds;
    fd_set Tcp_client_fds_temp;

    // 创建集合的最大值
    int Max_fd;

    // 清空母本
    FD_ZERO(&Tcp_client_fds);

    // 将监听的套接字给集合进行检测
    FD_SET(Sock_fd, &Tcp_client_fds);
    Max_fd = MAX_FDS(Sock_fd, Max_fd);

    // 就绪描述符个数
    int fds_num;

    /***************************************** 初始化IO复用集合 ***************************************/

    /***************************************** 初始化顺序表 ***************************************/

    tpc_sql_list list;

    list.len = 0;

    /***************************************** 初始化顺序表 ***************************************/

    /***************************************** 初始化缓冲区 ***************************************/

    char buf[1024];
    char buf_send[2048];
    char buf_recv[1024];

    memset(buf, 0, sizeof(buf));
    memset(buf_send, 0, sizeof(buf_send));
    memset(buf_recv, 0, sizeof(buf_recv));

    /***************************************** 初始化缓冲区 ***************************************/

    while (1)
    {

        memset(buf, 0, sizeof(buf));
        memset(buf_send, 0, sizeof(buf_send));
        memset(buf_recv, 0, sizeof(buf_recv));

        // 副本获取母本数据
        Tcp_client_fds_temp = Tcp_client_fds;
        // 开始检测
        fds_num = select(Max_fd + 1, &Tcp_client_fds_temp, NULL, NULL, NULL);

        /*
            说明: select 现在检测的是 服务器的TCP 套接字 和 客户端的TCP套接字
            当 服务器的TCP 套接字 Sock_fd 就绪时 就代表有新的用户产生了链接
            当 客户端的TCP 套接字 acc_fd 就绪时 就代表有用户发送了信息
        */

        // 服务器的 TCP 套接字 Sock_fd 就绪
        if (FD_ISSET(Sock_fd, &Tcp_client_fds_temp))
        {
            //  5、阻塞等待客户端连接 accept
            int acc_fd = accept(Sock_fd, NULL, NULL);
            if (-1 == acc_fd)
            {
                ERRLOG("accept err");
                return -1;
            }

            // 检测是否超过最大连接数
            if (list.len >= MAX_LIST)
            {
                printf("已经满了\n");
                send(acc_fd, "已经满了", strlen("已经满了"), 0);
                continue;
            }

            // 更新集合
            FD_SET(acc_fd, &Tcp_client_fds);
            Max_fd = MAX_FDS(acc_fd, Max_fd);

            // 更新顺序表
            list.data[list.len].Sock_fd = acc_fd;

            sprintf(list.data[list.len].name, "新用户_00%d", list.len);

            sprintf(buf_send, "连接成功 : %s", list.data[list.len].name);

            send(acc_fd, buf_send, strlen(buf_send), 0);

            printf("name [%s] fd = [%d]\n" , list.data[list.len].name , list.data[list.len].Sock_fd);

            list.len++;
        }

        // 非连接 通讯
        for (size_t i = 0; i < Max_fd + 1; i++)
        {
            // 判断那个文件描述符就绪
            if (i != Sock_fd && FD_ISSET(i, &Tcp_client_fds_temp))
            {
                int index;
                // 求当前的文件描述符 在 顺序表中的位置
                for (size_t j = 0; j < MAX_LIST ; j++)
                {
                    if (list.data[j].Sock_fd == i)
                    {
                        index = j;
                        break;
                    }
                }

                // 接收字符串
                int recv_len = recv(list.data[index].Sock_fd, buf_recv, sizeof(buf_recv), 0);

                // 客户端关闭了连接
                if (0 == recv_len)
                {
                    // 关闭了连接
                    printf("客户关闭了连接....\n");

                    // 从集合删除
                    FD_CLR(i, &Tcp_client_fds_temp);

                    close(i);

                    // 从顺序表删除

                    for (size_t j = index; j < MAX_LIST; j++)
                    {
                        list.data[j] = list.data[j];
                    }

                    list.len--;

                    continue;
                }
                // 客户端操作

                // 修改名字
                if (NULL != strstr(buf_recv, "set_name_"))
                {
                    // 写入姓名
                    strcpy(list.data[index].name , buf_recv + 9);
                    // printf("改名字:%s index%d fd %d\n" , list.data[index].name , index , list.data[index].Sock_fd);
                    sprintf(buf, "修改名称成功:%s", list.data[index].name);
                    send( list.data[index].Sock_fd , buf, strlen(buf), 0);
                    continue;
                }
                // 转发 群发

                // 发送给其他成员

                // 组装字符串
                sprintf(buf_send, "%s : %s", list.data[index].name, buf_recv);
                
                for (size_t j = 0; j < list.len ; j++)
                {
                    if (j != index)
                    {
                        printf("fd = [%d] name = [%s]\n", list.data[j].Sock_fd , list.data[j].name);
                        send(list.data[j].Sock_fd, buf_send , strlen(buf_send), 0);
                    }
                }
            }
        }
    }

    //  7、关闭套接字  close
    close(Sock_fd);
    return 0;
}