学习Netty源码之前,先搞清楚一些基础网络相关知识,这样在后续的学习的时候可以起到事半功倍的效果
Socket
Socket由于需要不同主机的进程之间相互通信,本质上Socket是操作系统抽象出的一层编程API供应用程序统一调用,使得底层的不同通信协议透明化(TCP,UDP)即开发人员只针对Socket编程,即可实现不同主机进程间的网络通信
Socket 五元组
Socket代表了一条通信,由本机IP + 端口 + 远端IP + 远端端口 + 协议构成的五元组标识
Socket何时创建
对于Http请求来说,传输层采用了TCP协议,而建立TCP连接需要经过三次握手,当三次握手成功,Client和Server端即会分别创建一个Socket标识这一次的通信连接
image.png以Server端为例,当ServerSocket监听到Client的连接请求,并经过三次握手成功后,内核便会创建一个socket用于标识此次连接
Socket何时销毁
image.png当主动发起close的一方,在收到来自被动方的FIN并发送ACK后,会进入TIME_WAIT状态持续2MSL时间后,主动关闭方的socket会进入closed状态,才允许重建相同的五元组
而TIME_WAIT状态的存在,就是为了避免出现由于最后一次ACK报文被动关闭的一方没有收到,会进行重发FIN给主动关闭的一方,若主动关闭的一方没有这个TIME_WAIT 状态的存在的话会出现以下两种错误的情况
-
主动方重建了相同的五元组(新的连接),却收到了FIN报文,导致连接异常
-
主动方还没有重建新的连接,但原连接已经关闭,主动方会回复RST(而不是ACK报文),会引起被动方关闭流程错乱
因此会需要这个状态以控制该五元组不能立即被重新使用
IO
现代操作系统为了维护其安全稳定,针对受保护的内存以及底层硬件操作权限设置了相应的权限,只允许内核操作,而应用程序需要访问的话,需要想内核发起系统调用
一次普通的read操作会经历两个阶段
- 等待数据准备
对于网络IO来说,很多时候数据在一开始还没有到达。比如,还没有收到一个完整的TCP包。这个时候kernel就要等待足够的数据到来
- 内核将数据拷贝到应用程序的地址空间
正是由于需要这两个步骤,Linux系统产生了不同的IO模式
阻塞IO (BIO)
阻塞IO当应用程序发起系统调用recvfrom后,内核会先等待数据准备,当数据准备好后,会将数据从内核缓冲区拷贝到应用程序的用户空间,拷贝完成后返回给应用程序,在此过程中,应用程序一直处于阻塞状态,即两个步骤均阻塞了,这就是传统的BIO模式
非阻塞IO (NIO)
NIOlinux下,可以通过设置socket使其成为non-blocking。当对一个non-blocking socket执行读操作时,流程是这个样子:
当应用程序发起系统调用时,若此时数据还没有准备好,此时并不会阻塞用户进程,而是立刻返回一个error。从用户进程角度讲 ,它发起一个read操作后,并不需要等待,而是马上就得到了一个结果。用户进程判断结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作。一旦kernel中的数据准备好了,并且又再次收到了用户进程的system call,那么它马上就将数据拷贝到了用户内存,然后返回。
即NIO的特点是用户进程不断的主动轮询内核数据好了没有,第2阶段的copy其实还是阻塞的,实际的情况是这个copy的时间非常短,几乎无感知
IO多路复用 (IO multiplexing)
我们常说的IO多路复用又叫事件驱动,底层依靠Linux操作系统内核提供的select,poll,epoll函数实现
多路复用的优势在于多,即通过一个用户线程就可以管理成百上千的连接,具体的实现就是依靠select等函数通过轮询所其所负责的socket,当某个socket有指定的事件发生时,就通知应用程序
当用户进程发起select系统调用后,整个进程会被阻塞,内核同时会监视所有select负责的socket,当任何一个socket有指定的事件发生,select就会返回,这个时候用户进程再调用相应的系统调用(read,write...)将数据从内核拷贝到用户空间
IO多路复用如上图所示,IO多路复用实际上发起了两次系统调用,其实际的IO性能并不比BIO高,它的优势即在于多,即可以同时处理多个连接,而不用为每个连接开一个单独的线程
在IO multiplexing Model中,实际中,对于每一个socket,一般都设置成为non-blocking,但是,整个用户的process其实是一直被block的。只不过process是被select这个函数block,而不是被socket IO给block(忽略copy阶段极短时间的阻塞)
异步 I/O(asynchronous IO)
异步IO才是真正意义上的异步非阻塞IO模型,即用户进程发起aio_read系统调用后,立即返回,所有事情(数据准备以及copy数据到用户空间)均由内核完成后以通知的方式告知用户进程read操作已经完成
用一张图来描述以下各种IO模式下的特点
IO模式对比最后记录一下select/epoll模式
select模式下所能监听的socket句柄数量有限,一般2048个
epoll模式则无此限制
epoll是在linux2.6内核中提出的,是之前的select和poll的增强版本。相对于select和poll来说,epoll更加灵活,没有描述符限制。epoll使用一个文件描述符管理多个描述符,将用户关心的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次 ,采用了三个函数实现
int epoll_create(int size);
该函数表示创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大,当创建好epoll句柄后,它就会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
该函数表示对指定描述符fd执行op操作,其中op分为以下几类:
-
添加EPOLL_CTL_ADD
-
删除EPOLL_CTL_DEL
-
修改EPOLL_CTL_MOD
分别表示添加、删除和修改对某个fd的监听事件
//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);
该函数表示等待epfd上的io事件,最多返回maxevents个事件。
参数events用来从内核得到事件的集合,maxevents告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size,参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。该函数返回需要处理的事件数目,如返回0表示已超时。
以上就是epoll模式内核提供的三个函数
文章大部分内容来自人云思云大神的博客
继续我的Netty源码学习之路...
网友评论