您的位置:首页 > 理论基础 > 计算机网络

网络IO模型

2015-09-09 11:12 651 查看
最基本的网络IO

1 文件描述符

-socket(int domain, int type, int protocol);

-bind(int sockfd, struct sockaddr *my_addr, int addrlen); 

2 服务端

-listen(int sockfd, int backlog); 

-accept(int sockfd, void *addr, int *addrlen); 

3 客户端

-connect(int sockfd, struct sockaddr *serv_addr, int addrlen);

4 通信

-send(int sockfd, const void *msg, int len, unsigned int flags);

-recv(int sockfd, void *buf, int len, unsigned int flags);  

Note: 什么是文件描述符?在linux中,所有的东西都可以看作是文件,它可能是一个网络连接、FIFO、管道、终端、磁盘上的文件或什么其他的东西。任何形式的IO都可以看作是在读或写一个文件描述符,文件描述符就是一个与打开的文件相关联的整数。socket就是利用文件描述符fd与其他程序通迅的方式。如fd(stdin)=0、fd(stdout)=1、fd(stderr)=2。

 

瓶颈在哪里

1 单进程、单线程

2 非多路复用

3 阻塞:accept、recv

-fcntl(sockfd, F_SETFL, O_NONBLOCK)

Note: 主要指服务端的瓶颈。多路复用是通信领域的术语,指把多个信号组合起来在一条物理信道上进行传输,在远距离传输时可大大节省电缆的安装和维护费用。这里指让一个线程或进程同时处理多个网络连接。

 

如何解决这些问题,下面逐一进行分析:
多进程模型

1 fork子进程在accept阻塞等待连接。

2 业务和网络层分离为不同进程模型。

3 负载均衡模型,多个进程绑定不同的端口。

 

多线程模型
1 主线程在accept处阻塞,当有新连接时实时创建新线程处理,处理完关闭。

2 创建线程池,都在accept处阻塞,当有新连接时notify一个线程去处理,处理完归还。

3 线程(池)关联一个线程消息队列,主线程在accept处阻塞,将新连接放入消息队列,线程(池)读取线程消息队列处理。

Note: 如何理解线程消息队列?生产者线程生产消息,放在一个空缓冲区中,供消费者线程消费,生产者生产消息,如果缓冲区满,则被阻塞,消费者消费消息,如果缓冲区空,则被阻塞。线程消息队列就是生产者消费者问题中的缓冲区,而它的生产者是不限定的,任何线程都可以作为生产者向其中进行EnQueue操作,消费线程则可能是一个,也可能是多个。因此对循环队列的任何操作都要加锁,以保证线程安全。

分析:从1到3方案逐渐优化。方案1并发数受限于线程数,线程创建、切换的开销,并发量上不去。方案2有惊群现象,当有新连接时,所有线程都被唤醒,只有一个线程处理其他线程再度休眠。方案3很好地解决了accept阻塞的问题,但还是未能解决read阻塞的问题,一个死连接(建立连接但是不发送数据的连接)就可以导致业务线程死掉。

 

多路复用模型 

1 事件多路分离与分派。

2 实用的网络模型都是多路分离的(select/poll/epoll),而且是非阻塞的。

3 Java NIO

-Selector、SocketChannel

Note: 一般地,I/O多路复用机制都依赖于一个事件多路分离器(Event Demultiplexer)。分离器对象可将来自事件源的I/O事件分离出来,并分发到对应的read/write事件处理器(Event Handler)。开发人员预先注册需要处理的事件及其事件处理器(或回调函数);事件分离器负责将请求事件传递给事件处理器。两个与事件分离器有关的模式是Reactor和Proactor。Reactor模式采用同步IO,而Proactor采用异步IO。

NIO 有一个主要的类Selector,这个类似一个观察者,只要我们把需要探知的socketchannel告诉Selector,我们接着做别的事情,当有事件发生时,他会通知我们,传回一组SelectionKey,我们读取这些Key,就会获得我们刚刚注册过的socketchannel,然后,我们从这个Channel中读取数据,接着我们可以处理这些数据。   

Selector内部原理实际是在做一个对所注册的channel的轮询访问,不断的轮询(目前就这一个算法),一旦轮询到一个channel有所注册的事情发生,比如数据来了,他就会站起来报告,交出一把钥匙,让我们通过这把钥匙来读取这个channel的内容。

 

select模型 

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

2 对fd_set的一些操作宏

FD_ZERO(fd_set *set) - clears a file descriptor set 

FD_SET(int fd, fd_set *set) - adds fd to the set 

FD_CLR(int fd, fd_set *set) - removes fd from the set 

FD_ISSET(int fd, fd_set *set) - tests to see if fd is in the set 

Note: 理解select模型的关键在于理解fd_set,它用每一位来标识fd是否被监听或是否发生事件。为说明方便,取fd_set长度为1字节,fd_set中的每一bit可以对应一个文件描述符fd。则1字节长的fd_set最大可以对应8个fd。

1)执行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)阻塞等待

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

 

select的特点
1 可监控的fd个数取决与sizeof(fd_set)的值,同是还会受限于内核的设置。

2 select每次只返回有事件的fd,没有事件的fd被清空,需另保存所有注册的fd,在每次select调用前清理fd_set,重新注册,同时取出最大的fd+1作为每一个参数。

3 需要遍历所有注册的fd判断是否有事件发生。

 

select服务端伪码 
array[slect_len]; nSock=0; array[nSock++]=listen_fd; maxfd=listen_fd;

while {

   FD_ZERO(&set);

   foreach (fd in array)  {

       fd大于maxfd,则maxfd=fd

       FD_SET(fd,&set)

   }

   res=select(maxfd+1,&set,0,0,0);

   if(FD_ISSET(listen_fd,&set)) {

       newfd=accept(listen_fd);

       array[nsock++]=newfd;

       if(--res<=0) continue

   }

   foreach 下标1开始 (fd in array)  {

       if(FD_ISSET(fd,&set))

           执行读等相关操作,如果错误或者关闭,则要删除该fd,将array中相应位置和最后一个元素互换就好,nsock减一

           if(--res<=0) continue

   }

}

 

poll模型

int poll(struct pollfd *ufds, unsigned int nfds, int timeout);

struct pollfd {

  int fd;           /* file descriptor */

  short events;     /* requested events */

  short revents;    /* returned events */

};

 

poll的特点
1 fd个数不再有上限限制,可以将参数ufds想象成栈底指针,nfds是栈中元素个数,该栈可以无限制增长。

2 pollfd结构将fd信息、需要监控的事件、返回的事件分开保存,则poll返回后不会丢失fd信息和需要监控的事件信息,也就省略了select模型中前面的循环操作,但返回后的循环仍然不可避免。另外每次poll操作会自动把上次的revents清空,不需要再清理。

 

poll服务端伪码

struct pollfd fds[POLL_LEN];

unsigned int nfds=0;

fds[0].fd=server_sockfd;

fds[0].events=POLLIN|POLLPRI;

nfds++;

while {

 res=poll(fds,nfds,-1);

 if(fds[0].revents&(POLLIN|POLLPRI)) {

     执行accept并加入fds中,nfds++

     if(--res<=0) continue

 }

 循环之后的fds,if(fds[i].revents&(POLLIN|POLLERR )) {

     读操作或处理异常等

         if(--res<=0) continue

 }

}

 

epoll模型

1 int epoll_create(int size);

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

-op: EPOLL_CTL_ADD、EPOLL_CTL_DEL

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

Note: epoll的设计思路,是把select/poll单个的操作拆分为1个epoll_create+多个epoll_ctl+一个epoll_wait。 由于在执行epoll_create和epoll_ctrl时,已经把用户态的信息保存到内核态了,所以之后即使反复地调用epoll_wait,也不会重复地拷贝参数,扫描文件描述符,反复地把当前进程放入/放出等待队列了。

 

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 events */

 epoll_data_t data;  /* User data variable */

};

 

epoll的特点
1 它保留了poll的两个相对与select的优点。

2 epoll_wait的参数events作为出参,直接返回了有事件发生的fd,epoll_wait的返回值既是发生事件的个数,省略了poll中返回之后的循环操作。

3 不再象select、poll一样将标识符局限于fd,epoll中可以将标识符扩大为指针,大大增加了epoll模型下的灵活性。

Note: epoll的优点:1. 支付FD的数目大(1GB内存的机器上大约是10万左右),epoll用红黑树来存储事件。2. 只响应活跃的FD,活跃的FD会主动callback。3. 使用mmap加速内核与用户空间的消息传递。

 

epoll服务端伪码

epollfd=epoll_create(EPOLL_LEN);

epoll_ctl(epollfd,EPOLL_CTL_ADD,server_sockfd,&ev)

struct epoll_event events[EPOLL_MAX_EVENT];

while {

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

 循环nfds,是server_sockfd则accept,否则执行响应操作

}

 

epoll工作模式 
1 LT(Level Triggered)缺省,支持block和no-block socket。水平触发,只要buffer里有未被处理的事件,内核会不断通知你就绪未处理的FD。

2 ET(Edge Triggered),只支持no-block socket。边缘触发,只在buffer大小发生变化时通知,只通知一次,控制不当有可能丢失。

3 指定socket可读、可写、ET模式

-ev.events = EPOLLIN | EPOLLOUT | EPOLLET;

 

select->poll->epoll演化路线

1 从readset、writeset等分离到将读写事件集中到统一的结构。

2 从阻塞操作前后的两次循环到之后的一次循环,再到精确返回有事件发生的fd。

3 从只能绑定fd信息,到可以绑定指针结构信息。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  Linux