发现自己近一年有些毛病,自己也算是研习了不少的源代码,看了不少的技术书籍,但是自己就是记忆力不行,总是过段时间就会忘记,忘记之后还得从头开始啃源码、啃书籍。而且有些重要技术点也会遗忘,导致再学习的时候发现自己又回到了起点!我总结为,就是自己近一年期间犯懒,没有再写一下博客,技能点不能很好的再回顾!
趁着发现自己的问题,同时自己也在做前后端rpc分离实践,现在将之前研习netty的结果再总结出来,写到博客上!
首先,我们要确定java中的netty用来做什么的?具体的工作模式优势不解释,网上能找一大堆,主要讲它通信这块的rpc,高效稳定的协议栈绕不开tcp/ip协议!
本来是不想记录tcp/ip的,这个实在是没有太多好说的,但是也发现虽然自己明白,有时候却也是会遗漏要点知识,所以也还是记录一下吧!
似乎作为上层程序员只需要了解tcp的握手与挥手情况即可!
首先要了解tcp/ip的头部结构
16位源端端口 | 16位目标端端口
32位序列号(发送端确认信息)
32位确认序号(服务端确认)
4位偏移量(每个数字表示1个4字节,所以最大表示15个4字节,也就是60字节)----
6位保留位(记不太清了) | 标志位:包括6种报文段 urg、ack、rst、syn、fin、psh
16位校验和 | 16位紧急指针
16位窗口大小(用于tcp缓冲区的控制)
40位填充字段。
我觉得挺容易理解的,作为通信双方必须知道相互的地址,所以要有源地址与目标地址的标记;序列号与确认序列号,这个涉及到数据包分片,比如mtu导致了数据分片,如何分片与如何重组分片;偏移数据表示tcp头部结构能够占用的最大数据长度;保留位大概就是保留用的吧;标记位,6个就是我们常说的同步报文段、确认报文段的标记;窗口大小表示一次传输数据的大小,由于是16位,按照二进制计算也就是65535字节;校验和据说是为了教研数据有效性的,我通过抓tcp的包看到类似这样的;16位紧急指针,这个是配和urg报文段设置优先紧急数据的。
然后是三次握手,与四次挥手中tcp状态的转移。都是基于上面tcp头部结构进行变化的。
三次握手
1):client将syn标志位置1,序列号seq置x(随机数), 发送到server端,client状态为同步发送状态,表示等待server端的确认;
2):server端收到client端数据包,判断syn=1,了解到为同步报文段。则这时,将要发送数据的tcp头部分别设置syn与ack为1,确认序列号x+1,序列号置y(随机数),发送到
client端,server端状态为同步接收状态;
3):client端收到serve数据包之后,判断是否ack报文段为1,确认序列号是否为x+1,符合则将要发送的数据的tcp头部的ack报文段置1,确认序列号y+1,然后发送;到达server端
之后,检查ack报文段是否为1 && 确认序列号是否为y+1,符合则建立连接。
此时client与server都进入了建立成功阶段。
四次挥手
1): client将要发送报文信息的tcp头部的fin报文段置1,序列号置随机值x,发送给server端,client端状态为结束等待状态;
2): server端收到fin结束报文段,将发送报文信息的tcp头部ack置1,确认序号为x+1,server端进入关闭等待阶段;
3):server端再次发送一个报文数据,tcp头部fin置1,此时server端进入了最后确认阶段;
4):client接收到了server的ack报文信息与fin报文信息之后,验证确认序列号x+1,然后再向server端发送一个tcp头部ack=1的报文段,client进入time_wait阶段,server端收到信息
验证后进入关闭状态;
最后的时刻client等待2*msl(最大报文传输时间)时间长度,进入close状态。
第二绕不开的一点就是网络的io模型!一般我们应该都知道有几种常谈的io模型,阻塞式、非阻塞、异步io、多路复用、信号驱动的io模型,这里java常用的就是非阻塞式io与多路复用io也就是nio!nio基reactor的模型进行设计的。所以接下来要讲一下nio与reactor模型。
Nio,这个我们需要去看epoll的解释,最好去看epoll的源码,这里我建议看一下深入理解nginx这本书中的epoll的解释,详细描述了代码状况!下面具体谈一下epoll的优秀思想设计。
首先要知道原来模式的弊端,原有linux2.6之前的操作系统,采用select/poll的形式这两种形式在进行连接事件的收集的时候,是将所有活跃与不活跃的套接字由用户态的内存向操作系统的内核内存传递,然后由操作系统内核扫描所有套接字,不管是内存还是cpu都造成巨大的浪费。而epoll则不是,它定义了一个文件系统,首先,调用epoll_create创建一个epoll对象;然后,调用epoll_ctl向epoll添加所有的连接套接字;第三,调用epoll_wait收集活跃的连接。这样它就避免了完全的内存复制,只进行收集活跃连接,然后也不用遍历原来所有的连接,使cpu也降温。
Eventpoll是创建epoll时候的数据结构,里面是一种红黑树的数据结构,用于存储epoll_ctl向epoll对象中添加的事件,为什么使用红黑树,因为如果出现重复的套接字可以很快的识别出来。据书上说,被添加到epoll中的时间都会与网卡建立回调关系epoll_callback。Eventpoll中含有一个红黑树的根节点rbr与双向链表rdllist,回调事件会放到rdllist,所以epoll_wait扫描事件连接的时候就只针对这个rdllist的链表进行遍历,然后将事件复制给用户内存,所以也就保证了效率。
第三,Reactor模式(也有人叫做反应器模式)!Netty中的nio线程模型命名了三种模式:Reactor单线程模型、Reactor多线程模型和主从多线程模型!实际上都是根据上边epoll的三阶段进行的划分,所以抽象出这三种阶段才能更好的理解这三种设计的模型,也就是得理解epoll的三种事件!所有这三种模型都是围绕着三种阶段进行的。
单线程模型:就是说把这三个阶段统统放在一个线程上操作。该线程负责,创建事件对象,accept收集活跃socket,分发器分发二进制消息进行读写操作(接收数据、发送数据)。
多线程模型:这个其实主要是将收集活跃的socket与事件分发分成了两步;一个专门的线程用于收集活跃的socket,另一个线程池专门负责数据的读写操作;
主从多线程模型:这个其实是在多线程模型上进一步的加固;用来收集活跃socket的线程改成了线程池,acceptor接收到tcp socket接收验证(这个应该是指的tcp的数据验证)连接后,将该连接的通道交给io处理线程池的一个线程上进行io的读写操作。
实际上就是这么回事,只是将原来串行的操作改成了并行化,同时保证线程的安全性!