一.何为通信协议
这里指的通信协议并不是狭义的TCP、UDP这类【标准通信协议】,而是指的Netty用于客户端与服务端之间数据交互的【自定义通信协议】。无论是使用 Netty 还是原始的 Socket 编程,基于 TCP 通信的数据包格式均为二进制,协议指的就是客户端与服务端事先商量好的,每一个二进制数据包中每一段字节分别代表什么含义的规则。如下图所示为一个简单的登陆所用的协议:
在这个数据包中,第一个字节为 1 表示这是一个登录指令,接下来是用户名和密码,这两个值以 \0 分割,客户端发送这段二进制数据包到服务端,服务端就能根据这个协议来取出用户名密码,进行登录逻辑。
如何设计一个通信协议包?
-
首先,第一个字段是魔数,通常情况下为固定的几个字节(我们这边规定为4个字节)。 为什么需要这个字段,而且还是一个固定的数?假设我们在服务器上开了一个端口,比如 80 端口,如果没有这个魔数,任何数据包传递到服务器,服务器都会根据自定义协议来进行处理,包括不符合自定义协议规范的数据包。例如,我们直接通过 http://服务器ip 来访问服务器(默认为 80 端口), 服务端收到的是一个标准的 HTTP 协议数据包,但是它仍然会按照事先约定好的协议来处理 HTTP 协议,显然,这是会解析出错的。而有了这个魔数之后,服务端首先取出前面四个字节进行比对,能够在第一时间识别出这个数据包并非是遵循自定义协议的,也就是无效数据包,为了安全考虑可以直接关闭连接以节省资源。在 Java 的字节码的二进制文件中,开头的 4 个字节为0xcafebabe 用来标识这是个字节码文件,亦是异曲同工之妙。
-
接下来一个字节为版本号,通常情况下是预留字段,用于协议升级的时候用到,有点类似 TCP 协议中的一个字段标识是 IPV4 协议还是 IPV6 协议,大多数情况下,这个字段是用不到的,不过为了协议能够支持升级,我们还是先留着。
-
第三部分,序列化算法表示如何把 Java 对象转换二进制数据以及二进制数据如何转换回 Java 对象,比如 Java 自带的序列化,json,hessian 等序列化方式。
-
第四部分的字段表示指令,关于指令相关的介绍,我们在前面已经讨论过,服务端或者客户端每收到一种指令都会有相应的处理逻辑,这里,我们用一个字节来表示,最高支持256种指令,对于我们这个 IM 系统来说已经完全足够了。
-
接下来的字段为数据部分的长度,占四个字节。
-
最后一个部分为数据内容,每一种指令对应的数据是不一样的,比如登录的时候需要用户名密码,收消息的时候需要用户标识和具体消息内容等等。
二.代码实现
- 项目目录
- utils/command
/*
* @Author ARong
* @Description 定义指令集
* @Date 2020/2/4 8:58 下午
**/
public interface Command {
byte LOGIN = 1;
}
- utils/MySerializer
/*
* @Author ARong
* @Description 定义序列化器
* @Date 2020/2/4 9:06 下午
**/
public interface MySerializer {
/*
* @Author ARong
* @Description 将Java对象转化为二进制字节流
* @Date 2020/2/4 9:07 下午
* @Param object
* @return byte[]
**/
byte[] serialize(Object object);
/*
* @Author ARong
* @Description 二进制转换成 java 对象
* @Date 2020/2/4 9:08 下午
* @Param [clazz, bytes]
* @return T
**/
<T> T deserialize(Class<T> clazz, byte[] bytes);
}
- utils/Serialize
/*
* @Author ARong
* @Description 定义序列化方式
* @Date 2020/2/4 9:05 下午
**/
public interface Serialize {
byte JSON = 1;
}
- utils/SerializerFactory
/*
* @Author ARong
* @Description 序列化器工厂
* @Date 2020/2/4 8:56 下午
**/
public class SerializerFactory {
/*
* @Author ARong
* @Description 通过序列化名称获取相应的序列化器
* @Date 2020/2/4 9:17 下午
* @Param [serName]
* @return io_learn.netty.part4_protocol.utils.MySerializer
**/
public static MySerializer getSerializer(byte serName) {
if (serName == Serialize.JSON) {
return new MyJsonSerializer();
}
return null;
}
}
- utils/MyJsonSerializer
/**
* @Auther: ARong
* @Date: 2020/2/4 9:09 下午
* @Description:
*/
public class MyJsonSerializer implements MySerializer {
/*
* @Author ARong
* @Description 将Java对象转化为二进制字节流
* @Date 2020/2/4 9:07 下午
* @Param object
* @return byte[]
**/
@Override
public byte[] serialize(Object object) {
return JSON.toJSONBytes(object);
}
/*
* @Author ARong
* @Description 二进制转换成 java 对象
* @Date 2020/2/4 9:08 下午
* @Param [clazz, bytes]
* @return T
**/
@Override
public <T> T deserialize(Class<T> clazz, byte[] bytes) {
return JSON.parseObject(bytes, clazz);
}
}
- utils/PacketUtil
/**
* @Auther: ARong
* @Date: 2020/2/4 9:14 下午
* @Description: 用于包编码和解码
*/
public class PacketUtil {
/*
* @Author ARong
* @Description 将Packet按照约定序列化方式编码成ByteBuf
* @Date 2020/2/4 9:15 下午
* @Param [packet]
* @return io.netty.buffer.ByteBuf
**/
public static ByteBuf encode(Packet packet) {
// 创建ByteBuf对象
ByteBuf byteBuf = ByteBufAllocator.DEFAULT.ioBuffer();
// 获取序列化器序列化对象
MySerializer serializer = SerializerFactory.getSerializer(packet.getSerMethod());
byte[] data = serializer.serialize(packet);
// 按照通信协议填充ByteBUf
byteBuf.writeInt(packet.getMagic());// 魔数
byteBuf.writeByte(packet.getVersion()); // 版本号
byteBuf.writeByte(packet.getSerMethod()); // 序列化方式
byteBuf.writeByte(packet.getCommand()); // 指令
byteBuf.writeInt(data.length);// 数据长度
byteBuf.writeBytes(data); // 数据
return byteBuf;
}
/*
* @Author ARong
* @Description 将ByteBuf按照约定序列化方式解码成Packet
* @Date 2020/2/4 9:21 下午
* @Param [byteBuf]
* @return io_learn.netty.part4_protocol.packet.Packet
**/
public static Packet decode(ByteBuf byteBuf) {
// 暂不判断魔数,跳过
byteBuf.skipBytes(4);
// 暂不判断魔数版本号,跳过
byteBuf.skipBytes(1);
// 获取序列化方式
byte serMethod = byteBuf.readByte();
// 获取指令
byte command = byteBuf.readByte();
// 获取数据包长度
int length = byteBuf.readInt();
// 获取存储数据的字节数组
byte[] data = new byte[length];
byteBuf.readBytes(data);
// 反序列化数据,获取Packet
Class<? extends Packet> packetType = getPacketType(command);
Packet res = SerializerFactory.getSerializer(serMethod).deserialize(packetType, data);
return res;
}
/*
* @Author ARong
* @Description 通过指令获取相应的Packet
* @Date 2020/2/4 9:31 下午
* @Param [commond]
* @return io_learn.netty.part4_protocol.packet.Packet
**/
public static Class<? extends Packet> getPacketType(byte commond) {
if (commond == Command.LOGIN) {
return LoginPacket.class;
}
return null;
}
}
- packet/Packet
/**
* @Auther: ARong
* @Date: 2020/2/4 8:44 下午
* @Description: 自定义通信协议的抽象包
*/
@Data
public abstract class Packet {
private int magic; // 魔数
private byte version; // 版本号
private byte serMethod; // 的序列化/反序列化方式
private byte command; // 指令
}
- packet/LoginPacket
/**
* @Auther: ARong
* @Date: 2020/2/4 8:52 下午
* @Description: 用以登陆的登陆协议包
*/
@Data
public class LoginPacket extends Packet {
private int magic = 20202020; // 魔数
private byte version = 1; // 版本号
private byte serMethod = Serialize.JSON; // 序列化反序列化方式为Json
private byte command = Command.LOGIN; // 登陆指令
// ---------以下为登陆数据----------
private long userId;
private String name;
private String password;
@Override
public String toString() {
return "LoginPacket{" +
"magic=" + magic +
", version=" + version +
", serMethod=" + serMethod +
", command=" + command +
", userId=" + userId +
", name='" + name + '\'' +
", password='" + password + '\'' +
'}';
}
}
- server/NettyServer
/**
* @Auther: ARong
* @Date: 2020/2/4 9:13 下午
* @Description: Netty Server
*/
public class NettyServer {
public static void main(String[] args) {
ServerBootstrap serverBootstrap = new ServerBootstrap();
NioEventLoopGroup bossGroup = new NioEventLoopGroup();
NioEventLoopGroup workerGroup = new NioEventLoopGroup();
serverBootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(
new ChannelInitializer<SocketChannel>() {
@Override
// 初始化channel
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new MyServerHandler());
}
}
).bind(8000);
}
}
- server/NettyClient
/**
* @Auther: ARong
* @Date: 2020/2/5 1:28 下午
* @Description: NettyClient
*/
public class NettyClient {
public static void main(String[] args) throws InterruptedException {
Bootstrap bootstrap = new Bootstrap();
NioEventLoopGroup group = new NioEventLoopGroup();
bootstrap.group(group).channel(NioSocketChannel.class).handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
// 责任链模式,添加第一次连接的客户端处理逻辑
// ch.pipeline().addLast(new FirstClientHandler());
}
});
Channel channel = bootstrap.connect("127.0.0.1", 8000).channel();
LoginPacket packet = new LoginPacket();
packet.setUserId(123l);
packet.setName("ARong");
packet.setPassword("123");
ByteBuf byteBuf = PacketUtil.encode(packet);
System.out.println("客户端正在发送包");
channel.writeAndFlush(byteBuf);
}
}
- handler/MyServerHandler
/**
* @Auther: ARong
* @Date: 2020/2/5 1:17 下午
* @Description:
*/
public class MyServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("服务端接收到消息,正在解码");
Packet packet = PacketUtil.decode((ByteBuf) msg);
if (packet.getCommand() == Command.LOGIN) {
packet = (LoginPacket)packet;
}
System.out.println(packet);
}
}