什么是通信协议

我们常用的聊天软件比如:微信,都是基于一组通信协议进行服务端与客户端数据交互。协议指的就是客户端与服务端事先约定好的,每个二进制数据包中,每一段字节分别代表什么含义的规则。如下图所示一个简单的登录指令:

在这个数据包中,第一个字节为 1 表示这是一个登录指令,接下来是用户名和密码,这两个值以 \0 分割,客户端发送这段二进制数据包到服务端,服务端就能根据这个协议来取出用户名密码,进行登录逻辑。实际的通信协议设计中,我们会考虑更多细节,要比这个稍微复杂一些。
那么,协议设计好之后,客户端与服务端的通信过程又是怎样的呢?

通信协议的设计

  1. 第一个字段为魔数(magic_number),通常情况下都是几个字节(这里我们定义 4 个字节),魔数的作用类似于协议内的标识,通过客户端与服务端魔数对比,我们就知道这组二进制数据是否属于当前通信协议。这和 Java 字节码 中的魔数 0xCAFEBABE 用来标识这个文件,有着异曲同工之妙。
  2. 第二个字段为版本号,占用 1 个字节,通常情况下是预留字段。
  3. 第三部分是序列化算法,占用 1 个字节,表示如何把 Java 对象转换成二进制,二进制转换成 java 对象,比如 Java 自带的序列化,json,hessian 等序列化方式。
  4. 第四部分是指令,占用 1 个字节,服务端或者客户端每收到一种指令都会有相应的处理逻辑,最高支持256种指令,对于我们这个通信系统来说已经完全足够了。
  5. 第五部分是数据长度,占用 4 个字节。
  6. 最后一部分则是数据内容,每一个指令对应的数据是不一样的,比如登录的时候需要用户名密码,收消息的时候需要用户标识和具体消息内容等等。

通常情况下,这样一套标准的协议能够适配大多数情况下的服务端与客户端的通信场景,接下来我们就来看一下我们如何使用 Netty 来实现这套协议。

通信协议的实现

我们把Java 对象封装成二进制的过程叫编码, 从二进制数据包解析成 Java 对象的过程叫解码,在学习如何使用 Netty 进行通信协议的编解码之前,我们先来定义一下客户端与服务端通信的 Java 对象。

Java 对象

public abstract class Packet {/*** 协议版本*/private Byte version = 1;/*** 指令* /public abstract Byte getCommand();
}

接下来,我们拿客户端登录请求为例,定义登录请求数据包

public interface Command {Byte LOGIN_REQUEST = 1;
}
public class LoginRequestPacket extends Packet {private Integer userId;private String username;private String password;@Overridepublic Byte getCommand() { return LOGIN_REQUEST;}
}

Command 定义一些指令,不同的 command 指令分别对应不同的数据内容。
这里 Command 定义了 LOGIN_REQUEST 指令表示登录请求,相应的就会有与之对应的 Java 对象 LoginRequestPacket。

Java 对象定义完成之后,接下来我们就需要定义一种规则,如何把一个 Java 对象转换成二进制数据,这个规则叫做 Java 对象的序列化。

序列化

定义序列化接口:

public interface Serializer {/*** 序列化算法*/byte getSerializerAlgorithm(); // (1)/*** java 对象转换成二进制*/byte[] serialize(Object object); // (2)/*** 二进制转换成 java 对象*/<T> T deserialize(Class<T> clazz, byte[] bytes); // (3)
}
  1. getSerializerAlgorithm() 获取具体的序列化算法标识。
  2. serialize() 将 Java 对象转换成字节数组。
  3. deserialize() 将字节数组转换成某种类型的 Java 对象。

序列化接口的实现,这里我们使用最简单的 json 序列化方式,使用阿里巴巴的 fastjson 作为序列化框架:

public interface SerializerAlgorithm {/*** json 序列化标识*/byte JSON = 1;
}public class JSONSerializer implements Serializer {@Overridepublic byte getSerializerAlgorithm() {return SerializerAlgorithm.JSON;} @Overridepublic byte[] serialize(Object object) {return JSON.toJSONBytes(object);}@Overridepublic <T> T deserialize(Class<T> clazz, byte[] bytes) {return JSON.parseObject(bytes, clazz);}
}

这样我们就实现了序列化的相关逻辑。

序列化定义了 Java 对象与二进制数据的互转过程,接下来,我们就来学习一下,如何把这部分的数据编码到通信协议的二进制数据包中去。

编码

private static final int MAGIC_NUMBER = 0x12345678;public ByteBuf encode(ByteBufAllocator byteBufAllocator, Packet packet){// 创建ByteBuf对象ByteBuf byteBuf = byteBufAllocator.ioBuffer(); // (1)// 序列化java对象byte[] bytes = Serializer.DEFAULT.serialize(packet); // (2)// 实际编码过程 (3)// 魔数byteBuf.writeInt(MAGIC_NUMBER);// 版本号byteBuf.writeByte(packet.getVersion());// 序列化算法byteBuf.writeByte(Serializer.DEFAULT.getSerializerAlgorithm());// 指令操作byteBuf.writeByte(packet.getCommand());// 数据长度byteBuf.writeInt(bytes.length);// 数据内容byteBuf.writeBytes(bytes);return byteBuf;
}

分为三个步骤:

  1. 首先,我们需要创建一个 ByteBuf,这里我们调用 Netty 的 ByteBuf 分配器来创建,ioBuffer() 方法会返回适配 io 读写相关的内存,它会尽可能创建一个直接内存,直接内存可以理解为不受 jvm 堆管理的内存空间,写到 IO 缓冲区的效果更高。
  2. 将 Java 对象序列化成二进制数据包
  3. 编码过程

编码完成之后会发送给另一端,另一端就需要进行解码操作。

解码

public Packet decode(ByteBuf byteBuf) {// 跳过 magic numberbyteBuf.skipBytes(4);// 跳过版本号byteBuf.skipBytes(1);// 序列化算法标识byte serializeAlgorithm = byteBuf.readByte();// 指令byte command = byteBuf.readByte();// 数据包长度int length = byteBuf.readInt();byte[] bytes = new byte[length];byteBuf.readBytes(bytes);// 根据指令获取数据内容Class<? extends Packet> requestType = getRequestType(command);// 根据序列化算法标识,找到对应的序列化方式Serializer serializer = getSerializer(serializeAlgorithm);if (requestType != null && serializer != null) {// 解码return serializer.deserialize(requestType, bytes);}return null;
}

解码过程刚刚好和编码过程相反。

总结

我们了解了什么是通信协议,并且自己动手实现一个简单的通信协议,还了解到了编码和解码的过程。
文章参考:https://juejin.im/book/5b4bc28bf265da0f60130116
代码下载:https://github.com/jeansTuo/-

自己动手写一个通信协议相关推荐

  1. java 手编线程池_死磕 java线程系列之自己动手写一个线程池

    欢迎关注我的公众号"彤哥读源码",查看更多源码系列文章, 与彤哥一起畅游源码的海洋. (手机横屏看源码更方便) 问题 (1)自己动手写一个线程池需要考虑哪些因素? (2)自己动手写 ...

  2. 自己动手写一个印钞机 第四章

    2019独角兽企业重金招聘Python工程师标准>>> 作者:阿布? 未经本人允许禁止转载 ipython notebook git版本 目录章节地址: 自己动手写一个印钞机 第一章 ...

  3. Spring Boot 动手写一个 Start

    我们在使用SpringBoot 项目时,引入一个springboot start依赖,只需要很少的代码,或者不用任何代码就能直接使用默认配置,再也不用那些繁琐的配置了,感觉特别神奇.我们自己也动手写一 ...

  4. 自己动手写一个nodejs的日志生成器

    自己动手写一个nodejs的logger 最近正在边学边用node.js开发个人应用的server,由于有用到websocket相关,想对websocket的通信选择性的做下日志记录,所以萌发了自己动 ...

  5. 自己动手写一个印钞机 第二章

    2019独角兽企业重金招聘Python工程师标准>>> 作者:阿布? 未经本人允许禁止转载 ipython notebook git版本 目录章节地址: 自己动手写一个印钞机 第一章 ...

  6. 学习较底层编程:动手写一个C语言编译器

    动手编写一个编译器,学习一下较为底层的编程方式,是一种学习计算机到底是如何工作的非常有效方法. 编译器通常被看作是十分复杂的工程.事实上,编写一个产品级的编译器也确实是一个庞大的任务.但是写一个小巧可 ...

  7. 自己动手写一个 strace

    这次主要分享一下一个动手的东西,就是自己动手写一个 strace 工具. 用过 strace 的同学都知道,strace 是用来跟踪进程调用的 系统调用,还可以统计进程对 系统调用 的统计等.stra ...

  8. java 同步锁_死磕 java同步系列之自己动手写一个锁Lock

    问题 (1)自己动手写一个锁需要哪些知识? (2)自己动手写一个锁到底有多简单? (3)自己能不能写出来一个完美的锁? 简介 本篇文章的目标一是自己动手写一个锁,这个锁的功能很简单,能进行正常的加锁. ...

  9. 吕文翰 php,自己动手写一个 iOS 网络请求库(三)——降低耦合

    自己动手写一个 iOS 网络请求库(三)--降低耦合 2015-5-22 / 阅读数:16112 / 分类: iOS & Swift 本文中,我们将一起降低之前代码的耦合度,并使用适配器模式实 ...

最新文章

  1. php如何实现根据地区内筛选,PHP区块查询实现方法分析
  2. 【报告】一手资料:四线城市移动互联网用户调研
  3. [CareerCup] 1.6 Rotate Image 翻转图像
  4. 关于Services.exe开机CPU内存使用暴增解决方案
  5. mysql查询会话池_用户会话,数据控件和AM池
  6. jQuery对checkbox的操作(转载)
  7. QQ自定义在线机型状态源码
  8. Linux—shell脚本化工具模板
  9. 解决浏览器跨域加载本地文件报错 Access to script at ‘xxx‘ from origin ‘null‘ has been blocked by CORS policy
  10. QTP学习笔记----2013.04.25
  11. 拓端tecdat|R语言社区发现算法检测心理学复杂网络:spinglass、探索性图分析walktrap算法与可视化
  12. php与mysql对接_PHP与MySql建立连接
  13. Java中static的用法,初始化块
  14. 华为NP课程笔记4-中间系统到中间系统
  15. Linux开发板启动需要登录密码设置方法
  16. keepalived的主备模式下,优先访问了从机的资源,原因尚未知
  17. 性能测试,你需要了解这款工具
  18. 系统服务器选型依据,1.2 服务器选型原则
  19. 聊聊阅读源码那些事儿
  20. 未来已来,看北京理工大学的智慧校园如何落地

热门文章

  1. 等价关系偏序关系全序关系
  2. locals()获取本地变量 和 exec()执行字符串中的代码
  3. Android 11.0 根据包名授予读取IMEI权限
  4. Linux软件--有道词典与Openyoudao的安装
  5. 服务器csr信息是什么,服务器生成csr文件
  6. Bootstrap(六)表单样式
  7. nagios网页监控check_http
  8. 大过年睡不着,聊聊云开发入门
  9. 计算机可用网络连接不上,电脑网络连接不可用?学会这招,轻松解决
  10. 睡觉的诀窍:睡五分钟等于六钟头?