socket通信高级模型

来源:互联网 发布:redis java集群 编辑:程序博客网 时间:2024/06/06 08:41

前几天我刚刚学习了socket网络编程的基本知识,整体的流程是创建socket,要么就是bind/listen/accept/通信,要么就是connect/通信,基本上每一步都是阻塞的,也就是必须得等到有了回应才会返回.当然,我们也可以设置超时参数提前返回,但是超时返回之后我们如果还要连接,仍然需要重新走一遍这样的过程.如果某一时刻有很多请求来连接,或者很多应答来返回,很多就需要等待了,结果自然就是很多超时.


此时,我们需要一种高级的机制.也就是说我们不等,我们把它交给某个调度者(一般就是可爱的OS),然后继续处理其他连接,当某个事情发生时(比如连接建立了,读/写完成了等等),调度者通知我们,我们过去处理,这样当然不会减少每个请求的处理时间,但是会提高我们单位时间处理请求的数量,俗语就是提高了吞吐量.


原生的Linux提供给我们三个socket高级模型供我们使用.虽然我现在还没有把握说哪些场合使用哪些模型最好,但此处我们只谈谈他们的特点而已,至于怎么使用,我想还是需要经验.


1.select模型

主要部份是select系统调用,

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

select阻塞在三个fd集合上,readfds是等待读入的fd集合,writefds是等待输出集合,exceptfds是异常的集合,只要三个集合的任意一个集合中出现了已经准备好的fd,那么select就会立即返回(超时也会返回的),然后我们通过比对判断是哪个fd,然后进行相关操作

fd_set是不透明的类型,需要通过接口来访问

void FD_CLR(int fd, fd_set *set);   // 从set中删除fdint  FD_ISSET(int fd, fd_set *set); // fd是否在set中而且准备好了void FD_SET(int fd, fd_set *set);   // 将fd纳入set中void FD_ZERO(fd_set *set);          // 清空set,一般用于初始化
基本的流程一般如下:

struct timeval tv = { /* time */};fd_set rs; FD_ZERO(&rs);FD_SET(fd, &rs);ret = select(fd + 1, &rs, NULL, NULL, &tv); // NULL as tv will wait foreverif (ret < 0){    // error}else if (ret == 0){    // time-out}else if (FD_ISSET(fd, &rs)){    // fd is ready}// optFD_CLR(fd, &rs);
注意,select中的nfds必须比集合中的最大fd要大1,否则会出现很奇怪的行为(我们需要确定哪个fd最大)


select的缺点主要有两个,一个是集合内fd的数量是有限的,有一个上限不能超过,对于那些很多连接的服务器来说是不好的;另一个则是我们每次需要遍历整个集合来找到合适的fd,比较耗费时间.


2.poll

通过系统调用poll来完成

int poll(struct pollfd *fds, nfds_t nfds, int timeout);
我们把需要等待的fd添加到fds队列中,队列的数目nfds我们可以自定义(大小任意),然后调用这个就会阻塞等待任意的fd就绪(或者超时)

队列的结构如下:

struct pollfd {      int   fd;         /* file descriptor */      short events;     /* requested events */      short revents;    /* returned events */};
events表示的是该fd等待哪些事件,而revents表示的是当fd准备就绪的时候,发生了什么事件.比如fd等待的是读,挂断,那么当任一事件发生时,revents就会记录究竟是哪件事件发生

主要事件有:

POLLIN: fd可以读入数据

POLLOUT: fd可以写数据了(二者都是指不会阻塞,也就是连接就绪,只差操作了)

POLLPRI: fd有紧急数据(TCP传送时的一种特殊数据)

POLLHUP: fd被挂断

POLLERR: fd出现错误

POLLNVAL: fd还没有open/socket(即fd是无效的fd)

这些都是作为位标来进行测试的,通过"|"可以连接起来,通过对应的"&"进行判断
一般来说poll的处理流程如下:

int t = 1; // 1msstruct pollfd fds[MAX_NUM];int num = 0;fds[num].fd = rfd; // rfd's been open-edfds[num].events = POLLIN | POLLHUP | POLLERR;num++;ret = poll(&fds, num, t); // negative as t will wait foreverif (ret < 0){    // error}else if (ret == 0){    // time-out}// ret is the num of ready fdselse{    for (i = 0; i < num; i++)    {        if (fds[i].revents != 0)        {            ret--;            // event happens        }    }    assert(ret == 0);}

poll解决了select数量限制的问题,但是还没有解决其遍历求解的毛病,还是需要一个一个查找,当然,有个进步,就是poll返回值是发生事件的fd数量,如果我们只需要获得数量的话,也不错(不过这种应用场景好奇怪)


3.epoll
通过三个相合的调用共同完成

int epoll_create(int size); // size is just a hintint 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创建一个epfd的系统资源,作为后续调用的指示.epoll_ctl可以增加删除epfd中的fd和对应的event,具体的op有

EPOLL_CTL_ADD: 给epfd增加fd和event

EPOLL_CTL_DEL: 给epfd删除fd和event

EPOLL_CTL_MOD: 修改fd对应的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 events */    epoll_data_t data;        /* User data variable */};
一个events和一个data,而data做成了union,我们可以存放任意的数据,或者就单单存放对应的fd(因为到时候不知道对应的fd)

events有:

EPOLLIN: fd可以读入数据

EPOLLOUT: fd可以写数据了(二者都是指不会阻塞,也就是连接就绪,只差操作了)

EPOLLPRI: fd有紧急数据(TCP传送时的一种特殊数据)

EPOLLHUP: fd被挂断

EPOLLERR: fd出现错误

EPOLLONESHOT: fd只能在对应的事件上使用一次,一旦有任一事件发生,该fd就被删除了,需要手工添加

EPOLLET: 默认的情况时如果fd没有及时处理,下次wait的时候还会出现,但如果设置了EPOLLET, 那么只有在wait之后发生的事件才会唤起fd,早已发生的则不会


epoll_wait则会对添加到epfd的所有fd及事件进行等待,events是响应事件的缓冲区,maxevents是缓冲区的大小,我们不需要赋值,这个是作为wait的结果返回的

epoll流程如下:

int epfd = epoll_create(0 /* ignore */);struct epoll_event ev;ev.events = EPOLLIN | EPOLLHUP | EPOLLET | EPOLLERR;ev.data.fd = fd;epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev);struct epoll_event evs[MAX_SIZE];ret = epoll_wait(epfd, evs, MAX_SIZE, -1);if (ret < 0){    // error}else if (ret == 0){    // time-out}else{    for (i = 0; i < ret; i++)    {        // do something    }}
epoll_wait返回的就是有多少fd准备好了,而events队列里就是每个准备好的fd对应的events,我们只需要遍历准备好的events队列就能处理所有的就绪fd了(不用在全体遍历了)


epoll就成功的解决了select和poll的难题,所以感觉这个非常赞,不过epoll是在linux 2.5.4添加的,如果你使用的旧版本的linux,拜托,怎么也得换成2.6吧..


这些仅仅是OS提供的关于socket通信,或者更广一点关于fd事件相应的基础部件,在这些之上,我们可以实现我们自己的调度模型,比如oop库版本的异步调度模型,就非常的酷,下次我们再回到这个话题的话,我们就自己设计一套异步调度模型
原创粉丝点击