《跟闪电侠学Netty》阅读笔记 - Netty入门程序解析(二)

简介: 《跟闪电侠学Netty》阅读笔记 - Netty入门程序解析

《跟闪电侠学Netty》阅读笔记 - Netty入门程序解析(一)https://developer.aliyun.com/article/1395286

Bootstrap

客户端连接不需要监听端口,为了和服务端区分直接被叫做Bootstrap,代表客户端的启动引导器。


Bootstrap bootstrap = new Bootstrap();

NioEventLoopGroup

Netty中客户端也同样需要设置线程模型才能和服务端正确交互,客户端的NioEventLoopGroup同样可以看作是线程池,负责和服务端的数据读写处理。


NioEventLoopGroup eventExecutors = new NioEventLoopGroup();

group

客户端 group 线程池的设置只需要一个即可,因为主要目的是和服务端建立连接(只需要一个线程即可)。


.group(eventExecutors)

channel

和服务端设置同理,作用是底层编程模型的设置。官方注释中推荐使用NIO / EPOLL / KQUEUE这几种,使用最多的是NIO


.channel(NioSocketChannel.class)

这里比较好奇如果用OIO模型的客户端连接NIO的服务端会怎么样? 于是做了个实验,把如下代码改为OioServerSocketChannel(生产禁止使用,此方式已被Deprecated),启动服务端之后启动客户端即可观察效果。


.channel(OioServerSocketChannel.class)

从实验结果来看,显然不允许这么干。


15:24:00.934 [main] WARN  io.netty.bootstrap.Bootstrap - Unknown channel option 'SO_KEEPALIVE' for channel '[id: 0xd0aaab57]'
15:24:00.934 [main] WARN  io.netty.bootstrap.Bootstrap - Unknown channel option 'TCP_NODELAY' for channel '[id: 0xd0aaab57]'

handler

上文介绍服务端的时候提到过 handler()代表服务端启动过程当中的逻辑,在这里自然就表示客户端启动过程的逻辑,客户端的handler()可以直接看作服务端引导器当中的childHandler()

这里读者可能会好奇为什么客户端代码用childHandler呢?答案是Netty为了防止使用者误解Bootstrap中只有handler,所以我们可以直接等同于服务端的childHandler()

吐槽:这个child不child的API名称看的比较蛋疼,不加以区分有时候确实容易用错。这里生活化理解服务端中的childHandler是身上带了连接,所以在连接成功之后会调用,没有child则代表此时没有任何连接,所以会发送在初始化的时候调用。

而客户端为什么只保留 handler() 呢?个人理解是客户端最关注的是连接上服务端之后所做的处理,增加初始化的时候做处理没啥意义,并且会导致设计变复杂。

handler内部是对于Channel进行初始化并且添加pipline自定义客户端的读写逻辑。这里同样添加Netty提供的StringEncoder默认会是用字符串编码模式对于发送的数据进行编码处理。


channel.pipeline().addLast(new StringEncoder());

ChannelInitializer可以直接类比SocketChannel

connect

当配置都准备好之后,客户端的最后一步是启动客户端并且和服务端进行TCP三次握手建立连接。这里方法会返回Channel对象,Netty的connect支持异步连接。


Channel channel = bootstrap.connect("127.0.0.1", 8000).channel();

再次强调,connect 是一个异步方法,同样可以通过给返回的channel对象调用addListner添加监听器,在Netty的客户端成功和服务端建立连接之后会回调相关方法告知监听器所有数据准备完成,此时可以在监听器中添加回调之后的处理逻辑。

我们还可以用监听器对于连接失败的情况做自定义处理逻辑,比如下面例子将会介绍利用监听器实现客户端连接服务端失败之后,定时自动重连服务端多次直到重连次数用完的例子。

实践:客户端失败重连

第二个实践代码是客户端在连接服务端的时候进行失败重连。失败重连在网络环境较差的时候十分有效,但是需要注意这里的代码中多次重试会逐渐增加时间间隔。

客户端失败重连的整体代码如下:


private static void connect(Bootstrap bootstrap, String host, int port, int retry) {  
    bootstrap.connect(host, port).addListener(future -> {  
        if (future.isSuccess()) {  
            System.out.println(new Date() + ": 连接成功,启动控制台线程……");  
            Channel channel = ((ChannelFuture) future).channel();  
            startConsoleThread(channel);  
        } else if (retry == 0) {  
            System.err.println("重试次数已用完,放弃连接!");  
        } else {  
            // 第几次重连  
            int order = (MAX_RETRY - retry) + 1;  
            // 本次重连的间隔  
            int delay = 1 << order;  
            System.err.println(new Date() + ": 连接失败,第" + order + "次重连……");  
            bootstrap.config().group().schedule(() -> connect(bootstrap, host, port, retry - 1), delay, TimeUnit  
                    .SECONDS);  
        }  
    });  
}
private static void startConsoleThread(Channel channel) {  
    ConsoleCommandManager consoleCommandManager = new ConsoleCommandManager();  
    LoginConsoleCommand loginConsoleCommand = new LoginConsoleCommand();  
    Scanner scanner = new Scanner(System.in);  
    new Thread(() -> {  
        while (!Thread.interrupted()) {  
            if (!SessionUtil.hasLogin(channel)) {  
                loginConsoleCommand.exec(scanner, channel);  
            } else {  
                consoleCommandManager.exec(scanner, channel);  
            }  
        }  
    }).start();  
}

加入失败重连代码之后,客户端的启动代码需要进行略微调整,在链式调用中不再使用直接connection,而是传递引导类和相关参数,通过递归的方式实现失败重连的效果:


connect(bootstrap, "127.0.0.1", 10999, MAX_RETRY);

客户端API其他方法和相关属性

attr()

  • NioChannel绑定自定义属性
  • 底层实际为Map
  • NioSocketChannel存储参数使用此方法取出

三种TCP关联参数

SO_KEEPALIVE

对应源码定义如下:


public static final ChannelOption<Boolean> SO_KEEPALIVE = valueOf("SO_KEEPALIVE");

主要关联TCP底层心跳机制。SO_KEEPALIVE用于开启或者关闭保活探测,默认情况下是关闭的。当SO_KEEPALIVE开启时,可以保持连接检测对方主机是否崩溃,避免(服务器)永远阻塞于TCP连接的输入。

TCP_NODELAY

Nagle 算法解释

这个参数的含义是:是否开启Nagle算法。首先需要注意这个参数和Linux操作系统的默认值不一样,true 传输到Linux是关闭调Nagle算法。

Nagele算法的出现和以前的网络带宽资源有限有关,为了尽可能的利用网络带宽,TCP总是希望尽可能的发送足够大的数据,Nagle算法就是为了尽可能发送大块数据,避免网络中充斥着许多小数据块

为了理解Nagle算法,我们需要了解TCP的缓冲区通常会设置 MSS 参数。

MSS 参数:除去 IP 和 TCP 头部之后,一个网络包所能容纳的 TCP 数据的最大长度; 最大 1460。MTU:一个网络包的最大长度,以太网中一般为 1500 字节;

为什么最大为1460个字节? 因为TCP传输过程中都会要求绑定 TCP 和 IP 的头部信息,这样服务端才能回送ACK确认收到包正确。

也就是说传输大数据包的时候,数据会按照MSS的值进行切割。回到Nagle算法,它的作用就是定义任意时刻只能有一个未被ACK确认的小段(MSS对应切割的一个块)。

这就意味着当有多个未被ACK确认的小段的时候,此时client端会小小的延迟一下等待合并为更大的数据包才发送。

Netty 默认关闭了这个算法,意味着一有数据就理解发送,满足低延迟和高并发的设计。

Netty源码关联

TCP_NODELAY 配置选项定义如下:


public static final ChannelOption<Boolean> TCP_NODELAY = valueOf("TCP_NODELAY");

此参数的配置介绍可以从 SocketChannelConfig 关联的配置中获取。


/**  
 * Gets the {@link StandardSocketOptions#TCP_NODELAY} option.  Please note that the default value of this option  
 * is {@code true} unlike the operating system default ({@code false}). However, for some buggy platforms, such as  
 * Android, that shows erratic behavior with Nagle's algorithm disabled, the default value remains to be * {@code false}.  
 */
 boolean isTcpNoDelay();

注释翻译如下。

获取 StandardSocketOptions.TCP_NODELAY 配置。请注意,该选项的默认值为 true,与操作系统的默认值(false)不同。然而,对于一些有问题的平台,比如Android,在禁用Nagle算法的情况下会出现不稳定的行为,默认值仍然为false。

CONNECTION_TIMEOUT

表示连接超时时间,单位为毫秒。

客户端和服务端通信

本部分可以参考作者代码,这里仅仅用笔记归档一下大致代码编写思路。

github.com/lightningMa…

客户端写入数据到服务端

  • handler 方法:指定客户端通信处理逻辑
  • initChannel 方法:给客户端添加逻辑处理器
  • pipeline:逻辑处理链添加逻辑处理器
  • addLast 添加自定义ChannelHandler
  • 逻辑处理器继承自ChannelHandler
  • 覆盖channelActive()方法
  • 客户端连接建立成功提示打印
  • 逻辑处理器可以通过继承适配类ChannelInboundHandlerAdapter实现简化开发
  • 写数据部分ByteBuf (Netty实现)
  1. alloc获得内存管理器
  2. byte[] 数据填充二进制数据
  3. writeAndFlush 刷缓存

服务端读取客户端数据

  • 逻辑处理器继承适配类
  • 逻辑处理器可以通过继承适配类ChannelInboundHandlerAdapter实现简化开发
  • 接收数据和服务端读取数据类似
  • 构建ByteBuf
  1. alloc获得内存管理器
  2. byte[] 数据填充二进制数据
  3. writeAndFlush 刷缓存
  • 通过writeAndFlush写出数据给客户端

服务端返回数据给客户端

  • 逻辑处理器继承适配类
  • 逻辑处理器可以通过继承适配类ChannelInboundHandlerAdapter实现简化开发
  • 接收数据和服务端读取数据类似
  • 构建ByteBuf
  1. alloc获得内存管理器
  2. byte[] 数据填充二进制数据
  3. writeAndFlush 刷缓存
  • 通过writeAndFlush写出数据给客户端

客户端读取服务端数据

  • 和服务端读取客户端数据思路类似
  • 关键是需要覆盖channelRead() 方法

核心概念

  • Netty当中,childHanlder 和 handler 对应客户端服务端处理逻辑
  • ByteBuf 数据载体,类似隧道两端的中间小推车。JDK官方实现java.nio.ByteBuffer存在各种问题,Netty官方重新实现了io.netty.buffer.ByteBuf
  • 服务端读取对象基本单位为Object,如果需要读取其他对象类型通常需要强转。
  • 逻辑处理器都可以通过继承适配器实现,客户端和服务端覆盖对应方法实现被动接收或者主动推送。

章节末尾问题

客户端API对比服务端少了什么内容?

  1. “group”。
  2. 客户端只有childHandler

新连接接入时候,如何实现服务端主动推送消息,然后客户端进行回复?

答案是添加监听器,在监听到客户端连接成功之后直接主动推送自定义信息。

handler()和childHandler()有什么区别

初学者比较容易困扰的问题。handler()childHandler()的主要区别是:handler()是发生在初始化的时候childHandler()是发生在客户端连接之后

“知其所以然”的部分放到后续的源码分析笔记当中,这里暂时跳过,初次阅读只需要记住结论即可。

八股

BIO的Socket和NIO的SocketChannel 区别?

本质上都是客户端和服务端进行网络通信的连接的一种抽象,但是使用上有不小的区别。下面的内容摘录自参考资料:

Socket、SocketChannel区别:blog.csdn.net/A350204530/…Netty Channel的理解:segmentfault.com/q/101000001…

相关文章
|
存储 索引 Python
Python入门:6.深入解析Python中的序列
在 Python 中,**序列**是一种有序的数据结构,广泛应用于数据存储、操作和处理。序列的一个显著特点是支持通过**索引**访问数据。常见的序列类型包括字符串(`str`)、列表(`list`)和元组(`tuple`)。这些序列各有特点,既可以存储简单的字符,也可以存储复杂的对象。 为了帮助初学者掌握 Python 中的序列操作,本文将围绕**字符串**、**列表**和**元组**这三种序列类型,详细介绍其定义、常用方法和具体示例。
Python入门:6.深入解析Python中的序列
|
存储 Linux iOS开发
Python入门:2.注释与变量的全面解析
在学习Python编程的过程中,注释和变量是必须掌握的两个基础概念。注释帮助我们理解代码的意图,而变量则是用于存储和操作数据的核心工具。熟练掌握这两者,不仅能提高代码的可读性和维护性,还能为后续学习复杂编程概念打下坚实的基础。
Python入门:2.注释与变量的全面解析
|
Web App开发 移动开发 前端开发
React音频播放器样式自定义全解析:从入门到避坑指南
在React中使用HTML5原生&lt;audio&gt;标签时,开发者常面临视觉一致性缺失、样式定制局限和交互体验割裂等问题。通过隐藏原生控件并构建自定义UI层,可以实现完全可控的播放器视觉风格,避免状态不同步等典型问题。结合事件监听、进度条拖拽、浏览器兼容性处理及性能优化技巧,可构建高性能、可维护的音频组件,满足跨平台需求。建议优先使用成熟音频库(如react-player),仅在深度定制需求时采用原生方案。
683 12
|
Java 关系型数据库 数据库连接
Javaweb之Mybatis入门程序的详细解析
本文详细介绍了一个MyBatis入门程序的创建过程,从环境准备、Maven项目创建、MyBatis配置、实体类和Mapper接口的定义,到工具类和测试类的编写。通过这个示例,读者可以了解MyBatis的基本使用方法,并在实际项目中应用这些知识。
392 11
|
移动开发 前端开发 JavaScript
从入门到精通:H5游戏源码开发技术全解析与未来趋势洞察
H5游戏凭借其跨平台、易传播和开发成本低的优势,近年来发展迅猛。接下来,让我们深入了解 H5 游戏源码开发的技术教程以及未来的发展趋势。
|
存储 缓存 NoSQL
跟着源码学IM(十一):一套基于Netty的分布式高可用IM详细设计与实现(有源码)
本文将要分享的是如何从零实现一套基于Netty框架的分布式高可用IM系统,它将支持长连接网关管理、单聊、群聊、聊天记录查询、离线消息存储、消息推送、心跳、分布式唯一ID、红包、消息同步等功能,并且还支持集群部署。
14140 1
|
算法 Java 容器
Netty源码—4.客户端接入流程
本文主要介绍了关于Netty客户端连接接入问题整理、Reactor线程模型和服务端启动流程、Netty新连接接入的整体处理逻辑、新连接接入之检测新连接、新连接接入之创建NioSocketChannel、新连接接入之绑定NioEventLoop线程、新连接接入之注册Selector和注册读事件、注册Reactor线程总结、新连接接入总结
|
安全 Java 调度
Netty源码—3.Reactor线程模型二
本文主要介绍了NioEventLoop的执行总体框架、Reactor线程执行一次事件轮询、Reactor线程处理产生IO事件的Channel、Reactor线程处理任务队列之添加任务、Reactor线程处理任务队列之执行任务、NioEventLoop总结。
|
安全 Java
Netty源码—2.Reactor线程模型一
本文主要介绍了关于NioEventLoop的问题整理、理解Reactor线程模型主要分三部分、NioEventLoop的创建和NioEventLoop的启动。
|
编解码 安全 Java
Netty源码—1.服务端启动流程
本文主要介绍了服务端启动整体流程及关键方法、服务端启动的核心步骤、创建服务端Channel的源码、初始化服务端Channel的源码、注册服务端Channel的源码、绑定服务端端口的源码、服务端启动流程源码总结。

推荐镜像

更多
  • DNS