共计 13794 个字符,预计需要花费 35 分钟才能阅读完成。
高清思维导图原件 (xmind/pdf/jpg
) 能够关注公众号:一枝花算不算浪漫
回复nio
即可。
前言
道歉好久没更原创文章了,看了下上篇更新工夫,曾经拖更一个多月了。
这段时间也始终在学习 Netty
相干常识,因为波及知识点比拟多,也走了不少弯路。目前网上对于 Netty 学习材料玲琅满目,不知如何下手,其实大家都是一样的,学习办法和技巧都是总结进去的,咱们在没有找到很好的办法之前不如循序渐进先从根底开始,个别从总分总的渐进形式,既观森林,又见草木。
之前凑巧跟杭州一个敌人 小飞 也提到过,两者在这方面的初衷是统一的,也心愿更多的敌人可能退出一起学习和探讨。(PS:本篇文章是和小飞一起学习整顿所得~)
Netty
是一款提供异步的、事件驱动的网络应用程序框架和工具,是基于 NIO
客户端、服务器端的编程框架。所以这里咱们先以 NIO
和依赖相干的根底铺垫来进行分析解说,从而作为 Netty
学习之旅的一个开始。
一、网络编程根底回顾
1. Socket
Socket
自身有“插座”的意思,不是 Java 中特有的概念,而是一个语言无关的规范,任何能够实现网络编程的编程语言都有 Socket
。在Linux
环境下,用于示意过程间网络通信的非凡文件类型,其本质为内核借助缓冲区造成的伪文件。既然是文件,那么天经地义的,咱们能够应用文件描述符援用套接字。
与管道相似的,Linux
零碎将其封装成文件的目标是为了对立接口,使得读写套接字和读写文件的操作统一。区别是管道次要利用于本地过程间通信,而套接字多利用于网络过程间数据的传递。
能够这么了解:Socket
就是网络上的两个应用程序通过一个双向通信连贯实现数据交换的编程接口 API。
Socket
通信的根本流程具体步骤如下所示:
(1)服务端通过 Listen
开启监听,期待客户端接入。
(2)客户端的套接字通过 Connect
连贯服务器端的套接字,服务端通过 Accept
接管客户端连贯。在 connect-accept
过程中,操作系统将会进行三次握手。
(3)客户端和服务端通过 write
和read
发送和接收数据,操作系统将会实现 TCP
数据的确认、重发等步骤。
(4)通过 close
敞开连贯,操作系统会进行四次挥手。
针对 Java 编程语言,java.net
包是网络编程的根底类库。其中 ServerSocket
和Socket
是网络编程的根底类型。
SeverSocket
是服务端利用类型。Socket
是建设连贯的类型。当连贯建设胜利后,服务器和客户端都会有一个 Socket
对象示例,能够通过这个 Socket
对象示例,实现会话的所有操作。对于一个残缺的网络连接来说,Socket
是平等的,没有服务器客户端分级状况。
2. IO 模型介绍
对于一次 IO 操作,数据会先拷贝到内核空间中,而后再从内核空间拷贝到用户空间中,所以一次 read
操作,会经验两个阶段:
(1)期待数据筹备
(2)数据从内核空间拷贝到用户空间
基于以上两个阶段就产生了五种不同的 IO 模式。
- 阻塞 IO:从过程发动 IO 操作,始终期待上述两个阶段实现,此时两阶段一起阻塞。
- 非阻塞 IO:过程始终询问 IO 筹备好了没有,筹备好了再发动读取操作,这时才把数据从内核空间拷贝到用户空间。第一阶段不阻塞但要轮询,第二阶段阻塞。
- 多路复用 IO:多个连贯应用同一个 select 去询问 IO 筹备好了没有,如果有筹备好了的,就返回有数据筹备好了,而后对应的连贯再发动读取操作,把数据从内核空间拷贝到用户空间。两阶段离开阻塞。
- 信号驱动 IO:过程发动读取操作会立刻返回,当数据筹备好了会以告诉的模式通知过程,过程再发动读取操作,把数据从内核空间拷贝到用户空间。第一阶段不阻塞,第二阶段阻塞。
- 异步 IO:过程发动读取操作会立刻返回,等到数据筹备好且曾经拷贝到用户空间了再告诉过程拿数据。两个阶段都不阻塞。
这五种 IO 模式不难发现存在这两对关系:同步和异步、阻塞和非阻塞。那么略微解释一下:
同步和异步
- 同步: 同步就是发动一个调用后,被调用者未解决完申请之前,调用不返回。
- 异步: 异步就是发动一个调用后,立即失去被调用者的回应示意已接管到申请,然而被调用者并没有返回后果,此时咱们能够解决其余的申请,被调用者通常依附事件,回调等机制来告诉调用者其返回后果。
同步和异步的区别最大在于异步的话调用者不须要期待处理结果,被调用者会通过回调等机制来告诉调用者其返回后果。
阻塞和非阻塞
- 阻塞: 阻塞就是发动一个申请,调用者始终期待申请后果返回,也就是以后线程会被挂起,无奈从事其余工作,只有当条件就绪能力持续。
- 非阻塞: 非阻塞就是发动一个申请,调用者不必始终等着后果返回,能够先去干其余事件。
阻塞和非阻塞是针对过程在拜访数据的时候,依据 IO 操作的就绪状态来采取的不同形式,说白了是一种读取或者写入操作方法的实现形式,阻塞形式下读取或者写入函数将始终期待,而非阻塞形式下,读取或者写入办法会立刻返回一个状态值。
如果组合后的同步阻塞 (blocking-IO
) 简称 BIO
、同步非阻塞(non-blocking-IO
) 简称 NIO
和异步非阻塞 (asynchronous-non-blocking-IO
) 简称 AIO
又代表什么意思呢?
- BIO (同步阻塞 I / O 模式): 数据的读取写入必须阻塞在一个线程内期待其实现。这里应用那个经典的烧开水例子,这里假如一个烧开水的场景,有一排水壶在烧开水,BIO 的工作模式就是,叫一个线程停留在一个水壶那,直到这个水壶烧开,才去解决下一个水壶。然而实际上线程在期待水壶烧开的时间段什么都没有做。
- NIO(同步非阻塞): 同时反对阻塞与非阻塞模式,但这里咱们以其同步非阻塞 I / O 模式来阐明,那么什么叫做同步非阻塞?如果还拿烧开水来说,NIO 的做法是叫一个线程一直的轮询每个水壶的状态,看看是否有水壶的状态产生了扭转,从而进行下一步的操作。
- AIO(异步非阻塞 I / O 模型): 异步非阻塞与同步非阻塞的区别在哪里?异步非阻塞无需一个线程去轮询所有 IO 操作的状态扭转,在相应的状态扭转后,零碎会告诉对应的线程来解决。对应到烧开水中就是,为每个水壶下面装了一个开关,水烧开之后,水壶会主动告诉我水烧开了。
java
中的 BIO
、NIO
和 AIO
了解为是 Java 语言
在操作系统层面对这三种 IO
模型的封装。程序员在应用这些 封装 API 的时候,不须要关怀操作系统层面的常识,也不须要依据不同操作系统编写不同的代码,只须要应用 Java
的 API 就能够了。由此,为了使读者对这三种模型有个比拟具体和递推式的理解,并且和本文主题 NIO
有个清晰的比照,上面持续延长。
Java BIO
BIO
编程形式通常是是 Java 的上古产品,自 JDK 1.0-JDK1.4 就有的货色。编程实现过程为:首先在服务端启动一个 ServerSocket
来监听网络申请,客户端启动 Socket
发动网络申请,默认状况下 SeverSocket
会建设一个线程来解决此申请,如果服务端没有线程可用,客户端则会阻塞期待或受到回绝。服务器实现模式为一个连贯一个线程,即客户端有连贯申请时服务器端就须要启动一个线程进行解决。大抵构造如下:
如果要让 BIO
通信模型可能同时解决多个客户端申请,就必须应用多线程(次要起因是 socket.accept()
、socket.read()
、socket.write()
波及的三个次要函数都是同步阻塞的),也就是说它在接管到客户端连贯申请之后为每个客户端创立一个新的线程进行链路解决,解决实现之后,通过输入流返回应答给客户端,线程销毁。这就是典型的 一申请一应答通信模型。咱们能够构想一下如果这个连贯不做任何事件的话就会造成不必要的线程开销,不过能够通过 线程池机制 改善,线程池还能够让线程的创立和回收老本绝对较低。应用线程池机制改善后的 BIO
模型图如下:
BIO
形式实用于连贯数目比拟小且固定的架构,这种形式对服务器资源要求比拟高,并发局限于利用中,是 JDK1.4 以前的惟一抉择,但程序直观简略易懂。Java BIO
编程示例网上很多,这里就不进行 coding 举例了,毕竟前面 NIO
才是重点。
Java NIO
NIO
(New IO 或者 No-Blocking IO),从 JDK1.4 开始引入的 非阻塞 IO
,是一种 非阻塞
+ 同步
的通信模式。这里的 No Blocking IO
用于辨别下面的BIO
。
NIO
自身想解决 BIO
的并发问题,通过 Reactor 模式
的事件驱动机制来达到 Non Blocking
的。当 socket
有流可读或可写入 socket
时,操作系统会相应的告诉应用程序进行解决,利用再将流读取到缓冲区或写入操作系统。
也就是说,这个时候,曾经不是一个连贯就 要对应一个解决线程了,而是无效的申请,对应一个线程,当连贯没有数据时,是没有工作线程来解决的。
当一个连贯创立后,不须要对应一个线程,这个连贯会被注册到 多路复用器
下面,所以所有的连贯只须要一个线程就能够搞定,当这个线程中的 多路复用器
进行轮询的时候,发现连贯上有申请的话,才开启一个线程进行解决,也就是一个申请一个线程模式。
NIO
提供了与传统 BIO 模型中的 Socket
和ServerSocket
绝对应的 SocketChannel
和ServerSocketChannel
两种不同的套接字通道实现,如下图构造所示。这里波及的 Reactor
设计模式、多路复用 Selector
、Buffer
等临时不必管,前面会讲到。
NIO 形式实用于连贯数目多且连贯比拟短 (轻操作) 的架构,比方聊天服务器,并发局 限于利用中,编程简单,JDK1.4 开始反对。同时,NIO
和一般 IO 的区别次要能够从存储数据的载体、是否阻塞等来辨别:
Java AIO
与 NIO
不同,当进行读写操作时,只须间接调用 API 的 read
或 write
办法即可。这两种办法均为异步的,对于读操作而言,当有流可读取时,操作系统会将可读的流传入 read
方 法的缓冲区,并告诉应用程序;对于写操作而言,当操作系统将 write
办法传递的流写入结束时,操作系统被动告诉应用程序。即能够了解为,read/write
办法都是异步的,实现后会被动调用回调函数。在 JDK7
中,提供了异步文件通道和异步套接字通道的实现,这部分内容被称作 NIO
.
AIO
形式应用于连贯数目多且连贯比拟长 (重操作) 的架构,比方相册服务器,充沛调用 OS
参加并发操作,编程比较复杂,JDK7
开始反对。
目前来说 AIO
的利用还不是很宽泛,Netty
之前也尝试应用过 AIO
,不过又放弃了。
二、NIO 外围组件介绍
1. Channel
在 NIO
中,根本所有的 IO 操作都是从 Channel
开始的,Channel
通过 Buffer(缓冲区)
进行读写操作。
read()
示意读取通道中数据到缓冲区,write()
示意把缓冲区数据写入到通道。
Channel
有好多实现类,这里有三个最罕用:
SocketChannel
:一个客户端发动 TCP 连贯的 ChannelServerSocketChannel
:一个服务端监听新连贯的 TCP Channel,对于每一个新的 Client 连贯,都会建设一个对应的 SocketChannelFileChannel
:从文件中读写数据
其中 SocketChannel
和ServerSocketChannel
是网络编程中最罕用的,一会在最初的示例代码中会有解说到具体用法。
2. Buffer
概念
Buffer
也被成为内存缓冲区,实质上就是内存中的一块,咱们能够将数据写入这块内存,之后从这块内存中读取数据。也能够将这块内存封装成 NIO Buffer
对象,并提供一组罕用的办法,不便咱们对该块内存进行读写操作。
Buffer
在 java.nio
中被定义为抽象类:
咱们能够将 Buffer
了解为一个数组的封装,咱们最罕用的 ByteBuffer
对应的数据结构就是byte[]
属性
Buffer
中有 4 个十分重要的属性:capacity、limit、position、mark
capacity
属性:容量,Buffer 可能包容的数据元素的最大值,在 Buffer 初始化创立的时候被赋值,而且不能被批改。
上图中,初始化 Buffer 的容量为 8(图中从 0~7,共 8 个元素),所以capacity = 8
-
limit
属性:代表 Buffer 可读可写的下限。- 写模式下:
limit
代表能写入数据的下限地位,这个时候limit = capacity
- 写模式下:
读模式下:在 Buffer
实现所有数据写入后,通过调用 flip()
办法,切换到读模式,此时 limit
等于 Buffer
中理论曾经写入的数据大小。因为 Buffer
可能没有被写满,所以limit<=capacity
-
position
属性:代表读取或者写入Buffer
的地位。默认为 0。- 写模式下:每往
Buffer
中写入一个值,position
就会主动加 1,代表下一次写入的地位。 - 读模式下:每往
Buffer
中读取一个值,position
就主动加 1,代表下一次读取的地位。
- 写模式下:每往
从上图就能很清晰看出,读写模式下 capacity、limit、position 的关系了。
mark
属性:代表标记,通过 mark()办法,记录以后 position 值,将 position 值赋值给 mark,在后续的写入或读取过程中,能够通过 reset()办法复原以后 position 为 mark 记录的值。
这几个重要属性讲完,咱们能够再来回顾下:
0 <= mark <= position <= limit <= capacity
当初应该很清晰这几个属性的关系了~
Buffer 常见操作
创立 Buffer
allocate(int capacity)
ByteBuffer buffer = ByteBuffer.allocate(1024);
int count = channel.read(buffer);
例子中创立的 ByteBuffer
是基于堆内存的一个对象。
wrap(array)
wrap
办法能够将数组包装成一个 Buffer
对象:
ByteBuffer buffer = ByteBuffer.wrap("hello world".getBytes());
channel.write(buffer);
allocateDirect(int capacity)
通过 allocateDirect
办法也能够疾速实例化一个 Buffer
对象,和 allocate
很类似,这里区别的是 allocateDirect
创立的是基于 堆外内存 的对象。
堆外内存不在 JVM 堆上,不受 GC 的治理。堆外内存进行一些底层零碎的 IO 操作时,效率会更高。
Buffer 写操作
Buffer
写入能够通过 put()
和channel.read(buffer)
两种形式写入。
通常咱们 NIO 的读操作的时候,都是从 Channel
中读取数据写入 Buffer
,这个对应的是Buffer
的写操作。
Buffer 读操作
Buffer
读取能够通过 get()
和channel.write(buffer)
两种形式读入。
还是同上,咱们对 Buffer
的读入操作,反过来说就是对 Channel
的写操作 。读取Buffer
中的数据而后写入 Channel
中。
其余常见办法
rewind()
:重置 position 地位为 0,能够从新读取和写入 buffer,个别该办法实用于读操作,能够了解为对 buffer 的反复读。
public final Buffer rewind() {
position = 0;
mark = -1;
return this;
}
flip()
:很罕用的一个办法,个别在写模式切换到读模式的时候会常常用到。也会将 position 设置为 0,而后设置 limit 等于原来写入的 position。
public final Buffer flip() {
limit = position;
position = 0;
mark = -1;
return this;
}
clear()
:重置 buffer 中的数据,该办法次要是针对于写模式,因为 limit 设置为了 capacity,读模式下会出问题。
public final Buffer clear() {
position = 0;
limit = capacity;
mark = -1;
return this;
}
mark()&reset()
:mark()
办法是保留以后position
到变量mark
z 中,而后通过reset()
办法复原以后position
为mark
,实现代码很简略,如下:
public final Buffer mark() {
mark = position;
return this;
}
public final Buffer reset() {
int m = mark;
if (m < 0)
throw new InvalidMarkException();
position = m;
return this;
}
罕用的读写办法能够用一张图总结一下:
3. Selector
概念
Selector
是 NIO 中最为重要的组件之一,咱们经常说的 多路复用器
就是指的 Selector
组件。Selector
组件用于轮询一个或多个 NIO Channel
的状态是否处于可读、可写。通过轮询的机制就能够治理多个 Channel,也就是说能够治理多个网络连接。
轮询机制
- 首先,须要将 Channel 注册到 Selector 上,这样 Selector 才晓得须要治理哪些 Channel
- 接着 Selector 会一直轮询其上注册的 Channel,如果某个 Channel 产生了读或写的工夫,这个 Channel 就会被 Selector 轮询进去,而后通过 SelectionKey 能够获取就绪的 Channel 汇合,进行后续的 IO 操作。
属性操作
- 创立 Selector
通过 open()
办法,咱们能够创立一个 Selector
对象。
Selector selector = Selector.open();
- 注册 Channel 到 Selector 中
咱们须要将 Channel
注册到 Selector
中,才可能被 Selector
治理。
channel.configureBlocking(false);
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
某个 Channel
要注册到 Selector
中,那么该 Channel 必须是 非阻塞 ,所有下面代码中有个configureBlocking()
的配置操作。
在 register(Selector selector, int interestSet)
办法的第二个参数,标识一个 interest
汇合,意思是 Selector 对哪些事件感兴趣,能够监听四种不同类型的事件:
public static final int OP_READ = 1 << 0;
public static final int OP_WRITE = 1 << ;
public static final int OP_CONNECT = 1 << 3;
public static final int OP_ACCEPT = 1 << 4;
Connect 事件
:连贯实现事件(TCP 连贯),仅实用于客户端,对应 SelectionKey.OP_CONNECT。Accept 事件
:承受新连贯事件,仅实用于服务端,对应 SelectionKey.OP_ACCEPT。Read 事件
:读事件,实用于两端,对应 SelectionKey.OP_READ,示意 Buffer 可读。Write 事件
:写工夫,实用于两端,对应 SelectionKey.OP_WRITE,示意 Buffer 可写。
Channel
触发了一个事件,表明该工夫曾经准备就绪:
- 一个 Client Channel 胜利连贯到另一个服务器,成为“连贯就绪”
- 一个 Server Socket 筹备好接管新进入的接,称为“接管就绪”
- 一个有数据可读的 Channel,称为“读就绪”
- 一个期待写数据的 Channel,称为”写就绪“
当然,Selector
是能够同时对多个事件感兴趣的,咱们应用或运算即可组合多个事件:
int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
Selector 其余一些操作
抉择 Channel
public abstract int select() throws IOException;
public abstract int select(long timeout) throws IOException;
public abstract int selectNow() throws IOException;
当 Selector 执行 select()
办法就会产生阻塞,等到注册在其上的 Channel 准备就绪就会立刻返回,返回准备就绪的数量。
select(long timeout)
则是在 select()
的根底上减少了超时机制。selectNow()
立刻返回,不产生阻塞。
有一点十分须要留神: select
办法返回的 int
值,示意有多少 Channel
曾经就绪。
自上次调用select
办法后有多少 Channel
变成就绪状态。如果调用 select
办法,因为有一个 Channel
变成就绪状态则返回了 1;
若再次调用 select
办法,如果另一个 Channel
就绪了,它会再次返回 1。
获取可操作的 Channel
Set selectedKeys = selector.selectedKeys();
当有新增就绪的 Channel
,调用select()
办法,就会将 key 增加到 Set 汇合中。
三、代码示例
后面铺垫了这么多,次要是想让大家可能看懂 NIO
代码示例,也不便后续大家来本人手写NIO
网络编程的程序。创立 NIO 服务端的次要步骤如下:
1. 关上 ServerSocketChannel,监听客户端连贯 2. 绑定监听端口,设置连贯为非阻塞模式 3. 创立 Reactor 线程,创立多路复用器并启动线程 4. 将 ServerSocketChannel 注册到 Reactor 线程中的 Selector 上,监听 ACCEPT 事件 5. Selector 轮询准备就绪的 key 6. Selector 监听到新的客户端接入,解决新的接入申请,实现 TCP 三次握手,建设物理链路 7. 设置客户端链路为非阻塞模式 8. 将新接入的客户端连贯注册到 Reactor 线程的 Selector 上,监听读操作,读取客户端发送的网络音讯 9. 异步读取客户端音讯到缓冲区 10. 对 Buffer 编解码,解决半包音讯,将解码胜利的音讯封装成 Task 11. 将应答音讯编码为 Buffer,调用 SocketChannel 的 write 将音讯异步发送给客户端
NIOServer.java
:
public class NIOServer {
private static Selector selector;
public static void main(String[] args) {init();
listen();}
private static void init() {
ServerSocketChannel serverSocketChannel = null;
try {selector = Selector.open();
serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
serverSocketChannel.socket().bind(new InetSocketAddress(9000));
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("NioServer 启动实现");
} catch (IOException e) {e.printStackTrace();
}
}
private static void listen() {while (true) {
try {selector.select();
Iterator<SelectionKey> keysIterator = selector.selectedKeys().iterator();
while (keysIterator.hasNext()) {SelectionKey key = keysIterator.next();
keysIterator.remove();
handleRequest(key);
}
} catch (Throwable t) {t.printStackTrace();
}
}
}
private static void handleRequest(SelectionKey key) throws IOException {
SocketChannel channel = null;
try {if (key.isAcceptable()) {ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
channel = serverSocketChannel.accept();
channel.configureBlocking(false);
System.out.println("承受新的 Channel");
channel.register(selector, SelectionKey.OP_READ);
}
if (key.isReadable()) {channel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int count = channel.read(buffer);
if (count > 0) {System.out.println("服务端接管申请:" + new String(buffer.array(), 0, count));
channel.register(selector, SelectionKey.OP_WRITE);
}
}
if (key.isWritable()) {ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("收到".getBytes());
buffer.flip();
channel = (SocketChannel) key.channel();
channel.write(buffer);
channel.register(selector, SelectionKey.OP_READ);
}
} catch (Throwable t) {t.printStackTrace();
if (channel != null) {channel.close();
}
}
}
}
NIOClient.java
:
public class NIOClient {public static void main(String[] args) {new Worker().start();}
static class Worker extends Thread {
@Override
public void run() {
SocketChannel channel = null;
Selector selector = null;
try {channel = SocketChannel.open();
channel.configureBlocking(false);
selector = Selector.open();
channel.register(selector, SelectionKey.OP_CONNECT);
channel.connect(new InetSocketAddress(9000));
while (true) {selector.select();
Iterator<SelectionKey> keysIterator = selector.selectedKeys().iterator();
while (keysIterator.hasNext()) {SelectionKey key = keysIterator.next();
keysIterator.remove();
if (key.isConnectable()) {System.out.println();
channel = (SocketChannel) key.channel();
if (channel.isConnectionPending()) {channel.finishConnect();
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("你好".getBytes());
buffer.flip();
channel.write(buffer);
}
channel.register(selector, SelectionKey.OP_READ);
}
if (key.isReadable()) {channel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int len = channel.read(buffer);
if (len > 0) {System.out.println("[" + Thread.currentThread().getName()
+ "]收到响应:" + new String(buffer.array(), 0, len));
Thread.sleep(5000);
channel.register(selector, SelectionKey.OP_WRITE);
}
}
if(key.isWritable()) {ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("你好".getBytes());
buffer.flip();
channel = (SocketChannel) key.channel();
channel.write(buffer);
channel.register(selector, SelectionKey.OP_READ);
}
}
}
} catch (Exception e) {e.printStackTrace();
} finally{if(channel != null){
try {channel.close();
} catch (IOException e) {e.printStackTrace();
}
}
if(selector != null){
try {selector.close();
} catch (IOException e) {e.printStackTrace();
}
}
}
}
}
}
打印后果:
// Server 端
NioServer 启动实现
承受新的 Channel
服务端接管申请:你好
服务端接管申请:你好
服务端接管申请:你好
// Client 端
[Thread-0]收到响应:收到
[Thread-0]收到响应:收到
[Thread-0]收到响应:收到
四、总结
回顾一下应用 NIO
开发服务端程序的步骤:
- 创立
ServerSocketChannel
和业务解决线程池。 - 绑定监听端口,并配置为非阻塞模式。
- 创立
Selector
,将之前创立的ServerSocketChannel
注册到Selector
上,监听SelectionKey.OP_ACCEPT
。 - 循环执行
Selector.select()
` 办法,轮询就绪的Channel
。 - 轮询就绪的
Channel
时,如果是处于OP_ACCEPT
状态,阐明是新的客户端接入,调用ServerSocketChannel.accept
接管新的客户端。 - 设置新接入的
SocketChannel
为非阻塞模式,并注册到Selector
上,监听OP_READ
。 - 如果轮询的
Channel
状态是OP_READ
,阐明有新的就绪数据包须要读取,则结构ByteBuffer
对象,读取数据。
那从这些步骤中根本晓得开发者须要相熟的知识点有:
jdk-nio
提供的几个要害类:Selector
,SocketChannel
,ServerSocketChannel
,FileChannel
,ByteBuffer
,SelectionKey
- 须要晓得网络常识:tcp 粘包拆包、网络闪断、包体溢出及反复发送等
- 须要晓得
linux
底层实现,如何正确的敞开channel
,如何退出登记selector
,如何防止selector
太过于频繁 - 须要晓得如何让
client
端取得server
端的返回值, 而后才返回给前端,须要如何期待或在怎么作熔断机制 - 须要晓得对象序列化,及序列化算法
- 省略等等,因为我曾经有点不难受了,作为程序员的我习惯了舒舒服服简略的 API,不必太晓得底层细节,就能写出比拟强壮和没有 Bug 的代码 …
NIO 原生 API 的弊病 :
① NIO 组件简单 : 应用原生 NIO
开发服务器端与客户端 , 须要波及到 服务器套接字通道 (ServerSocketChannel
) , 套接字通道 (SocketChannel
) , 选择器 (Selector
) , 缓冲区 (ByteBuffer
) 等组件 , 这些组件的原理 和 API 都要相熟 , 能力进行 NIO
的开发与调试 , 之后还须要针对利用进行调试优化
② NIO 开发根底 : NIO
门槛略高 , 须要开发者把握多线程、网络编程等能力开发并且优化 NIO
网络通信的应用程序
③ 原生 API 开发网络通信模块的根本的传输解决 : 网络传输不光是实现服务器端和客户端的数据传输性能 , 还要解决各种异常情况 , 如 连贯断开重连机制 , 网络梗塞解决 , 异样解决 , 粘包解决 , 拆包解决 , 缓存机制 等方面的问题 , 这是所有成熟的网络应用程序都要具备的性能 , 否则只能说是入门级的 Demo
④ NIO BUG : NIO
自身存在一些 BUG , 如 Epoll
, 导致 选择器 (Selector
) 空轮询 , 在 JDK 1.7 中还没有解决
Netty
在 NIO
的根底上 , 封装了 Java 原生的 NIO API
, 解决了上述哪些问题呢?
相比 Java NIO,应用 Netty
开发程序,都简化了哪些步骤呢?… 等等这系列问题也都是咱们要问的问题。不过因为这篇只是介绍 NIO
相干常识,没有介绍 Netty API
的应用,所以介绍 Netty API
应用简略开发门槛低等长处有点站不住脚。那么就留到前面跟大家一起开启 Netty
学习之旅,探讨人人说好的 Netty
到底是不是江湖传言的那么好。
一起期待后续的 Netty
之旅吧!