NIO有一个零拷贝的特性。Java的内存有分为堆和栈,以及还有字符串常量池等等。如果有一些数据需要从IO里面读取并且放到堆里面,中间其实会经过一些缓冲区。我们要去读,它会分成两个步骤,第一块它会把我们的数据从IO流里面读出来放到我们的缓冲区,然后从缓冲区里面再去读出来放到堆里面。其实它会经历两次,数据会拷贝两次才能到达堆或者堆内存里面。如果数据量很大,那么就会造成资源的浪费。Netty使用了NIO中的零拷贝特性,当它需要去接收数据或者传输数据的时候,那么它会去开辟一个新的堆内存,然后数据直接是从IO读到那块新的一个开辟的内存里面去了,那么这样子就加快了我们数据传输的速度。这就是一个大致的Netty。
Netty is an asynchronous event-driven network application framework
for rapid development of maintainable high performance protocol servers & clients.
Netty是一个异步的事件驱动的一个网络应用框架,它是可以迅速地开发像一些高性能的服务端和客户端,这是它的一句话简介。
Netty is a NIO client server framework which enables quick and easy development of network applications such as protocol servers and clients. It greatly simplifies and streamlines network programming such as TCP and UDP socket server.
Netty它是一个NIO的客户端、服务端框架,它是可以快速地并且简单地去开发一些进入网络的一些应用程序。像一些有服务端和客户端是有一些自定义的协议的,你可以去开发一些有协议的一些服务端和客户端都是没有问题的。它又可以非常简单地去开发一些网络应用编程,比方说TCP和UDP。为什么说简单?因为Netty它简化了网络编程,并且进行了优化,提供了非常多的API你可以去使用。那么使用起来会极大地增加你开发的速度,会比你在很早很早以前你去编写TCP编程要快速便捷的多。
Netty官网提供的一张图。Protocol Support是Netty所提供的协议的支持。WebSocket是主要需要去学习的,其他的协议它也有,比如像一些SSL,另外还有谷歌的Protobuf、大文件传输Large File Transfer,但是我们整套课程不会涉及的太多。
Core里面有一个Zero-Copy,这就是Netty的特性——零拷贝机制,它的传输速度就是基于一个零拷贝会非常的快。
Ease of use
- Well-documented Javadoc, user guide and examples
- No additional dependencies, JDK 5 (Netty 3.x) or 6 (Netty 4.x) is enough
- Note: Some components such as HTTP/2 might have more requirements. Please refer to the Requirements page for more information.
Netty官方提供了一些很好的文档,Java文档。建议使用Java8,因为我们的Spring Boot是使用的2.0。Spring Boot2.0它需要你的JDK是在1.8。所以我们会使用JDK8和Netty4进行我们本套实战的开发。
为什么5.0会被废止?因为主要是作者在进行开发的时候,他们发现5.0版本的性能优化在4.0的基础上并没有一个很大的提升,但是它们的代码复杂度又非常的高,所以最终就废止了,当然还是有一些其他的原因。4.1分支的最新的更新,两三个月之前都有过更新。最近的一次更新是在五天三天之前,所以它的更新也是非常的频繁。5.0版本在三年前就已经废止了。
网络上的一些关于Netty的代码有的是基于3.0,有的是基于4.0,有的是基于5.0,非常的杂。我们是根据Netty4.0来进行系统地学习。
文档的话是4.0和4.1都有,https://netty.io/4.1/api/index.html https://netty.io/4.0/api/index.html
版本下载我们会采用Maven,我们使用Maven会加一些相应的依赖,那么这些依赖我在代码里面都会提供给大家或者自己去下一些相应的jar包也是没有问题的。
这个就是Netty的一个大致的介绍。
基本的概念:阻塞和非阻塞。线程在访问某一个资源的时候,这个资源是否准备就绪的一种处理方式。如果说这个资源当前没有准备就绪,在这个时候,那么就会有两种处理的方式,一种就是阻塞,一种就是非阻塞。阻塞就是指这个线程会一直持续等待这个资源处理完毕直到它响应返回一个结果,那么这个时候我们的线程是一直阻塞状态,它不可以去做任何的事情。那么非阻塞的话它是指,这个线程是直接返回一个结果,它不会持续地等待这个资源处理完毕才会去响应,它会去请求别的资源。
其实就像我们在Web开发中使用Ajax那样,是有同步和异步,其实道理是差不多的。同步是指我们主动请求并且会等待我们的IO。操作完成之后它会有一个通知,它会通知到你它是一个同步的。异步是指当我们的线程主动请求数据之后,它可以去继续处理其他的任务,它可以去发起其他的请求。当我们有很多请求处理完毕之后,它再逐一地通过一种异步的通知的方式来通知你,这就是异步。
图中的虚线是一种异步的方式。这些概念两两组合又可以分为同步阻塞、同步非阻塞、异步阻塞和异步非阻塞。这些会和我们的NIO、BIO和AIO相互结合起来。
IO在进行读写的时候,这个线程是会被阻塞的。这个线程它无法去做其他的操作。这是非常传统、非常简单的一种模式。它的通信方式和使用非常方便,但是它的并发处理的能力会非常的低。并且线程之间访问资源通信的时候,它们的耗时也是比较久的。另外它也是相应地会比较依赖我们的网速和带宽,这个是在JDK1.4之前都是这样子。
这张图有一个Server和三个客户端。它可以有很多客户端。我们的服务器Server其实它会有一个专门的线程,称之为Accepter。它是专门来负责监听和客户端之间的请求。只要客户端和服务端建立一个请求,这个时候客户端和服务端它们之间都会创建一个新的线程来进行处理。这其实是一种非常典型的一应一答的模式。如果客户端逐渐增多,那么服务端和客户端之间它会频繁地创建和销毁相应的线程。这个时候我们的服务器会有很大的压力,甚至会导致我们的服务器崩溃。所以这种方式是非常老的一种IO流的处理方式。在后续的一段时间它会进行一段改良,改良之后它就不是通过额外地新增线程了,它就是会创建一个线程池,通过线程池来进行处理。这种方式其实也可以称之为是一种伪异步IO。这就是BIO的一个基本概念。
我上厕所,但是厕所的坑都满了,那么我什么不干只是干等着,我会“主动”地观察哪一个坑好了,只要有坑位释放了,那么这个时候我就立马去占坑。这是我们生活中的一种实例。这是一种同步阻塞的IO。
现在我去上厕所,厕所的坑全满了,这个时候我会跑出去抽根烟或者拿出手机来摇一摇。这个时候我不是干等着了,我会时不时主动地回到厕所去看一下,看一看有没有坑释放,如果有坑释放了,那么这个时候我再去占一坑。我不是在那边干等着,而是我同时在做一些别的事情。NIO是JDK1.4之后出现的,它有一些基本概念,比方说selector选择器,也称之为多路复用器,另外还有buffer缓冲区还有channel双向通道。
selector选择器,也可以称之为多路复用器。它其实就是一个线程,它这个线程会主动地轮询,如果说客户端和服务端要建立链接的时候,它其实是进行一个注册,那么注册完毕之后我们就会有一个channel one。每一个客户端和selector建立链接之后,都会有一个channel。channel是一个双向通道,它可以进行一些相应的数据的读写。这些数据的读写都会到我们的buffer缓冲区里面去。通过使用channel注册到selector上之后,其实就可以实现一种客户端和服务端的通信方式。channel中的数据它的读取/读写都是通过buffer。它是一种非阻塞的读取。如果没有数据它会直接跳过,它不会同步地等着你有数据。selector多路复用器是一个单线程,它的线程的资源开销会非常的少。光这一个线程它就可以处理成千上万个客户端。客户端的增多不会去影响它的性能。这个就是和BIO之间的一个差别。channel相当于是一个读取的工具,每一个客户端都可以理解为一个单独的channel。每一个客户端在和服务端建立链接之后,注册完毕之后就会有一个单独的channel。它是一个一对一的,一个客户端就会有一个channel。然后每一个服务端会有一个selector。buffer里面的数据会用于进行读写,数据被读完之后还是会存在buffer里面的。数据不会因为读取之后就消失了,这个就是区别于Stream。Stream里面的数据读完之后就没有了。一个selector可以让多个客户端进行注册,然后它就会有多个channel,可以理解为一种一对多的方式。
异步阻塞IO在平时开发中几乎是用不到了,因为这种方式非常少。我们用生活中一个实例就可以来了解它的一种工作原理。我上厕所,厕所的坑全满了,我比较懒我就站在厕所里面什么都不干干等着,让每一个坑位的用户在释放完之后让他来跟我说这个坑已经释放完了。有人来通知我之后我再去上。我一开始是站在厕所里面什么都不干我干等着,这个非常傻的方式就是异步阻塞IO。
异步非阻塞IO是相反的。我没有在厕所的里面干等着,我是在厕所外面玩手机或者抽烟,如果有用户释放完厕所的坑位他会出来主动地通知我。我在做自己的事情的时候,它会让其他的用户在释放完之后再来通知我。
AIO是NIO2.0。它是一种非阻塞异步的处理方式。它是在NIO原有的基础上引入了一个异步的概念。这种异步的概念其实主要就是在读写的时候它的所有的返回的类型其实就是一个future对象。这个future的模型就是一个异步的。因为它在这个模型里边它会有一些相应的事件监听。如果你有相应的事件处理完毕之后,你就可以通知到我。
NIO: 我是一个选择器,多路复用器,我会时不时一直经常地主动回去看有没有哪个坑位释放了。哪个坑位释放了就是一个就绪的状态了。坑可以理解为channel,这个channel如果释放那么你就可以去做你的相应的处理的事件了。我是一个不停地主动轮询的一个多路复用器,它是一个单线程。
异步阻塞很傻,没什么人会去用。我在厕所里面等着有人来通知我然后再去占坑。这个时候我是不会去做任何事情的。
AIO是一个非常高效的方式。我在做自己的事情的时候让别人来通知我。我不仅可以做好自己的事情,也可以节省更多的时间。
直到处理完成之后线程才会被释放、才会被销毁。整个链路比较耗时,也比较浪费线程资源。NIO由多路复用器selector,不停地主动轮询我们的channel,每一个channel就绪之后,就会让我们的selector进行相应的请求的处理。NIO是比较高效的。AIO是发起请求等你回调,此时我们的线程会做别的额外的事情。通知回调会通知给你的future对象,你可以在future对象里面去做一些相应的listener,去做一些相应的监听。
Netty它是一个Java的开源框架,提供了异步的事件驱动的网络应用程序框架以及小组件。它可以用于快速地开发高性能、高可靠性的网络服务器以及客户端程序。Netty它是一个NIO的客户端和服务端框架,它允许快速地、简单地开发网络应用程序,比方说像客户端和服务端之间的协议就可以用它来做。它也简化了网络编程的一些相关的规范,它也可以去用于开发WebSocket应用服务器。
NIO的类库和API是比较复杂的,使用起来也并不简单比较麻烦。开发者还需要具备Java多线程的知识和技能才可以很好地使用NIO里面相关的API。客户端会碰到断线重连或者网络不稳定的情况下,或者说网络阻塞,它的处理方式的难度就比较大也非常的复杂。
NIO内置存在一部分的bug。由于NIO相应的问题,所以就有了Netty。Netty的API使用起来非常的简单,它的开发门槛也是非常的低。通过几行代码就可以去写一个服务器,非常的简单。Netty本身的功能是非常的强大。它内置了很多的编解码功能,你可以去设置或者自定义。它支持一些主流的协议,你可以去定制,它的定制性非常的强。你可以通过对channel handler定制,可以对我们整体的通信进行一个灵活的扩展。你可以扩展成多层的channel handler都是没有问题的。另外Netty的性能非常的高,如果你要去和同类型的NIO框架做对比的,那么Netty综合来说它的可用性,综合性能是最好的也是最可靠的。从3.x到4.x,也就是从第3个版本到第4个版本发展至今,Netty已经非常的成熟、稳定了。NIO它其实存在一部分的bug,Netty针对那些bug做了一些相应的修复,所以这也是Netty做的比较好的一点。Netty社区非常的活跃,从它的纸质上就可以看的出来,它的迭代更新的频率也是非常高的。很多大型的项目都在使用Netty,Netty经过了非常多的商用项目或者说框架的历练、考验,所以它是经得起考验的。比方说Dubbo,Dubbo的底层就是用的Netty,很多项目都会使用Dubbo,Dubbo使用的是Netty。Dubbo经过了考验,就代表Netty也经过了大型项目的考验。我们要去使用NIO、开发NIO的话,那么我们来使用Netty毋庸置疑是一个最好的一个网络框架。
Netty提供了三种线程模型,线程模型又可以称之为Reactor线程模型。你可以把Reactor线程当成是一个线程。
单线程模型:所有的IO操作都由同一个NIO线程处理的。连接的时候是由单线程去处理的。连接完之后要去做一些额外的操作,也是会由我们当前的单线程去做的。作为服务端的话,它用于接收客户端所有的链接。如果你把它当做客户端,它就会向我们的服务端发起所有的链接。同时它又可以去读取请求,响应消息,发送请求,其实这就是一个request和response。这种线程模型它其实使用的是异步非阻塞的IO。所有的IO操作它其实都不会阻塞的。理论上这样的一个单线程它是可以独立去处理所有的IO的相关的操作都是没有问题的。如果说我们简单地从它的一个整体架构层面上去看的话这样的单独的一个单线程理论上可以完成相应的所有的操作IO处理。但是这仅仅只限于一些小型的应用场景。在高负载、大并发的场景,那么使用单线程肯定就不太合适。这主要是因为一个NIO的线程要去同时处理成百上千上万的请求的时候,它的性能上会支撑不了,即便我们给与我们服务器的一个CPU负载,让它去达到100%的话,对于我们巨大的庞大的海量的数据、海量的消息,去进行处理、编解码以及去读取消息发送消息,在各种情况下它还是会满足不了。当我们的NIO这样的一个线程,单线程负载负荷过重之后,我们整体的服务器处理的速度、性能就会变慢,这样子就会导致我们的客户端在向我们的服务端发起请求,发起第一次链接的时候就会超时。超时之后由于我们的客户端都会有一种超时机制,我们的客户端会反复地发起请求,向服务端重置。其实这时候就会陷入了一个死循环,这样子就会更加地加重了我们的一个服务器的负载。最终整体的服务器可能就会造成一个宕机,或者单节点故障都是有可能的。我开了一个夜店,招了一个小哥哥或者小姐姐在门口帮我接待客人,他又在我的大堂分配一些相应的任务,比方说端茶水端酒水等等,那么我只有这样的一个人。如果我的生意好起来了,就会有大量的客人过来,这时候一个人不够,所以Netty提供了第二种的线程模型。
第二种线程模型我们可以称之为多线程模型。左边的单线程用于处理相应的客户端链接。当客户端链接链到我这边之后,我直接把这些客户端丢给我后边的线程池。我在大堂招揽生意,招揽到生意之后你们就直接到大堂去,我就不负责你们了,因为大堂有更加多的服务员可以去服务你们,这样子就大大地增加了我的并发量,我接待客户的一个能力。这样子就相当于是有单独的专门的一个NIO线程用于去监听我们的服务器,去接收我们客户端的一些相应的请求。额外的读写请求都会由线程池负责去做。如果我们现在达到了一个百万级别的并发,就是说我们一下子有很多很多的客人,门口的一个小哥哥或者小姐姐肯定是接待能力有限,这个时候我肯定又要额外去招人了。
一个独立的线程池处理请求,当线程池接收到客户端的请求之后,这个时候它会把我们的所有的请求都会注册到我们的一个IO线程池,让IO线程池负责后续的处理,比方说编解码、读写等等的操作。主线程池主要完成客户端的登录、握手、安全认证等等。一旦我们整体的链路在建立完毕之后,它就会把我们的客户端丢到我们的从线程池里面去。这种线程模型是Netty官方推荐使用的线程模型。在后续的代码里面我们也是会使用这种主从线程模型进行开发,因为它非常高效。
以上三个就是我们Netty最基本的线程模型。