JAVA网络通信IO-NIO-AIO-Netty
Socket实现网络通信(阻塞式IO)
阻塞式IO在两个地方会阻塞
- 在使用IO和Socket构造网络服务时 接收连接:accept(),接收请求数据,发送响应数据都可能引起阻塞的操作。(Handler必须使用多线程异步操作,不然别的连接进不来)
- 线程从Socket输入流读数据时,如果没有足够的数据就会进入阻塞状态,直到读够了足够的数据,或者达到输入流的末尾,或者出现了异常,才能从输入流的read()方法返回或异常中断。
public class Server { public static void main(String[] args) throws IOException { ServerSocket ss = new ServerSocket(); ss.bind(new InetSocketAddress("127.0.0.1", 8888)); while(true) { Socket s = ss.accept(); //阻塞方法 new Thread(() -> { handle(s); }).start(); } } static void handle(Socket s) { try { byte[] bytes = new byte[1024]; int len = s.getInputStream().read(bytes); System.out.println(new String(bytes, 0, len)); s.getOutputStream().write(bytes, 0, len); s.getOutputStream().flush(); } catch (IOException e) { e.printStackTrace(); } } }
NIO对网络通信改进
网络通信在阻塞模式下,
- read()方法会争取读到n个字节,如果输入流中不足n个字节,就进入阻塞状态,直到读取了n个字节,或者读到了输入流末尾,或者出现了I/O异常。
- socket.accept()方法如果没有接收到连接,也会一直等待
大量线程连接进来的时候,效率比较低,所有线程都阻塞在接收和read数据的地方
网络通信在非阻塞模式下(NIO对BIO的改进)
- read()方法奉行能读到多少数据就读到多少数据的原则。read()方法读取当前通道中的可读数据,有可能不足n个字节,或者为0个字节,read()方法总会立刻返回。而不会等到读取了n个字节才返回,read()方法返回实际上读入的字节数。SocketChannel extends AbstractSelectableChannel 类的中 int read(ByteBuffer dst)方法是非阻塞式的。
- ServerSocketChannel或SockeChannel通过register()方法向Selector注册事件时,register()方法会创建一个SelectionKey对象,这个SelectionKey对象是跟踪注册事件的句柄。在SelectionKey对象有效期,Selector会一直监控与SelectorKey对象相关的事件,如果事件发生,就会把SelectionKey对象加入到Selector-keys集合中。
1)NIO-Single(单线程模型)
大管家selector
除了管理客户端的连接之外
连接通道建立后:还盯着有没有需要读写的数据(一个大管家 领着一帮工人)
使用Selector创建一个非阻塞的服务器。
public static void main(String[] args) throws IOException { ServerSocketChannel ssc = ServerSocketChannel.open(); ssc.socket().bind(new InetSocketAddress("127.0.0.1", 8888)); ssc.configureBlocking(false); System.out.println("server started, listening on :" + ssc.getLocalAddress()); Selector selector = Selector.open(); ssc.register(selector, SelectionKey.OP_ACCEPT); while(true) { selector.select(); Set<SelectionKey> keys = selector.selectedKeys(); Iterator<SelectionKey> it = keys.iterator(); while(it.hasNext()) { SelectionKey key = it.next(); it.remove(); handle(key); } } }
handler(key)没有异步的去处理,这是单线程模型的NIO
NIO的读写都是用ByteBuffer 一块一块的读 相比BIO的一个byte的读,提高了很多效率 但是特别难用:推荐NIO类库介绍
当你忘记flip复位的操作,你可以把消息读成一半,这也是Netty受欢迎的原因。
2)NIO-Reactor(多线程模型)
Selector任务是BOSS 不干别的事,就负责客户端的连接
要不要写 交给Worker工人来做,工人是一个池子(线程池)
也就是NIO单线程模型中 Handler用线程池调用
AIO对网络通信改进
AIO是类似观察者模式的事件回调,而不在需要轮循
当客户端需要连接的时候 交给操作系统去链接
操作系统一旦连接上了客户端,会给大管家Selector说有人要连上来了
大管家只负责连接的功能 而不需要轮循环,连好的通过交给工人worker处理通道里面的信息
参考文章:AIO
public class Server { public static void main(String[] args) throws Exception { final AsynchronousServerSocketChannel serverChannel = AsynchronousServerSocketChannel.open() .bind(new InetSocketAddress(8888)); serverChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Object>() { @Override public void completed(AsynchronousSocketChannel client, Object attachment) { serverChannel.accept(null, this); try { System.out.println(client.getRemoteAddress()); ByteBuffer buffer = ByteBuffer.allocate(1024); client.read(buffer, buffer, new CompletionHandler<Integer, ByteBuffer>() { @Override public void completed(Integer result, ByteBuffer attachment) { attachment.flip(); System.out.println(new String(attachment.array(), 0, result)); client.write(ByteBuffer.wrap("HelloClient".getBytes())); } @Override public void failed(Throwable exc, ByteBuffer attachment) { exc.printStackTrace(); } }); } catch (IOException e) { e.printStackTrace(); } } @Override public void failed(Throwable exc, Object attachment) { exc.printStackTrace(); } }); while (true) { Thread.sleep(1000); } } }
serverChannel.accept的时候 就可以走了,回调函数 观察者设计模式,把这个CompletionHandler方法交给操作系统去执行
下面之所以写while(true)是为了防止程序结束,如果想写的严谨一些,可以用countDownLatch
Netty
Netty是对NIO进行了封装,封装的API更像AIO
netty的写法和AIO差不多
netty把NIO中难用的byteBuffer封装的特别好
疑问点:有了AIO为什么还需要NIO
因为AIO和NIO在linux底层都是用的epoll模型实现的,epoll本身就是轮循模型
所以你上层在怎么封装,下层还是轮循(Netty就是用的NIO而不是AIO),在Linux中AIO的效率未必比NIO高
而Windows的AIO是自己单独实现的,不是轮训模型而是事件模型(Windows的Server比较少 Netty未做重点)
Netty服务端
public class HelloNetty { public static void main(String[] args) { new NettyServer(8888).serverStart(); } } class NettyServer { int port = 8888; public NettyServer(int port) { this.port = port; } public void serverStart() { /**定义两个线程池**/ EventLoopGroup bossGroup = new NioEventLoopGroup(); EventLoopGroup workerGroup = new NioEventLoopGroup(); ServerBootstrap b = new ServerBootstrap(); /**把这两个group传给Server启动的封装类**/ b.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class)/**指定Server启动之后 客户端连接上来的通道类型**/ .childHandler(new ChannelInitializer<SocketChannel>() {/**每一个客户端连上来之后 监听器处理**/ @Override protected void initChannel(SocketChannel ch) throws Exception { /**通道一旦init 在这个通道上就添加对这个通道的处理器**/ ch.pipeline().addLast(new Handler()); } }); try { ChannelFuture f = b.bind(port).sync(); f.channel().closeFuture().sync(); } catch (InterruptedException e) { e.printStackTrace(); } finally { workerGroup.shutdownGracefully(); bossGroup.shutdownGracefully(); } } } class Handler extends ChannelInboundHandlerAdapter { @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { //super.channelRead(ctx, msg); System.out.println("server: channel read"); ByteBuf buf = (ByteBuf)msg; System.out.println(buf.toString(CharsetUtil.UTF_8)); ctx.writeAndFlush(msg); ctx.close(); //buf.release(); } /**发生异常的回调方法**/ @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { //super.exceptionCaught(ctx, cause); cause.printStackTrace(); ctx.close(); } }
BossGroup:只负责客户端的连接过程
WorkerGroup:只负责数据的处理
Netty客户端
package com.mashibing.netty; import io.netty.bootstrap.Bootstrap; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.*; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioSocketChannel; import io.netty.util.ReferenceCountUtil; public class Client { public static void main(String[] args) { new Client().clientStart(); } private void clientStart() { EventLoopGroup workers = new NioEventLoopGroup(); Bootstrap b = new Bootstrap(); b.group(workers) .channel(NioSocketChannel.class) .handler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel ch) throws Exception { System.out.println("channel initialized!"); ch.pipeline().addLast(new ClientHandler()); } }); try { System.out.println("start to connect..."); ChannelFuture f = b.connect("127.0.0.1", 8888).sync(); f.channel().closeFuture().sync(); } catch (InterruptedException e) { e.printStackTrace(); } finally { workers.shutdownGracefully(); } } } class ClientHandler extends ChannelInboundHandlerAdapter { @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { System.out.println("channel is activated."); final ChannelFuture f = ctx.writeAndFlush(Unpooled.copiedBuffer("HelloNetty".getBytes())); f.addListener(new ChannelFutureListener() { @Override public void operationComplete(ChannelFuture future) throws Exception { System.out.println("msg send!"); //ctx.close(); } }); } @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { try { ByteBuf buf = (ByteBuf)msg; System.out.println(buf.toString()); } finally { ReferenceCountUtil.release(msg); } } }
同步异步关注的是消息通信机制
阻塞非阻塞关注的是等待消息时的状态
同步阻塞 IO :
在此种方式下,用户进程在发起一个 IO 操作以后,必须等待 IO 操作的完成,只有当真正完成了 IO 操作以后,用户进程才能运行。
JAVA传统的 IO 模型属于此种方式!
同步非阻塞 IO:
在此种方式下,用户进程发起一个 IO 操作以后 边可 返回做其它事情,但是用户进程需要时不时的询问 IO 操作是否就绪,这就要求用户进程不停的去询问,从而引入不必要的 CPU 资源浪费。
其中目前 JAVA 的 NIO 就属于同步非阻塞 IO 。
参考文章CSDN:https://blog.csdn.net/lovewebeye/article/details/105502035