Socket Select & Epoll Code

之前看过Socket,一直比较懒,没有总结一下,趁着有兴致赶紧写一下

Socket在Linux中被当作文件看待,对应的sock_fd也是一个文件符被操作,因为端口需要监听多个socket_fd,所以采用select机制来进行非阻塞监听。

直接上一段源码说明原理

//socket select example
//源代码copied from http://blog.sina.com.cn/s/blog_5f84dc840100edej.html
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet.h>
#include <arpa/inet.h>

#define MYPORT 1234    //listening port
#define BACKLOG 5     //max recieve client
#define BUF_SIZE 200

int fd_A[BACKLOG];        //connected FD array
int conn_amount;        //connect number

int main(int argc, char **argv) {
    int sock_fd, new_fd;    //fd for listening, new connected fd
    struct sockaddr_in server_addr;        //server addresss info
    struct sockaddr_in client_addr;        //client address info
    socklen_t    sin_size;
    int yes =1;
    char buf[BUF_SIZE];
    int ret, i;

    //create a listing socket
    if((sock_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
        perror("Create listening socket error
");
        exit(1);
    }
    //configure the listening socket
    //SO_REUSEADDR BOOL allow aport reuse
    if(setsockopt(sock_fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int)) == -1) {
        perror("setsockopt error!
");
        exit(1);
    }
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(MYPORT);
    server_addr.sin_addr.s_addr = INADDR_ANY;
    memset(server_addr.sin_zero, '', sizeof(server_addr.sin_zero));

    //bind sock_fd and server_addr
    if(bind(sock_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
        perror("bind error!
");
        exit(1);
    }

    //begin listen
    if(listen(sock_fd, BACKLOG) == -1) {
        perror("listen error!
");
        exit(1);
    }

    //monitor fd set
    fd_set fdsr;
    //max file number
    int maxsock;
    //Select TimeOut time
    struct tim tv;
    conn_amount = 0;
    sin_size = sizeof(client_addr);
    maxsock = sock_fd;
    while(1) {
        FD_ZERO(&fdsr);
        FD_SET(sock_fd, &fdsr);
        tv.tv_sec = 30;
        tv.tv_usec = 0;
        //join the active socket handle into fdset
        for(i = 0; i < BACKLOG; i++) {
            if(fd_A[i] != 0) {
                FD_SET(fd_A[i], &fdsr);
            }
        }
        ret = select(maxsock + 1, &fdsr, NULL, NULL, &tv);
        if(ret < 0) {
            perror("select error!");
            break;
        }
        else if (ret == 0)
        {
            printf("timeoutn");
            continue;
        }
        //monitor every sock_fd
        for(i = 0; i < conn_amount; i++) {
            if(FD_ISSET(fd_A[i], &fdsr) {
                ret = recv(fd_A[i], buf, sizeo(buf), 0);
                if(ret <= 0) {
                    close(fd_A[i]);
                    FD_CLR(fd_A[i], &fdsr);
                    fd_A[i] = 0;
                }
                else {
                    if(ret < BUF_SIZE) {
                        memset(&buf[ret], '', 1);
                    }

                }
            }
        }
        if(FD_ISSET(sock_fd, &fdsr)) {
            new_fd = accept(sock_fd, (struct sockaddr *)&client_addr, &sin_size);
            if(new_fd <= 0) {
                perror("accept socket error!");
                continue;
            }
            if(conn_amount < BACKLOG) {
                fd_A[conn_amount++] = new_fd;
                if(new_fd > maxsock) {
                    maxsock = new_fd;
                }
            }
            else {
                send(new_fd, "bye", 4, 0);
                close(new_fd);
                break;
            }
        }
    }
    for(i = 0; i < BACKLOG; i++) {
        if(fd_A[i] != 0) {
            close(fd_A[i]);
        }
    }
    return 0;
}

上面的代码循环过程中不断地把用作listen 的sock_fd和用sock_fd监听到的new_fd加入到fdsr中,然后select去超时监听。如果返回值为正值证明监听到了可读信息,接着去检查对应的每一个fd是否在fdsr中,如果在就accept或读取信息。这种过程就可以进行非阻塞的监听,但是由于过程中select后需要将已经连接的fd循环一遍看是否有可读信息,在大规模的连接但数据偶发的情况下,这种循环检测会很慢且浪费资源。需要一种更好的解决方法——epoll

Epoll所以能够比select更加高效是因为

1.epoll不需要对每一个sock_fd做循环检测,避免了大量的连接时的循环延时.epoll将需要处理的文件符从内核态返回到用户态的一个链表中,当用户态需要处理时,只需要遍历链表中的事件即可。

2.epoll在监听之前就已经把句柄存储在内核中,每次添加新的监听句柄时才会向内核写入少量数据。而Select则是每次调用select函数都会将所有的监听的socket全部写入到内核态一遍,上万计的句柄写入到内核态,可能会有几十KB的大小,非常低效。

epoll有三个常用函数:

int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event * event);
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

通过epoll_create创建一个epoll对象,size是最大的句柄数,系统允许的最大句柄数和机器内存有关,1G 大约时10亿的句柄

epoll_ctl函数则用于将某句柄加入或移出epoll对象

epoll_wait类似与select函数,用于监听超时控制

Linux的手册上对于epoll有这样的例子:

#define MAX_EVENTS 10
struct epoll_event ev, events[MAX_EVENTS];
int listen_sock, conn_sock, nfds, epollfd;
 /* Set up listening socket, 'listen_sock' (socket(), bind(), listen()) */

epollfd = epoll_create(10);
if (epollfd == -1) {
    perror("epoll_create");
    exit(EXIT_FAILURE);
}

ev.events = EPOLLIN;
ev.data.fd = listen_sock;
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, listen_sock, &ev) == -1) {
    perror("epoll_ctl: listen_sock");
    exit(EXIT_FAILURE);
}

for (;;) {
    nfds = epoll_wait(epollfd, events, MAX_EVENTS, -1);
    if (nfds == -1) {
        perror("epoll_pwait");
        exit(EXIT_FAILURE);
    }

    for (n = 0; n < nfds; ++n) {
        if (events[n].data.fd == listen_sock) {
            conn_sock = accept(listen_sock,
                    (struct sockaddr *) &local, &addrlen);
            if (conn_sock == -1) {
                perror("accept");
                exit(EXIT_FAILURE);
            }
            setnonblocking(conn_sock);
            ev.events = EPOLLIN | EPOLLET;
            ev.data.fd = conn_sock;
            if (epoll_ctl(epollfd, EPOLL_CTL_ADD, conn_sock,
                        &ev) == -1) {
                perror("epoll_ctl: conn_sock");
                exit(EXIT_FAILURE);
            }
        } else {
            do_use_fd(events[n].data.fd);
        }
    }
}

结构非常简洁明了

epoll在内核初始化时,会开辟出epoll自己的内核高速cache区,这是一个slab层,申请了一块统一大小的内存区域,便于申请释放和赋值。

这些socket在cache中以红黑数的形式被管理保存,提升了查找插入和删除的速度。

Epoll有对应的两种模式, ET和LT。因为有事件发生时内核态会将事件句柄发送到用户态的list中,如果时ET 模式,用户态只会返回一次。而如果时LT模式,当用户态没有处理完该句柄时,下次epoll_wait后仍然会将该句柄提交到用户态。这个没有实践过,还不能体会这两种实际的应用场景区别。

socket的总结大致就是这么多。之后在实践中有新的学习和体会再补充。

原文地址:https://www.cnblogs.com/noanswer/p/3656585.html