Linux下IO多路复用—select,poll,epoll

一.概述

1.IO多路复用介绍

  IO多路复用是一种操作系统的技术,用于在单个线程或进程中管理多个输入输出操作。它的主要目的是通过将多个IO操作合并到一个系统调用中来提高系统的性能和资源利用率,避免了传统的多线程或多进程模型中因为阻塞IO而导致的资源浪费和低效率问题。

在IO多路复用中,通常使用的系统调用有 select()、poll()、epoll() 等,它们允许程序等待多个文件描述符(sockets、文件句柄等)中的任何一个变为可读或可写,然后再进行实际的IO操作。这种模型相比于传统的多线程或多进程模型,具有更高的并发处理能力和更低的系统开销。

2.应用

多路复用I/O就是我们说的 select,poll,epoll 等操作,复用的好处就在于 单个进程 就可以同时处理 多个 网络连接的I/O,能实现这种功能的原理就是 select、poll、epoll 等函数会不断的 轮询 它们所负责的所有 socket ,当某个 socket 有数据到达了,就通知用户进程。

一般来说I/O复用多用于以下情况:

当客户处理多个描述符时。

服务器在高并发处理网络连接的时候。

服务器既要处理监听套接口,又要处理已连接套接口,一般也要用到I/O复用。

如果一个服务器即要处理TCP,又要处理UDP,一般要使用I/O复用。

如果一个服务器要处理多个服务或多个协议,一般要使用I/O复用。

与多进程和多线程技术相比, I/O多路复用技术的最大优势是系统开销小,系统不必创建进程/线程,也不必维护这些进程/线程 ,从而大大减小了系统的开销。但select,poll,epoll本质上都是同步I/O,因为他们都需要 在读写事件就绪后自己负责进行读写 ,也就是说这个读写过程是 阻塞 的,而 异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间 。

二.select原理及使用

1.select介绍

系统提供select函数来实现多路复用输入/输出模型。

select系统调用是用来让我们的程序监视多个文件描述符的状态变化的;

程序会停在select这里等待,直到被监视的文件描述符有一个或多个发生了状态改变。

注意:

select函数在Linux系统中最多接入的fd(文件描述符)数量确实受到一定限制,这个限制通常是1024个

2.select的函数使用 

int select(int nfds, fd_set *readfds, fd_set *writefds,

  fd_set *exceptfds, struct timeval *timeout);

函数参数:

nfds:是需要监视的最大的文件描述符值+1。

readfds:需要检测的可读文件描述符的集合。

writefds:需要检测的可写文件描述符的集合。

exceptfds:需要检测的异常文件描述符的集合。

timeout:为结构体timeval,用来设置select()的等待时间;

当timeout等于NULL:则表示select()没有timeout,select将一直被阻塞,直到某个文件描述符上发生了事件;

当timeout为0:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生。

当timeout为特定的时间值:如果在指定的时间段里没有事件发生,select将超时返回。

其中的可读,可写,异常文件描述符的集合是一个fd_set类型,fd_set是系统提供的位图类型,位图的位置是否是1,表示是否关系该事件。

例如:

输入时:假如我们要关心 0 1 2 3 文件描述符

0000 0000->0000 1111 比特位的位置,表示文件描述符的编号

 比特位的内容 0or1 表示是否需要内核关心

输出时:

0000 0100->此时表示文件描述符的编号

 比特位的内容 0or1哪些用户关心的fd 上面的读事件已经就绪了,这里表示2描述符就绪了

系统提供了关于fd_set的接口,便于我们使用位图:

 void FD_CLR(int fd, fd_set *set); // 用来清除描述词组set中相关fd 的位

 int FD_ISSET(int fd, fd_set *set); // 用来测试描述词组set中相关fd 的位是否为真

 void FD_SET(int fd, fd_set *set); // 用来设置描述词组set中相关fd的位

 void FD_ZERO(fd_set *set); // 用来清除描述词组set的全部位

函数返回值:

执行成功则返回文件描述词状态已改变的个数。

如果返回0代表在描述词状态改变前已超过timeout时间,没有返回。

当有错误发生时则返回-1,错误原因存于errno,此时参数readfds,writefds, exceptfds和timeout的值变成不可预测。

错误值可能为:

EBADF 文件描述词为无效的或该文件已关闭

EINTR 此调用被信号所中断

EINVAL 参数n 为负值。

ENOMEM 核心内存不足

select的执行过程:

(1)执行fd_set set; FD_ZERO(&set);则set用位表示是0000,0000。

(2)若fd=5,执行FD_SET(fd,&set);后set变为0001,0000(第5位置为1) 。

(3)若再加入fd=2,fd=1,则set变为0001,0011 。

(4)执行select(6,&set,0,0,0)阻塞等待,表示最大文件描述符+1是6,监控可读事件,立即返回。

(5)若fd=1,fd=2上都发生可读事件,则select返回,此时set变为0000,0011。注意:没有事件发生的fd=5被清空。

3.select的优缺点

select的特点:

(1)可监控的文件描述符个数取决与sizeof(fd_set)的值。一般大小是1024,但是fd_set的大小可以调整。

(2)将fd加入select监控集的同时,还要再使用一个数据结构array保存放到select监控集中的fd。

     a.是用于再select 返回后,array作为源数据和fd_set进行FD_ISSET判断。

     b.是select返回后会把以前加入的但并无事件发生的fd清空,则每次开始select前都要重新从array取得fd逐一加入(FD_ZERO最先),扫描array的同时取得fd最大值maxfd,用于select的第一个参数。

select缺点

(1)每次调用select, 都需要手动设置fd集合, 从接口使用角度来说也非常不便。

(2)每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大。

(3)同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大。

(4)select支持的文件描述符数量太小。

4.select使用代码:

#pragma once

#include <iostream>

#include <sys/select.h>

#include <sys/time.h>

#include "Socket.hpp"

using namespace std;

static const uint16_t defaultport = 888;

static const int fd_num_max = (sizeof(fd_set) * 8);

int defaultfd = -1;

class SelectServer

{

public:

    SelectServer(uint16_t port = defaultport) : _port(port)

    {

        for (int i = 0; i < fd_num_max; i++)

        {

            fd_array[i] = defaultfd;

            // std::cout << "fd_array[" << i << "]" << " : " << fd_array[i] << std::endl;

        }

    }

    bool Init()

    {

        _listensock.Socket();

        _listensock.Bind(_port);

        _listensock.Listen();

        return true;

    }

    void Accepter()

    {

        // 我们的连接事件就绪了

        std::string clientip;

        uint16_t clientport = 0;

        int sock = _listensock.Accept(&clientip, &clientport); // 会不会阻塞在这里?不会

        if (sock < 0) return;

        lg(Info, "accept success, %s: %d, sock fd: %d", clientip.c_str(), clientport, sock);

        // sock -> fd_array[]

        int pos = 1;

        for (; pos < fd_num_max; pos++) // 第二个循环

        {

            if (fd_array[pos] != defaultfd)

                continue;

            else

                break;

        }

        if (pos == fd_num_max)

        {

            lg(Warning, "server is full, close %d now!", sock);

            close(sock);

        }

        else

        {

            fd_array[pos] = sock;

            PrintFd();

            // TODO

        }

    }

    void Recver(int fd, int pos)

    {

        // demo

        char buffer[1024];

        ssize_t n = read(fd, buffer, sizeof(buffer) - 1); // bug?

        if (n > 0)

        {

            buffer[n] = 0;

            cout << "get a messge: " << buffer << endl;

        }

        else if (n == 0)

        {

            lg(Info, "client quit, me too, close fd is : %d", fd);

            close(fd);

            fd_array[pos] = defaultfd; // 这里本质是从select中移除

        }

        else

        {

            lg(Warning, "recv error: fd is : %d", fd);

            close(fd);

            fd_array[pos] = defaultfd; // 这里本质是从select中移除

        }

    }

    void Dispatcher(fd_set &rfds)

    {

        for (int i = 0; i < fd_num_max; i++) // 这是第三个循环

        {

            int fd = fd_array[i];

            if (fd == defaultfd)

                continue;

            if (FD_ISSET(fd, &rfds))

            {

                if (fd == _listensock.Fd())

                {

                    Accepter(); // 连接管理器

                }

                else // non listenfd

                {

                    Recver(fd, i);

                }

            }

        }

    }

    void Start()

    {

        int listensock = _listensock.Fd();

        fd_array[0] = listensock;

        for (;;)

        {

            fd_set rfds;

            FD_ZERO(&rfds);

            int maxfd = fd_array[0];

            for (int i = 0; i < fd_num_max; i++) // 第一次循环

            {

                if (fd_array[i] == defaultfd)

                    continue;

                FD_SET(fd_array[i], &rfds);

                if (maxfd < fd_array[i])

                {

                    maxfd = fd_array[i];

                    lg(Info, "max fd update, max fd is: %d", maxfd);

                }

            }

            // accept?不能直接accept!检测并获取listensock上面的事件,新连接到来,等价于读事件就绪

            // struct timeval timeout = {1, 0}; // 输入输出,可能要进行周期的重复设置

            struct timeval timeout = {0, 0}; // 输入输出,可能要进行周期的重复设置

            // 如果事件就绪,上层不处理,select会一直通知你!

            // select告诉你就绪了,接下来的一次读取,我们读取fd的时候,不会被阻塞

            // rfds: 输入输出型参数。 1111 1111 -> 0000 0000

            int n = select(maxfd + 1, &rfds, nullptr, nullptr, /*&timeout*/ nullptr);

            switch (n)

            {

            case 0:

                cout << "time out, timeout: " << timeout.tv_sec << "." << timeout.tv_usec << endl;

                break;

            case -1:

                cerr << "select error" << endl;

                break;

            default:

                // 有事件就绪了,TODO

                cout << "get a new link!!!!!" << endl;

                Dispatcher(rfds); // 就绪的事件和fd你怎么知道只有一个呢???

                break;

            }

        }

    }

    void PrintFd()

    {

        cout << "online fd list: ";

        for (int i = 0; i < fd_num_max; i++)

        {

            if (fd_array[i] == defaultfd)

                continue;

            cout << fd_array[i] << " ";

        }

        cout << endl;

    }

    ~SelectServer()

    {

        _listensock.Close();

    }

private:

    Sock _listensock;

    uint16_t _port;

    int fd_array[fd_num_max];   // 数组, 用户维护的!

    // int wfd_array[fd_num_max];

};

三.poll原理及应用

1.poll的概念

poll和select实现原理基本类似,

poll只为了解决select的两个硬伤:

a.等待的fd是有上限的,(底层类似链表储存实现,而不是位图)

b.每次要对关心的fd进行事件重置,(pollfd结构包含了要监视的event和发生的event,使用前后不用初始化fd_set)

2.poll的函数使用

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

// pollfd结构

struct pollfd {

int fd; /* file descriptor */

short events; /* requested events */

short revents; /* returned events */

};

函数参数解释:

fds:是一个poll函数监听的结构列表. 每一个元素中, 包含了三部分内容: 文件描述符, 监听的事件集合, 返回的事件集合。

nfds:表示fds数组的长度。

timeout:表示poll函数的超时时间, 单位是毫秒(ms)。

返回结果:

返回值小于0, 表示出错。

返回值等于0, 表示poll函数等待超时。

返回值大于0, 表示poll由于监听的文件描述符就绪而返回。

3.poll的优缺点

poll的优点

(1)pollfd结构包含了要监视的event和发生的event,不再使用select“参数-值”传递的方式. 接口使用比 select更方便。

(2)poll并没有最大数量限制 (但是数量过大后性能也是会下降)。

poll的缺点

(1)和select函数一样,poll返回后,需要轮询pollfd来获取就绪的描述符。

(2)每次调用poll都需要把大量的pollfd结构从用户态拷贝到内核中。

(3)同时连接的大量客户端在一时刻可能只有很少的处于就绪状态, 因此随着监视的描述符数量的增长, 其效率也会线性下降。

4.poll函数使用案例: 应用在TCP客户端

#include <stdio.h>

#include <unistd.h>

#include <string.h>

#include <sys/types.h>

#include <sys/stat.h>

#include <fcntl.h>

#include <dirent.h>

#include <stdlib.h>

#include <pthread.h>

#include <semaphore.h>

#include <signal.h>

#include <sys/socket.h>

#include <arpa/inet.h>

#include <netinet/in.h>

#include <pthread.h>

#include <sys/select.h>

#include <sys/time.h>

#include <poll.h>

//消息结构体

struct MSG_DATA

{

    char type; //消息类型.  0表示有聊天的消息数据  1表示好友上线  2表示好友下线

    char name[50]; //好友名称

    int number;   //在线人数的数量

    unsigned char buff[100];  //发送的聊天数据消息

};

struct MSG_DATA msg_data;

//文件接收端

int main(int argc,char **argv)

{   

    if(argc!=4)

    {

        printf("./app  <IP地址> <端口号> <名称>\n");

        return 0;

    }

    int sockfd;

    //忽略 SIGPIPE 信号--方式服务器向无效的套接字写数据导致进程退出

    signal(SIGPIPE,SIG_IGN);

    /*1. 创建socket套接字*/

    sockfd=socket(AF_INET,SOCK_STREAM,0);

    /*2. 连接服务器*/

    struct sockaddr_in addr;

    addr.sin_family=AF_INET;

    addr.sin_port=htons(atoi(argv[2])); // 端口号0~65535

    addr.sin_addr.s_addr=inet_addr(argv[1]); //IP地址

    if(connect(sockfd,(const struct sockaddr *)&addr,sizeof(struct sockaddr_in))!=0)

    {

        printf("客户端:服务器连接失败.\n");

        return 0;

    }

    /*3. 发送消息表示上线*/

    msg_data.type=1;

    strcpy(msg_data.name,argv[3]);

    write(sockfd,&msg_data,sizeof(struct MSG_DATA));

    int cnt;

    struct pollfd fds[2];

    fds[0].fd=sockfd;

    fds[0].events=POLLIN;

    fds[1].fd=0;

    fds[1].events=POLLIN;

    

    while(1)

    {

       //监听事件

       cnt=poll(fds,2,-1);

        if(cnt)

        {

            if(fds[0].events&fds[0].revents) //判断收到服务器的消息

            {

                cnt=read(sockfd,&msg_data,sizeof(struct MSG_DATA));

                if(cnt<=0) //判断服务器是否断开了连接

                {

                    printf("服务器已经退出.\n");

                    break;

                }

                else if(cnt>0)

                {

                    if(msg_data.type==0)

                    {

                        printf("%s:%s  在线人数:%d\n",msg_data.name,msg_data.buff,msg_data.number);

                    }

                    else if(msg_data.type==1)

                    {

                        printf("%s 好友上线. 在线人数:%d\n",msg_data.name,msg_data.number);

                    }

                    else if(msg_data.type==2)

                    {

                        printf("%s 好友下线. 在线人数:%d\n",msg_data.name,msg_data.number);

                    }

                }

            }

            

            if(fds[1].events&fds[1].revents)  //判断键盘上有输入

            {

                gets(msg_data.buff); //读取键盘上的消息

                msg_data.type=0; //表示正常消息

                strcpy(msg_data.name,argv[3]); //名称

                write(sockfd,&msg_data,sizeof(struct MSG_DATA));

            }

        }

    }

    close(sockfd);

    return 0;

}

四.epoll原理及应用

1.epoll的概念

epoll: 是为处理大批量句柄而作了改进的poll(真的是大改进)。

epoll是IO多路复用技术,在实现上维护了一个用于返回触发事件的Socket的链表和一个记录监听事件的红黑树,epoll的高效体现在:

(1)对监听事件的修改是 log N(红黑树)。

(2)用户程序无需遍历所有的Socket(发生事件的Socket被放到链表中直接返回)。

(3)内核无需遍历所有的套接字,内核使用回调函数在事件发生时直接转到对应的处理函数。

2.epoll的函数使用

epoll 有3个相关的系统调用:

int epoll_create(int size);

创建一个epoll的句柄,自从linux2.6.8之后,size参数是被忽略的,用完之后, 必须调用close()关闭。

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

epoll的事件注册函数:

它不同于select()是在监听事件时告诉内核要监听什么类型的事件, 而是在这里先注册要监听的事件类型。

第一个参数是epoll_create()的返回值(epoll的句柄)。

第二个参数表示动作,用三个宏来表示。

第三个参数是需要监听的fd。

第四个参数是告诉内核需要监听什么事。

第二个参数的取值:

EPOLL_CTL_ADD :注册新的fd到epfd中。

EPOLL_CTL_MOD :修改已经注册的fd的监听事件。

EPOLL_CTL_DEL :从epfd中删除一个fd。

struct epoll_event结构如下:

typedef union epoll_data

{

void *ptr;

int fd;

uint32 t u32;

uint64 t u64;

} epoll_data t;

struct epoll_event

{

uint32 t events;

epoll_data_t data;

}EPOLL_PACKED;

events可以是以下几个宏的集合:

EPOLLIN : 表示对应的文件描述符可以读 (包括对端SOCKET正常关闭)。

EPOLLOUT : 表示对应的文件描述符可以写。

EPOLLPRI : 表示对应的文件描述符有紧急的数据可读 (这里应该表示有带外数据到来)。

EPOLLERR : 表示对应的文件描述符发生错误。

EPOLLHUP : 表示对应的文件描述符被挂断。

EPOLLET : 将EPOLL设为边缘触发(Edge Triggered)模式, 这是相对于水平触发(Level Triggered)来说的。

EPOLLONESHOT:只监听一次事件, 当监听完这次事件之后, 如果还需要继续监听这个socket的话, 需要再次把这个socket加入到EPOLL队列里。

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

收集在epoll监控的事件中已经发送的事件:

参数events是分配好的epoll_event结构体数组。

epoll将会把发生的事件赋值到events数组中 (events不可以是空指针,内核只负责把数据复制到这个events数组中,不会去帮助我们在用户态中分配内存)。

maxevents告之内核这个events有多大,这个 maxevents的值不能大于创建epoll_create()时的size。

参数timeout是超时时间 (毫秒,0会立即返回,-1是永久阻塞)。

如果函数调用成功,返回对应I/O上已准备好的文件描述符数目,如返回0表示已超时, 返回小于0表示函数失败。

3.epoll原理

(1)当某一进程调用epoll_create方法时,Linux内核会创建一个eventpoll结构体,这个结构体中有两个成员与epoll的使用方式密切相关。

(2)每一个epoll对象都有一个独立的eventpoll结构体,用于存放通过epoll_ctl方法向epoll对象中添加进来的事件。

(3)这些事件都会挂载在红黑树中,如此,重复添加的事件就可以通过红黑树而高效的识别出来(红黑树的插入时间效率是lgn,其中n为树的高度)。

(4)而所有添加到epoll中的事件都会与设备(网卡)驱动程序建立回调关系,也就是说,当响应的事件发生时会调用这个回调方法。

(5)这个回调方法在内核中叫ep_poll_callback,它会将发生的事件添加到rdlist双链表中。

(6)在epoll中,对于每一个事件,都会建立一个epitem结构体。

(7)当调用epoll_wait检查是否有事件发生时,只需要检查eventpoll对象中的rdlist双链表中是否有epitem元素即可。

(8)如果rdlist不为空,则把发生的事件复制到用户态,同时将事件数量返回给用户. 这个操作的时间复杂度是O(1)。

struct eventpoll{

....

/*红黑树的根节点,这颗树中存储着所有添加到epoll中的需要监控的事件*/

struct rb_root rbr;

/*双链表中则存放着将要通过epoll_wait返回给用户的满足条件的事件*/

struct list_head rdlist;

....

};

struct epitem{

struct rb_node rbn;//红黑树节点

struct list_head rdllink;//双向链表节点

struct epoll_filefd ffd; //事件句柄信息

struct eventpoll *ep; //指向其所属的eventpoll对象

struct epoll_event event; //期待发生的事件类型

}

4.总结一下, epoll的使用过程简单看就三步:

(1)调用epoll_create创建一个epoll句柄。

(2)调用epoll_ctl, 将要监控的文件描述符进行注册。

(3)调用epoll_wait, 等待文件描述符就绪。

5.epoll的优点

(1)接口使用方便: 虽然拆分成了三个函数,但是反而使用起来更方便高效,不需要每次循环都设置关注的文件描述符,也做到了输入输出参数分离开。

(2)数据拷贝轻量: 只在合适的时候调用 EPOLL_CTL_ADD 将文件描述符结构拷贝到内核中,这个操作并不频繁(而select/poll都是每次循环都要进行拷贝)。

(3)事件回调机制: 避免使用遍历,而是使用回调函数的方式,将就绪的文件描述符结构加入到就绪队列中,epoll_wait 返回直接访问就绪队列就知道哪些文件描述符就绪,这个操作时间复杂度O(1),即使文件描述符数目很多,效率也不会受到影响。

(4)没有数量限制: 文件描述符数目无上限。

6. epoll工作模式

epoll默认:LT模式,事件到来但是上层不处理,高电平,一直有效。

           ET模式,数据或者连接,从无到有,从有到多,变化的时候才通知我们一次。

           

ET的通知效率更高:倒逼程序员,每次通知都必须把本轮的数据取走 -> 循环读取,读取错误 -> fd默认是阻塞的 -> ET,所有的fd必须是非阻塞的。

ET的IO效率也更高 -> tcp会向对方通告一个更大的窗口,从而概率上让对方一次给我发生更多数据,如果LT每次也可以就绪,那效率差不多。

本质就是向就绪队列,添加一次或者是多次就绪节点。

7. epoll函数使用案例: 应用TCP客户端

#include <stdio.h>

#include <unistd.h>

#include <string.h>

#include <sys/types.h>

#include <sys/stat.h>

#include <fcntl.h>

#include <dirent.h>

#include <stdlib.h>

#include <pthread.h>

#include <semaphore.h>

#include <signal.h>

#include <sys/socket.h>

#include <arpa/inet.h>

#include <netinet/in.h>

#include <pthread.h>

#include <sys/select.h>

#include <sys/time.h>

#include <poll.h>

#include <sys/epoll.h>

//消息结构体

struct MSG_DATA

{

    char type; //消息类型.  0表示有聊天的消息数据  1表示好友上线  2表示好友下线

    char name[50]; //好友名称

    int number;   //在线人数的数量

    unsigned char buff[100];  //发送的聊天数据消息

};

struct MSG_DATA msg_data;

#define MAX_EVENTS 10

struct epoll_event ev, events[MAX_EVENTS];

int epollfd;

int nfds;

//文件接收端

int main(int argc,char **argv)

{   

    if(argc!=4)

    {

        printf("./app  <IP地址> <端口号> <名称>\n");

        return 0;

    }

    int sockfd;

    //忽略 SIGPIPE 信号--方式服务器向无效的套接字写数据导致进程退出

    signal(SIGPIPE,SIG_IGN);

    /*1. 创建socket套接字*/

    sockfd=socket(AF_INET,SOCK_STREAM,0);

    /*2. 连接服务器*/

    struct sockaddr_in addr;

    addr.sin_family=AF_INET;

    addr.sin_port=htons(atoi(argv[2])); // 端口号0~65535

    addr.sin_addr.s_addr=inet_addr(argv[1]); //IP地址

    if(connect(sockfd,(const struct sockaddr *)&addr,sizeof(struct sockaddr_in))!=0)

    {

        printf("客户端:服务器连接失败.\n");

        return 0;

    }

    /*3. 发送消息表示上线*/

    msg_data.type=1;

    strcpy(msg_data.name,argv[3]);

    write(sockfd,&msg_data,sizeof(struct MSG_DATA));

    int cnt;

    int i;

    //创建专用文件描述符

    epollfd = epoll_create(10);

    //添加要监听的文件描述符

    ev.events = EPOLLIN;

    ev.data.fd = sockfd;

    epoll_ctl(epollfd, EPOLL_CTL_ADD, sockfd, &ev);

    ev.events = EPOLLIN;

    ev.data.fd = 0; //标准输入文件描述符

    epoll_ctl(epollfd, EPOLL_CTL_ADD, 0, &ev);

    while(1)

    {

        //监听事件

        nfds=epoll_wait(epollfd,events,MAX_EVENTS,-1);

        if(nfds)

        {

            for(i=0;i<nfds;i++)

            {

                if(events[i].data.fd==sockfd) //判断收到服务器的消息

                {

                    cnt=read(sockfd,&msg_data,sizeof(struct MSG_DATA));

                    if(cnt<=0) //判断服务器是否断开了连接

                    {

                        printf("服务器已经退出.\n");

                        goto SERVER_ERROR;

                    }

                    else if(cnt>0)

                    {

                        if(msg_data.type==0)

                        {

                            printf("%s:%s  在线人数:%d\n",msg_data.name,msg_data.buff,msg_data.number);

                        }

                        else if(msg_data.type==1)

                        {

                            printf("%s 好友上线. 在线人数:%d\n",msg_data.name,msg_data.number);

                        }

                        else if(msg_data.type==2)

                        {

                            printf("%s 好友下线. 在线人数:%d\n",msg_data.name,msg_data.number);

                        }

                    }

                }

                else if(events[i].data.fd==0) //表示键盘上有数据输入

                {

                    gets(msg_data.buff); //读取键盘上的消息

                    msg_data.type=0; //表示正常消息

                    strcpy(msg_data.name,argv[3]); //名称

                    write(sockfd,&msg_data,sizeof(struct MSG_DATA));

                }

            }

        }

    }

SERVER_ERROR:

    close(sockfd);

    return 0;

}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/411762.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

在Linux下搭建go环境

下载go go官网&#xff1a;All releases - The Go Programming Language 我们可以吧压缩包下载到Windows上再传到Linux上&#xff0c;也可以直接web下载&#xff1a; wget https://golang.google.cn/dl/go1.23.0.linux-amd64.tar.gz 解压 使用命令解压&#xff1a; tar -x…

解决有向图中节点出度和入度计算问题

解决有向图中节点出度和入度计算问题 引言邻接链表表示法邻接链表的数据结构创建图添加边计算节点的出度伪代码C代码计算节点的入度伪代码C代码时间复杂度示例结论引言 在图论中,有向图是一种重要的数据结构,用于表示元素之间的方向性关系。有向图中的节点(顶点)通过边连接…

VBA之正则表达式(47)-- 快速将公式转换为静态值计算

实例需求&#xff1a;工作表I列包含多种计算公式&#xff0c;为了便于演示&#xff0c;将I列公式显示在J列单元格中&#xff0c;现在需要将公式的单元格引用转换为静态值&#xff0c;如K列所示。 示例代码如下。 Sub RegExpDemoReplace()Dim Res()Dim objRegEx As ObjectDim o…

[解决]Invalid configuration `aarch64-openwrt-linux‘: machine `aarch64-openwrt

背景 交叉编译libev-4.19 问题 checking host system type… Invalid configuration aarch64-openwrt-linux: machine aarch64-openwrt’ not recognized 解决 打开config.sub&#xff0c;在244行后添加"| aarch64-openwrt \ "

Git学习(001 git介绍以及安装)

尚硅谷2024最新Git企业实战教程&#xff0c;全方位学习git与gitlab 总时长 5:42:00 共40P 此文章包含第1p-第p4的内容 文章目录 介绍Git介绍GitLab介绍 概述Git安装版本控制工具介绍 介绍 Git介绍 GitLab介绍 相当于中央仓库 概述 Git安装 进入官网(下载当前版本 2.43.0) …

解决 RT-Thread bsp stm32l476-st-nucleo STM32L4 HAL库缺失问题

问题描述 当前最新的 RT-Thread 版本&#xff1a;5.2.0&#xff0c;发现在 编译 BSP stm32l476-st-nucleo&#xff0c;缺少了 STM32L4xx_HAL 驱动库&#xff0c;造成生成的 工程&#xff0c;如 Keil MDK5 工程无法编译通过 初步的【临时】解决方法是 回退 RT-Thread 的版本&am…

rabbitmq发送的消息接收不到

1.消息被其他消费者消费 2.主要说的2这种情况&#xff0c;就是在延迟队列中&#xff0c;忘记给一个bean加注解导致日志报exchange not found. 这个报错&#xff0c;进而引发了bindings没有绑定。没有绑定的话&#xff0c;发送消息就会接收不到。

心脑血管科曹启富医生谈:引起高血压的原因

曹医生指出&#xff0c;高血压这一日益普遍的健康问题&#xff0c;其根源深藏于多重复杂因素之中。首要提及的便是年龄因素&#xff0c;它如同时间的刻度&#xff0c;悄然影响着我们的血管健康。随着年龄的增长&#xff0c;血管逐渐失去往昔的弹性与活力&#xff0c;变得僵硬而…

HTMl标签;知识回忆;笔记分享;

HTML标签是用于定义和组织网页内容的基础构建块。每个标签都有特定的作用。 一&#xff0c;标准结构标签&#xff1a; HTML文档标准结构&#xff1a; <html><head></head><body>this is my second html... </body> </html> 【1】htm…

python-FastApi框架

文章目录 FastApi一. 简介二. 特性三. 安装1. 安装fastapi模块2. 安装ASGI服务器( Uvicorn 或者 Hypercorn) 四. 实例1. 创建**main.py**文件(GET请求)2. 运行3. 测试4. 更新main_py(加入PUT请求) 五. 自动化API文档1. Swagger UI(交互式文档)2. ReDoc(可选式文档) FastApi 一…

企微获客链接 中文乱码问题处理

企微获客链接 中文乱码问题处理 问题背景问题处理补充内容 问题背景 为了推广产品&#xff0c;同时更好的服务客户&#xff0c;公司在接入企业微信后&#xff0c;需要用到企微获客链接相关推广操作&#xff0c;那么通过API 接口创建企微获客链接时&#xff0c;出现了中文乱码问…

OpenCV 图像处理基础算法介绍c++

VS2022配置OpenCV环境 关于OpenCV在VS2022上配置的教程可以参考&#xff1a;VS2022 配置OpenCV开发环境详细教程 图像处理 图像处理是一个广泛的领域&#xff0c;它涉及到对图像数据进行分析、修改和改进的各种技术。以下是一些基本的图像处理操作&#xff0c;这些操作通常可…

AntV G6 的坑之——渲染残留/残影

G6 4.x 依赖的渲染引擎 antv/g4.x 版本支持了局部渲染&#xff0c;带了性能提升的同时&#xff0c;也带来了图形更新时可能存在渲染残影的问题。比如拖拽节点时&#xff0c;节点的文本会留下轨迹。 解决办法&#xff1a; 关闭局部渲染&#xff0c;graph.get("canvas"…

uni-app组件

一. 什么是组件,有什么好处? 在uni-app中&#xff0c;组件是构成应用的基本单位&#xff0c;它们是用来定义用户界面的一部分&#xff0c;并且通常包含了视图和逻辑。组件的设计使得开发者能够以声明式的方式构建应用界面&#xff0c;并且通过组件化的开发方式来提高代码的复…

Centos7 java安装

卸载自带 jdk 查询所有的 java( 使用最小配置命令行则查询不出 java 直接进行安装即可 ) 卸载以下4个java软件 删除之后在使用命令进行查询 rpm -qa | grep java 解压jdk&#xff1a; 找到下载位置 使用Ctrlf搜索 配置环境变量&#xff1a; 打开/ etc/profile 在末尾添加以下…

旅游行业怎么利用C#接口发送短信

旅游企业一般拥有众多的分支机构&#xff0c;同时各地分支机构又有众多下属分散在当地各区的旅游营业报名点&#xff0c;以前传统的解决方案是采用专线、MODEM拔号等方式&#xff0c;专线的成本很高&#xff0c;MODEM拔号更费时&#xff0c;且长途拔号互联成本在多点情况下费用…

【一起学Rust | 框架篇 | Tauri2.0框架】rust和前端的相互调用(前端调用rust)

文章目录 前言1. 前端调用rust&#xff08;command&#xff09;1. 在后端定义一个command2. 注册command3. 前端调用command 2. 前端调用rust&#xff08;event&#xff09;4. command完整实例 前言 本期将继续接着上一期&#xff0c;继续探索tauri中rust和前端的相互调用&…

【ceph学习】ceph如何进行数据的读写(1)

版本 ceph版本为17. ceph如何进行读写接口的实现 Ceph的客户端通过librados的接口进行集群的访问&#xff0c;这里的访问包括&#xff1a; 1&#xff09;对集群的整体访问 2&#xff09;对象的访问 两类接口&#xff0c;这套接口&#xff08;API&#xff09;包括C、C和Pytho…

XXE-labs靶场通关攻略

环境地址自行查找 1.寻找靶机地址 使用工具goby进行扫描 因为我的靶场是搭在ubuntu上 直接查找系统是Ubuntu的就可以找到 靶机IP 172.16.1.183 2.访问靶场 3.使用目录扫描工具进行扫描 使用kali自带的dirsearch进行扫描 可以看到一个robots.txt文件 4.访问robots.txt文件 …

发顶会首选:具身智能!新成果直接霸榜CVPR

最近无论是斯坦福机器人炒虾&#xff0c;还是特斯拉官宣机器人进厂&#xff0c;都赚足了眼球&#xff0c;实力证明了具身智能的火爆。 先不说具身智能是实现AGI的关键环节&#xff0c;也是未来研究的重要方向&#xff0c;我们就从发论文的角度来看&#xff0c;今年的各大顶会&…