IO多路复用之---select----poll----epoll

五种IO模型

1、阻塞式IO:
recv recvfrom read 读文件描述符当文件描述符里面没有数据则阻塞式等待。等待的时候这个等待的线程/进程被挂起。

2、非阻塞式IO:
轮询式。 recvfrom read recv 函数通过设置参数 不停的查看内核数据是否准备好。
调用recvfrom 如果没有数据 返回特定错误码 EWOULDBLOOK
在应用程序中循环调用recvfrom。知道数据准备好了循环调用停止。
优点:可以省下时间处理别的事物。 缺点:代码复杂。CPU浪费较大。

3、信号驱动IO:
信号驱动式IO 当内核缓冲区的数据准备好了以后 发SIGIO信号通知用户应用程序进行IO操作
sigaction函数注册一个SIGIO的信号处理函数,捕捉这个信号。当内核缓冲区中有数据了SIGIO信号发送给进程
进程信号处理函数调用recvfrom读数据。
有点:代码简单节省等待时间。 缺点:信号处理函数处理SIGIO信号的时候整个进程的所有线程都阻塞了。

4、异步IO:
调用aio_read,让内核等数据准备好,并且复制到用户进程空间后执行事先指定好的函数。由内核在数据拷贝完成之后通知进程/线程。

异步IO适用于服务器集群之间的请求和应答, 因为在服务器集群中不会在短时间内有大量的client 连接server
关于异步IO的资料:
https://blog.csdn.net/Shreck66/article/details/48765533

5、IO多路复用:

什么是IO多路复用

IO多路复用: 一个线程/进程同时等待多个可能有就绪事件要处理的的文件描述符。 IO多路复用适用于服务器有多个客户端链接,但是仅有少量的链接是活跃的情况。

select

一般情况下一个线程等待一个文件描述符,而多路复用一个线程监听多个文件描述符。只要有一个线程的读事件就绪了,就读。

也就是用select函数一次等待多个文件描述符的方式来代替,read、recv、readfrom函数一次等待一个文件描述符。

 int select(int nfds, fd_set *readfds, fd_set *writefds,
                  fd_set *exceptfds, struct timeval *timeout);

       void FD_CLR(int fd, fd_set *set);
       int  FD_ISSET(int fd, fd_set *set);
       void FD_SET(int fd, fd_set *set);
       void FD_ZERO(fd_set *set);

第一个参数是要监视的最大文件描述符+1 这样做是为了减少遍历文件描述符集的事件,帮助我们提高效率。

第二 、三、四个参数是关心读就绪的文件描述符集合(位图)的指针, 写就绪的文件描述符集合(位图)的指针, 异常就绪的文件描述符集合(位图)的指针。 这三个参数是输入输出型参数 输出时表示set中的那些文件描述符已经就绪了。

所以每次调用select函数之前都需要把要关注的文件描述符集合保存起来。这样才可以和输出的文件描述符集合对比。
保证每次调用select的时候传参数中的 。set表示有监听得文件描述符。

第五个参数
第四个参数是一微妙为单位的表示select的等待时间。
0 表示非阻塞, NULL表示阻塞。在这个时间之内没有监听的文件描述符就绪则返回0。

fd_set 是操作系统为select设置的表示文件描述符的集合。(本质上是一个位图)

FD_ZERO(&set); /*将set清零使集合中不含任何fd*/
FD_SET(fd, &set); /*将fd加入set集合*/
FD_CLR(fd, &set); /*将fd从set集合中清除*/
FD_ISSET(fd, &set); /*在调用select()函数后,用FD_ISSET来检测fd是否在set集合中,当检测到fd在set中则返回真,否则,返回假(0)*/
socket事件就绪的条件

读事件:
1、socket内核中,按接收缓冲区中数据量超过低水位标记SO_RCVLOWWAT 此时无阻塞的读取文件描述符 返回值大于0。
2、TCP通信中, 对端关闭连接,此时文件描述符读就绪,返回0。
3、监听sock上有新的连接请求。
4、sock上有未读取的数据。

写事件:
1、socket内核中, 发送缓冲区中可用的字节数大于低水位标记SO_SNDLOWWAT 。此时可以无阻塞的写,返回值大于0.
2、写操作被关闭(close(fd) 或 shutdown)这时对文件描述符进行写操作 ,触发SIGPIPE信号。
3、socket使用非阻塞connrect链接成功或失败。
4、socket上有未读取得错误。

异常就绪:
socket收到带外事件, (和TCP报文的紧急指针有关)

select服务器代码:

#include
#include
#include
#include
#include
#include
#include


typedef struct FdSet{//自定义一个结构体,结构体中保存关系的文件描述符就绪集合 
    fd_set set;
    int max_fd;      //以及集合中最大的文件描述符
}FdSet;

void FdSetinit(FdSet* fds){//初始化一个文件描述符集合
    FD_ZERO(&fds->set);
    fds->max_fd = 0;
}

void FdSetAdd(FdSet* fds, int fd){//向一个文件描述符集添加一个服务器关心的文件描述符
    FD_SET(fd, &fds->set);
    if(fd > fds->max_fd){
        fds->max_fd = fd;//更新最大的文件描述符记录
    }
}

void FdSetDel(FdSet* fds, int fd){
    FD_CLR(fd, &fds->set);//在文件描述符集中清除一个关心的文件描述符
    int i = 0;
    int max_fd = -1;
    for(; i < fds->max_fd; ++i){//更新最大的文件描述符记录。
        if(!FD_ISSET(i , &fds->set)){
            continue;
        }
        if(i > max_fd){
            max_fd = i;
        }
    }
    fds->max_fd = max_fd;
}

int serverinit(const char* ip, uint16_t port)
{//创建监听套接字。
    int fd = socket(AF_INET, SOCK_STREAM, 0);
    if(fd < 0){
        perror("socket");
        return -1;
    }
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = inet_addr(ip);
    addr.sin_port = htons(port);

    int ret = bind(fd, (struct  sockaddr*)&addr, sizeof(addr));
    if(ret < 0){
        perror("bind");
        return -2;
    }
    ret = listen(fd, 10);
    if(ret < 0){
        perror("listen");
        return -3;
    }
    return fd;
}

void ProcessRequest(int new_sock, FdSet* fds){
    char buf[1024] ={0};
    ssize_t read_size = read(new_sock, buf, sizeof(buf) - 1);
    if(read_size < 0){
        perror("read");
        return;
    }else if(read_size == 0){
        printf("read Done!\n");
        close(new_sock);//对端客户端关闭了,则服务器关闭文件描述符
        FdSetDel(fds, new_sock);
        printf("%s\n", buf);       
    }
    buf[read_size] = '\0';
    write(new_sock, buf, sizeof(buf) - 1);//这里做简单的处理, 将读到的数据写回client
}

int main(int argc, char* argv[])
{
    if(argc != 3){
        printf("use error! ip port \n");
        return -1;
    }
    int listen_sock = serverinit(argv[1], atoi(argv[2]));


    printf("server init OK!\n");
    FdSet fds;
    FdSetinit(&fds);
    FdSetAdd(&fds, listen_sock);//将listen_sock加入关心的(等待的)文件描述符集中
    while(1){
        FdSet tmp = fds;//备份这个文件描述符集, 因为select的参数是输入输出型参数,所以需要
        //                 保存下来等待的是哪些文件描述符这样的信息。
        int ret = select(tmp.max_fd + 1, &tmp.set, NULL, NULL, NULL);//阻塞式等待读就绪事件
        if(ret < 0){
            perror("select");
            continue;
        }
        if(FD_ISSET(listen_sock, &tmp.set)){//listen_sock读就绪了
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            int new_sock = accept(listen_sock, (struct sockaddr*)&peer, &len);
            if(new_sock < 0){
                perror("accept");
                continue;
            }
            FdSetAdd(&fds, new_sock);//将accept到的用于服务器向客户端收发消息的fd加入到等待的文件描述符集中去
            printf("client %s, %d connect! new_sock=%d\n", inet_ntoa(peer.sin_addr), ntohs(peer.sin_port)\
                   ,new_sock);
        }else{//走到这里说明服务器中与某一个或几个客户收发消息的文件描述符读事件就绪了
            int i = 0; 
            for(; i <= tmp.max_fd; ++i){
                if(!FD_ISSET(i, &tmp.set)){
                    continue;
                }
                ProcessRequest(i, &fds);//处理请求
            }
        }
    }
    close(listen_sock);
    return 0;
}

select得特点:
1、 可以同时等待(监听)就绪事件的文件描述符数量取决于
sizeof(fd_set) 一般来讲是 4096.

2、调用select之前必须先拷贝一份fd_set (姑且副本叫tmp), 因为调用结束后fd_set变成了已经就绪的文件描述符集合
需要和tmp集合 FDISSET() 比较, 而且tmp还保存了本来要关心的却在上一次调用select函数时因为没有就绪而被清0的文件描述符。

select的缺点:
1、用静态数组表示等待的文件描述符集合,可以同时等待的文件描述符是有限的。
2、作为参数的文件描述符集合每次调用select函数是都要从用户态拷贝到内核态,修改后,再从内核态拷贝回用户态,拷贝的开销较大。
3、每次调用之前都必须拷贝一份或2 三份文件描述符数组,使用起来不方便。
4、每次调用select都需要在内核中调用传进的文件描述符集合。

poll

也是IO多路复用的一种形式

 #include 

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

struct pollfd {
               int   fd;         /* file descriptor */
               short events;     /* requested events */
               short revents;    /* returned events */
           };

第一个参数: fds数组。 数组元素是三个成员得结构体数组, 表示文件描述符, 关心这个文件描述付事件类型得位图, 这个文件描述符就绪得事件位图。
第二个参数: fds数组的元素个数。
第三个参数:等待的时间, 同select

返回值 小于0表示出错
返回值等于0 表示等待超时
大于0 表示由于监听文件描述符就绪而返回。

poll函数的事件标志符值
POLLIN 普通或优先级带数据可读
POLLOUT 普通数据可写
POLLERR 发生错误
POLLHUP 发生挂起
POLLNVAL 描述字不是一个打开的文件

相对于select的好处:
1、 可以用户手动设置需要等待的未见描述符数组的大小
2、 以每一个文件描述符为单位的struct 具体表示他的关心的事件和就绪的事件。比select使用起来简单

poll的坏处:
1、和select函数一样, poll返回后还是要遍历fds数组得到,就绪的文件描述符。效率比较低。要监听的文件描述符越多,效率线性降低。

2、从每一个文件描述符的角度来看,他的pollfd结构体在每一次调用poll的时候拷贝入内核,再从内核拷贝出来,效率更低。

poll函数使用例子:

#include
#include
#include
#include
#include


int main()
{
    struct pollfd fds;
    fds.fd = 0;
    fds.events =POLLIN;
    while(1){
        int ret = poll(&fds, 1, 0);
        if(ret < 0){
            perror("poll");
            break;
        }
        char buf[1024] = {0};
        ssize_t read_size = read(0, buf, sizeof(buf)- 1);
        if(read_size < 0){
            perror("read");
            return 1;
        }
        if(read_size == 0){
            printf("read done\n");
            return 0;
        }
        printf(": %s ", buf);
    }
    return 0;
}

epoll

epoll是综合来讲最好的IO多路复用方式。 linux 2.6 下最好的IO多路复用通知方法。

关于epoll的系统调用:

 #include 

 int epoll_create(int size);

epoll_create() 用于创建一个epoll对象。创建一个epoll句柄 访问内核中的epoll对象。 参数今天已经可以省略了。
返回值是epoll对象的句柄

#include 

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

epoll_ctl() 函数勇于对epoll对象的属性进行设置。
第一个参数是操作的epoll对象。
第二个参数是要对该epoll对像进行什么操作。(宏)
EPOLL_CTL_ADD epoll对象增加某个要等待的fd
EPOLL_CTL_MOD epoll对象更改某个要等待的fd的事件
EPOLL_CTL_DEL epoll不再等待某个fd

第三个参数表示关心的文件描述符。

第四个参数表示关心文件描述符的什么就绪状态。

typedef union epoll_data { 
void *ptr; 
int fd;              //表示某个文件描述符
uint32_t u32; 
uint64_t u64; 
} epoll_data_t;

struct epoll_event { 
uint32_t events; /* Epoll events */ //保存关心的事件
epoll_data_t data; /* User data variable */ //保存用户数据
}; 

uint32_t events 用宏表示关心的就绪状态
EPOLLIN表示读就绪 EPOLLOUT表示写就绪 还有其他很多状态。 比如EPOLLPRI 表示带外数据 EPOLLET表示边缘触发。

 #include 

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

第一个参数是epoll对象句柄
第二个参数是关心的文件描述符和关心就绪事件的集合(输出型参数)
第三个参数表示这次等待了多少个文件描述符。
第四个参数表示等待时间和select相同

epoll的底层机制:

RBtree + 就绪队列

调用epoll_create() 实际上是创建了一棵空的红黑树

epoll_ctl() 函数时间上是在向红黑树中 添加删除修改结点,结点实际上表示的是等待的某个文件描述的绪符的就绪状态。
所有在红黑树中的结点都会与网卡驱动建立回调关心,当等待的事件真的就绪的时候,红黑树中表示等待事件的结点会被拿到就绪队列中去。

epoll_wait()函数看就绪队列中有没有结点,有结点就直接在就绪队列中取元素就可以了。

epoll的优点:
1、等待的文件描述符数量不限制(直接用epoll_ctl()函数在红黑树中注册一个表示等待事件的结点就好了)。
2、当一个被等待的文件描述符就绪了,内核采用回调机制,直接激活这个文件描述符(把该文件描述符的结点放在就绪队列中)
3、epoll_wait()在文件描述符就绪队列中取就绪文件描述符的时候可以以O(1)的时间复杂度获取到已经就绪的文件描述符。
4、内核直接将就绪队列通过mmap方式映射到用户态,避免了拷贝内存的开销。

LT模式:水平触发模式:
当epoll检测到sock 文件描述符上有就绪事件的时候、可以不立刻处理或者不一次处理完。下次调用epoll_wait的时候仍然返回,通报该文件描述符就绪并可以继续进行处理。直到文件描述符上的数据被彻底处理完的时候, epoll_wait()函数才不立刻返回。

支持阻塞与非阻塞读写

LT模式epoll服务器代码:

#include
#include
#include
#include
#include
#include



#define SIZE 1024

int startUp(char* ip, char* port)
{//服务器创建监听套接字。
    int sock = socket(AF_INET, SOCK_STREAM, 0);
    if(sock < 0){
        perror("socket");
        return 2;
    }
    int opt = 1;
    setsockopt(sock, SOL_SOCKET,SO_REUSEADDR, &opt , sizeof(opt));
    struct sockaddr_in server;
    server.sin_addr.s_addr = inet_addr(ip);
    server.sin_port = htons(atoi(port));
    if( bind(sock, (struct sockaddr*)&server, sizeof(struct sockaddr_in)) < 0 ){
        perror("bind");
        return 3;
    }
    if( listen(sock , 10) < 0 ){
        perror("listen");
        return 4;
    }
    return sock;
}

int main(int argc, char* argv[])
{
    if(argc != 3){
        printf("Usage: ./tcp_epoll ip port\n");
        return 1;
    }
    int listen_sock = startUp(argv[1], argv[2]);
    int epfd = epoll_create(256);//创建一个epoll对象 (创建一棵空的红黑树和一个空的就绪队列)
    if(epfd < 0){
        perror("epoll_create");
        return 5;
    }
    struct epoll_event ev;//初始化 epoll_event 结构体
    ev.events = EPOLLIN;  //表示关心文件描述符的读事件
    ev.data.fd = listen_sock;  //这里在data联合体中保存 要等待的文件描述符 后面从epoll_wait函数中的输出型参数epoll_event
                               //结构体中可以知道 那个文件描述符就绪了。
    epoll_ctl(epfd, EPOLL_CTL_ADD, listen_sock, &ev);//向epoll对象中添加要等待的文件描述符listen_sock和关心该文件描述符的事件&ev。
                                                     //实际上是向红黑树中插入一个结点
    struct epoll_event revs[SIZE];
    int nums = -1;
    int timeout = 5000;
    while(1){
        switch((nums = epoll_wait(epfd, revs, SIZE, timeout))){//等待关心得文件描述符
            case -1: perror("epoll_wait");//返回值-1 函数错误
            case 0 : printf("timeout\n");//返回值0 说明超过了设置的等待时间。
            default:
                     {
                        int i = 0;
                        for(i = 0 ; i < nums; ++i){//遍历epoll_wait数组  
                            //printf("%d\n", nums);
                            int fd = revs[i].data.fd;//取出就绪的文件描述符
                            if(fd == listen_sock && revs[i].events & EPOLLIN){//监听文件描述符的读就绪
                                struct sockaddr_in client;
                                socklen_t len = sizeof(client);
                                int new_sock = accept(listen_sock, (struct sockaddr*)&client, &len);
                                if(new_sock < 0){
                                    perror("accept");
                                    continue;
                                }
                                printf("%s, %d", inet_ntoa(client.sin_addr), ntohs(client.sin_port));
                                ev.events = EPOLLIN;
                                ev.data.fd = new_sock;
                                epoll_ctl(epfd, EPOLL_CTL_ADD,new_sock, &ev);//将accept返回的用于服务器向客户端收发消息的new_sock读事件加入epoll对向中
                            }
                            else if(fd != listen_sock){//某个new_sock就绪了
                                //printf("new_sock read or write event jiuxu\n");
                                if(revs[i].events & EPOLLIN){
                                    char buf[1024];
                                    ssize_t s = read(fd, buf, sizeof(buf) - 1);
                                    if(s > 0){//都成功了 该向new_sock写数据了
                                        buf[s] = 0;
                                        printf("client say: %s\n",buf );
                                        ev.events = EPOLLOUT;
                                        ev.data.fd = fd;
                                        epoll_ctl(epfd, EPOLL_CTL_MOD, fd, &ev);//改为等待new_sock的写就绪状态

                                    }else if( s == 0 ){//客户端关闭或者read错误  关闭文件描述符 从epoll对象中删除一个等待的文件描述符
                                        printf("client exit!\n");
                                        close(fd);
                                        epoll_ctl(epfd, EPOLL_CTL_DEL, fd, &ev);
                                    }else{
                                        perror("read");
                                        close(fd);
                                        epoll_ctl(epfd, EPOLL_CTL_DEL, fd, &ev);
                                    }
                                }
                                else if(revs[i].events & EPOLLOUT){//写事件就绪
                                    char buf[1024] = {0};
                                    read(0, buf, sizeof(buf) - 1);
                                    write(fd, buf, strlen(buf) - 1);
                                    ev.events = EPOLLIN;
                                    ev.data.fd = fd;
                                    epoll_ctl(epfd, EPOLL_CTL_MOD, fd, &ev);//写动作完成了 改为关心文件描述符的读事件。
                                }
                            }
                        }
                     }
        }
    }
    close(listen_sock);
    return 0;
}

ET模式:epoll的边沿触发模式
即:每一个有就绪事件的文件描述符只有一次被处理的机会,用户必须一次处理完毕文件描述符上的数据。即使一次没有处理完毕或者没有处理,下次epoll_wait()函数也不会再返回并记录没有处理完的这个文件描述符。

ET模式调用epoll_wait函数的次数比LT模式少,所以更高效。nginx服务器就采用的ET模式。

ET模式只支持非阻塞IO。
ET 模式是一种边沿触发模型,在它检测到有 I/O 事件时,通过 epoll_wait 调用会得到有事件通知的文件描述符,每于每一个被通知的文件描述符,如可读,则必须将该文件描述符一直读到空,让 errno 返回 EAGAIN 为止,否则下次的 epoll_wait 不会返回余下的数据,会丢掉事件。而如果你的文件描述符如果不是非阻塞的且对端没有关闭,那这个一直读或一直写势必会在最后一次阻塞。

这个时候需要把文件描述符设置为非阻塞的,这样在read函数读完数据的时候没有数据可读了不会hang,而是可以让程序在读其他fd或者epoll_wait();

ET模式epoll_wait()代码:

#include
#include
#include
#include
#include
#include
#include
#include
#include
#include



int startUp(char* ip, char* port)
{
    int sock = socket(AF_INET, SOCK_STREAM, 0);
    if(sock < 0){
        perror("socket");
        return 2;
    }
    int opt = 1;
    setsockopt(sock, SOL_SOCKET,SO_REUSEADDR, &opt , sizeof(opt));
    struct sockaddr_in server;
    server.sin_addr.s_addr = inet_addr(ip);
    server.sin_port = htons(atoi(port));
    if( bind(sock, (struct sockaddr*)&server, sizeof(struct sockaddr_in)) < 0 ){
        perror("bind");
        return 3;
    }
    if( listen(sock , 10) < 0 ){
        perror("listen");
        return 4;
    }
    return sock;
}

int setNoBlock(int fd)//封装fcntl()函数将文件描述符甚至为非阻塞的
{
    int fl = fcntl(fd, F_GETFL);
    if(fd < 0){
        perror("fcntl");
        return 7;
    }
    fcntl(fd, F_SETFL, fl | O_NONBLOCK);
}

ssize_t NoBlockRead(int fd, char* buf, size_t size)
{//非阻塞的读 一次读1024个字节
    (void) size;
    ssize_t total_size = 0;
    for(; ; ){
        ssize_t cur_size = read(fd, buf+total_size, 1024);
        total_size += cur_size;
        if(cur_size < 1024 || errno == EAGAIN){
            break;
        }
    }
    buf[total_size] = '\0';
    return total_size;
}

int main(int argc, char* argv[])
{
    if(argc != 3){
        printf("Usage: ./tcp_epoll ip port\n");
        return 1;
    }
    int listen_sock = startUp(argv[1], argv[2]);
    int epfd = epoll_create(256);
    if(epfd < 0){
        perror("epoll_create");
        return 5;
    }
    setNoBlock(listen_sock);//设置为非阻塞
    struct epoll_event ev;
    ev.events = EPOLLIN | EPOLLET;
    ev.data.fd = listen_sock;
    epoll_ctl(epfd, EPOLL_CTL_ADD, listen_sock, &ev);
    struct epoll_event revs[64];
    int nums = -1;
    int timeout = 5000;
    while(1){
        switch((nums = epoll_wait(epfd, revs, 64, timeout))){
            case -1: perror("epoll_wait");
            case 0 : printf("timeout\n");
            default:
                     {
                        int i = 0;
                        for(i = 0 ; i < nums; ++i){
                            //printf("%d\n", nums);
                            int fd = revs[i].data.fd;
                            if(fd == listen_sock && revs[i].events & EPOLLIN){
                                struct sockaddr_in client;
                                socklen_t len = sizeof(client);
                                int new_sock = accept(listen_sock, (struct sockaddr*)&client, &len);
                                if(new_sock < 0){
                                    perror("accept");
                                    continue;
                                }
                                setNoBlock(new_sock);//设置为阻塞
                                printf("%s, %d", inet_ntoa(client.sin_addr), ntohs(client.sin_port));
                                ev.events = EPOLLIN | EPOLLET;//设置为ET模式等待读就绪
                                ev.data.fd = new_sock;
                                epoll_ctl(epfd, EPOLL_CTL_ADD,new_sock, &ev);
                            }
                            else if(fd != listen_sock){
                                //printf("new_sock read or write event jiuxu\n");
                                if(revs[i].events & EPOLLIN){
                                    char buf[1024];
                                    ssize_t s = NoBlockRead(fd, buf, sizeof(buf) - 1);//非阻塞的读
                                    if(s > 0){
                                        buf[s] = 0;
                                        printf("client say: %s\n",buf );
                                        ev.events = EPOLLOUT;
                                        ev.data.fd = fd;
                                        epoll_ctl(epfd, EPOLL_CTL_MOD, fd, &ev);
                                    }else if( s == 0 ){
                                        printf("client exit!\n");
                                        close(fd);
                                        epoll_ctl(epfd, EPOLL_CTL_DEL, fd, &ev);
                                    }else{
                                        perror("read");
                                        close(fd);
                                        epoll_ctl(epfd, EPOLL_CTL_DEL, fd, &ev);
                                    }
                                }
                                else if(revs[i].events & EPOLLOUT){
                                    char buf[1024] = {0};
                                    read(0, buf, sizeof(buf) - 1);
                                    write(fd, buf, strlen(buf) - 1);
                                    ev.events = EPOLLIN;
                                    ev.data.fd = fd;
                                    epoll_ctl(epfd, EPOLL_CTL_MOD, fd, &ev);
                                }
                            }
                        }
                     }
        }
    }
    close(listen_sock);
    return 0;
}











你可能感兴趣的:(linux,网络基础)