一、BIO
1.阻塞IO与非阻塞IO
Java IO的各种流是阻塞的。这意味着,当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。
2.特点
- 每个请求都需要独立的线程完成数据read,业务处理,数据write的完整操作问题。
- 当并发数较大时,需要创建大量线程来处理连接,系统资源占用较大。
- 连接建立后,如果当前线程暂时没有数据可读,则线程就阻塞在read上,造成线程资源浪费。
每个连接都会建立一个线程。虽然线程消耗比进程小,但是一台机器实际上能建立的有效线程有限,以Java来说,1.5以后,一个线程大致消耗1M内存!且随着线程数量的增加,CPU切换线程上下文的消耗也随之增加,在高过某个阀值后,继续增加线程,性能不增反降!而同样因为一个连接就新建一个线程,所以编码模型很简单!就性能瓶颈这一点,就确定了BIO并不适合进行高性能服务器的开发!像Tomcat这样的Web服务器,从7开始就从BIO改成了NIO,来提高服务器性能。
package com.huay.demo.io; import java.io.IOException; import java.io.InputStream; import java.net.ServerSocket; import java.net.Socket; public class IOServer { public static void main(String[] args) { try { ServerSocket serverSocket = new ServerSocket(8000); System.out.println("oio server is already start..."); while (true){ Socket socket = serverSocket.accept(); handler(socket); } } catch (IOException e) { e.printStackTrace(); } } private static void handler(Socket socket) { System.out.println("new Client connected..."); byte[] bytes = new byte[1024]; try { InputStream inputStream = socket.getInputStream(); while(true){ //读取数据(阻塞) int read = inputStream.read(bytes); if(read != -1){ System.out.println(new String(bytes, 0, read)); }else{ break; } } } catch (IOException e) { e.printStackTrace(); } finally { try { System.out.println("socket is closed..."); socket.close(); } catch (IOException e) { e.printStackTrace(); } } } } |
package com.huay.demo.iothread; import java.io.IOException; import java.io.InputStream; import java.net.ServerSocket; import java.net.Socket; public class IOThreadServer { public static void main(String[] args) { try { ServerSocket serverSocket = new ServerSocket(8000); new Thread(() -> { while (true){ try { Socket socket = serverSocket.accept(); System.out.println("new Client is connected..."); new Thread(() -> { byte[] bytes = new byte[1024]; try { InputStream inputStream = socket.getInputStream(); while (true){ int len; while ((len = inputStream.read(bytes)) != -1){ System.out.println(new String(bytes,0,len)); } } } catch (IOException e) { e.printStackTrace(); } }).start(); } catch (IOException e) { e.printStackTrace(); } } }).start(); } catch (IOException e) { e.printStackTrace(); } } } |
二、NIO
Java NIO是一个可以替代标准Java IO API的IO API(从Java 1.4开始),Java NIO提供了与标准IO不同的IO工作方式。注:JDK1.4的版本为 NIO 1.0, 而JDK7的版本为 NIO 2.0。
示例图:
步骤:
(1)Acceptor注册Selector,监听accept事件
(2)当客户端连接后,触发accept事件
(3)服务器构建对应的Channel,并在其上注册Selector,监听读写事件
(4)当发生读写事件后,进行相应的读写处理
1.NIO核心组成
- Channels(通道):标准的IO基于字节流和字符流进行操作的,而NIO是基于通道(Channel)和缓冲区(Buffer)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Channels通道是一个对象,通过它可以读取和写入数据,所有数据都通过Buffer对象来处理,所有操作不会将字节直接写入通道中,相反是将数据写入包含一个或者多个字节的缓冲区。同样不会直接从通道中读取字节,而是将数据从通道读入缓冲区,再从缓冲区获取这个字节。
- Non-blocking IO(非阻塞IO):Java NIO可以让你非阻塞的使用IO,例如:当线程从通道读取数据到缓冲区时,线程还是可以进行其他事情。当数据被写入到缓冲区时,线程可以继续处理它。从缓冲区写入通道也类似。
- Selectors(选择器):Java NIO引入了多路复用(选择器)的概念,选择器用于监听多个通道的事件(比如:连接打开,数据到达)。因此,单个的线程可以监听多个数据通道。当有任何注册的事件发生时,可以从Selector中获得相应的SelectionKey,同时从SelectionKey中可以找到发生的事件和该事件所发生的具体的SelectableChannel,以获得客户端发送过来的数据。
- Buffers(缓冲区):NIO中的Buffer主要是当作缓冲区的作用。缓冲区实际上是一个容器对象,更直接的说,其实就是一个数组,在NIO库中,所有数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中的; 在写入数据时,它也是写入到缓冲区中的;任何时候访问 NIO 中的数据,都是将它放到缓冲区中,在NIO中,所有的缓冲区类型都继承于抽象类Buffer,最常用的就是ByteBuffer。
2.NIO特点
- 一个高性能、异步事件驱动的NIO框架,它提供了对TCP、UDP和文件传输的支持
- 使用更高效的socket底层,对epoll空轮询引起的cpu占用飙升在内部进行了处理,避免了直接使用NIO的陷阱,简化了NIO的处理方式。
- 采用多种decoder/encoder 支持,对TCP粘包/分包进行自动化处理
- 可使用接受/处理线程池,提高连接效率,对重连、心跳检测的简单支持
- 可配置IO线程数、TCP参数, TCP接收和发送缓冲区使用直接内存代替堆内存,通过内存池的方式循环利用ByteBuf
- 通过引用计数器及时申请释放不再引用的对象,降低了GC频率
- 使用单线程串行化的方式,高效的Reactor线程模型
- 大量使用了volitale、使用了CAS和原子类、线程安全类的使用、读写锁的使用
3.优缺点:NIO的优缺点和BIO就完全相反了!性能高,不用一个连接就建一个线程,可以一个线程处理所有的连接!NIO虽然性能高,但是编码复杂。
---->Zero-Copy:“零拷贝”是指计算机操作的过程中,CPU不需要为数据在内存之间的拷贝消耗资源。而它通常是指计算机在网络上发送文件时,不需要将文件内容拷贝到用户空间(User Space)而直接在内核空间(Kernel Space)中传输到网络的方式。
4.半包问题
TCP/IP在发送消息的时候,可能会拆包(如上图)!这就导致接收端无法知道什么时候收到的数据是一个完整的数据。例如:发送端分别发送了ABC,DEF,GHI三条信息,发送时被拆成了AB,CDRFG,H,I这四个包进行发送,接受端如何将其进行还原呢?在BIO模型中,当读不到数据后会阻塞,而NIO中不会!所以需要自行进行处理!例如,以换行符作为判断依据,或者定长消息发生,或者自定义协议!这种情况主要是由于TCP为提高传输效率,将一个包分配的足够大,导致接受方并不能一次接受完。(在长连接和短连接中都会出现)。
解决思路:定义数据发送协议格式,数据头中包含包体的长度大小,接收方首先读取包体的长度,然后按照包体的长度进行一次或多次读取数据,从而组装成完整合法的数据内容
5.步骤
nio server
package com.huay.demo.nio.server; 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.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.util.Date; import java.util.Iterator; public class NIOServer { // 通道管理器 private Selector selector; public static void main(String[] args) { NIOServer nioServer = new NIOServer(); nioServer.initServer(8001); nioServer.listen(); } /** * 采用轮询的方式监听selector上是否有需要处理的事件,如果有,则进行处理 */ /** * 5、启动IO线程,在循环体中执行Selector.select()方法,轮询就绪的通道 * */ private void listen() { System.out.println("nio server is already start..."); // 轮询访问selector while (true) { try { if(selector.select(10000)==0){ //在等待信道准备的同时,也可以异步地执行其他任务, 这里打印* System.out.print("*/"); continue; } selector.select(1000); Iterator<SelectionKey> iterator = selector.selectedKeys().iterator(); while (iterator.hasNext()){ SelectionKey next = iterator.next(); iterator.remove(); handler(next); } } catch (IOException e) { e.printStackTrace(); } } } private void handler(SelectionKey key) { if (key.isAcceptable()) { handlerAccept(key); // 获得了可读的事件 } else if (key.isReadable()) { handelerRead(key); } } private void handelerRead(SelectionKey key) { try { // 服务器可读取消息:得到事件发生的Socket通道 SocketChannel channel = (SocketChannel) key.channel(); ByteBuffer buffer = ByteBuffer.allocate(1024); int read = channel.read(buffer); if(read > 0){ byte[] data = buffer.array(); String msg = new String(data).trim(); System.out.println("server receive message:" + msg); }else{ System.out.println("client is closed..."); key.cancel(); } } catch (IOException e) { e.printStackTrace(); } } /** * 6、当轮询到处于就绪的通道时,需要进行判断操作位,如果是ACCEPT状态,说明是新的客户端介入,则调用accept方法接受新的客户端。 * @param key */ private void handlerAccept(SelectionKey key) { ServerSocketChannel server = (ServerSocketChannel) key.channel(); try { // 获得和客户端连接的通道 SocketChannel channel = server.accept(); // 设置成非阻塞 channel.configureBlocking(false); // 在这里可以给客户端发送信息哦 System.out.println("new Client is connected..."); channel.write(ByteBuffer.wrap(new String(new Date()+": nio client,channel is build").getBytes())); // 在和客户端连接成功之后,为了可以接收到客户端的信息,需要给通道设置读的权限。 channel.register(this.selector, SelectionKey.OP_READ); } catch (IOException e) { e.printStackTrace(); } } /** *获得一个ServerSocket通道,并对该通道做一些初始化的工作 * 1、创建ServerSocketChannel,为它配置非阻塞模式 * 2、绑定监听,配置TCP参数,录入backlog大小等 * 3、创建一个独立的IO线程,用于轮询多路复用器Selector * 4、创建Selector,将之前的ServerSocketChannel注册到Selector上,并设置监听标识位SelectionKey.ACCEPT * @param port */ private void initServer(int port) { try { // 获得一个ServerSocket通道 ServerSocketChannel serverChannel = ServerSocketChannel.open(); //设置socket通道为非阻塞 serverChannel.configureBlocking(false); // 将该通道对应的ServerSocket绑定到port端口 serverChannel.socket().bind(new InetSocketAddress(port)); // 获得一个通道管理器 this.selector = Selector.open(); // 将通道管理器和该通道绑定,并为该通道注册SelectionKey.OP_ACCEPT事件,注册该事件后, // 当该事件到达时,selector.select()会返回,如果该事件没到达selector.select()会一直阻塞。 serverChannel.register(selector, SelectionKey.OP_ACCEPT); } catch (IOException e) { e.printStackTrace(); } } } |
nio client
package com.huay.demo.nio.client; 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; import java.util.Set; public class NIOClient { private Selector selector; public static void main(String[] args) { String host ="127.0.0.1"; NIOClient nioClient = new NIOClient(); nioClient.init(host,8000); nioClient.listen(); } private void listen() { while (true){ try { selector.select(); //获取所有selectionkey Set<SelectionKey> selectionKeys = selector.selectedKeys(); //遍历所有selectionkey Iterator<SelectionKey> iterator = selectionKeys.iterator(); while (iterator.hasNext()) { SelectionKey key = iterator.next(); if(key.isConnectable()){ SocketChannel channel=(SocketChannel)key.channel(); if(channel.isConnectionPending()){ channel.finishConnect();//如果正在连接,则完成连接 } channel.register(selector, SelectionKey.OP_READ); }else if(key.isReadable()){ //有可读数据事件。 SocketChannel channel = (SocketChannel)key.channel(); ByteBuffer buffer = ByteBuffer.allocate(1024); channel.read(buffer); byte[] data = buffer.array(); String message = new String(data); System.out.println("recevie message from server:, size:" + buffer.position() + " msg: " + message); } } }catch (Exception e){ e.printStackTrace(); } } } private void doWrite(SocketChannel socketChannel) { byte[] bytes = "hello nio server".getBytes(); //为字节缓冲区分配指定字节大小的容量 ByteBuffer writeBuffer = ByteBuffer.allocate(1024); writeBuffer.put(bytes); writeBuffer.flip(); //System.out.println(writeBuffer.remaining()); try { socketChannel.write(writeBuffer); if (!writeBuffer.hasRemaining()) { //若缓冲区中无可读字节,则说明成功发送给服务器消息 System.out.println("Send order 2 server succeed."); } //writeBuffer.clear(); } catch (IOException e) { e.printStackTrace(); } } private void init(String host, int port) { try { this.selector = Selector.open(); // 获得一个ServerSocket通道 SocketChannel socketChannel = SocketChannel.open(); socketChannel.configureBlocking(false); socketChannel.connect(new InetSocketAddress(host,port)); socketChannel.register(selector, SelectionKey.OP_READ); while (!socketChannel.finishConnect()){ System.out.println("is not connect"); } System.out.println("connected"); doWrite(socketChannel); } catch (IOException e) { e.printStackTrace(); } } } |
6.Selector
java NIO采用了双向通道(channel)进行数据传输,而不是单向的流(stream),在通道上可以注册我们感兴趣的事件,为了实现Selector管理多个SocketChannel,必须将具体的SocketChannel对象注册到Selector,并声明需要监听的事件(这样Selector才知道需要记录什么数据),一共有4种事件:
- 服务端接收客户端连接事件【SelectionKey.OP_ACCEPT(16)】
- 客户端连接服务端事件【SelectionKey.OP_CONNECT(8)】
- 读事件【SelectionKey.OP_READ(1)】
- 写事件【SelectionKey.OP_WRITE(4)】
服务端和客户端各自维护一个管理通道的对象,我们称之为selector,该对象能检测一个或多个通道 (channel) 上的事件。我们以服务端为例,如果服务端的selector上注册了读事件,某时刻客户端给服务端发送了一些数据,阻塞I/O这时会调用read()方法阻塞地读取数据,而NIO的服务端会在selector中添加一个读事件。服务端的处理线程会轮询地访问selector,如果访问selector时发现有感兴趣的事件到达,则处理这些事件,如果没有感兴趣的事件到达,则处理线程会一直阻塞直到感兴趣的事件到达为止。
public static SelectorProvider provider() { synchronized (lock) { if (provider != null) return provider; return AccessController.doPrivileged( new PrivilegedAction<SelectorProvider>() { public SelectorProvider run() { if (loadProviderFromProperty()) return provider; if (loadProviderAsService()) return provider; provider = sun.nio.ch.DefaultSelectorProvider.create(); return provider; } }); } } |
7.不同
BIO | NIO |
---|---|
面向流 | 面向缓冲 |
阻塞IO | 非阻塞IO |
无 | 选择器 |
- 面向流与面向缓冲
Java NIO和IO之间第一个最大的区别是,IO是面向流的,NIO是面向缓冲区的。 Java IO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。 Java NIO的缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含所有您需要处理的数据。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据。
- 阻塞与非阻塞IO
Java IO的各种流是阻塞的。这意味着,当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。 Java NIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取。而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)。
- 选择器(Selectors)
Java NIO的选择器允许一个单独的线程来监视多个输入通道,你可以注册多个通道使用一个选择器,然后使用一个单独的线程来“选择”通道:这些通道里已经有可以处理的输入,或者选择已准备写入的通道。这种选择机制,使得一个单独的线程很容易来管理多个通道。
三、Netty
1.什么是netty
Netty是基于NIO的网络编程框架。提供异步的、事件驱动的、高可靠的、高性能的服务,简化了开发复杂度,提高了可靠性。开发过程包含连接、io处理、拆包和粘包、编码和解码。Netty采用基于非阻塞I/O的实现,底层依赖的是JDKNIO框架的Selector。Netty是一个异步事件驱动的网络应用程序框架,用于快速开发可维护的高性能协议服务器和客户端
2.netty简介
Netty是一个高性能、异步事件驱动的NIO框架,基于JAVA NIO提供的API实现。它提供了对TCP、UDP和文件传输的支持,作为一个异步NIO框架,Netty的所有IO操作都是异步非阻塞的,通过Future-Listener机制,用户可以方便的主动获取或者通过通知机制获得IO操作结果。 作为当前最流行的NIO框架,Netty在互联网领域、大数据分布式计算领域、游戏行业、通信行业等获得了广泛的应用,一些业界著名的开源组件也基于Netty的NIO框架构建。
3.模块组件
Bootstrap 意思是引导,一个 Netty 应用通常由一个 Bootstrap 开始,主要作用是配置整个Netty程序,串联各个组件,Netty中Bootstrap类是客户端程序的启动引导类,ServerBootstrap是服务端启动引导类。
Channel:是 Netty 网络操作抽象类,它除了包括基本的 I/O 操作,如 bind、connect、read、write 之外,还包括了 Netty 框架相关的一些功能,如获取该 Channe l的 EventLoop。
EventLoop:Netty基于事件驱动模型,使用不同的事件来通知我们状态的改变或者操作状态的改变。它定义了在整个连接的生命周期里当有事件发生的时候处理的核心抽象。Channel 为Netty 网络操作抽象类,EventLoop 主要是为Channel 处理 I/O 操作,两者配合参与 I/O 操作。
ChannelFuture:Netty 为异步非阻塞,即所有的 I/O 操作都为异步的,因此,我们不能立刻得知消息是否已经被处理了。Netty 提供了 ChannelFuture 接口,通过该接口的 addListener() 方法注册一个 ChannelFutureListener,当操作执行成功或者失败时,监听就会自动触发返回结果。
ChannelHandler:为Netty 中最核心的组件,它充当了所有处理入站和出站数据的应用程序逻辑的容器。ChannelHandler主要用来处理各种事件,这里的事件很广泛,比如可以是连接、数据接收、异常、数据转换等。ChannelHandler 有两个核心子类 ChannelInboundHandler 和 ChannelOutboundHandler,其中 ChannelInboundHandler 用于接收、处理入站数据和事件,而 ChannelOutboundHandler 则相反。
ChannelPipeline:ChannelPipeline 为 ChannelHandler 链提供了一个容器并定义了用于沿着链传播入站和出站事件流的 API。一个数据或者事件可能会被多个 Handler 处理,在这个过程中,数据或者事件经流 ChannelPipeline,由 ChannelHandler 处理。在这个处理过程中,一个 ChannelHandler 接收数据后处理完成后交给下一个 ChannelHandler,或者什么都不做直接交给下一个 ChannelHandler。
4.线程模型
Reactor模型:Netty通过Reactor模型基于多路复用器接收并处理用户请求,内部实现了两个线程池,boss线程池和work线程池,其中boss线程池的线程负责处理请求的accept事件,当接收到accept事件的请求时,把对应的socket封装到一个NioSocketChannel中,并交给work线程池,其中work线程池负责请求的read和write事件,由对应的Handler处理。
Reactor中的组件:
- Reactor:Reactor是IO事件的派发者。-------→相当于有分发功能的Selector
- Acceptor:Acceptor接受client连接,建立对应client的Handler,并向Reactor注册此Handler。-------→NIO中建立连接的那个判断分支
- Handler:和一个client通讯的实体,按这样的过程实现业务的处理。一般在基本的Handler基础上还会有更进一步的层次划分, 用来抽象诸如decode,process和encoder这些过程。比如对Web Server而言,decode通常是HTTP请求的解析, process的过程会进一步涉及到Listener和Servlet的调用。业务逻辑的处理在Reactor模式里被分散的IO事件所打破, 所以Handler需要有适当的机制在所需的信息还不全(读到一半)的时候保存上下文,并在下一次IO事件到来的时候(另一半可读了)能继续中断的处理。为了简化设计,Handler通常被设计成状态机,按GoF的state pattern来实现。--------->消息读写处理等操作类。
Reactor从线程池和Reactor的选择上可以细分为如下几种:
Reactor单线程模型:这个模型和上面的NIO流程很类似,指的是所有的I/O操作都在同一个NIO线程上面完成,Reactor线程是个多面手,负责多路分离套接字,Accept新连接,并分派请求到Handler处理器中。对于一些小容量应用场景,可以使用单线程模型。
所有I/O操作都由一个线程完成,即多路复用、事件分发和处理都是在一个Reactor线程上完成的。既要接收客户端的连接请求,向服务端发起连接,又要发送/读取请求或应答/响应消息。一个NIO 线程同时处理成百上千的链路,性能上无法支撑,速度慢,若线程进入死循环,整个程序不可用,对于高负载、大并发的应用场景不合适。
缺点:当其中某个 handler 阻塞时,会导致其他所有的 client 的 handler 都得不到执行,并且更严重的是,handler 的阻塞也会导致整个服务不能接收新的client请求(因为acceptor也被阻塞了)。因为有这么多的缺陷, 因此单线程Reactor 模型用的比较少。这种单线程模型不能充分利用多核资源,所以实际使用的不多。因此,单线程模型仅仅适用于handler 中业务处理组件能快速完成的场景。
Reactor多线程模型:将处理器的执行放入线程池,多线程进行业务处理。但Reactor仍为单个线程。
有一个NIO 线程(Acceptor) 只负责监听服务端,接收客户端的TCP 连接请求;NIO 线程池负责网络IO 的操作,即消息的读取、解码、编码和发送;1 个NIO 线程可以同时处理N 条链路,但是1 个链路只对应1 个NIO 线程,这是为了防止发生并发操作问题。但在并发百万客户端连接或需要安全认证时,一个Acceptor 线程可能会存在性能不足问题。
package com.huay.demo.netty.server; import com.huay.demo.netty.handler.ServerHandler; import org.jboss.netty.bootstrap.ServerBootstrap; import org.jboss.netty.channel.ChannelPipeline; import org.jboss.netty.channel.ChannelPipelineFactory; import org.jboss.netty.channel.Channels; import org.jboss.netty.channel.socket.nio.NioServerSocketChannelFactory; import org.jboss.netty.handler.codec.string.StringDecoder; import org.jboss.netty.handler.codec.string.StringEncoder; import java.net.InetSocketAddress; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /** * @Author Ke * @Date 2019/7/17 11:42 * @Version 1.0 */ public class NettyServer { public static void main(String[] args) { //服务类 ServerBootstrap serverBootstrap = new ServerBootstrap(); //boss线程监听端口,worker线程负责数据读写 ExecutorService boss = Executors.newCachedThreadPool(); ExecutorService worker = Executors.newCachedThreadPool(); //设置niosocket工厂 serverBootstrap.setFactory(new NioServerSocketChannelFactory(boss, worker)); //设置管道的工厂 serverBootstrap.setPipelineFactory(new ChannelPipelineFactory() { @Override public ChannelPipeline getPipeline() throws Exception { ChannelPipeline pipeline = Channels.pipeline(); pipeline.addLast("decoder", new StringDecoder()); pipeline.addLast("encoder", new StringEncoder()); pipeline.addLast("helloHandler", new ServerHandler()); return pipeline; } }); serverBootstrap.bind(new InetSocketAddress(8000)); } } |
package com.huay.demo.netty.handler; import org.jboss.netty.channel.*; /** * @Author Ke * @Date 2019/7/17 11:46 * @Version 1.0 */ public class ServerHandler extends SimpleChannelHandler { /** * 接收消息 */ @Override public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception { String s = (String) e.getMessage(); System.out.println(s); //回写数据 ctx.getChannel().write("hi"); super.messageReceived(ctx, e); } /** * 捕获异常 */ @Override public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) throws Exception { System.out.println("exceptionCaught"); super.exceptionCaught(ctx, e); } /** * 新连接 */ @Override public void channelConnected(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception { System.out.println("channelConnected"); super.channelConnected(ctx, e); } /** * 必须是链接已经建立,关闭通道的时候才会触发 */ @Override public void channelDisconnected(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception { System.out.println("channelDisconnected"); super.channelDisconnected(ctx, e); } /** * channel关闭的时候触发 */ @Override public void channelClosed(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception { System.out.println("channelClosed"); super.channelClosed(ctx, e); } } |
主从Reactor模型:mainReactor负责监听连接,accept连接给subReactor处理,为什么要单独分一个Reactor来处理监听呢?因为像TCP这样需要经过3次握手才能建立连接,这个建立连接的过程也是要耗时间和资源的,单独分一个Reactor来处理,可以提高性能。
Acceptor 线程用于绑定监听端口,接收客户端连接,将SocketChannel 从主线程池的Reactor 线程的多路复用器上移除,重新注册到Sub 线程池的线程上,用于处理I/O 的读写等操作,从而保证mainReactor只负责接入认证、握手等操作;
package com.huay.demo.netty.masteslave.server; import com.huay.demo.netty.masteslave.handler.ServerChannelHandler; import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.*; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.nio.NioServerSocketChannel; import io.netty.channel.socket.nio.NioSocketChannel; import io.netty.handler.codec.string.StringDecoder; import io.netty.handler.codec.string.StringEncoder; import java.util.Date; /** * @Author Ke * @Date 2019/7/24 10:50 * @Version 1.0 */ public class NettySv { public static void main(String[] args) { // 创建mainReactor NioEventLoopGroup boosGroup = new NioEventLoopGroup();//boosGroup 用于 Accetpt 连接建立事件并分发请求 // 创建工作线程组 NioEventLoopGroup workerGroup = new NioEventLoopGroup();//workerGroup 用于处理 I/O 读写事件和业务逻辑。 //基于 ServerBootstrap(服务端启动引导类),配置 EventLoopGroup、Channel 类型,连接参数、配置入站、出站事件 handler ServerBootstrap serverBootstrap = new ServerBootstrap(); serverBootstrap.group(boosGroup, workerGroup).channel(NioServerSocketChannel.class).option(ChannelOption.SO_BACKLOG, 1024).childOption(ChannelOption.SO_KEEPALIVE, true).childOption(ChannelOption.TCP_NODELAY, true) .childHandler(new ChannelInitializer<NioSocketChannel>() { @Override protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception { // 配置入站、出站事件channel nioSocketChannel.pipeline().addLast("decoder", new StringDecoder()); nioSocketChannel.pipeline().addLast("encoder", new StringEncoder()); nioSocketChannel.pipeline().addLast("channelHandler", new ServerChannelHandler()); } }); // 绑定端口,开始工作。 int port = 8000; serverBootstrap.bind(port).addListener(future -> { if (future.isSuccess()) { System.out.println(new Date() + ": 端口[" + port + "]绑定成功!"); } else { System.err.println("端口[" + port + "]绑定失败!"); } }); } } |
package com.huay.demo.netty.masteslave.handler; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandler; /** * @Author Ke * @Date 2019/7/24 21:39 * @Version 1.0 */ public class ServerChannelHandler implements ChannelInboundHandler { @Override public void channelRegistered(ChannelHandlerContext channelHandlerContext) throws Exception { System.out.println("channelRegistered"); } @Override public void channelUnregistered(ChannelHandlerContext channelHandlerContext) throws Exception { System.out.println("channelUnregistered"); } @Override public void channelActive(ChannelHandlerContext channelHandlerContext) throws Exception { System.out.println("channelActive"); } @Override public void channelInactive(ChannelHandlerContext channelHandlerContext) throws Exception { System.out.println("channelInactive"); } @Override public void channelRead(ChannelHandlerContext channelHandlerContext, Object o) throws Exception { System.out.println(o.toString()); channelHandlerContext.write("hi"); System.out.println("channelRead"); } @Override public void channelReadComplete(ChannelHandlerContext channelHandlerContext) throws Exception { System.out.println("channelReadComplete"); } @Override public void userEventTriggered(ChannelHandlerContext channelHandlerContext, Object o) throws Exception { System.out.println("userEventTriggered"); } @Override public void channelWritabilityChanged(ChannelHandlerContext channelHandlerContext) throws Exception { System.out.println("channelWritabilityChanged"); } @Override public void handlerAdded(ChannelHandlerContext channelHandlerContext) throws Exception { System.out.println("handlerAdded"); } @Override public void handlerRemoved(ChannelHandlerContext channelHandlerContext) throws Exception { System.out.println("handlerRemoved"); } @Override public void exceptionCaught(ChannelHandlerContext channelHandlerContext, Throwable throwable) throws Exception { System.out.println("exceptionCaught"); } } |
4.Reactor编程的优点和缺点
优点
- 响应快,不必为单个同步时间所阻塞,虽然Reactor本身依然是同步的;
- 编程相对简单,可以最大程度的避免复杂的多线程及同步问题,并且避免了多线程/进程的切换开销;
- 可扩展性,可以方便的通过增加Reactor实例个数来充分利用CPU资源;
- 可复用性,reactor框架本身与具体事件处理逻辑无关,具有很高的复用性;
缺点:
- 相比传统的简单模型,Reactor增加了一定的复杂性,因而有一定的门槛,并且不易于调试。
- Reactor模式需要底层的Synchronous Event Demultiplexer支持,比如Java中的Selector支持,操作系统的select系统调用支持,如果要自己实现Synchronous Event Demultiplexer可能不会有那么高效。
- Reactor模式在IO读写数据时还是在同一个线程中实现的,即使使用多个Reactor机制的情况下,那些共享一个Reactor的Channel如果出现一个长时间的数据读写,会影响这个Reactor中其他Channel的相应时间,比如在大文件传输时,IO操作就会影响其他Client的相应时间,因而对这种操作,使用传统的Thread-Per-Connection或许是一个更好的选择,或则此时使用改进版的Reactor模式如Proactor模式。
5.应用场景
(1)在分布式系统中,各个节点之间需要远程服务调用,高性能的 RPC 框架必不可少,Netty 作为异步高性能的通信框架,往往作为基础通信组件被这些 RPC 框架使用。
(2)手游服务端还是大型的网络游戏,Java 语言得到了越来越广泛的应用。Netty作为高性能的基础通信组件,它本身提供了TCP/UDP和 HTTP 协议栈。
(3)Hadoop 的高性能通信和序列化组件Avro的RPC框架,默认采用Netty进行跨界点通信,它的Netty Service基于Netty框架二次封装实现。