本节笔记承接上一节《netty 学习笔记一》,介绍 netty 服务端、客户端启动流程中关注的一些要素。
1 public void minimalServer() { 2 // 两大线程组,boss线程组负责监听端口、accept新连接;worker线程组负责处理连接上数据的读写 3 NioEventLoopGroup boss = new NioEventLoopGroup(); 4 NioEventLoopGroup worker = new NioEventLoopGroup(); 5 6 // 服务端引导类,直接 new 出来用 7 ServerBootstrap serverBootstrap = new ServerBootstrap(); 8 serverBootstrap 9 // 1.给引导类配置线程模型 10 .group(boss, worker) 11 // 2.指定服务端IO模型为 NIO——如果要配置为 BIO,可以选择 OioServerSocketChannel 12 .channel(NioServerSocketChannel.class) 13 // 3. IO处理逻辑 14 // 方法的 child 怎么理解呢?这是因为底层 accept 返回的连接和监听的端口会使用不同的端口,因此 childHandler 就是为新连接的读写所设置的 handler 15 // 新连接中对应的泛型抽象是 NioSocketChannel, 该对象标识一条客户端连接 16 .childHandler(new ChannelInitializer<NioSocketChannel>() { 17 @Override 18 protected void initChannel(NioSocketChannel ch) throws Exception { 19 20 } 21 }); 22 // 以上就是 netty 最小化参数配置,需要三类属性:1.线程模型 2.IO模型 3.连接读写处理逻辑 23 // 有了这三类属性,调用 ServerBootstrap#bind 方法就可以在本地运行起来 24 25 /* 26 服务端启动的其他方法 27 */ 28 // handler方法和 childHandler方法对应起来。那么 handler方法就对应服务器自身启动时的一些逻辑 29 serverBootstrap.handler(new ChannelInitializer<NioServerSocketChannel>() { 30 @Override 31 protected void initChannel(NioServerSocketChannel ch) throws Exception { 32 System.out.println("服务器启动中"); 33 } 34 }); 35 // attr方法可以给服务端的 channel 也就是 NioServerSocketChannel 对象指定一些自定义属性,说简单点就是给它维护一个 map 36 // 通过 channel.attr() 取出属性 37 serverBootstrap.attr(AttributeKey.newInstance("serverName"), "nettyServer"); 38 // childAttr方法 和 attr方法对应,那么前者就是为每一条新连接指定自定义属性,依然通过 channel.attr() 取出属性 39 serverBootstrap.childAttr(AttributeKey.newInstance("clientKey"), "clientValue"); 40 // 设置TCP底层相关的属性,同样也有两个对应的方法:option() 和 childOption() 41 // 服务端最常见的TCP底层属性,表示系统用于临时存放已完成三次握手的请求的连接队列的最大长度。如果连接建立频繁,服务器处理创建新连接较慢,可以适当调大这个参数 42 serverBootstrap.option(ChannelOption.SO_BACKLOG, 1024); 43 // 为每一条新连接设置的 TCP底层属性: 44 // SO_KEEPALIVE:是否使用心跳机制; 45 // TCP_NODELAY:nagle算法开关, TCP 用来处理 small packet problem 的算法,字面意思为是否立马发送。如果要求实时性高该值设为 true,如 redis 就是这么做的; 46 // 通常默认操作系统该值为 false, 这允许 TCP 在未发送数据中最多可以有一个未被确认的小分组, 47 // 举例来说客户端每次发送一个字节,将 HELLO 分五次发给服务端,客户端在应用层会调用 5次send方法。 48 // Nagle 算法会将 HELLO 分为 H 和 LLEO 两个分组,即 H 发送出去时 LLEO 在 nagle算法处理下被合并,在 H 的 ACK回来之后 LLEO 才被发出去,总共发送两次 49 // 于是开启 Nagle算法后应用层调用 5次send 实际在 TCP 层面只发了两个包。如果 TCP_NODELAY 为 true,即禁用 Nagle算法,那么 HELLO 就会分成 5个 TCP packet,每个包都占 41字节,比较浪费带宽 50 serverBootstrap.childOption(ChannelOption.TCP_NODELAY, true) 51 .childOption(ChannelOption.SO_KEEPALIVE, true); 52 53 // 修改为自动绑定递增端口 54 // serverBootstrap.bind(8000); 55 bind(serverBootstrap, 8000); 56 } 57 58 private void bind(ServerBootstrap serverBootstrap, int port) { 59 serverBootstrap.bind(port).addListener(future -> { 60 if (future.isSuccess()) { 61 System.out.println("成功绑定端口[" + port + "]"); 62 } else { 63 System.out.println("绑定端口[" + port + "]失败"); 64 bind(serverBootstrap, port + 1); 65 } 66 }); 67 }
1 private static final int MAX_RETRY = 5; 2 3 public void minimalClient() { 4 NioEventLoopGroup worker = new NioEventLoopGroup(); 5 6 Bootstrap bootstrap = new Bootstrap(); 7 bootstrap 8 // 参考 NettyServerBootstrap, client启动同样也有几个类似的流程 9 // 1.指定线程模型 10 .group(worker) 11 // 2.指定IO类型为 NIO 12 .channel(NioSocketChannel.class) 13 // 3.IO处理逻辑 14 // 这里的泛型官网demo用的也是 SocketChannel 接口, 实现类由上面的 channel() 方法指定 15 .handler(new ChannelInitializer<SocketChannel>() { 16 @Override 17 protected void initChannel(SocketChannel ch) throws Exception { 18 19 } 20 }); 21 /* 22 客户端启动的其他方法 23 */ 24 // attr方法,给 NioSocketChannel 绑定属性,通过 channel.attr() 取出属性 25 bootstrap.attr(AttributeKey.newInstance("clientName"), "christmad-netty"); 26 // option方法,给客户端连接设置TCP底层属性. 27 bootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000) 28 .option(ChannelOption.SO_KEEPALIVE, true) 29 .option(ChannelOption.TCP_NODELAY, true); 30 31 32 // 4.建立连接——指数退避时间间隔的失败重连 33 int port = 8000; 34 int retry = 5; 35 connect(bootstrap, port, retry); 36 } 37 38 // 为重连设计一个指数退避的重连间隔 39 private void connect(Bootstrap bootstrap, int port, int retry) { 40 bootstrap.connect("localhost", 8000).addListener(future -> { 41 if (future.isSuccess()) { 42 System.out.println("连接成功"); 43 } else if (retry == 0) { 44 System.err.println("连接失败,重连次数已用完,放弃连接!"); 45 } else { 46 // 第几次重连 47 int order = MAX_RETRY - retry + 1; 48 // 本次重连间隔 49 int delay = 1 << order; 50 System.err.println(new Date() + ": 连接失败,第" + order + "次重连......"); 51 // Bootstrap 的定时任务 52 bootstrap.config().group().schedule(() -> connect(bootstrap, port, retry - 1), delay, TimeUnit.SECONDS); 53 54 } 55 }); 56 }