网络编程学习——3

一、IO多路复用

1.poll函数

(1)poll

需要轮询,需要两次拷贝,监听的文件描述符不受限制

使用结构体数组存储文件描述符

功能

poll 函数是一种 I/O 多路复用机制(在select基础上优化),用于检测多个文件描述符的状态,判断它们是否有数据可读、可写或者发生异常等事件,从而实现同时处理多个 I/O 操作

函数头文件

#include <poll.h>

函数原型

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

参数

参数名 类型 作用与细节
fds struct pollfd * 指向一个包含struct pollfd结构体的数组指针。每个struct pollfd结构体代表一个要检测的文件描述符及其相关信息。
nfds nfds_t 指定fds数组中元素的数量,即要检测的文件描述符的个数。
timeout int 设置poll函数的超时时间,单位是毫秒(ms)。
timeout > 0:最多等待timeout毫秒
timeout = 0:不等待,立即返回
timeout = -1:一直阻塞,直到有文件描述符就绪才返回。

常见事件宏定义

宏定义 含义
POLLIN 表示有数据可读
POLLOUT 表示可写
POLLPRI 表示有紧急数据可读。
POLLERR 表示发生错误。
POLLHUP 表示被挂起。
POLLNVAL 表示fd不是一个有效的文件描述符

返回值

返回情况 含义说明
大于 0 成功,返回值是fds数组中就绪的文件描述符的数量
等于 0 表示poll函数超时
等于 - 1 表示poll函数调用失败,此时errno会被设置
EBADFfds数组中存在无效的文件描述符
EFAULTfds指针指向的内存区域无效
EINTR:函数被信号中断

结构体

struct pollfd {
    int fd;         /* 文件描述符 */
    short events;   /* 等待的事件,如POLLIN(可读)、POLLOUT(可写)等 */
    short revents;  /* 实际发生的事件,由poll函数填充 */
};

(2)poll示例

/*===============================================
*   文件名称:poll.c
*   创 建 者:青木莲华
*   创建日期:2025年08月15日
*   描    述:并发TCP通信server 基于poll
================================================*/
#include "socket_head.h"
#define MAX 64


int main(int argc, char *argv[])
{ 
    char buf[1024] = {0};               //缓冲区
    int ret = -1;                       //recv返回值
    struct sockaddr_in addr;            //IPV4结构体
    struct pollfd pfds[MAX];            //poll数组
    char err_msg[24];                   //错误信息
    int nfds = 0;                       //文件描述符个数
    socklen_t addlen = sizeof(addr);    //客户端地址信息实际大小
    //1.创建套接字 IPV4 TCP
    int sockfd = socket(AF_INET,SOCK_STREAM,0);
    if(-1 == sockfd)
    {
        strcpy(err_msg,"socket");
        goto err;
    }
    //2.绑定套接字+初始化结构体
    addr.sin_family = AF_INET;
    addr.sin_port   = htons(6666);
    addr.sin_addr.s_addr = inet_addr("192.168.18.220");

    if(-1 == bind(sockfd,(struct sockaddr *)&addr,sizeof(addr)))
    {
        strcpy(err_msg,"bind");
        goto err;
    }

    //3.监听
    if(-1 == listen(sockfd,5))
    {
        strcpy(err_msg,"listen");
        goto err;
    }
    printf("Server is waiting for connection now!\n");
    //4.初始化结构体数组
    pfds[0].fd = sockfd;
    pfds[0].events = POLLIN;    //当前套接字可以读
    nfds++;

    while(1)
    {
        //5.poll
        if(-1 == poll(pfds,nfds,-1))
        {
            strcpy(err_msg,"poll");
            goto err;
        }
        //6.1遍历结构体数组  做相应操作
        int i;
        for(i = 0 ; i < nfds ; i++)
        {
            //6.2 是否是sockfd就绪(有新连接)  
            if(pfds[i].fd == sockfd && pfds[i].revents == POLLIN)
            {   
                //6.2.1建立新连接
                int connfd = accept(sockfd,(struct sockaddr *)&addr,&addlen);
                if(-1 == connfd)
                {
                    strcpy(err_msg,"accept");
                    goto err;
                }
                printf("Client [%s:%d] has connected!\n",
                       inet_ntoa(addr.sin_addr),
                       ntohs(addr.sin_port));
                //6.2.2将新连接文件描述符加入结构体数组供poll处理
                //找插入位置
                int flag = -1;
                for(int j = 1 ; j < nfds ; j++)
                {
                    if(pfds[j].fd == -1)
                    {
                        flag = j;
                    }
                }
                if(-1 != flag)
                {
                    pfds[flag].fd = connfd;
                    pfds[flag].events = POLLIN;
                }
                else
                {
                    pfds[nfds].fd = connfd;
                    pfds[nfds].events = POLLIN;
                    nfds++;
                }
            }
            else if(pfds[i].fd != -1 && pfds[i].revents == POLLIN)
            {
                //6.3有已连接的connfd就绪
                //6.3.1读操作
                ret = recv(pfds[i].fd,buf,sizeof(buf),0);
                if(-1 == ret)
                {
                    strcpy(err_msg,"recv");
                    goto err;
                }
                //6.3.2若Client 断开连接
                if(0 == ret)
                {
                    printf("A Client has disconnected!\n");
                    close(pfds[i].fd);
                    pfds[i].fd = -1;
                    continue;
                }
                puts(buf);
                memset(buf,0,sizeof(buf));
            }
        }
    }
    close(sockfd);
    return 0;

//错误处理
err:
    perror(err_msg);
    return -1;
} 

2.epoll机制

不需要轮询,只需要一次拷贝,文件描述符不受限

核心机制:红黑树+双向链表 实现

核心使用流程

  1. 创建epoll
  2. 插入关心文件描述符到epoll
  3. 让内核监听
  4. 处理就绪的文件描述符
  5. 添加新的文件描述符/删除退出的文件描述符

(1)创建epoll(epoll_create函数)

功能

​ Linux 系统特有的 I/O 多路复用机制 epoll 的初始化函数,用于创建一个 epoll 实例(事件表),用于后续监听多个文

件描述符的 I/O 事件

函数头文件

#include <sys/epoll.h>

函数原型

int epoll_create(int size);

参数

​ 仅需传入一个大于 0 的值即可,早期 Linux 版本中,size 表示 epoll 实例可监听的最大文件描述符数量(需大于 0),用于内核预先分配资源。现代 Linux 系统(2.6.8 及以上)中,size 参数已被忽略,内核会动态调整资源,不再受限于该值。

返回值

​ 成功——返回epoll文件描述符(epfd)

​ 失败——返回 -1 ,并设置errno

(2) 操作 epoll 实例(epoll_ctl 函数)

功能

​ 用于操作 epoll 实例(事件表),实现 添加、修改、删除 文件描述符的监听事件,是 epoll 机制中管理文件描述符与事件关联的核心函数。

函数头文件

#include <sys/epoll.h>  

函数原型

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);  

参数

参数名 类型 作用与细节
epfd int epoll 实例的文件描述符(由 epoll_create 返回,标识要操作的事件表)
op int EPOLL_CTL_ADD添加 fdepoll 实例,监听 event 事件
EPOLL_CTL_DEL删除 fd 及关联事件
EPOLL_CTL_MOD修改 fd 的监听事件为 event
fd int 要操作的文件描述符(如套接字 socket、管道 pipe 等)
event struct epoll_event* 指向 epoll_event 结构体的指针,描述监听的事件类型(如可读、可写),仅 EPOLL_CTL_DEL 时可传 NULL

返回值

​ 成功——返回0

​ 失败——返回-1,并设置errno

struct epoll_event 结构体

epoll_ctl 通过该结构体定义监听的事件,原型如下:

struct epoll_event {  
 uint32_t events;  // 事件类型(位掩码),如 EPOLLIN(文件描述符可读)、EPOLLOUT(可写) 
 epoll_data_t data;// 用户数据(可存 fd、指针等,用于事件回调时区分来源)  
};  

// 其中 epoll_data_t 是联合类型:  
typedef union epoll_data {  
 void   *ptr;   // 指针(可自定义数据)  
 int    fd;     // 文件描述符(常用,直接关联监听的 fd)  
 uint32_t u32;  // 无符号整数  
 uint64_t u64;  // 无符号长整数  
} epoll_data_t;  

(3)让内核监听(epoll_wait函数)

功能

​ 用于等待在 epoll 实例中注册的文件描述符上有事件发生。当有事件就绪(如可读、可写、异常等),函数会返回就绪事件的数量,并将就绪事件信息填充到指定的数组中,方便开发者进行后续处理。

函数头文件

#include <sys/epoll.h>  

函数原型

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);  

参数

参数名 类型 作用与细节
epfd int epoll 实例的文件描述符
events struct epoll_event * 指向 epoll_event 结构体数组的指针,用于存储 epoll 检测到的就绪事件。函数返回时,该数组会被填充。
maxevents int 表示 events 数组的大小,即最多能返回的就绪事件数量。
timeout int timeout > 0:最多等待 timeout 毫秒后返回。
timeout = 0:不等待,立即返回,检查是否有就绪事件。
timeout = -1:一直阻塞,直到有事件发生才返回。

struct epoll_event 结构体(补充说明)

epoll_wait 会将就绪事件的信息填充到 events 数组中的 epoll_event 结构体里,其原型如下:

struct epoll_event {  
    uint32_t events;  // 事件类型(位掩码),如 EPOLLIN(文件描述符可读)、EPOLLOUT(可写) 
    epoll_data_t data;// 用户数据(可存 fd、指针等,用于事件回调时区分来源)  
};  

// 其中 epoll_data_t 是联合类型:  
typedef union epoll_data {  
    void   *ptr;   // 指针(可自定义数据)  
    int    fd;     // 文件描述符(常用,直接关联监听的 fd)  
    uint32_t u32;  // 无符号整数  
    uint64_t u64;  // 无符号长整数  
} epoll_data_t;  

返回值

返回情况 含义说明
大于 0 成功,返回值是 events 数组中就绪事件的数量,可通过遍历 events 数组获取具体的就绪事件信息并处理。
等于 0 表示 epoll_wait 超时,在指定的 timeout 时间内没有检测到任何就绪事件。
等于 -1 失败,此时 errno 会被设置,
常见错误原因包括:
EBADFepfd 不是有效的文件描述符。
EINTR:函数被信号中断。

(4)处理准备好的文件描述符

示例

//1.先要获取epoll_wait函数返回的就绪epoll_event的个数
int size = epoll_wait(......);
//此处省略错误处理
//2.遍历(假设结构体数组为event_arr)
for(int i = 0 ; i < size ; i++)
{
    //如果读就绪(EPOLLIN)
    if(event_arr[i].events & EPOLLIN)
    {
        int fd = -1;
        //获取文件描述符然后进行操作
        fd = event_arr[i].data.fd;
        //假设要实现recv读操作
        int ret = recv(fd,buf,sizeof(buf));
        //此处省略后续操作
    }
}

(5)添加/删除(EPOLL_CTL_ADD / EPOLL_CTL_DEL)

int fd = 0; // 示例文件描述符(标准输入)
struct epoll_event event;

// 添加文件描述符(EPOLL_CTL_ADD)
event.events = EPOLLIN;  // 关注读事件
event.data.fd = fd;
if (-1 == epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, &event)) {
    perror("EPOLL_CTL_ADD failed");
    return 1;
}
printf("已添加fd: %d\n", fd);

// 业务逻辑处理...

// 删除文件描述符(EPOLL_CTL_DEL)
if (epoll_ctl(epoll_fd, EPOLL_CTL_DEL, fd, NULL) == -1) {
    perror("EPOLL_CTL_DEL failed");
    return 1;
}
printf("已删除fd: %d\n", fd);

close(epoll_fd);

(6)epoll示例

/*===============================================
*   文件名称:epoll.c
*   创 建 者:青木莲华
*   创建日期:2025年08月16日
*   描    述:Server(TCP通信+epoll并发)
================================================*/
#include "socket_head.h"
#define MAX 64

int main(int argc, char *argv[])
{ 
    char buf[1024] = {0};                   //缓冲区
    char err_msg[24] = {0};                 //错误信息
    int ret = -1;                           //recv返回值
    int sockfd;                             //服务器套接字
    int epfd;                               //epoll文件描述符
    struct sockaddr_in addr;                //IPV4 结构体
    socklen_t addrlen = sizeof(addr);       //客户端地址长度
    struct epoll_event ep;                  //epoll结构体
    struct epoll_event eparr[MAX];          //epoll结构体数组
    
    //1.创建套接字
    sockfd = socket(AF_INET,SOCK_STREAM,0); //服务器套接字(IPV4 + TCP)
    if(-1 == sockfd)
    {
        strcpy(err_msg,"socket");
        goto err;
    }
    
    //2.1 初始化地址结构体
    addr.sin_family = AF_INET;                          //协议族为IPV4
    addr.sin_port = htons(6666);                        //端口号为6666
    addr.sin_addr.s_addr = inet_addr("192.168.18.220"); //设置服务器IP          
    
    //2.2 绑定套接字
    if(-1 == bind(sockfd,(struct sockaddr *)&addr,sizeof(addr)))
    {
        strcpy(err_msg,"bind");
        goto err;
    }
    
    //3.监听
    if(-1 == listen(sockfd,5))
    {
        strcpy(err_msg,"listen");
        goto err;
    }
    printf("Server is waiting for connecting now!\n");

    //4.1 创建epoll
    epfd = epoll_create(1);
    if(-1 == epfd)
    {
        strcpy(err_msg,"epoll_create");
        goto err;
    }
    
    //4.2 将服务器套接字在epoll内初始化
    ep.events = EPOLLIN;
    ep.data.fd = sockfd;
    if(-1 == epoll_ctl(epfd,EPOLL_CTL_ADD,sockfd,&ep))        //添加进event数组
    {
        strcpy(err_msg,"epoll_ctl");
        goto err;
    }
    
    //5.通信
    while(1)
    {
        //5.1 epoll_wait 阻塞等待就绪
        int size = epoll_wait(epfd,eparr,MAX,-1);
        if(-1 == size)
        {
            strcpy(err_msg,"epoll_wait");
            goto err;
        }
        //5.2 遍历events数组 对就绪的连接进行操作
        int i;
        for(i = 0 ; i < size ; i++)
        {
            //5.2.1 就绪状态为可读
            if(eparr[i].events & EPOLLIN)
            {
                //5.2.2 是否是sockfd,是就是有新连接
                if(eparr[i].data.fd == sockfd)
                {
                    int connfd = accept(sockfd,(struct sockaddr *)&addr,&addrlen);
                    if(-1 == connfd)
                    {
                        strcpy(err_msg,"accept");
                        goto err;
                    }
                    printf("Client [%s:%d] has connected!\n",
                            inet_ntoa(addr.sin_addr),ntohs(addr.sin_port));
                    ep.data.fd = connfd;
                    ep.events = EPOLLIN;
                    //ADD新连接
                    epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ep);
                }
                else
                {
                    //接收客户端
                    ret = recv(eparr[i].data.fd,buf,sizeof(buf),0);
                    if(-1 == ret)
                    {
                        strcpy(err_msg,"recv");
                        close(eparr[i].data.fd);
                        continue;
                    }
                    else if(0 == ret)
                    {
                        //客户端断开连接
                        close(eparr[i].data.fd);
                        printf("A Client has disconnected!\n");
                        epoll_ctl(epfd,EPOLL_CTL_DEL,eparr[i].data.fd,NULL);
                    }
                    else
                    {
                        puts(buf);
                        memset(buf,0,sizeof(buf));
                    }
                }
            }
        }

    }
    close(sockfd);
    return 0;

//错误处理
err :
    perror(err_msg);
    return -1;
} 

二、客户端和服务器通信(练习)

功能:登录+注册,用户数据保存在服务器路径下的login_data.txt

server.c

/*===============================================
*   文件名称:server.c
*   创 建 者:青木莲华
*   创建日期:2025年08月16日
*   描    述:简单登录/通信 Server(基于IPV4+TCP+epoll)
================================================*/
#include "socket_head.h"

//登录数据文件路径
#define PATH "./login_data.txt"
//最大并发数
#define MAXFDS 64
//端口号
#define PORT 31111
//ip地址
#define IP "192.168.6.174"

//账号信息结构体
typedef struct
{
    char username[20];
    char password[20];
}data_t;

//所有用户信息
data_t users[100];

//通信结构体
typedef struct
{
    //head
    //数据data的长度
    int length;
    //客户端想做的操作 :1.登录  2.正常发送数据
    short op;
    //body
    char data[128];
}msg_t;

//通信缓冲区
msg_t msg;

int server_fd = -1;                 //服务器套接字 文件描述符
int epfd = -1;                      //epoll 文件描述符
FILE *fp = NULL;                    //login_data.txt 文件指针
int count = 0;                      //用户信息个数
int ready = 0;                      //关注文件描述符 就绪数
struct sockaddr_in addr;            //地址结构体
socklen_t len;                      //客户端地址大小
struct epoll_event ev;              //epoll 结构体 用于EPOLL_CTL_ADD
struct epoll_event evs[MAXFDS];     //epoll 结构体数组 用于接收 epoll_wait
char buf[128] = {0};                //文件缓冲区

//全局错误处理
void error_exit(char *err_msg)
{

    if(-1 < server_fd)
        close(server_fd);
    if( NULL != fp)
        fclose(fp);
    if(-1 < epfd)
        close(epfd);

    perror(err_msg);
    exit(0);
}

/*
函数功能:查找账号密码是否正确
函数参数:name 账号  psw 密码
返回值  :成功返回 用户信息中的index
          失败返回 -1(账号或这密码错误)
*/
int login(char *name,char *psw)
{
    int i;
    for(i = 0 ; i < count ; i++)
    {
        //用户名和密码是否匹配
        if(0 == strcmp(users[i].username,name) && 0 == strcmp(users[i].password,psw))
                return i;
    }
    return -1;
}

/*
函数功能:注册(注册信息保存到login_data.txt)
函数参数:name 账号 psw 密码
返回值  :成功返回  1
          账号存在  0
          失败返回  -1
*/
int regist(char *name,char *psw)
{
    if(count+1 >= 100)
        return -1;
    
    //判断账号是否重复
    int i;
    for(i = 0 ; i < count ; i++)
    {
        if(0 == strcmp(users[i].username,name))
            return 0;
    }

    fp = fopen(PATH,"a");
    
    if(NULL == fp)
        return -1; 
    
    memset(buf,0,sizeof(buf));
    
    //写入文件内
    sprintf(buf,"%s : %s\n",name,psw);

    if(1 > fwrite(buf,strlen(buf),1,fp))
    {
        return -1;
    }

    fclose(fp);
    
    strcpy(users[count].username,name);
    strcpy(users[count].password,psw);
    count++;

    return 1;
}


//函数功能:创建套接字 --> 绑定 --> 监听
void server_init()
{
    //1.创建套接字
    server_fd = socket(AF_INET,SOCK_STREAM,0);
    
    if(-1 == server_fd)
        error_exit("socket");
    
    //2.初始化结构体
    addr.sin_family = AF_INET;
    addr.sin_port = htons(PORT);
    addr.sin_addr.s_addr = inet_addr(IP);

    //3.绑定套接字
    if(-1 == bind(server_fd,(struct sockaddr *)&addr,sizeof(addr)))
        error_exit("bind");
    
    //4.监听
    if(-1 == listen(server_fd,5))
        error_exit("listen");
    
    len = sizeof(addr);
    
    //5.初始化epoll
    epfd = epoll_create(1);
    if(-1 == epfd)
        error_exit("epoll_create");
    
    //6.添加server_fd 关注
    ev.events = EPOLLIN;
    ev.data.fd = server_fd;

    if(-1 == epoll_ctl(epfd,EPOLL_CTL_ADD,server_fd,&ev))
        error_exit("EPOLL_CTL_ADD");

    //7.1 打开login_data
    fp = fopen(PATH,"r");
    if(NULL == fp)
        error_exit("fopen");
    
    //7.2 读取所有用户信息 
    while(fgets(buf,128,fp))
    {
        sscanf(buf,"%s : %s\n",users[count].username,users[count].password);
        count++;
    }
    //7.3 关闭文件
    fclose(fp);
    fp = NULL;
}

/*
函数功能:通信接收函数,head+body
函数参数:接收的文件描述符
返回值  :成功返回1 / 断开连接返回0
          失败返回-1
*/
int recv_data(int fd)
{
    int ret = -1;
    //1. 每次通信前对通信缓冲区进行清空
    memset(&msg,0,sizeof(msg));

    ret = recv(fd,&(msg.length),4,0);
    //2.1 接收head 的 length参数
    if(-1 == ret)
        return -1;
    if(0 == ret)
        return 0;
    //2.2 转端序
    msg.length = ntohl(msg.length); 
    
    //3.1 接收head 的 op参数
    if(-1 == recv(fd,&(msg.op),2,0))
        return -1;
    //3.2 转端序
    msg.op = ntohs(msg.op);

    //4. 接收data数据
    if(-1 == recv(fd,&(msg.data),msg.length,0))
        return -1;
    
    return 1;
}
/*
函数功能:发送信息 head+body
函数参数:Client文件描述符
*/
void send_data(int fd)
{
    //发送字节数
    int len = msg.length + 6;
    //端序处理
    msg.length = htonl(msg.length);
    msg.op = htons(msg.op);
    
    send(fd,&msg,len,0);
}



int main(int argc, char *argv[])
{ 
    //1.调用服务器初始化函数
    server_init();
    printf("Server is waiting for connection now...\n");

    //2.开始建立通信
    while(1)
    {
        //2 epoll_wait 处理就绪
        ready = epoll_wait(epfd,evs,MAXFDS,-1);
        if(-1 == ready)
        {
            error_exit("epoll_wait");
        }
        //3 处理就绪文件描述符
        int i;
        for(i = 0 ; i < ready ; i++)
        {
            if(evs[i].events == EPOLLIN)
            {
                //3.1 如果是服务器套接字
                if(evs[i].data.fd == server_fd)
                {
                    //3.1 建立新连接
                    int connfd = accept(server_fd,(struct sockaddr*)&addr,&len);
                    
                    if(-1 == connfd)
                        error_exit("accept");

                    //3.2添加进epoll进行关注
                    ev.events = EPOLLIN;
                    ev.data.fd = connfd;
                    
                    if(-1 == epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev))
                        error_exit("EPOLL_CTL_ADD");
                    
                    printf("Client [%s:%d] has connected!\n",inet_ntoa(addr.sin_addr),ntohs(addr.sin_port));
                }
                else
                {
                    //4 接收客户端信息
                    int ret = recv_data(evs[i].data.fd);
                    if(-1 == ret)
                    {
                        close(evs[i].data.fd);
                        error_exit("recv");
                        continue;
                    }
                    else if(0 == ret)
                    {
                        //断开连接
                        if(-1 == epoll_ctl(epfd,EPOLL_CTL_DEL,evs[i].data.fd,NULL))
                            error_exit("EPOLL_CTL_DEL"); 
                        close(evs[i].data.fd);
                        printf("A Client has disconnected!\n");
                        continue;
                    }
                    //4.1 登录操作
                    if(msg.op == 1)
                    {
                        char name[20] = {0};
                        char psw[20] = {0};
                        //解析用户名和密码
                        sscanf(msg.data,"%s : %s",name,psw);
                        int flag = login(name,psw);
                        
                        if(-1 == flag)
                        {
                            //发送登录失败信息
                            strcpy(msg.data,"Server_msg : name or psw wrong.");
                            msg.length = strlen(msg.data);
                            send_data(evs[i].data.fd);
                            continue;
                        }
                        else
                        {
                            //发送登录成功信息
                            strcpy(msg.data,"Server_msg : welcome,");
                            strcat(msg.data,name);
                            msg.length = strlen(msg.data);
                            
                            send_data(evs[i].data.fd);
                            printf("Client[%d] %s is login!\n",evs[i].data.fd,name);
                            continue;
                        }
                    }
                    //4.2 正常通信操作
                    else if(msg.op == 2)
                    {
                        printf("Client[%d] : %s",evs[i].data.fd,msg.data);

                    }
                    else if(msg.op == 3)
                    {         
                        char name[20] = {0};
                        char psw[20] = {0};
                        //解析用户名和密码
                        sscanf(msg.data,"%s : %s",name,psw);
                        int flag = regist(name,psw);
                        
                        if(0 == flag)
                        {
                            //注册失败,用户名重复
                            strcpy(msg.data,"Server_msg : regist failed , the username has existed!");
                            msg.length = strlen(msg.data);
                            send_data(evs[i].data.fd);
                            continue;
                        }
                        else if(-1 == flag)
                        {
                            //发送登录成功信息
                            strcpy(msg.data,"Server_msg : regist failed , unknow error!");
                            msg.length = strlen(msg.data);
                            
                            send_data(evs[i].data.fd);
                            continue;
                        }
                        else if(1 == flag)
                        {
                            strcpy(msg.data,"Server_msg : regist successful!");
                            msg.length = strlen(msg.data);
                            send_data(evs[i].data.fd);
                            printf("Client[%d] is regist a name!\n",evs[i].data.fd);
                            continue;
                        }
                    }
                }
            }

        }

    }
    close(server_fd);
    return 0;
} 

client.c

/*===============================================
*   文件名称:client.c
*   创 建 者:青木莲华
*   创建日期:2025年08月17日
*   描    述:简单登录/通信 Client(基于IPV4+TCP+epoll)
================================================*/
#include "socket_head.h"

//端口号
#define PORT 31111
//ip地址
#define IP "192.168.6.174"

//通信结构体
typedef struct
{
    //head
    //数据data的长度
    int length;
    //客户端想做的操作 :1.登录  2.正常发送数据
    short op;
    //body
    char data[128];
}msg_t;

//通信缓冲区
msg_t msg;

//地址结构体
struct sockaddr_in addr;

//全局错误信息
char err_msg[32];
/*
函数功能:通信接收函数,head+body
函数参数:接收的文件描述符
返回值  :成功返回1 / 断开连接返回0
          失败返回-1
*/
int recv_data(int fd)
{
    int ret = -1;
    //1. 每次通信前对通信缓冲区进行清空
    memset(&msg,0,sizeof(msg));

    ret = recv(fd,&(msg.length),4,0);
    //2.1 接收head 的 length参数
    if(-1 == ret)
        return -1;
    if(0 == ret)
        return 0;
    //2.2 转端序
    msg.length = ntohl(msg.length); 
    
    //3.1 接收head 的 op参数
    if(-1 == recv(fd,&(msg.op),2,0))
        return -1;
    //3.2 转端序
    msg.op = ntohs(msg.op);

    //4. 接收data数据
    if(-1 == recv(fd,&(msg.data),msg.length,0))
        return -1;
    
    return 1;
}
/*
函数功能:发送信息 head+body
函数参数:Client文件描述符
*/
void send_data(int fd)
{
    //发送字节数
    int len = msg.length + 6;
    //端序处理
    msg.length = htonl(msg.length);
    msg.op = htons(msg.op);
    
    send(fd,&msg,len,0);
}

/*
函数功能:连接服务器
返回值  :成功返回 套接字文件描述符
          失败返回 -1
*/
int conn()
{
    //1.创建套接字  IPV4+TCP
    int sockfd = socket(AF_INET,SOCK_STREAM,0);    
    
    if(-1 == sockfd)
    {
        strcpy(err_msg,"socket");
        return -1;
    }
    //2.初始化结构体
    addr.sin_family = AF_INET;
    addr.sin_port = htons(PORT);
    addr.sin_addr.s_addr = inet_addr(IP);

    //3. 连接
    if(-1 == connect(sockfd,(struct sockaddr *)&addr,sizeof(addr)))
    {
        strcpy(err_msg,"connect");
        close(sockfd);
        return -1;
    }
    return sockfd;
}


int main(int argc, char *argv[])
{ 
    int fd = conn();        //套接字文件描述符
    if(-1 == fd)
    {
        printf("客户端错误: %s\n",err_msg);
        perror(err_msg);
        return -1;
    }
    
    short menu = 0;         //菜单选项
    char name[20] = {0};    //用户名
    char psw[20] = {0};     //密码
    int ret = -1;
    printf("--------------菜单--------------\n");
    printf("1.登录 2.发送数据 3.注册 4.退出\n");
    printf("--------------------------------\n");
    //通信
    while(1)
    {
        printf("信息 : 选择菜单\n");
        scanf("%hd",&menu);
        
        //清空通信缓冲区
        memset(&msg,0,sizeof(msg));
        
        switch(menu)
        {
            case 1:
            {
                memset(name,0,sizeof(name));
                memset(psw,0,sizeof(psw));

                printf("请输入用户名 : ");
                scanf("%19s",name);
                printf("请输入密码 : ");
                scanf("%19s",psw);
            
                sprintf(msg.data,"%s : %s",name,psw);
                msg.length = strlen(msg.data);
                msg.op = 1;

                //发送数据
                send_data(fd);

                //接收数据
                ret = recv_data(fd);
                
                if(-1 == ret)
                {
                    close(fd);
                    perror("recv");
                    return -1;
                }
                puts(msg.data); 
                printf("------------------------\n");
                break;
            }
            case 2:
            {
                getc(stdin);
                fgets(msg.data,128,stdin);
                msg.length = strlen(msg.data);
                msg.op = 2;
                send_data(fd);
                printf("------------------------\n");
                break;
            }
            case 3:
            { 
                memset(name,0,sizeof(name));
                memset(psw,0,sizeof(psw));

                printf("请输入用户名 : ");
                scanf("%19s",name);
                printf("请输入密码 : ");
                scanf("%19s",psw);
            
                sprintf(msg.data,"%s : %s",name,psw);
                msg.length = strlen(msg.data);
                msg.op = 3;

                //发送数据
                send_data(fd);

                //接收数据
                ret = recv_data(fd);
                
                if(-1 == ret)
                {
                    close(fd);
                    perror("recv");
                    return -1;
                }
                puts(msg.data); 
                printf("------------------------\n");
                break;
            }
            case 4:
            {   
                close(fd);
                printf("信息 : 退出成功\n");
                return 0;
            }
        }
    }
    return 0;
} 

运行截图

alt