NIO是Java 4里面提供的新的API,目的是用来解决传统IO的问题。本文下面分别从Java NIO的几个基础概念介绍起。
以下是本文的目录大纲:
一.NIO中的几个基础概念
二.Channel
三.Buffer
四.Selector
若有不正之处,请多多谅解并欢迎批评指正。
请尊重作者劳动成果,转载请标明原文链接:
http://www.cnblogs.com/dolphin0520/p/3919162.html
一.NIO中的几个基础概念
在NIO中有几个比较关键的概念:Channel(通道),Buffer(缓冲区),Selector(选择器)。
首先从Channel说起吧,通道,顾名思义,就是通向什么的道路,为某个提供了渠道。在传统IO中,我们要读取一个文件中的内容,通常是像下面这样读取的:
1 2 3 4 5 6 7 8 9 |
|
这里的InputStream实际上就是为读取文件提供一个通道的。
因此可以将NIO 中的Channel同传统IO中的Stream来类比,但是要注意,传统IO中,Stream是单向的,比如InputStream只能进行读取操作,OutputStream只能进行写操作。而Channel是双向的,既可用来进行读操作,又可用来进行写操作。
Buffer(缓冲区),是NIO中非常重要的一个东西,在NIO中所有数据的读和写都离不开Buffer。比如上面的一段代码中,读取的数据时放在byte数组当中,而在NIO中,读取的数据只能放在Buffer中。同样地,写入数据也是先写入到Buffer中。
下面介绍一下NIO中最核心的一个东西:Selector。可以说它是NIO中最关键的一个部分,Selector的作用就是用来轮询每个注册的Channel,一旦发现Channel有注册的事件发生,便获取事件然后进行处理。
比如看下面的这个例子:
用单线程处理一个Selector,然后通过Selector.select()方法来获取到达事件,在获取了到达事件之后,就可以逐个地对这些事件进行响应处理。
二.Channel
在前面已经提到,Channel和传统IO中的Stream很相似。虽然很相似,但是有很大的区别,主要区别为:通道是双向的,通过一个Channel既可以进行读,也可以进行写;而Stream只能进行单向操作,通过一个Stream只能进行读或者写;
以下是常用的几种通道:
- FileChannel
- SocketChanel
- ServerSocketChannel
- DatagramChannel
通过使用FileChannel可以从文件读或者向文件写入数据;通过SocketChannel,以TCP来向网络连接的两端读写数据;通过ServerSocketChanel能够监听客户端发起的TCP连接,并为每个TCP连接创建一个新的SocketChannel来进行数据读写;通过DatagramChannel,以UDP协议来向网络连接的两端读写数据。
下面给出通过FileChannel来向文件中写入数据的一个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
通过上面的程序会向工程目录下的data.txt文件写入字符串"java nio",注意在调用channel的write方法之前必须调用buffer的flip方法,否则无法正确写入内容,至于具体原因将在下篇博文中具体讲述Buffer的用法时阐述。
三.Buffer
Buffer,故名思意,缓冲区,实际上是一个容器,是一个连续数组。Channel提供从文件、网络读取数据的渠道,但是读取或写入的数据都必须经由Buffer。具体看下面这张图就理解了:
上面的图描述了从一个客户端向服务端发送数据,然后服务端接收数据的过程。客户端发送数据时,必须先将数据存入Buffer中,然后将Buffer中的内容写入通道。服务端这边接收数据必须通过Channel将数据读入到Buffer中,然后再从Buffer中取出数据来处理。
在NIO中,Buffer是一个顶层父类,它是一个抽象类,常用的Buffer的子类有:
- ByteBuffer
- IntBuffer
- CharBuffer
- LongBuffer
- DoubleBuffer
- FloatBuffer
- ShortBuffer
如果是对于文件读写,上面几种Buffer都可能会用到。但是对于网络读写来说,用的最多的是ByteBuffer。
关于Buffer的具体使用以及它的limit、posiion和capacity这几个属性的理解在下一篇文章中讲述。
四.Selector
Selector类是NIO的核心类,Selector能够检测多个注册的通道上是否有事件发生,如果有事件发生,便获取事件然后针对每个事件进行相应的响应处理。这样一来,只是用一个单线程就可以管理多个通道,也就是管理多个连接。这样使得只有在连接真正有读写事件发生时,才会调用函数来进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程,并且避免了多线程之间的上下文切换导致的开销。
与Selector有关的一个关键类是SelectionKey,一个SelectionKey表示一个到达的事件,这2个类构成了服务端处理业务的关键逻辑。
本篇主要讲解Java中的IO机制和网络通讯中处理高并发的NIO
分为两块:
第一块讲解多线程下的IO机制
第二块讲解如何在IO机制下优化CPU资源的浪费(New IO)
Echo服务器
单线程下的socket机制就不用我介绍了,不懂得可以去查阅下资料
那么多线程下,如果进行套接字的使用呢?
我们使用最简单的echo服务器来帮助大家理解
首先,来看下多线程下服务端和客户端的工作流程图:
可以看到,多个客户端同时向服务端发送请求
服务端做出的措施是开启多个线程来匹配相对应的客户端
并且每个线程去独自完成他们的客户端请求
原理讲完了我们来看下是如何实现的
在这里我写了一个简单的服务器
用到了线程池的技术来创建线程(具体代码作用我已经加了注释):
public class MyServer {
private static ExecutorService executorService = Executors.newCachedThreadPool(); //创建一个线程池
private static class HandleMsg implements Runnable{ //一旦有新的客户端请求,创建这个线程进行处理
Socket client; //创建一个客户端
public HandleMsg(Socket client){ //构造传参绑定
this.client = client;
}
@Override
public void run() {
BufferedReader bufferedReader = null; //创建字符缓存输入流
PrintWriter printWriter = null; //创建字符写入流
try {
bufferedReader = new BufferedReader(new InputStreamReader(client.getInputStream())); //获取客户端的输入流
printWriter = new PrintWriter(client.getOutputStream(),true); //获取客户端的输出流,true是随时刷新
String inputLine = null;
long a = System.currentTimeMillis();
while ((inputLine = bufferedReader.readLine())!=null){
printWriter.println(inputLine);
}
long b = System.currentTimeMillis();
System.out.println("此线程花费了:"+(b-a)+"秒!");
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
bufferedReader.close();
printWriter.close();
client.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) throws IOException { //服务端的主线程是用来循环监听客户端请求
ServerSocket server = new ServerSocket(8686); //创建一个服务端且端口为8686
Socket client = null;
while (true){ //循环监听
client = server.accept(); //服务端监听到一个客户端请求
System.out.println(client.getRemoteSocketAddress()+"地址的客户端连接成功!");
executorService.submit(new HandleMsg(client)); //将该客户端请求通过线程池放入HandlMsg线程中进行处理
}
}
}
上述代码中我们使用一个类编写了一个简单的echo服务器
在主线程中用死循环来开启端口监听
简单客户端
有了服务器,我们就可以对其进行访问,并且发送一些字符串数据
服务器的功能是返回这些字符串,并且打印出线程占用时间
下面来写个简单的客户端来响应服务端:
public class MyClient {
public static void main(String[] args) throws IOException {
Socket client = null;
PrintWriter printWriter = null;
BufferedReader bufferedReader = null;
try {
client = new Socket();
client.connect(new InetSocketAddress("localhost",8686));
printWriter = new PrintWriter(client.getOutputStream(),true);
printWriter.println("hello");
printWriter.flush();
bufferedReader = new BufferedReader(new InputStreamReader(client.getInputStream())); //读取服务器返回的信息并进行输出
System.out.println("来自服务器的信息是:"+bufferedReader.readLine());
} catch (IOException e) {
e.printStackTrace();
}finally {
printWriter.close();
bufferedReader.close();
client.close();
}
}
}
代码中,我们用字符流发送了一个hello字符串过去,如果代码没问题
服务器会返回一个hello数据,并且打印出我们设置的日志信息
很好,一个简单的多线程套接字编程就实现了
但是试想一下:
如果一个客户端请求中,在IO写入到服务端过程中加入Sleep,
使每个请求占用服务端线程10秒
然后有大量的客户端请求,每个请求都占用那么长时间
那么服务端的并能能力就会大幅度下降
这并不是因为服务端有多少繁重的任务,而仅仅是因为服务线程在等待IO(因为accept,read,write都是阻塞式的)
让高速运行的CPU去等待及其低效的网络IO是非常不合算的行为
这时候该怎么办?
NIO
New IO成功的解决了上述问题,它是怎样解决的呢?
IO处理客户端请求的最小单位是线程
而NIO使用了比线程还小一级的单位:通道(Channel)
可以说,NIO中只需要一个线程就能完成所有接收,读,写等操作
要学习NIO,首先要理解它的三大核心
Selector,选择器
Buffer,缓冲区
Channel,通道
博主不才,画了张丑图给大家加深下印象 ^ . ^
再给一张TCP下的NIO工作流程图(好难画的线条...)
大家大致看懂就行,我们一步步来
Buffer
首先要知道什么是Buffer
在NIO中数据交互不再像IO机制那样使用流
而是使用Buffer(缓冲区)
博主觉得图才是最容易理解的
所以...
可以看出Buffer在整个工作流程中的位置
buffer实际上是一个容器,一个连续数组,它通过几个变量来保存这个数据的当前位置状态:
1.capacity:容量,缓冲区能容纳元素的数量
2.position:当前位置,是缓冲区中下一次发生读取和写入操作的索引,当前位置通过大多数读写操作向前推进
3.limit:界限,是缓冲区中最后一个有效位置之后下一个位置的索引
如图:
几个常用方法:
.flip() //将limit设置为position,然后position重置为0,返回对缓冲区的引用
.clear() //清空调用缓冲区并返回对缓冲区的引用
来点实际点的,上面图中的具体代码如下:
1.首先给Buffer分配空间,以字节为单位
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
创建一个ByteBuffer对象并且指定内存大小
2.向Buffer中写入数据:
1).数据从Channel到Buffer:channel.read(byteBuffer);
2).数据从Client到Buffer:byteBuffer.put(...);
3.从Buffer中读取数据:
1).数据从Buffer到Channel:channel.write(byteBuffer);
2).数据从Buffer到Server:byteBuffer.get(...);
Selector
选择器是NIO的核心,它是channel的管理者
通过执行select()阻塞方法,监听是否有channel准备好
一旦有数据可读,此方法的返回值是SelectionKey的数量
所以服务端通常会死循环执行select()方法,直到有channl准备就绪,然后开始工作
每个channel都会和Selector绑定一个事件,然后生成一个SelectionKey的对象
需要注意的是:
channel和Selector绑定时,channel必须是非阻塞模式
而FileChannel不能切换到非阻塞模式,因为它不是套接字通道,所以FileChannel不能和Selector绑定事件
在NIO中一共有四种事件:
1.SelectionKey.OP_CONNECT:连接事件
2.SelectionKey.OP_ACCEPT:接收事件
3.SelectionKey.OP_READ:读事件
4.SelectionKey.OP_WRITE:写事件
Channel
共有四种通道:
FileChannel:作用于IO文件流
DatagramChannel:作用于UDP协议
SocketChannel:作用于TCP协议
ServerSocketChannel:作用于TCP协议
本篇文章通过常用的TCP协议来讲解NIO
我们以ServerSocketChannel为例:
打开一个ServerSocketChannel通道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
关闭ServerSocketChannel通道:
serverSocketChannel.close();
循环监听SocketChannel:
while(true){
SocketChannel socketChannel = serverSocketChannel.accept();
clientChannel.configureBlocking(false);
}
clientChannel.configureBlocking(false);
语句是将此通道设置为非阻塞,也就是异步
自由控制阻塞或非阻塞便是NIO的特性之一
SelectionKey
SelectionKey是通道和选择器交互的核心组件
比如在SocketChannel上绑定一个Selector,并注册为连接事件:
SocketChannel clientChannel = SocketChannel.open();
clientChannel.configureBlocking(false);
clientChannel.connect(new InetSocketAddress(port));
clientChannel.register(selector, SelectionKey.OP_CONNECT);
核心在register()方法,它返回一个SelectionKey对象
来检测channel事件是那种事件可以使用以下方法:
selectionKey.isAcceptable();
selectionKey.isConnectable();
selectionKey.isReadable();
selectionKey.isWritable();
服务端便是通过这些方法 在轮询中执行相对应操作
当然通过Channel与Selector绑定的key也可以反过来拿到他们
Channel channel = selectionKey.channel();
Selector selector = selectionKey.selector();
在Channel上注册事件时,我们也可以顺带绑定一个Buffer:
clientChannel.register(key.selector(), SelectionKey.OP_READ,ByteBuffer.allocateDirect(1024));
或者绑定一个Object:
selectionKey.attach(Object);
Object anthorObj = selectionKey.attachment();
NIO的TCP服务端
讲了这么多,都是理论
我们来看下最简单也是最核心的代码(加那么多注释很不优雅,但方便大家看懂):
package cn.blog.test.NioTest;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.charset.Charset;
import java.util.Iterator;
import java.util.Set;
public class MyNioServer {
private Selector selector; //创建一个选择器
private final static int port = 8686;
private final static int BUF_SIZE = 10240;
private void initServer() throws IOException {
//创建通道管理器对象selector
this.selector=Selector.open();
//创建一个通道对象channel
ServerSocketChannel channel = ServerSocketChannel.open();
channel.configureBlocking(false); //将通道设置为非阻塞
channel.socket().bind(new InetSocketAddress(port)); //将通道绑定在8686端口
//将上述的通道管理器和通道绑定,并为该通道注册OP_ACCEPT事件
//注册事件后,当该事件到达时,selector.select()会返回(一个key),如果该事件没到达selector.select()会一直阻塞
SelectionKey selectionKey = channel.register(selector,SelectionKey.OP_ACCEPT);
while (true){ //轮询
selector.select(); //这是一个阻塞方法,一直等待直到有数据可读,返回值是key的数量(可以有多个)
Set keys = selector.selectedKeys(); //如果channel有数据了,将生成的key访入keys集合中
Iterator iterator = keys.iterator(); //得到这个keys集合的迭代器
while (iterator.hasNext()){ //使用迭代器遍历集合
SelectionKey key = (SelectionKey) iterator.next(); //得到集合中的一个key实例
iterator.remove(); //拿到当前key实例之后记得在迭代器中将这个元素删除,非常重要,否则会出错
if (key.isAcceptable()){ //判断当前key所代表的channel是否在Acceptable状态,如果是就进行接收
doAccept(key);
}else if (key.isReadable()){
doRead(key);
}else if (key.isWritable() && key.isValid()){
doWrite(key);
}else if (key.isConnectable()){
System.out.println("连接成功!");
}
}
}
}
public void doAccept(SelectionKey key) throws IOException {
ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
System.out.println("ServerSocketChannel正在循环监听");
SocketChannel clientChannel = serverChannel.accept();
clientChannel.configureBlocking(false);
clientChannel.register(key.selector(),SelectionKey.OP_READ);
}
public void doRead(SelectionKey key) throws IOException {
SocketChannel clientChannel = (SocketChannel) key.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(BUF_SIZE);
long bytesRead = clientChannel.read(byteBuffer);
while (bytesRead>0){
byteBuffer.flip();
byte[] data = byteBuffer.array();
String info = new String(data).trim();
System.out.println("从客户端发送过来的消息是:"+info);
byteBuffer.clear();
bytesRead = clientChannel.read(byteBuffer);
}
if (bytesRead==-1){
clientChannel.close();
}
}
public void doWrite(SelectionKey key) throws IOException {
ByteBuffer byteBuffer = ByteBuffer.allocate(BUF_SIZE);
byteBuffer.flip();
SocketChannel clientChannel = (SocketChannel) key.channel();
while (byteBuffer.hasRemaining()){
clientChannel.write(byteBuffer);
}
byteBuffer.compact();
}
public static void main(String[] args) throws IOException {
MyNioServer myNioServer = new MyNioServer();
myNioServer.initServer();
}
}
我打印了监听channel,告诉大家ServerSocketChannel是在什么时候开始运行的
如果配合NIO客户端的debug,就能很清楚的发现,进入select()轮询前
虽然已经有了ACCEPT事件的KEY,但select()默认并不会去调用
而是要等待有其它感兴趣事件被select()捕获之后,才会去调用ACCEPT的SelectionKey
这时候ServerSocketChannel才开始进行循环监听
也就是说一个Selector中,始终保持着ServerSocketChannel的运行
而serverChannel.accept();
真正做到了异步(在initServer方法中的channel.configureBlocking(false);)
如果没有接受到connect,会返回一个null
如果成功连接了一个SocketChannel,则此SocketChannel会注册写入(READ)事件
并且设置为异步
NIO的TCP客户端
有服务端必定有客户端
其实如果能完全理解了服务端
客户端的代码大同小异
package cn.blog.test.NioTest;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
public class MyNioClient {
private Selector selector; //创建一个选择器
private final static int port = 8686;
private final static int BUF_SIZE = 10240;
private static ByteBuffer byteBuffer = ByteBuffer.allocate(BUF_SIZE);
private void initClient() throws IOException {
this.selector = Selector.open();
SocketChannel clientChannel = SocketChannel.open();
clientChannel.configureBlocking(false);
clientChannel.connect(new InetSocketAddress(port));
clientChannel.register(selector, SelectionKey.OP_CONNECT);
while (true){
selector.select();
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()){
SelectionKey key = iterator.next();
iterator.remove();
if (key.isConnectable()){
doConnect(key);
}else if (key.isReadable()){
doRead(key);
}
}
}
}
public void doConnect(SelectionKey key) throws IOException {
SocketChannel clientChannel = (SocketChannel) key.channel();
if (clientChannel.isConnectionPending()){
clientChannel.finishConnect();
}
clientChannel.configureBlocking(false);
String info = "服务端你好!!";
byteBuffer.clear();
byteBuffer.put(info.getBytes("UTF-8"));
byteBuffer.flip();
clientChannel.write(byteBuffer);
//clientChannel.register(key.selector(),SelectionKey.OP_READ);
clientChannel.close();
}
public void doRead(SelectionKey key) throws IOException {
SocketChannel clientChannel = (SocketChannel) key.channel();
clientChannel.read(byteBuffer);
byte[] data = byteBuffer.array();
String msg = new String(data).trim();
System.out.println("服务端发送消息:"+msg);
clientChannel.close();
key.selector().close();
}
public static void main(String[] args) throws IOException {
MyNioClient myNioClient = new MyNioClient();
myNioClient.initClient();
}
}
输出结果
这里我打开一个服务端,两个客户端:
接下来,你可以试下同时打开一千个客户端,只要你的CPU够给力,服务端就不可能因为阻塞而降低性能
什么是Reactor模式
Reactor模式是一种设计模式,它是基于事件驱动的,可以并发的处理多个服务请求,当请求抵达后,依据多路复用策略,同步的派发这些请求至相关的请求处理程序。
Reactor模式角色构成
在早先的论文An Object Behavioral Pattern for
Demultiplexing and Dispatching Handles for Synchronous Events中Reactor模式主要有五大角色组成,分别如下:
Handle:操作系统提供的一种资源,用于表示一个个的事件,在网络编程中可以是一个连接事件,一个读取事件,一个写入事件,Handle是事件产生的发源地
Synchronous Event Demultiplexer:本质上是一个系统调用,用于等待事件的发生,调用方在调用它的时候会被阻塞,一直阻塞到同步事件分离器上有事件产生为止
Initiation Dispatcher:定义了一些用于控制事件的调度方式的规范,提供对事件管理。它本身是整个事件处理器的核心所在,Initiation Dispatcher会通过Synchronous Event Demultiplexer来等待事件的发生。一旦事件发生,Initiation Dispatcher首先会分离出每一个事件,然后调用事件处理器,最后调用相关的回调方法来处理这些事件
Event Handler:定义事件处理方法以供InitiationDispatcher回调使用
Concrete Event Handler:是事件处理器的实现。它本身实现了事件处理器所提供的各种回调方法,从而实现了特定于业务的逻辑。它本质上就是我们所编写的一个个的处理器实现。
Reactor模式实现流程
初始化 Initiation Dispatcher,然后将若干个Concrete Event Handler注册到 Initiation Dispatcher中,应用会标识出该事件处理器希望Initiation Dispatcher在某些事件发生时向其发出通知
Initiation Dispatcher 会要求每个事件处理器向其传递内部的Handle,该Handle向操作系统标识了事件处理器
当所有的Concrete Event Handler都注册完毕后,就会启动 Initiation Dispatcher的事件循环,使用Synchronous Event Demultiplexer同步阻塞的等待事件的发生
当与某个事件源对应的Handle变为ready状态时,Synchronous Event Demultiplexer就会通知 Initiation Dispatcher
Initiation Dispatcher会触发事件处理器的回调方法响应这个事件
Java NIO对Reactor的实现
在Java的NIO中,对Reactor模式有无缝的支持,即使用Selector类封装了操作系统提供的Synchronous Event Demultiplexer功能。Doug Lea(Java concurrent包的作者)在Scalable IO in Java中对此有非常详细的描述。概况来说其主要流程如下:
服务器端的Reactor线程对象会启动事件循环,并使用Selector来实现IO的多路复用
注册Acceptor事件处理器到Reactor中,Acceptor事件处理器所关注的事件是ACCEPT事件,这样Reactor会监听客户端向服务器端发起的连接请求事件
客户端向服务器端发起连接请求,Reactor监听到了该ACCEPT事件的发生并将该ACCEPT事件派发给相应的Acceptor处理器来进行处理。Acceptor处理器通过accept()方法得到与这个客户端对应的连接(SocketChannel),然后将该连接所关注的READ/WRITE事件以及对应的READ/WRITE事件处理器注册到Reactor中,这样一来Reactor就会监听该连接的READ/WRITE事件了。
当Reactor监听到有读或者写事件发生时,将相关的事件派发给对应的处理器进行处理
每当处理完所有就绪的感兴趣的I/O事件后,Reactor线程会再次执行select()阻塞等待新的事件就绪并将其分派给对应处理器进行处理
Netty
有了Netty,你可以实现自己的HTTP服务器,FTP服务器,UDP服务器,RPC服务器,WebSocket服务器,Redis的Proxy服务器,MySQL的Proxy服务器等等。
如果你想知道Nginx是怎么写出来的,如果你想知道Tomcat和Jetty是如何实现的,如果你也想实现一个简单的Redis服务器,那都应该好好理解一下Netty,它们高性能的原理都是类似的。
我们回顾一下传统的HTTP服务器的原理
- 创建一个ServerSocket,监听并绑定一个端口
- 一系列客户端来请求这个端口
- 服务器使用Accept,获得一个来自客户端的Socket连接对象
- 启动一个新线程处理连接
- 读Socket,得到字节流
- 解码协议,得到Http请求对象
- 处理Http请求,得到一个结果,封装成一个HttpResponse对象
- 编码协议,将结果序列化字节流
- 写Socket,将字节流发给客户端
- 继续循环步骤3
HTTP服务器之所以称为HTTP服务器,是因为编码解码协议是HTTP协议,如果协议是Redis协议,那它就成了Redis服务器,如果协议是WebSocket,那它就成了WebSocket服务器,等等。
使用Netty你就可以定制编解码协议,实现自己的特定协议的服务器。
上面我们说的是一个传统的多线程服务器,这个也是Apache处理请求的模式。在高并发环境下,线程数量可能会创建太多,操作系统的任务调度压力大,系统负载也会比较高。那怎么办呢?
于是NIO诞生了,NIO并不是Java独有的概念,NIO代表的一个词汇叫着IO多路复用。它是由操作系统提供的系统调用,早期这个操作系统调用的名字是select,但是性能低下,后来渐渐演化成了Linux下的epoll和Mac里的kqueue。我们一般就说是epoll,因为没有人拿苹果电脑作为服务器使用对外提供服务。而Netty就是基于Java NIO技术封装的一套框架。为什么要封装,因为原生的Java NIO使用起来没那么方便,而且还有臭名昭著的bug,Netty把它封装之后,提供了一个易于操作的使用模式和接口,用户使用起来也就便捷多了。
那NIO究竟是什么东西呢?
NIO的全称是NoneBlocking IO,非阻塞IO,区别与BIO,BIO的全称是Blocking IO,阻塞IO。那这个阻塞是什么意思呢?
- Accept是阻塞的,只有新连接来了,Accept才会返回,主线程才能继
- Read是阻塞的,只有请求消息来了,Read才能返回,子线程才能继续处理
- Write是阻塞的,只有客户端把消息收了,Write才能返回,子线程才能继续读取下一个请求
所以传统的多线程服务器是BlockingIO模式的,从头到尾所有的线程都是阻塞的。这些线程就干等在哪里,占用了操作系统的调度资源,什么事也不干,是浪费。
那么NIO是怎么做到非阻塞的呢。它用的是事件机制。它可以用一个线程把Accept,读写操作,请求处理的逻辑全干了。如果什么事都没得做,它也不会死循环,它会将线程休眠起来,直到下一个事件来了再继续干活,这样的一个线程称之为NIO线程。
while true {
events = takeEvents(fds) // 获取事件,如果没有事件,线程就休眠
for event in events {
if event.isAcceptable {
doAccept() // 新链接来了
} elif event.isReadable {
request = doRead() // 读消息
if request.isComplete() {
doProcess()
}
} elif event.isWriteable {
doWrite() // 写消息
}
}
}
NIO的流程大致就是上面的伪代码描述的过程,跟实际真实的代码有较多差异,不过对于初学者,这样理解也是足够了。
Netty是建立在NIO基础之上,Netty在NIO之上又提供了更高层次的抽象。
在Netty里面,Accept连接可以使用单独的线程池去处理,读写操作又是另外的线程池来处理。
Accept连接和读写操作也可以使用同一个线程池来进行处理。而请求处理逻辑既可以使用单独的线程池进行处理,也可以跟放在读写线程一块处理。线程池中的每一个线程都是NIO线程。用户可以根据实际情况进行组装,构造出满足系统需求的并发模型。
Netty提供了内置的常用编解码器,包括行编解码器[一行一个请求],前缀长度编解码器[前N个字节定义请求的字节长度],可重放解码器[记录半包消息的状态],HTTP编解码器,WebSocket消息编解码器等等
Netty提供了一些列生命周期回调接口,当一个完整的请求到达时,当一个连接关闭时,当一个连接建立时,用户都会收到回调事件,然后进行逻辑处理。
Netty可以同时管理多个端口,可以使用NIO客户端模型,这些对于RPC服务是很有必要的。
Netty除了可以处理TCP Socket之外,还可以处理UDP Socket。
在消息读写过程中,需要大量使用ByteBuffer,Netty对ByteBuffer在性能和使用的便捷性上都进行了优化和抽象。
总之,Netty是Java程序员进阶的必备神奇。如果你知其然,还想知其所以然,一定要好好研究下Netty。如果你觉得Java枯燥无谓,Netty则是重新开启你对Java兴趣大门的钥匙。
Netty 在哪些行业得到了应用?
-
互联网行业:随着网站规模的不断扩大,系统并发访问量也越来越高,传统基于 Tomcat 等 Web 容器的垂直架构已经无法满足需求,需要拆分应用进行服务化,以提高开发和维护效率。从组网情况看,垂直的架构拆分之后,系统采用分布式部署,各个节点之间需要远程服务调用,高性能的 RPC 框架必不可少,Netty 作为异步高性能的通信框架,往往作为基础通信组件被这些 RPC 框架使用。
典型的应用有:阿里分布式服务框架 Dubbo 的 RPC 框架使用 Dubbo 协议进行节点间通信,Dubbo 协议默认使用 Netty 作为基础通信组件,用于实现各进程节点之间的内部通信。它的架构图如下:
其中,服务提供者和服务消费者之间,服务提供者、服务消费者和性能统计节点之间使用 Netty 进行异步/同步通信。
除了 Dubbo 之外,淘宝的消息中间件 RocketMQ 的消息生产者和消息消费者之间,也采用 Netty 进行高性能、异步通信。
除了阿里系和淘宝系之外,很多其它的大型互联网公司或者电商内部也已经大量使用 Netty 构建高性能、分布式的网络服务器。
-
游戏行业:无论是手游服务端、还是大型的网络游戏,Java 语言得到了越来越广泛的应用。Netty 作为高性能的基础通信组件,它本身提供了 TCP/UDP 和 HTTP 协议栈,非常方便定制和开发私有协议栈。账号登陆服务器、地图服务器之间可以方便的通过 Netty 进行高性能的通信,架构示意图如下:
图1-2 Netty 在游戏服务器架构中的应用
-
大数据领域:经典的 Hadoop 的高性能通信和序列化组件 Avro 的 RPC 框架,默认采用 Netty 进行跨节点通信,它的 Netty Service 基于 Netty 框架二次封装实现。
大数据计算往往采用多个计算节点和一个/N个汇总节点进行分布式部署,各节点之间存在海量的数据交换。由于 Netty 的综合性能是目前各个成熟 NIO 框架中最高的,因此,往往会被选中用作大数据各节点间的通信。
-
企业软件:企业和 IT 集成需要 ESB,Netty 对多协议支持、私有协议定制的简洁性和高性能是 ESB RPC 框架的首选通信组件。事实上,很多企业总线厂商会选择 Netty 作为基础通信组件,用于企业的 IT 集成。
-
通信行业:Netty 的异步高性能、高可靠性和高成熟度的优点,使它在通信行业得到了大量的应用。