netty 学习笔记一:感受 IO编程 NIO编程 与 Netty 编程
代码和注释:https://github.com/christmad/code-share/tree/master/share-netty/src/main/java/code.share.netty
(1)IO编程模式
IO server端代码:
1 public void IOserver() throws IOException { 2 // IO模型-服务端监听端口 3 ServerSocket server = new ServerSocket(8000); 4 5 new Thread(() -> { 6 while (true) { 7 try { 8 // (1) 阻塞方式获取新连接 9 Socket socket = server.accept(); 10 11 // (2) 将新连接绑定到一条新线程上去处理 12 new Thread(() -> { 13 try { 14 int len; 15 byte[] data = new byte[1024]; 16 InputStream input = socket.getInputStream(); 17 // (3) 按字节流方式读取数据 18 while ((len = input.read(data)) != -1) { 19 System.out.println(new String(data, 0, len)); 20 } 21 } catch (IOException e) { 22 } 23 }).start(); 24 } catch (IOException ex) { 25 } 26 } 27 }).start(); 28 }
由上我们可用看出 IO server端编码三要素:
1. 阻塞方式获取新连接
2. 将新连接绑定到一条新线程上处理
3. 按字节流方式读取数据
IO client端代码:
public void IOclient() { new Thread(() -> { try { // IO模型-客户端连接服务器 Socket socket = new Socket("localhost", 8000); while (true) { try { // 每隔 2s 向服务器发送一条信息 socket.getOutputStream().write((new Date() + " : hello").getBytes()); TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { } } } catch (IOException e) { } }).start(); }
(2)NIO编程模式
NIO server端代码:
1 public void NIOserver() throws IOException { 2 // throw ex 3 // 创建 selector:NIO模型中通常会有两个线程,每个线程绑定一个轮询器 selector 4 Selector serverSelector = Selector.open(); // serverSelector 负责轮询是否有新的连接 5 Selector clientSelector = Selector.open(); // clientSelector 负责轮询连接是否有数据可读 6 7 // 模拟服务端接收新连接,demo中只用一条线程去处理 8 new Thread(() -> { 9 try { 10 // NIO服务端启动 11 ServerSocketChannel listenerChannel = ServerSocketChannel.open(); 12 // 在 bind 的时候底层已经开始监听了,windows 1.8 JDK 默认实现 backlog=50,即最多允许50条全连接 13 listenerChannel.socket().bind(new InetSocketAddress(8000)); 14 listenerChannel.configureBlocking(false); // non-blocking 15 16 // ServerSocketChannel#register 函数是将与操作系统交互(I/O)的工作交给持有 serverSelector, 17 // 持有 serverSelector 对象的线程在 while 循环中处理新连接:Selector.select() 函数会获得操作系统层面做好三次握手的客户端连接,一次可获得批量 18 // 实际开发中可能是多条线程来共同负责处理一大批连接,即 1个serverSelector、多个clientSelector。如果可以对同一个端口多次 bind 的话,有可能多个 serverSelector? 19 // 注意:register 方法是来自 ServerSocketChannel,这个方法的作用是让 selector 获得 channel 的引用。 20 // 操作系统底层处理的经过三次握手的连接应该是体现在 Channel 上,channel 负责存储网络数据,而 Selector 负责数据操作 21 22 // SelectionKey.OP_ACCEPT 指明 serverSelector 要监听的是新连接 23 listenerChannel.register(serverSelector, SelectionKey.OP_ACCEPT); 24 25 while (true) { 26 // 整个 while 循环处理的是将 serverSelector#select 获取到的连接事件绑定到 clientSelector 上,由它来监听这些连接的 SelectionKey.OP_READ 事件 27 28 // 等待新连接,阻塞时间为 1ms 29 if (serverSelector.select(1) > 0) { 30 Set<SelectionKey> acceptIdSet = serverSelector.selectedKeys(); 31 Iterator<SelectionKey> acceptIdItor = acceptIdSet.iterator(); 32 while (acceptIdItor.hasNext()) { 33 SelectionKey key = acceptIdItor.next(); 34 35 if (key.isAcceptable()) { 36 // 客户端连接经过三次握手进来后,我们需要处理客户端连接的事件:读或写 37 // 本例中模拟的是监听连接的可读事件——请求 38 try { 39 // 通过 ServerSocketChannel#accept 函数获取 SocketChannel 40 // 底层原理是客户端请求和上面绑定的 8000 端口连接,三次握手后,操作系统会启用另一个端口来标识这条连接, 41 // 这条新连接在 NIO API 上反应出来就是一个 SocketChannel 对象,我们将这个对象注册到 clientSelector 以达到统一管理客户端连接读事件的目的 42 SocketChannel clientChannel = ((ServerSocketChannel) key.channel()).accept(); 43 clientChannel.configureBlocking(false); 44 // 调用 SocketChannel#register 函数,表示 clientSelector 将会关注 channel 的可读事件;一旦对应的 channel 标志位变化,就可以进行下一步操作 45 clientChannel.register(clientSelector, SelectionKey.OP_READ); 46 } finally { 47 // 只是在本轮的 Selector#selectedKeys 中移除了,可能是帮助清空此次 Selector#selectedKeys 产生的临时 Set<SelectionKey> 空间 48 acceptIdItor.remove(); 49 } 50 } 51 52 } 53 } 54 } 55 } catch (IOException e) { 56 e.printStackTrace(); 57 } 58 }).start(); 59 60 // 在上面的代码中,我们只是把连接注册到 clientSelector 中并监听它们的读事件,下面则是批量处理读事件的代码 61 new Thread(() -> { 62 while (true) { 63 try { 64 // 批量轮询有哪些连接有数据可读,阻塞时间为 1ms 65 if (clientSelector.select(1) > 0) { 66 Set<SelectionKey> readIdSet = clientSelector.selectedKeys(); 67 Iterator<SelectionKey> readIdItor = readIdSet.iterator(); 68 while (readIdItor.hasNext()) { 69 SelectionKey key = readIdItor.next(); 70 71 if (key.isReadable()) { 72 try { 73 SocketChannel clientChannel = (SocketChannel) key.channel(); 74 ByteBuffer buf = ByteBuffer.allocate(1024); // 申请 1KB 缓冲区 75 // NIO 读面向 buf, 将 clientChannel 数据缓存到 buf 对象中 76 clientChannel.read(buf); 77 buf.flip(); 78 System.out.println(Charset.defaultCharset().newDecoder().decode(buf).toString()); 79 } finally { 80 readIdItor.remove(); 81 // 暂时不知道有什么用 82 key.interestOps(SelectionKey.OP_READ); 83 } 84 85 } 86 } 87 } 88 } catch (IOException e) { 89 e.printStackTrace(); 90 } 91 } 92 }).start(); 93 94 95 }
由上我们可以看出 NIO server端编码概念比较多:
Channel、Selector、SelectionKey、select逻辑、register逻辑、ByteBuffer缓冲区读写逻辑......特别是 ByteBuffer 里面那一套 flip、capacity、reset、mark 等操作缓冲游标的方法,稍不注意就会出错。如果开发者都是基础比较过硬的,那么可以封装一套更易用的代码出来,毕竟 Java 作为一门业务语言的优势在于快速开发,而不是每一样都需要注重到底层。而前面说的一套好的封装,netty 已经帮我们做好了,接下来我们会介绍 netty编程模式。
NIO client端代码:太繁琐,直接看下面 netty 如何实现 server、client 端编码吧
(3)Netty编程模式
netty server端代码:
1 public void nettyServer() { 2 ServerBootstrap serverBootstrap = new ServerBootstrap(); 3 NioEventLoopGroup boss = new NioEventLoopGroup(); 4 NioEventLoopGroup worker = new NioEventLoopGroup(); 5 6 serverBootstrap 7 .group(boss, worker) 8 .channel(NioServerSocketChannel.class) 9 .childHandler(new ChannelInitializer<NioSocketChannel>() { 10 @Override 11 protected void initChannel(NioSocketChannel ch) throws Exception { 12 ch.pipeline().addLast(new StringDecoder()); 13 ch.pipeline().addLast(new SimpleChannelInboundHandler<String>() { 14 15 @Override 16 protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception { 17 System.out.println(msg); 18 } 19 }); 20 } 21 }); 22 23 // 定义一个自动寻找端口绑定的方法,在预设端口被占用时比较灵活 24 // windows 上 1000 端口是可以直接绑定成功的,linux 上据说 0-1023 都不能绑定,是预留端口 25 int port = 1000; 26 bind(serverBootstrap, port); 27 } 28 29 private void bind(ServerBootstrap serverBootstrap, int port) { 30 serverBootstrap.bind(port).addListener(future -> { 31 if (future.isSuccess()) { 32 System.out.println("监听端口绑定成功,port=" + port); 33 } else { 34 System.out.println("监听端口绑定失败,port=" + port); 35 System.out.println("尝试下一个端口, next port= " + (port + 1)); 36 bind(serverBootstrap, port + 1); 37 } 38 }); 39 }
netty client端代码:
1 public void nettyClient() { 2 Bootstrap bootstrap = new Bootstrap(); 3 NioEventLoopGroup worker = new NioEventLoopGroup(); 4 5 bootstrap 6 .group(worker) 7 .channel(NioSocketChannel.class) 8 .handler(new ChannelInitializer<Channel>() { 9 @Override 10 protected void initChannel(Channel ch) throws Exception { 11 ch.pipeline().addLast(new StringEncoder()); 12 } 13 }); 14 15 Channel channel = bootstrap.connect("localhost", 8000).channel(); 16 17 while (true) { 18 channel.writeAndFlush(new Date() + " : hello"); 19 try { 20 TimeUnit.SECONDS.sleep(2); 21 } catch (InterruptedException e) { 22 } 23 } 24 }
经过本节介绍,应该可以看到 netty 的封装已经比原生API优雅了不止一点点,各种分布式框架的网络模块大量应用 netty 作为第三方网络包也验证了 netty 的封装在性能方面绝对可以榨干你的 CPU 。下一篇将会分析 netty编程 server端、client端的一些基本要素。链接如下:https://www.cnblogs.com/christmad/p/11625674.html