Netty系列三Netty实战篇

Posted roykingw

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Netty系列三Netty实战篇相关的知识,希望对你有一定的参考价值。

配合示例代码

​ 这一篇我们就玩起来,通过一些常用的实战问题,来理解如何使用Netty进行网络编程。

一、传递POJO

​ 第一个示例参见示例代码中的com.roy.netty.pojoTransfer。

​ 这个示例实现的功能是这样的:

1、客户端建立连接后,就会往服务端发送一个User对象。
2、服务端接受到User对象后,给User加100薪水(salary属性),然后返回给客户端。 感觉挺爽的把!!
3、客户端接收到服务端的消息后打印出来。

​ 整体的数据流程是这样的:

在这里插入图片描述

示例解读

​ 这个示例的重点是理解Netty的编码器与解码器。在这个示例中,客户端与服务端是希望以User对象来互相传递数据,但是在编写网络应用程序时,数据只能以0和1组成的二进制字节码数据在网络中传输。所以需要在出站(发送数据)时,通过PojoEncoder将User对象按照一定的规则转化成二进制的字节码数据,在Netty中,就是通过ByteBuf来对二进制数据进行封装。而在入站(接收到数据)后,也需要通过PojoDecoder将二进制数据转换成为User对象,然后再进行具体的业务处理。而在具体实现时,就是将需要传输的字段转成byte,然后按照固定的顺序传输或者解码就行。这个跟Hadoop的Mappreduce定制传输对象的思路是一样的。

​ 在Netty中提供了两组编码解码的抽象类: MessageToByteEncoder和MessageToByteDecoder,以及MessageToMessageEncoder和MessageToMessageDecoder。

​ 这两组编解码抽象类都是由ChannelHandler扩展出来的抽象实现。他们都提供了一个泛型,只对泛型对应的类型的数据才进行编解码操作。所以在定制开发时,如果有多个对象,可以定制多个不同泛型的编解码器,然后添加到pipeline中就可以了。

​ 而这两组编解码器的区别就在于MessageToByte是把消息转成一个字节流,然后就会立即写到context里。而MessageToMessage是把消息转换成另外的多个消息,然后再依次将这些消息写入到Context当中。我们示例中只在一个User对象与字节流中进行编解码,所有用MessageToByte就足够了。但是假如是要在一个User数组与字节流中进行编解码转换,那用MessageToMessage就更好一点。

​ 另外,在Netty中,其实也提供了很多的编解码器,比如MessageToMessageEncoder的子类:StringEncoder,RedisEnoder,LineEncoder,HttpObjectEncoder, 还有MessageToByteEncoder的子类:ObjectEncoder 这些都是一看名字就很容易明白的编解码器。其中这个RedisEncoder看着是不是比较有意思?是的,这就是用Redis来传递网络数据,这样相当于给网络数据做了一个中转站。有了Redis后,是不是对这些底层的网络协议找到了一些熟悉的感觉?另外还有LineBasedFrameDecoder,使用行尾控制符(\\n或者\\r\\n)作为分隔符来解析数据,在Netty内部也有使用。DelimiterBasedFrameDecoder,使用自定义的特殊字符作为消息分隔符。LengthFieldBasedFrameDecoder:通过指定长度来标识整包信息,这样可以处理TCP的粘包和半包问题。

​ 还有,其实针对以对象为基础的网络请求,Netty中自带了ObjectDecoder和ObjectEncoder可以实现POJO对象或各种业务对象的编解码工作。但是这些编解码底层使用的是java自带的序列化技术,而java序列化技术本身效率不是很高,存在一些问题。比如无法跨语言,序列化后的体积会非常大,序列化性能太低等。在某些追求效率的场景,就会采用性能更高的序列化方案。最为常用的就是Google的Protobuf。

二、Google Protobuf

​ Protobuf是Google发布的一个开源项目,全称Google Protocol Buffers,是一种轻便高效的结构化数据存储格式,可以用于结构化数据串行化。他很适合做数据存储或者RPC远程调用的数据交换格式。

​ 参考文档地址:https://developers.google.com/protocol-buffers/docs/proto

​ 他支持跨平台、跨语言。支持目前绝大多数语言,比如C,C++,Java,Python等。他是通过编写一个.proto文件来对类进行描述,然后可以通过下载下来的protoc.exe编译器自动生成.java文件。然后用这个生成出来的java对象来进行传输。

​ 示例参见示例代码中的com.roy.netty.protobufSingleObject包。这个包下实现了只传输一个Student对象的示例。另一个示例在com.roy.netty.protobuf包下。这个包下实现了可传输多个对象的示例。

​ 注意使用Protobuf需要加入maven依赖包:

<dependency>
	<groupId>com.google.protobuf</groupId>
	<artifactId>protobuf-java</artifactId>
	<version>3.6.1</version>
</dependency>

三、TCP粘包与拆包

​ TCP粘包是编写网络应用时经常会遇到的问题。其实在我们的第一个示例pojoTransfer中,已经接触到了粘包的问题。在那个示例中,客户端的PojoNettyClientHandler中的channelActive方法中,如果使用for循环发十个user用户信息,服务端最终能收到的只有一个用户。而最终返回给客户端的是这样的一个用户信息:

在这里插入图片描述

​ 用户名变成了很长的一串,并且还有一些乱码。这个问题其实就是因为TCP的粘包问题。

​ TCP是面向连接,面向流的,提供高可靠的服务。消息发送端如果一次要发送多个数据包,为了更有效的发送数据,就会使用优化算法Nagle算法,将多次间隔发送的较小的数据包合并成一个大的数据块,然后进行封包。这样能提高传输消息的效率。但是接收端如果不做处理,就会无法分辨出数据包之间的边界,造成消息混乱。

在这里插入图片描述

​ 处理粘包问题的关键就是要在接收端确认边界。处理的方式有很多,比如针对我们那个示例,可以这样处理:

方法1、去掉userName属性,固定User的数据包长度: 在User对象转成ByteBuf时,就是因为userName属性转换成字节流后大小不固定,所以整个User对象转换成字节流后,长度也不固定。如果User对象中的各个属性都是一些长度固定的基础类型,那整个User对象的字节流长度也就固定了。Decoder解析字节流时,只要固定读取的长度,就可以还原成正确的User对象。

public class User {
    private int userId;
    private Double userSalary;
    //去掉userName属性
}

**方法2、定制字节流发送方式,在字节流前面加上整个User对象的字节流长度:**例如在PojoEncoder中把username的长度也写到字节流当中

public class PojoEncoder extends MessageToByteEncoder<User> {
    @Override
    protected void encode(ChannelHandlerContext ctx, User msg, ByteBuf out) throws Exception {
        out.writeInt(msg.getUserId());
        out.writeDouble(msg.getUserSalary());
        //处理粘包问题的一种方法:写入username字符串的长度。
        final byte[] usernameBytes = msg.getUserName().getBytes("utf-8");
        out.writeInt(usernameBytes.length);
        
        out.writeBytes(msg.getUserName().getBytes("utf-8"));
    }
}

然后在PojoDecoder中也按照这个长度来解析username

public class PojoDecoder extends ByteToMessageDecoder {
    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        System.out.println("读到的字符长度:"+in.readableBytes());
        final int userId = in.readInt();
        final double userSalary = in.readDouble();

        //处理粘包问题的一种方法,按固定长度读取username的字节流
        final int usernameLength = in.readInt();
        final ByteBuf userNameBuffer = in.readBytes(usernameLength);
        final String userName = userNameBuffer.toString(Charset.forName("utf-8"));

        User user = new User();
        user.setUserId(userId);
        user.setUserSalary(userSalary);
        user.setUserName(userName);
        out.add(user);
    }
}

​ 这样的话,在PojoNettyClientHandler中发多少个User对象都不会粘包了。

​ 通常,在开发过程中,定义字节流长度的方式,通常会被封装成一些自定义的协议,比如Dubbo框架中定义服务地址会以 dubbo: 开头,这其实就是Dubbo定义的一种报文协议,其本质也是通过定义报文长度,定制化报文的编解码方式。另外在Redis中,客户端与Redis的每个交互指令也是基于这样的机制。

四、心跳检测

​ 在基于TCP协议的网络应用开发中,客户端与服务端会建立一个长连接,而要维护这个长连接不被断开,就需要有心跳检测机制定期检查并且维护连接的状态。就好比两个人打电话,不管两个人说不说话,只要不挂断电话,那两个人的电话连接是永远不断开的。

​ 心跳检测机制是基于长连接的网路应用中非常重要也非常常见的一个底层机制。很多开源框架都需要心跳机制来及时检查并维护分布式系统的稳定性。微服务体系中,服务端要注册到注册中心,要通过心跳机制保证连接的有效性。RocketMQ的Broker也要注册到Nameserver上,并通过心跳机制保证连接的有效性。

​ 如果要自己开发一个心跳检测机制,还是挺麻烦的,需要有大量的定时任务。但是,使用Netty实现心跳检测就非常简单了,因为Netty全都帮我们封装好了。他的核心就是IdleStateHandler这个Handler。

​ 示例代码参见:com.roy.netty.heartbeat包。示例中演示了如何在客户端与服务端之间发心跳检测包。把数据交互都去掉,就会出现心跳超时的事件。另外,这个IdleStateHandler也是一个非常好的学习handler声明周期的地方。在实际开发中,心跳检测一般就只监控读写空闲即可。

五、Netty整合Log4j

​ 关于Netty如何整合Log4j,其实在上一个心跳检测的示例中已经有了。 只需要添加一个Netty封装的LoggingHandler就可以了。这个时候还可以在classpath下添加一个log4j.properties,对log4j日志格式进行配置。

六、WebSocket

​ 通常,前端与后端都是通过Http请求来交互的,而Http协议是无状态的,浏览器与服务器之间的每请求 一次,就要重新创建新的连接。这种方式,对于像聊天室这样的场景是不合适的。每次发消息都要重建连接,效率很低。

​ 而WebSocket是在前端页面与后端服务端之间建立一个长连接,基于长连接的数据交互就省掉了很多创建销毁连接的性能开销。并且,基于webSocket的长连接,浏览器端也可以及时感知服务端推送的消息。比如像商家版的饿了么,当有订单时,商家接单机器上会播报语音,这种场景如果不使用长连接,就需要前端做个定时任务访问后端,看有没有订单。性能消耗非常大并且也不及时。这个时候使用websocket长连接就比较合适了。

​ 示例参见com.roy.netty.websocket包。里面给出了一个简单的websocket示例。这个示例中有一个问题就是前端如果多次上线,就会注册多个channel,而从前端只能关闭最后一个。

七、Netty群聊

​ 有了前面这些示例,再来用Netty重新开发一下之前NIO阶段的群聊项目,就比较简单了。 核心就是在服务端维护一个客户端的channel集合。

八、Netty实现RPC服务调用

​ 示例代码com.roy.netty.rpc包中提供了一个模拟Dubbo的方式实现远程调用的Demo。整体的处理流程如下图:

在这里插入图片描述

​ 这是网上有的Demo,参考一下就行。其实整个过程中麻烦点的也就是客户端的接口代理处理,以及如何将Netty的异步请求转换成同步请求。

九、短连接与长连接

​ 最后com.roy.netty.swroddemo包中整理了一个企业级的Netty封装应用。示例中使用短连接的方式提高服务的吞吐量。只抽取了部分核心代码,作为参考把。

以上是关于Netty系列三Netty实战篇的主要内容,如果未能解决你的问题,请参考以下文章

Netty系列二Netty原理篇

Netty实战三之Netty的组件和设计

Netty系列・高级篇Netty核心源码解析

Netty系列:基础篇 BIO-NIO-AIO

Netty4.x实战专题案例文章列表

Netty系列进阶篇一:阻塞和多路复用到底是个啥?