如果觉得看起来比较麻烦,需要PDF版本,或是需要更多学习资料,都可以加上QQ群领取
本群由我创立,目前已将群主权限交由合作方便于进行日常管理,介意的朋友们在GitHub上看最新版就好了
这份笔记资料是会免费提供的,特地向你们保证…毕竟还是要恰饭的嘛…
祝愿每一位有追求的Java开发同胞都能进大厂拿高薪!
Java架构交流QQ群:578486082 (备注一下GitHub,免得被认成打无良广告的)
快捷加群方式:点击此处加入群聊:java高级程序猿①
PS:
平常很忙,找miffy小姐姐领取就好了,免费获取的!
TCP与UDP区别总结:
- TCP面向连接(如打电话要先拨号建立连接);UDP是无连接的,即发送数据之前不需要建立连接。
- TCP提供可靠的服务。也就是说,通过TCP连接传送的数据,无差错,不丢失,不重复,且按序到达;UDP尽最大努力交付,即不保证可靠交付
- TCP面向字节流,实际上是TCP把数据看成一连串无结构的字节流;UDP是面向报文的 UDP没有拥塞控制,因此网络出现拥塞不会使源主机的发送速率降低(对实时应用很有用,如IP电话,实时视频会议等)
- 每一条TCP连接只能是点到点的;UDP支持一对一,一对多,多对一和多对多的交互通信
- TCP首部开销20字节;UDP的首部开销小,只有8个字节
- TCP的逻辑通信信道是全双工的可靠信道,UDP则是不可靠信道
序列号:TCP传输时将每个字节的数据都进行了编号,这就是序列号。
确认应答:TCP传输的过程中,每次接收方收到数据后,都会对传输方进行确认应答。也就是发送ACK报文。
这个ACK报文当中带有对应的确认序列号,告诉发送方,接收到了哪些数据,下一次的数据从哪里发。
序列号的作用不仅仅是应答的作用,有了序列号能够将接收到的数据根据序列号排序,并且去掉重复序列号的数据。这也是TCP传输可靠性的保证之一。
当TCP发出一个段后,它启动一个定时器,等待目的端确认收到这个报文段。如果不能及时收到一个确认,将重发这个报文段。
TCP连接的每一方都有固定大小的缓冲空间,TCP的接收端只允许发送端发送接收端缓冲区能接纳的数据。当接收方来不及处理发送方的数据,能提示发送方降低发送的速率,防止包丢失。
TCP使用的流量控制协议是可变大小的滑动窗口协议。接收方有即时窗口(滑动窗口),随ACK报文发送
如果网络出现拥塞,分组将会丢失,此时发送方会继续重传,从而导致网络拥塞程度更高。因此当出现拥塞时,应当控制发送方的速率。
这一点和流量控制很像,但是出发点不同。流量控制是为了让接收方能来得及接收,而拥塞控制是为了降低整个网络的拥塞程度。
TCP 主要通过四个算法来进行拥塞控制:慢开始、拥塞避免、快重传、快恢复。
TCP的握手机制:
TCP的挥手机制:
详情参考: https://github.com/ThinkingHan/Java-interview-guide
为什么会发生TCP粘包、拆包? 发生TCP粘包、拆包主要是由于下面一些原因:
-
应用程序写入的数据大于套接字缓冲区大小,这将会发生拆包。
-
应用程序写入数据小于套接字缓冲区大小,网卡将应用多次写入的数据发送到网络上,这将会发生粘包。
-
进行MSS(最大报文长度)大小的TCP分段,当TCP报文长度-TCP头部长度>MSS的时候将发生拆包。
-
接收方法不及时读取套接字缓冲区数据,这将发生粘包。
粘包、拆包解决办法:
TCP本身是面向流的,作为网络服务器,如何从这源源不断涌来的数据流中拆分出或者合并出有意义的信息呢?通常会有以下一些常用的方法:
-
发送端给每个数据包添加包首部,首部中应该至少包含数据包的长度,这样接收端在接收到数据后,通过读取包首部的长度字段,便知道每一个数据包的实际长度了。
-
发送端将每个数据包封装为固定长度(不够的可以通过补0填充),这样接收端每次从接收缓冲区中读取固定长度的数据就自然而然的把每个数据包拆分开来。
-
可以在数据包之间设置边界,如添加特殊符号,这样,接收端通过这个边界就可以将不同的数据包拆分开。
对于粘包和拆包问题,常见的解决方案有四种:
-
客户端在发送数据包的时候,每个包都固定长度,比如1024个字节大小,如果客户端发送的数据长度不足1024个字节,则通过补充空格的方式补全到指定长度;Netty提供的FixedLengthFrameDecoder
-
客户端在每个包的末尾使用固定的分隔符,例如rn,如果一个包被拆分了,则等待下一个包发送过来之后找到其中的rn,然后对其拆分后的头部部分与前一个包的剩余部分进行合并,这样就得到了一个完整的包;Netty提供LineBasedFrameDecoder与DelimiterBasedFrameDecoder
-
将消息分为头部和消息体,在头部中保存有当前整个消息的长度,只有在读取到足够长度的消息之后才算是读到了一个完整的消息;Netyy提供了LengthFieldBasedFrameDecoder与LengthFieldPrepender
-
通过自定义协议进行粘包和拆包的处理。Netty提供了通过实现MessageToByteEncoder和ByteToMessageDecoder来实现
简单点理解就是:
-
同步,就是我调用一个功能,该功能没有结束前,我死等结果。
-
异步,就是我调用一个功能,不需要知道该功能结果,该功能有结果后通知我(回调通知)
-
阻塞,就是调用我(函数),我(函数)没有接收完数据或者没有得到结果之前,我不会返回。
-
非阻塞,就是调用我(函数),我(函数)立即返回,通过select通知调用者
**同步IO和异步IO的区别就在于:**数据拷贝的时候进程是否阻塞
**阻塞IO和非阻塞IO的区别就在于:**应用程序的调用是否立即返回
**BIO:**同步并阻塞 ,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序直观简单易理解。
**NIO:**同步非阻塞 ,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4开始支持。
**AIO:**异步非阻塞 ,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理.AIO方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持。
- 单个进程打开的文件描述符(fd文件句柄)不一致
select :有最大连接数限制数为1024,单个进程所能打开的最大连接数由FD_ZETSIZE宏定义。
poll:poll本质上与select没有区别,但是它没有最大连接数的限制,原因是它是基于链表来存储的。
epoll:虽然连接有上限,但是很大,1G内存的机器可以打开10万左右的连接,以此类推。
- 监听Socket的方式不一致
select :轮询的方式,一个一个的socket检查过去,发现有socket活跃时才进行处理,当线性socket增多时,轮询的速度将会变得很慢,造成线性造成性能下降问题。
poll:对select稍微进行了优化,只是修改了文件描述符,但是监听socket的方式还是轮询。
expoll:epoll内核中实现是根据每个fd上的callback函数来实现的,只有活跃的socket才会主动调用callback,通知expoll来处理这个socket。(会将连接的socket注册到epoll中, 相当于socket的花名册, 如果有一个socket活跃了, 会回调一个函数, 通知epoll,赶紧过来处理)
- 内存空间拷贝方式(消息传递方式)不一致
select:内核想将消息传递到用户态,需要将数据从内核态拷贝到用户态,这个过程非常的耗时
poll:同上
epoll:epoll的内核和用户空间共享一块内存,因此内存态数据和用户态数据是共享的select、poll、epoll时间复杂度分别是:O(n)、O(n)、O(1)
NIO有什么缺点:
NIO的类库和API还是有点复杂,比如Buffer的使用 Selector编写复杂,如果对某个事件注册后,业务代码过于耦合 需要了解很多多线程的知识,熟悉网络编程 面对断连重连、保丢失、粘包等,处理复杂 NIO存在BUG,根据网上言论说是selector空轮训导致CPU飙升,具体有兴趣的可以看看JDK的官网
Netty主要的优点有:
-
框架设计优雅,底层模型随意切换适应不同的网络协议要求
-
提供很多标准的协议、安全、编码解码的支持
-
解决了很多NIO不易用的问题
-
在很多开源框架中使用,如Dubbo、RocketMQ、Spark等
-
**底层核心有:**Zero-Copy-Capable Buffer,非常易用的灵拷贝Buffer(这个内容很有意思,稍后专门来说);统一的API;标准可扩展的时间模型
-
传输方面的支持有:管道通信(具体不知道干啥的,还请老司机指教);Http隧道;TCP与UDP
-
协议方面的支持有:基于原始文本和二进制的协议;解压缩;大文件传输;流媒体传输;protobuf编解码;安全认证;http和websocket
总之提供了很多现成的功能可以直接供开发者使用。
-
Channel ----Socket
-
EventLoop ----控制流,多线程处理,并发;
-
ChannelHandler和ChannelPipeline
-
Bootstrap 和 ServerBootstrap
-
创建ServerBootStrap实例
-
设置并绑定Reactor线程池:EventLoopGroup,EventLoop就是处理所有注册到本线程的Selector上面的Channel
-
设置并绑定服务端的channel
-
创建处理网络事件的ChannelPipeline和handler,网络时间以流的形式在其中流转,handler完成多数的功能定制:比如编解码 SSl安全认证
-
绑定并启动监听端口
-
当轮训到准备就绪的channel后,由Reactor线程:NioEventLoop执行pipline中的方法,最终调度并执行channelHandler
https://www.infoq.cn/article/...
Reactor线程模型
Reactor单线程模型 一个NIO线程+一个accept线程:
Reactor多线程模型
Reactor主从模型 主从Reactor多线程:多个acceptor的NIO线程池用于接受客户端的连接
Netty可以基于如上三种模型进行灵活的配置。
总结
Netty是建立在NIO基础之上,Netty在NIO之上又提供了更高层次的抽象。在Netty里面,Accept连接可以使用单独的线程池去处理,读写操作又是另外的线程池来处理。Accept连接和读写操作也可以使用同一个线程池来进行处理。而请求处理逻辑既可以使用单独的线程池进行处理,也可以跟放在读写线程一块处理。线程池中的每一个线程都是NIO线程。用户可以根据实际情况进行组装,构造出满足系统需求的高性能并发模型。
传统意义的拷贝是在发送数据的时候, 传统的实现方式是:
\1. File.read(bytes)
\2. Socket.send(bytes)
这种方式需要四次数据拷贝和四次上下文切换:
\1. 数据从磁盘读取到内核的read buffer
\2. 数据从内核缓冲区拷贝到用户缓冲区
\3. 数据从用户缓冲区拷贝到内核的socket buffer
\4. 数据从内核的socket buffer拷贝到网卡接口(硬件)的缓冲区
零拷贝的概念明显上面的第二步和第三步是没有必要的,通过java的FileChannel.transferTo方法,可以避免上面两次多余的拷贝(当然这需要底层操作系统支持)
\1. 调用transferTo,数据从文件由DMA引擎拷贝到内核read buffer
\2. 接着DMA从内核read buffer将数据拷贝到网卡接口buffer上面的两次操作都不需要CPU参与,所以就达到了零拷贝。
Netty中的零拷贝主要体现在三个方面:
1、bytebufferNetty发送和接收消息主要使用bytebuffer,bytebuffer使用对外内存(DirectMemory)直接进行Socket读写。原因:如果使用传统的堆内存进行Socket读写,JVM会将堆内存buffer拷贝一份到直接内存中然后再写入socket,多了一次缓冲区的内存拷贝。DirectMemory中可以直接通过DMA发送到网卡接口
2、Composite Buffers传统的ByteBuffer,如果需要将两个ByteBuffer中的数据组合到一起,我们需要首先创建一个size=size1+size2大小的新的数组,然后将两个数组中的数据拷贝到新的数组中。但是使用Netty提供的组合ByteBuf,就可以避免这样的操作,因为CompositeByteBuf并没有真正将多个Buffer组合起来,而是保存了它们的引用,从而避免了数据的拷贝,实现了零拷贝。
3、对于FileChannel.transferTo的使用Netty中使用了FileChannel的transferTo方法,该方法依赖于操作系统实现零拷贝。
netty内存池实现原理
netty内存池可以分配堆内存和非堆内存(Direct内存),内存分配的核心算法是类似的,从堆内存分配代码入手来学习整个内存池的原理。netty框架处理IO事件时,使用ByteBuf承载数据。ByteBuf的内存分配由PooledByteBufAllocator来执行,最终的内存分配工作会被委托给PoolArena,堆内存分配的PoolArena实现是HeapArena。
netty通常被用于高并发系统,多线程竞争加锁会影响内存分配的效率,为了缓解高并发时的线程竞争,netty允许使用者创建多个分配器(PoolArena)来分离线程竞争,提高内存分配效率。可通过PooledByteBufAllocator构造子中的nHeapArena参数来设置PoolArena的数量,或者直接取框架中的默认值,通过以下代码决定默认值,默认值根据CPU核心数、JVM最大可用内存以及默认内存块(PoolChunk)大小等参数来计算这个默认值,计算逻辑是:
1)获取系统变量io.netty.allocator.numHeapArenas,通过System.setProperty("io.netty.allocator.numHeapArenas",xxx)或者增加JVM启动参数-Dio.netty.allocator.numHeapArenas=xxx设置,如果设置了值,把这个值当做Arena的个数。
2)如果没有设置io.netty.allocator.numHeapArenas系统变量,计算CPU核心数*2和JVM最大可用内存/默认内存块大小/2/3,取其中一个较少的值当做PoolArena的个数。
确定PoolArena个数之后框架会创建一个PoolArena数组,数组中所有的PoolArena都会用来执行内存分配。线程申请内存分配时,线程会在这个PoolArena数组中挑选一个当前被占用次数最少的Arena执行内存分配。
此外,netty使用扩展的线程对象FastThreadLocalThread来优化ThreadLocal性能,具体的优化思路是:默认的ThreadLocal使用ThreadLocalMap存储线程局部变量,它的实现方式类似于HashMap,需要计算hashCode定位到线程局部变量所在Entry的索引,而FastThreadLocalThread使用FastThreadLocal代替ThreadLocal,FastThreadLocalThread用一个数组来维护线程变量,每个FastThreadLocal维护一个index,该index就是线程局部变量在数组中的位置,线程变量直接通过index访问无需计算hashCode,FastThreadLocal的优势是减少了hashCode的计算过程,虽然性能只会有轻微的提升,但在高并发系统中,即使只是轻微的提升也会成倍放大。
更多请阅读文章:http://baijiahao.baidu.com/s?...
Netty 并没有使用第三方库实现对象池,而是自己实现了一个相对轻量的对象池。通过使用 threadLocal,避免了多线程下取数据时可能出现的线程安全问题,同时,为了实现多线程回收同一个实例,让每个线程对应一个队列,队列链接在 Stack 对象上形成链表,这样,就解决了多线程回收时的安全问题。同时,使用了软引用的map 和 软引用的 thradl 也避免了内存泄漏。
更详细的可阅读文章:
https://www.jianshu.com/p/834... https://www.cnblogs.com/hzmar...