前面的一篇文章我们了解了Netty一些基础知识,这篇文章我们来讨论一下Netty的相关的一些概念。
1.长连接和短连接
Netty底层使用的Socket通信,Socket使用的TCP通信。Netty的长连接和短连接是基于TCP长连接和短连接实现的。
长连接:
所谓长连接,指在一个TCP连接上可以连续发送多个数据包,在TCP连接保持期间,如果没有数据包发送,需要双方发检测包以维持此连接,一般需要自己做在线维持(不发生RST包和四次挥手)。
连接→数据传输→保持连接(心跳)→数据传输→保持连接(心跳)→……→关闭连接(一个TCP连接通道多个读写通信);
这就要求长连接在没有数据通信时,定时发送数据包(心跳),以维持连接状态
短连接:
短连接是指通信双方有数据交互时,就建立一个TCP连接,数据发送完成后,则断开此TCP连接(管理起来比较简单,存在的连接都是有用的连接,不需要额外的控制手段);
连接→数据传输→关闭连接;
应用场景:
长连接多用于操作频繁(读写),点对点的通讯,而且连接数不能太多情况,。每个TCP连接都需要三步握手,这需要时间,如果每个操作都是先连接,再操作的话那么处理速度会降低很多,所以每个操作完后都不断开,次处理时直接发送数据包就OK了,不用建立TCP连接。例如:数据库的连接用长连接, 如果用短连接频繁的通信会造成socket错误,而且频繁的socket 创建也是对资源的浪费。
而像WEB网站的http服务一般都用短链接(http1.0只支持短连接,1.1keep alive 带时间,操作次数限制的长连接),因为长连接对于服务端来说会耗费一定的资源,而像WEB网站这么频繁的成千上万甚至上亿客户端的连接用短连接会更省一些资源,如果用长连接,而且同时有成千上万的用户,如果每个用户都占用一个连接的话,那可想而知吧。所以并发量大,但每个用户无需频繁操作情况下需用短连好;
在长连接中一般是没有条件能够判断读写什么时候结束,所以必须要加长度报文头。读函数先是读取报文头的长度,再根据这个长度去读相应长度的报文。我们使用Netty通信的时候,如果自己实现编码器和解码器,定制协议的时候需要定制Head和Body部分。
2.Channel writeAndFlush函数
客户端连接服务端后生成一个用于通信的Channel对象,如果多个线程同时调用writeAndFlush函数写数据,数据会相互覆盖么,writeAndFlush是线程安全的么?如果是线程安全的,实现线程安全的方式又是什么么?比如我们通过下面的代码来多个线程同时写数据。
//获取到线程池的Executor的引用
Executor executor = Executors.newCachedThreadPool();
//提交到某个线程中执行
executor.execute(writer);
//提交到另一个线程中执行
executor.execute(writer);
通过调用NioEventLoop的execute(Runnable task)方法实现,Netty有很多系统Task,创建他们的主要原因是:当I/O线程和用户线程同时操作网络资源时,为了防止并发操作导致的锁竞争,将用户线程的操作封装成Task放入消息队列中,由I/O线程负责执行,这样就实现了局部无锁化。
3.IO多路复用
Netty通信实现了IO多路复用。IO多路复用的意思就是一条线程可以监听多个IO操作。NioEventLoop是基于select,EpollEventLoop是基于Epoll。下面解释了一下select,poll,epoll的概念和区别。
(1)select==>时间复杂度O(n)
它仅仅知道了,有I/O事件发生了,却并不知道是哪那几个流(可能有一个,多个,甚至全部),我们只能无差别轮询所有流,找出能读出数据,或者写入数据的流,对他们进行操作。所以select具有O(n)的无差别轮询复杂度,同时处理的流越多,无差别轮询时间就越长。
(2)poll==>时间复杂度O(n) (Java NIO的selector基于poll)
poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态, 但是它没有最大连接数的限制,原因是它是基于链表来存储的.
(3)epoll==>时间复杂度O(1) (Netty 的epollEventLoop基于epoll)
epoll可以理解为event poll,不同于忙轮询和无差别轮询,epoll会把哪个流发生了怎样的I/O事件通知我们。所以我们说epoll实际上是事件驱动(每个事件关联上fd)的,此时我们对这些流的操作都是有意义的。(复杂度降低到了O(1))
select,poll,epoll都是IO多路复用的机制。I/O多路复用就通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。
epoll跟select都能提供多路I/O复用的解决方案。在现在的Linux内核里有都能够支持,其中epoll是Linux所特有,而select则应该是POSIX所规定,一般操作系统均有实现
4.Reactor线程模型,Proactor线程模型
在处理web请求时,通常有两种体系结构,分别为:(基于线程)例如http请求,一个http请求一个线程来处理、(事件驱动)一系列的事件处理器来响应事件的发生,并且将服务端接受连接与对事件的处理分离。其中,事件是一种状态的改变。比如,tcp中socket的new incoming connection、ready for read、ready for write。
Reactor模式
reactor设计模式是event-driven architecture的一种实现方式,处理多个客户端并发的向服务端请求服务的场景。每种服务在服务端可能由多个方法组成。reactor会解耦并发请求的服务并分发给对应的事件处理器来处理。目前,许多流行的开源框架都用到了reactor模式,如:netty、node.js等,包括java的nio。对着下面的图我们来解释一下,client连接服务器后,client的各种事件通过dispatch分发到各种事件处理器去处理。对应Netty的组件:
Netty客户端:client
Reactor : workerGroup(包含多个NioEventGroup,也就是多个线程处理器)
Handler:处理线程,NioEventGroup
Proactor模式
Reactor和Proactor的区别在于读取或者写入函数是否阻塞。Netty通信的时候NioEventLoop是一个线程,它里面有一个队列,依次处理多个Channel的read,write事件,但是read和write操作的时候线程其实还是阻塞的,如果其中一个read操作阻塞了会影响整个NioEventLoop事件的处理。Proactor模式下的read,write操作是异步的,不会阻塞,操作系统操作完成以后会通知处理器。现在大部分的通信都是基于Reactor的,Proactor需要操作系统的支持,如果操作系统不支持异步IO是没法实现Proactor模式的。
5.ByteBuffer 堆外内存
Java NIO DirectByteBuffer和ByteBuffer都有一个堆外内存的概念,这里我们来讨论下。
- 堆内内存:堆内内存是由JVM所管控的Java进程内存,我们平时在Java中创建的对象都处于堆内内存中,并且它们遵循JVM的内存管理机制,JVM会采用垃圾回收机制统一管理它们的内存。
- 堆外内存:就是存在于JVM管控之外的一块内存区域,因此它是不受JVM的管控。操作系统直接管控。
讨论之前需要先了解一下操作系统的一些基本概念:
- 内核态:控制计算机的硬件资源,并提供上层应用程序运行的环境。比如socket I/0操作或者文件的读写操作等
- 用户态:上层应用程序的活动空间,应用程序的执行必须依托于内核提供的资源
- 系统调用:为了使上层应用能够访问到这些资源,内核为上层应用提供访问的接口
使用堆外内存的好处:
- 减少了垃圾回:收使用堆外内存的话,堆外内存是直接受操作系统管理( 而不是虚拟机 )。这样做的结果就是能保持一个较小的堆内内存,以减少垃圾收集对应用的影响。
- 提升复制速度(io效率):堆内内存由JVM管理,属于“用户态”;而堆外内存由OS管理,属于“内核态”。如果从堆内向磁盘写数据时,数据会被先复制到堆外内存,即内核缓冲区,然后再由OS写入磁盘,使用堆外内存避免了这个操作。
使用对外内存的坏处:
- 堆外内存难以控制,如果内存泄漏,那么很难排查
- 堆外内存相对来说,不适合存储很复杂的对象。需要自己做内存管理。