NIO编程
客户端
NIO客户端—NioClient
-
package cn.enjoyedu.nio.nio; import java.util.Scanner; import static cn.enjoyedu.nio.Const.DEFAULT_PORT; import static cn.enjoyedu.nio.Const.DEFAULT_SERVER_IP; /** * @author Mark老师 * 类说明:nio通信客户端 */ public class NioClient { private static NioClientHandle nioClientHandle; public static void start(){ //nioClientHandle = new NioClientHandle(DEFAULT_SERVER_IP,DEFAULT_PORT); nioClientHandle = new NioClientHandle(DEFAULT_SERVER_IP,9999); new Thread(nioClientHandle,"Server").start(); } //向服务器发送消息 public static boolean sendMsg(String msg) throws Exception{ nioClientHandle.sendMsg(msg); return true; } public static void main(String[] args) throws Exception { start(); // 读取键盘的输入,并发送给服务器端 Scanner scanner = new Scanner(System.in); while(NioClient.sendMsg(scanner.next())); } }
client里专门处理网络读写的类–
-
package cn.enjoyedu.nio.nio; 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.Iterator; import java.util.Set; /** * @author Mark老师 * 类说明:nio通信客户端处理器 */ public class NioClientHandle implements Runnable{ private String host; private int port; private volatile boolean started; private Selector selector; private SocketChannel socketChannel; public NioClientHandle(String ip, int port) { this.host = ip; this.port = port; try { /*创建选择器的实例*/ // 创建selector,不能new selector = Selector.open(); /*创建SocketChannel的实例*/ // 不再需要创建ServerSocketChannel // 因为客户端不需要监听 // 所以只需要创建SocketChannel // 同时不需要绑定端口了 socketChannel = SocketChannel.open(); /*设置通道为非阻塞模式*/ socketChannel.configureBlocking(false); started = true; } catch (IOException e) { e.printStackTrace(); } } public void stop(){ started = false; } @Override public void run() { try{ // 发起一个连接 // 写成一个单独的方法 doConnect(); }catch(IOException e){ e.printStackTrace(); System.exit(1); } //循环遍历selector // 循环获取事件集,并做响应的处理 while(started){ try{ //无论是否有读写事件发生,selector每隔1s被唤醒一次 selector.select(1000); //获取当前有哪些事件可以使用 Set<SelectionKey> keys = selector.selectedKeys(); //转换为迭代器 Iterator<SelectionKey> it = keys.iterator(); SelectionKey key = null; while(it.hasNext()){ key = it.next(); /*我们必须首先将处理过的 SelectionKey 从选定的键集合中删除。 如果我们没有删除处理过的键,那么它仍然会在主集合中以一个激活 的键出现,这会导致我们尝试再次处理它。*/ it.remove(); try{ handleInput(key); }catch(Exception e){ if(key != null){ key.cancel(); if(key.channel() != null){ key.channel().close(); } } } } }catch(Exception e){ e.printStackTrace(); System.exit(1); } } //selector关闭后会自动释放里面管理的资源 if(selector != null) try{ selector.close(); }catch (Exception e) { e.printStackTrace(); } } //具体的事件处理方法 private void handleInput(SelectionKey key) throws IOException{ if(key.isValid()){ //获得关心当前事件的channel SocketChannel sc = (SocketChannel) key.channel(); //连接事件 // 相比服务端,多关注了一个连接事件 if(key.isConnectable()){ // 如果连接完成,则注册一个读事件 if(sc.finishConnect()){ socketChannel.register(selector, SelectionKey.OP_READ);} else System.exit(1); } //有数据可读事件 if(key.isReadable()){ //创建ByteBuffer,并开辟一个1M的缓冲区 ByteBuffer buffer = ByteBuffer.allocate(1024); //读取请求码流,返回读取到的字节数 int readBytes = sc.read(buffer); //读取到字节,对字节进行编解码 if(readBytes>0){ //将缓冲区当前的limit设置为position,position=0, // 用于后续对缓冲区的读取操作 buffer.flip(); //根据缓冲区可读字节数创建字节数组 byte[] bytes = new byte[buffer.remaining()]; //将缓冲区可读字节数组复制到新建的数组中 buffer.get(bytes); String result = new String(bytes,"UTF-8"); System.out.println("客户端收到消息:" + result); } //链路已经关闭,释放资源 else if(readBytes<0){ key.cancel(); sc.close(); } } } } // 写操作 private void doWrite(SocketChannel channel,String request) throws IOException { //将消息编码为字节数组 byte[] bytes = request.getBytes(); //根据数组容量创建ByteBuffer ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length); //将字节数组复制到缓冲区 // 写模式 writeBuffer.put(bytes); //flip操作 // 切换成读模式 writeBuffer.flip(); //发送缓冲区的字节数组 /*关心事件和读写网络并不冲突*/ // 读模式 channel.write(writeBuffer); } private void doConnect() throws IOException{ /*非阻塞的连接*/ // 建立连接后返回一个布尔值 // 如果方法执行完,连接还没有完成? if(socketChannel.connect(new InetSocketAddress(host,port))){ // 如果建立连接成功了,就只需要关注读写事件了 socketChannel.register(selector,SelectionKey.OP_READ); }else{ // 如果建立连接失败,就注册一个连接事件,当注册成功了再来通知我 socketChannel.register(selector,SelectionKey.OP_CONNECT); } } //写数据对外暴露的API // 往socketChannel里面写数据 public void sendMsg(String msg) throws Exception{ doWrite(socketChannel, msg); } }
测试nio
- 先启动NioServer
- 再启动NioClient
为什么没有写事件
- 改造服务端代码,之前是直接通过socketChannel.write()方法直接写的
新建类–创建写事件
-
与之前的类相比,doWrite()、handleInput()有变化
-
package cn.enjoyedu.nio.nio; 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.Iterator; import java.util.Set; import static cn.enjoyedu.nio.Const.response; /** * @author Mark老师 * 类说明:nio通信服务端处理器 */ public class NioServerHandleWriteable implements Runnable{ private Selector selector; private ServerSocketChannel serverChannel; private volatile boolean started; /** * 构造方法 * @param port 指定要监听的端口号 */ public NioServerHandleWriteable(int port) { try{ //创建选择器 selector = Selector.open(); //打开监听通道 serverChannel = ServerSocketChannel.open(); //如果为 true,则此通道将被置于阻塞模式; // 如果为 false,则此通道将被置于非阻塞模式 serverChannel.configureBlocking(false);//开启非阻塞模式 //绑定端口 backlog设为1024 serverChannel.socket() .bind(new InetSocketAddress(port),1024); //监听客户端连接请求 serverChannel.register(selector, SelectionKey.OP_ACCEPT); //标记服务器已开启 started = true; System.out.println("服务器已启动,端口号:" + port); }catch(IOException e){ e.printStackTrace(); System.exit(1); } } @Override public void run() { //循环遍历selector while(started){ try{ //阻塞,只有当至少一个注册的事件发生的时候才会继续. selector.select(); Set<SelectionKey> keys = selector.selectedKeys(); Iterator<SelectionKey> it = keys.iterator(); SelectionKey key = null; while(it.hasNext()){ key = it.next(); it.remove(); try{ handleInput(key); }catch(Exception e){ if(key != null){ key.cancel(); if(key.channel() != null){ key.channel().close(); } } } } }catch(Throwable t){ t.printStackTrace(); } } //selector关闭后会自动释放里面管理的资源 if(selector != null) try{ selector.close(); }catch (Exception e) { e.printStackTrace(); } } private void handleInput(SelectionKey key) throws IOException{ // 打印当前key上面有哪些事件 System.out.println("当前通道的事件:"+ key.interestOps()); if(key.isValid()){ //处理新接入的请求消息 if(key.isAcceptable()){ //获得关心当前事件的channel ServerSocketChannel ssc = (ServerSocketChannel) key.channel(); //通过ServerSocketChannel的accept创建SocketChannel实例 //完成该操作意味着完成TCP三次握手,TCP物理链路正式建立 SocketChannel sc = ssc.accept(); System.out.println("======socket channel 建立连接======="); //设置为非阻塞的 sc.configureBlocking(false); //连接已经完成了,可以开始关心读事件了 sc.register(selector, SelectionKey.OP_READ); } //读消息 if(key.isReadable()){ System.out.println("======socket channel 数据准备完成," + "可以去读==读取======="); SocketChannel sc = (SocketChannel) key.channel(); //创建ByteBuffer,并开辟一个1M的缓冲区 ByteBuffer buffer = ByteBuffer.allocate(1024); //读取请求码流,返回读取到的字节数 int readBytes = sc.read(buffer); //读取到字节,对字节进行编解码 if(readBytes>0){ //将缓冲区当前的limit设置为position,position=0, // 用于后续对缓冲区的读取操作 buffer.flip(); //根据缓冲区可读字节数创建字节数组 byte[] bytes = new byte[buffer.remaining()]; //将缓冲区可读字节数组复制到新建的数组中 buffer.get(bytes); String message = new String(bytes,"UTF-8"); System.out.println("服务器收到消息:" + message); //处理数据 String result = response(message) ; //发送应答消息 doWrite(sc,result); } //链路已经关闭,释放资源 else if(readBytes<0){ key.cancel(); sc.close(); } } // 修改2 // 处理写事件 if(key.isWritable()){ SocketChannel sc = (SocketChannel) key.channel(); // 从key上获得附着的buffer,并强制转型成ByteBuffer ByteBuffer buffer = (ByteBuffer)key.attachment(); // 判定这个buffer还有没有要写的 if(buffer.hasRemaining()){ // 往socketChannel里面写 int count = sc.write(buffer); System.out.println("write :"+count +"byte, remaining:"+buffer.hasRemaining()); }else{ /*取消对写的注册*/ key.interestOps(SelectionKey.OP_READ); } } } } //发送应答消息 private void doWrite(SocketChannel channel,String response) throws IOException { //将消息编码为字节数组 byte[] bytes = response.getBytes(); //根据数组容量创建ByteBuffer ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length); //将字节数组复制到缓冲区 writeBuffer.put(bytes); //flip操作 writeBuffer.flip(); // 修改1 // 不是直接写,而是注册一个写事件 // 但是光注册写时间不行,会把之前注册的读事件冲掉 // 因此|上一个读事件,说明当前的channel既关注读事件,又关注写事件 // 每一个selectKEy上还可以做附着,把writeBuffer作为附件附着到这个key上 // 之后应对写事件,就不需要new一个buffer出来了,可以直接获得 channel.register(selector,SelectionKey.OP_WRITE|SelectionKey.OP_READ, writeBuffer); } public void stop(){ started = false; } }
-
再次按上面的步骤测试下
-
打印结果
- 当前通道的事件:16 ---------- accept事件
-
测试2
-
打印台输入Mark
一直刷屏:当前通道的事件:5
这也就是为什么很多书上不会去写操作写事件的代码!
-
什么时候发生写事件?
操作系统的发送缓冲区内有空位的时候,写事件就会不断的触发!selector就会不断触发写事件
所以buffer里面没有数据了,就要把写事件注销掉-- key.interestOps(SelectionKey.OP_READ);只保存读事件,就相当于取消了写事件
-
什么时候注册写事件
当发送的数据有2m,buffer只有8k,必须拆分成很多份的8k,注册一个写事件,写完8K,再写8K…,一直发送完
-
原生JDK网络编程- NIO之单线程Reactor模式
反应器模式
- 控制逆转,反应倒置
- 在设计模式中,称之为好莱坞法则,不要调用我,而是我在合适的时候来调用你
解释
- reactor是一个线程,这个线程里面会启动一个相关的事件循环
- 并且通过之前讲的selector来实现io多路复用
- 首先注册一个accept事件,产生一个socket,然后socket又在reactor里面注册一个关注读的事件
- 当reactor监听到有读事件或者写事件发生时,进行一个相关的派发,就是handlerInput,交给不同的socketChannel进行相关的事件处理
- 在这种模式里面,所有的事情都是在一个线程里面做的,不管是事件的处理,还是实际去读取网络上的数据,是一个单线程的reactor模式,
- 而且我们的selector也是一个线程处理的,所有的事情,不管网络读写、发送、接受连接、具体业务数据的解析、都是由一个线程做的
原生JDK网络编程- NIO之单线程Reactor、工作者线程池模式
-
把网络读写相关的数据拿出来,这个线程就只关注跟网络通讯有关的事,比如说接受连接、比如读数据、比如发送数据,而在实际情况中其余做的那些解码呀、业务的计算,把它拿出来之后,交给另外一个线程池来做。
-
好处
- 1.不是单线程处理全部任务,业务逻辑出了错,不会整个通讯功能挂掉
- 2.专人做专事,负责网络读写的就负责网络读写,负责业务处理的就负责业务处理,这样相比之前一个线程而言,负责网络读写的线程就轻松了很多
-
缺点
- 1.所有的网络操作都是由一个线程来处理的,如果负载不高、并发不高,无所谓,如果是高负载、大并发、大数据量,线程忙着网络读写,这种情况这个线程可能支撑不住
原生JDK网络编程- NIO之多线程的Reactor模式
- 接受连接单独用一个线程来做
- 接受完连接之后,具体的每个服务器之上的每一个socketChannel和对端的通讯交给另外一个线程或者线程池来处理
反应器模式与观察者模式的区别?
- 观察者模式
- 被观察者发生变化时,依次去通知所有的观察者
- 反应器模式
- 会有很多很多的事件发生,在观察者模式里面只有一个事件发生,把这个事件通知所有的观察者,而在反应器模式里面,事件可能有很多个,而且每个时间接触到的人也是不相同的,一个是单事件源,一个是多事件源,而且在反应器模式里面,使用回调更多一点,当事件完成了,我们的接受者会注册事件处理的入口,交给我们的反应器,反应器进来直接调用关注事件的人的方法,告诉它直接取处理
Netty是什么?为什么要用Netty?
-
Netty是由JBOSS提供的一个java开源框架。
-
Netty提供异步的、事件驱动的网络应用程序框架和工具,用以快速开发高性能、高可靠性的网络服务器和客户端程序。
-
互联网公司必备 http://netty.io/wiki/adopters.html
-
性能高
-
netty5已经停止开发了,基于aio开发的,linux上的aio没有真正实现,windows上是真正的实现,所以在linux上是没有意义的,所以现在的高版本是netty4
-
当前用的代码用的是4.1.28.Final
-
包名----io.netty
第一个Netty程序
-
Netty核心组件初步了解
-
EventLoop(Group) 、Channel
EventLoop近似的看成一个线程,EventLoop(Group) 看成线程池;
Channel对应于nio中的ServerSocketChannel、SocketChannel,负责具体的网络通讯
-
事件和ChannelHandler、ChannelPipeline
事件:去动态通知改变,netty创造的概念,根据事件的不同,来做相关业务的处理;
入栈:网络数据传入到应用程序中进行处理;
出栈:数据由应用程序处理完后重新发往网络,再发往通讯对端;
网络和应用程序之间的流转,在流转过程中,事件需要进行处理,通过ChannelHandler来处理,这个handler可能有很多个,ChannelPipeline把这些handler装起来,相当于是保存handler的容器;
-
ChannelFuture
在netty里面的操作,都是异步的,ChannelFuture代表某个方法的执行结果,在未来的某个时刻去拿某个方法的执行结果,类似于并发编程中的Future
-
netty服务器—EchoServer
-
package cn.enjoyedu.nettybasic.echo; import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelInitializer; import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioServerSocketChannel; import java.net.InetSocketAddress; /** * 作者:Mark/Maoke * 创建日期:2018/08/25 * 类说明:基于Netty的服务器 */ public class EchoServer { private final int port; public EchoServer(int port) { this.port = port; } public static void main(String[] args) throws InterruptedException { int port = 9999; EchoServer echoServer = new EchoServer(port); System.out.println("服务器即将启动"); echoServer.start(); System.out.println("服务器关闭"); } public void start() throws InterruptedException { // 创建EchoServerHandler final EchoServerHandler serverHandler = new EchoServerHandler(); /*线程组*/ // EventLoopGroup相当于线程池的概念 // 为什么用线程池? // 为了网络通讯和业务处理分离 EventLoopGroup group = new NioEventLoopGroup(); try { /*服务端启动必备*/ // 做初始化工作、引导化工作 ServerBootstrap b = new ServerBootstrap(); // b.group传入线程组 // .channel(NioServerSocketChannel.class)指定使用nio // .localAddress(new InetSocketAddress(port))指定监听端口 // .childHandler配置处理的handler b.group(group) .channel(NioServerSocketChannel.class) .localAddress(new InetSocketAddress(port) .childHandler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel ch) throws Exception { // 把新创建的handler加到SocketChannel里面 ch.pipeline().addLast(serverHandler); } }); // 异步绑定到服务器,sync()会阻塞到完成 // .sync()保证完成 ChannelFuture f = b.bind().sync(); // 阻塞当前线程,直到服务器的ServerChannel被关闭 f.channel().closeFuture().sync(); } finally { group.shutdownGracefully().sync(); } } }
EchoServerHandler–继承ChannelInboundHandlerAdapter
-
package cn.enjoyedu.nettybasic.echo; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerAdapter; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.util.CharsetUtil; @ChannelHandler.Sharable public class EchoServerHandler extends ChannelInboundHandlerAdapter { // 重写读方法---channelRead // 读取客户端传过来的数据,再回传回去 // nio中的buffer对应于这里的ByteBuf @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { ByteBuf in = (ByteBuf)msg; System.out.println("Server accept: "+in.toString(CharsetUtil.UTF_8)); // 回传给客户端 ctx.writeAndFlush(in); //ctx.close(); } // 发生异常时的对应处理 @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { cause.printStackTrace(); ctx.close(); } }
netty客户端
-
package cn.enjoyedu.nettybasic.echo; import io.netty.bootstrap.Bootstrap; import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelInitializer; import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioServerSocketChannel; import io.netty.channel.socket.nio.NioSocketChannel; import java.net.InetSocketAddress; /** * 作者:Mark/Maoke * 创建日期:2018/08/26 * 类说明:基于Netty的客户端 */ public class EchoClient { private final int port; private final String host; public EchoClient(int port, String host) { this.port = port; this.host = host; } public void start() throws InterruptedException { /*线程组*/ EventLoopGroup group = new NioEventLoopGroup(); try { /*客户端启动必备*/ // 服务端要用Bootstrap Bootstrap b = new Bootstrap(); b.group(group) // 用NioSocketChannel.class // 不能再用NioServerSocketChannel // 不能用localAddress,而是要用remoteAddress // 为什么客户端要用handler,不用childHandler? // 服务端告诉netty,流经客户端的处理要用childHandler,客户端没有这个概念 .channel(NioSocketChannel.class)/*指定使用NIO的通信模式*/ .remoteAddress(new InetSocketAddress(host,port))/*指定服务器的IP地址和端口*/ .handler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel ch) throws Exception { // 把创建的handler加入到SocketChannel中 ch.pipeline().addLast(new EchoClientHandler()); } }); // 客户端不存在绑定,而是连接远端的服务器 // 连接是异步的 ChannelFuture f = b.connect().sync();/*异步连接到服务器,sync()会阻塞到完成*/ f.channel().closeFuture().sync();/*阻塞当前线程,直到客户端的Channel被关闭*/ } finally { group.shutdownGracefully().sync(); } } public static void main(String[] args) throws InterruptedException { // 启动客户端 new EchoClient(9999,"127.0.0.1").start(); } }
EchoClientHandler—继承SimpleChannelInboundHandler
-
package cn.enjoyedu.nettybasic.echo; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.util.CharsetUtil; // SimpleChannelInboundHandler是更简单的实现 public class EchoClientHandler extends SimpleChannelInboundHandler<ByteBuf> { // 这个方法是继承父类强制重写的 // 读取到网络数据后进行业务处理 @Override protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception { System.out.println("client Accept"+msg.toString(CharsetUtil.UTF_8)); //ctx.close(); } // 客户端和服务器建立连接后,客户端发出起始数据 /*channel活跃后,做业务处理*/ @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { // 往服务端发送数据 // 发送的数据必须是buffer类型 // Unpooled.copiedBuffer工具类把字符串转成buffer ctx.writeAndFlush(Unpooled.copiedBuffer( "Hello,Netty",CharsetUtil.UTF_8)); } }
测试1
- 先启动EchoServer
- 再启动EchoClient
- 打印正常
测试2
-
先启动EchoServer
-
再启动NioClient
-
报错,Failed to initialize channel
-
pipeline里面每一个socketChannel里面都会有handler,而且handler不能在每一个socketChannel里面共享的,
怎么解决?
1.像EchoClient,每次都是new一个handler出来
2.在EchoServerHandler加@ChannelHandler.Sharable的注解
-
大多时候不注册写事件?
- 因为写数据的buffer只要有空闲,就会触发一个写事件,所以都是单次全部直接写入,但是当写入的数据超过了buffer容量时,就必须要创建写事件,然后一次一次的写,直到写完