Netty 入门与实战:仿写微信 IM 即时通讯系统 小册学习纪要

156 阅读3分钟

1. 学习纪要

NioSocketChannel,这个类呢,就是 Netty 对 NIO 类型的连接的抽象,而我们前面NioServerSocketChannel也是对 NIO 类型的连接的抽象,NioServerSocketChannelNioSocketChannel的概念可以和 BIO 编程模型中的ServerSocket以及Socket两个概念对应上

要启动一个Netty服务端,必须要指定三类属性,分别是线程模型、IO 模型、连接读写处理逻辑,有了这三者,之后在调用bind(8000),我们就可以在本地绑定一个 8000 端口启动起来

serverBootstrap.bind(8000);这个方法呢,它是一个异步的方法,调用之后是立即返回的,他的返回值是一个ChannelFuture,我们可以给这个ChannelFuture添加一个监听器GenericFutureListener,然后我们在GenericFutureListeneroperationComplete方法里面,我们可以监听端口是否绑定成功,接下来是监测端口是否绑定成功的代码片段

childOption()可以给每条连接设置一些TCP底层相关的属性,比如上面,我们设置了两种TCP属性,其中

ChannelOption.SO_KEEPALIVE表示是否开启TCP底层心跳机制,true为开启

ChannelOption.TCP_NODELAY表示是否开启Nagle算法,true表示关闭,false表示开启,通俗地说,如果要求高实时性,有数据发送时就马上发送,就关闭,如果需要减少发送次数减少网络交互,就开启。

serverBootstrap.option(ChannelOption.SO_BACKLOG, 1024)

表示系统用于临时存放已完成三次握手的请求的队列的最大长度,如果连接建立频繁,服务器处理创建新连接较慢,可以适当调大这个参数

>>1  等价于 /2
<<1  等价于 *2

与读写 API 类似的 API 还有 getBytes、getByte() 与 setBytes()、setByte() 系列,唯一的区别就是 get/set 不会改变读写指针,而 read/write 会改变读写指针,这点在解析数据的时候千万要注意

无论是使用 Netty 还是原始的 Socket 编程,基于 TCP 通信的数据包格式均为二进制

无论是从服务端来看,还是客户端来看,在 Netty 整个框架里面,一条连接对应着一个 Channel,这条 Channel 所有的处理逻辑都在一个叫做 ChannelPipeline 的对象里面,ChannelPipeline 是一个双向链表结构,他和 Channel 之间是一对一的关系。

ChannelPipeline 里面每个节点都是一个 ChannelHandlerContext 对象,这个对象能够拿到和 Channel 相关的所有的上下文信息,然后这个对象包着一个重要的对象,那就是逻辑处理器 ChannelHandler

image.png

Netty 自带的拆包器

1. 固定长度的拆包器 FixedLengthFrameDecoder

2. 行拆包器 LineBasedFrameDecoder

3. 分隔符拆包器 DelimiterBasedFrameDecoder

4. 基于长度域拆包器 LengthFieldBasedFrameDecoder

长度域相对整个数据包的偏移量是多少,这里显然是 4+1+1+1=7。

有了长度域偏移量长度域的长度,我们就可以构造一个拆包器。

什么是服务端与客户端的通信协议

无论是使用 Netty 还是原始的 Socket 编程,基于 TCP 通信的数据包格式均为二进制,协议指的就是客户端与服务端事先商量好的,每一个二进制数据包中每一段字节分别代表什么含义的规则。

通信协议的设计

Java 对象根据协议封装成二进制数据包的过程成为编码,而把从二进制数据包中解析出 Java 对象的过程成为解码

自己设计的通信协议是属于应用层协议,和http协议是同一级别

自定义协议属于私有协议,http属于共有协议

Channel 绑定属性,通过 channel.attr(xxx).set(xx) 的方式

channel 的 attr() 的实际用法:可以通过给 channel 绑定属性来设置某些状态,获取某些状态,不需要额外的 map 来维持。

SimpleChannelInboundHandler 从字面意思也可以看到,使用它非常简单,我们在继承这个类的时候,给他传递一个泛型参数,然后在 channelRead0() 方法里面,我们不用再通过 if 逻辑来判断当前对象是否是本 handler 可以处理的对象,也不用强转,不用往下传递本 handler 处理不了的对象,这一切都已经交给父类 SimpleChannelInboundHandler 来实现了,我们只需要专注于我们要处理的业务逻辑即可。

注意区别两者

public class InBoundHandlerA extends ChannelInboundHandlerAdapter {

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        System.out.println("InBoundHandlerA: " + msg);
        super.channelRead(ctx, msg);
    }
}
public class LoginRequestHandler extends SimpleChannelInboundHandler<LoginRequestPacket> {
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, LoginRequestPacket loginRequestPacket) {
        System.out.println(new Date() + ": 收到客户端登录请求……");

        LoginResponsePacket loginResponsePacket = new LoginResponsePacket();
        loginResponsePacket.setVersion(loginRequestPacket.getVersion());
        if (valid(loginRequestPacket)) {
            loginResponsePacket.setSuccess(true);
            System.out.println(new Date() + ": 登录成功!");
        } else {
            loginResponsePacket.setReason("账号密码校验失败");
            loginResponsePacket.setSuccess(false);
            System.out.println(new Date() + ": 登录失败!");
        }

        // 登录响应
        ctx.channel().writeAndFlush(loginResponsePacket);
    }

    private boolean valid(LoginRequestPacket loginRequestPacket) {
        return true;
    }
}

outBound类和inBound类之间的顺序一般没有限制,通常情况下,同一种类型的 handler 的添加顺序需要注意

2. handlerAdded() 与 handlerRemoved()

这两个方法通常可以用在一些资源的申请和释放

3. channelActive() 与 channelInActive()

  1. 对我们的应用程序来说,这两个方法表明的含义是 TCP 连接的建立与释放,通常我们在这两个回调里面统计单机的连接数,channelActive() 被调用,连接数加一,channelInActive() 被调用,连接数减一
  2. 另外,我们也可以在 channelActive() 方法中,实现对客户端连接 ip 黑白名单的过滤,具体这里就不展开了

5. channelReadComplete()

前面小节中,我们在每次向客户端写数据的时候,都通过 writeAndFlush() 的方法写并刷新到底层,其实这种方式不是特别高效,我们可以在之前调用 writeAndFlush() 的地方都调用 write() 方法,然后在这个方面里面调用 ctx.channel().flush() 方法,相当于一个批量刷新的机制,当然,如果你对性能要求没那么高,writeAndFlush() 足矣。

channelRead()和channelReadComplete()不总是成对出现,可能好几个channelReade()才会有一个channelReadComplete()出现。比如某次连续10个分包读取,每读取一个分包就会channelRead(),但读取完全部10个分包才会一次channelReadComplete()。

AuthHandler 继承自 ChannelInboundHandlerAdapter,覆盖了 channelRead() 方法,表明他可以处理所有类型的数据

ChannelOption.SO_KEEPALIVE表示是否开启TCP底层心跳机制,true为开启 ChannelOption.TCP_NODELAY表示是否开启Nagle算法,true表示关闭,false表示开启,通俗地说,如果要求高实时性,有数据发送时就马上发送,就关闭,如果需要减少发送次数减少网络交互,就开启。

serverBootstrap.option(ChannelOption.SO_BACKLOG, 1024)

表示系统用于临时存放已完成三次握手的请求的队列的最大长度,如果连接建立频繁,服务器处理创建新连接较慢,可以适当调大这个参数

2. 代码仓

flash-netty