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     }
View Code

由上我们可用看出 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();

    }
View Code

(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     }
View Code

由上我们可以看出 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     }
View Code

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     }
View Code

经过本节介绍,应该可以看到 netty 的封装已经比原生API优雅了不止一点点,各种分布式框架的网络模块大量应用 netty 作为第三方网络包也验证了 netty 的封装在性能方面绝对可以榨干你的 CPU 。下一篇将会分析 netty编程 server端、client端的一些基本要素。敬请期待......TO BE CONTINUED

猜你喜欢

转载自www.cnblogs.com/christmad/p/11622366.html