实战Netty!基于私有协议,怎样快速开发网络通信服务
wptr33 2025-07-01 23:41 58 浏览
私有协议
编写目的
本文档用于描述边缘计算单元(以下简称边缘盒)与上位机配置软件(以下简称上位机)之间进行的数据交互通信协议。
通信方式
边缘盒作为服务端,上位机作为客户端,采用TCP/IP协议的socket连接,端口号默认为6000,数据包采用字节的二进制数据传输。
数据包
包头(10字节) | 负载(消息内容) |
数据包由包头和消息内容组成,包头固定10个字节,其内容如下:
标志(4) | 负载长度(2) | 协议版本号(1) | 包类型(1) | 校验位(1) | Reserve(1) |
标志: 包的前导字符,固定CYRC;
负载长度: 负载字节数(不包括包头的长度);
协议版本号: 标识通信协议的版本,初版值为0x10;
包类型: 标识数据包的操作类型,具体见下表:
取值 | 含义 | 说明 |
1 | 查询 | 上位机发送查询消息。 |
2 | 设置 | 上位机发送设置消息。 |
3 | 查询应答 | 边缘盒对查询请求的应答。 |
4 | 设置应答 | 边缘盒对设置请求的应答。 |
5 | 订阅 | 上位机发送给边缘盒订阅数据主动上报请求。 |
6 | 主动上报 | 边缘盒主动向上位机发送数据。 |
7 | 心跳 | 上位机发送心跳消息 |
8 | 心跳应答 | 边缘盒对心跳消息的应答 |
其他 | 保留 |
校验位: 负载数据所有字节之和;
Reserve: 预留,值填0;
包体负载(消息内容)表示具体的数据对象,其内容如下:
对象标识(1) | 对象数据内容(0…n) |
对于查询、心跳等包类型,包体负载(消息内容)只需要对象标识,对象数据内容省略。
对象标识: 标识数据表的操作对象,具体如下:
取值 | 含义 | 说明 |
0 | 心跳 | 上位机连接后间隔时间发送心跳消息给边缘盒。 |
具体的协议内容就不做展示了,下面就开始服务的编写。
服务开发
这里我们开发一个上位机的配置软件(客户端),我们首先要来分析,怎么对数据包进行编解码,其实工作中,这个也是服务开发的核心所在,也是难点所在。
编写消息类
public class MyProtocol
{
/**
* 消息的开头的信息标志
*/
private String head = "CYRC";
/**
* 消息的长度
*/
private int contentLength;
/**
* 消息的内容
*/
private byte[] content;
public MyProtocol(int contentLength, byte[] content)
{
this.contentLength = contentLength;
this.content = content;
}
public String getHead()
{
return head;
}
public void setHead(String head)
{
this.head = head;
}
public int getContentLength()
{
return contentLength;
}
public void setContentLength(int contentLength)
{
this.contentLength = contentLength;
}
public byte[] getContent()
{
return content;
}
public void setContent(byte[] content)
{
this.content = content;
}
public String byteToHex(byte[] bytes, int cnt)
{
String strHex;
StringBuilder sb = new StringBuilder();
for (int n = 0; n < cnt; n++)
{
strHex = Integer.toHexString(bytes[n] & 0xFF);
sb.append((strHex.length() == 1) ? "0" + strHex : strHex);
sb.append(" ");
}
return sb.toString().trim();
}
@Override
public String toString()
{
return "MyProtocol [head=" + head + ", contentLength="
+ contentLength + ", content=" + byteToHex(content, contentLength) + "]";
}
}
复制代码
MyDecoder解码器
@Slf4j
public class MyDecoder extends ByteToMessageDecoder
{
/**
* <pre>
* 协议开始的标准head_data,CYRC,占据4个字节.
* 表示数据的长度contentLength,占据2个字节.
* </pre>
*/
public final int BASE_LENGTH = 10;
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf buffer, List<Object> out) throws Exception
{
if (buffer.readableBytes() >= BASE_LENGTH)
{
if (buffer.readableBytes() > 2048)
{
buffer.skipBytes(buffer.readableBytes());
}
// 记录包头开始的index
int beginReader;
//CYRC 43 59 52 43
while (true)
{
// 获取包头开始的index
beginReader = buffer.readerIndex();
// 标记包头开始的index
buffer.markReaderIndex();
// 读到了协议的开始标志,结束while循环
int head1 = buffer.readUnsignedShort();
int head2 = buffer.readUnsignedShort();
if (head1 == 17241 && head2 == 21059)
{
break;
}
// 未读到包头,略过一个字节
// 每次略过,一个字节,去读取,包头信息的开始标记
buffer.resetReaderIndex();
buffer.readByte();
// 当略过,一个字节之后,数据包的长度,又变得不满足
// 此时,应该结束。等待后面的数据到达
if (buffer.readableBytes() < BASE_LENGTH)
{
return;
}
}
// 消息的长度
int length = buffer.readUnsignedShort() + 4;
// 判断请求数据包数据是否到齐
if (buffer.readableBytes() < length)
{
// 还原读指针
buffer.readerIndex(beginReader);
return;
}
// 读取data数据
byte[] data = new byte[length];
buffer.readBytes(data);
MyProtocol protocol = new MyProtocol(data.length, data);
out.add(protocol);
}
}
}
复制代码
MyEncoder编码器
public class MyEncoder extends MessageToByteEncoder<MyProtocol>
{
@Override
protected void encode(ChannelHandlerContext channelHandlerContext, MyProtocol myProtocol, ByteBuf out) throws Exception
{
// 1.写入消息的开头的信息标志(CYCR)
out.writeBytes(myProtocol.getHead().getBytes());
// 2.写入消息的长度(负载长度)
out.writeShort(myProtocol.getContentLength() - 4);
// 3.写入消息的内容(byte[]类型)
out.writeBytes(myProtocol.getContent());
}
}
复制代码
自定义ChannelInboundHandlerAdapter
@Slf4j
public class BootNettyClientChannelInboundHandlerAdapter extends ChannelInboundHandlerAdapter
{
public BootNettyClientChannelInboundHandlerAdapter()
{
}
/**
* 从服务端收到新的数据时,这个方法会在收到消息时被调用
*
* @param ctx
* @param msg
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception
{
MyProtocol protocol = (MyProtocol) msg;
log.info("接收到服务端的消息:" + protocol);
}
/**
* 从服务端收到新的数据、读取完成时调用
*
* @param ctx
*/
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws IOException
{
ctx.flush();
}
/**
* 当出现 Throwable 对象才会被调用,即当 Netty 由于 IO 错误或者处理器在处理事件时抛出的异常时
*
* @param ctx
* @param cause
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws InterruptedException
{
log.error("exceptionCaught:{}", cause.getMessage());
ctx.close();//抛出异常,断开与客户端的连接
}
/**
* 客户端与服务端第一次建立连接时 执行
*
* @param ctx
* @throws Exception
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception
{
super.channelActive(ctx);
InetSocketAddress insocket = (InetSocketAddress) ctx.channel().remoteAddress();
String clientIp = insocket.getAddress().getHostAddress();
log.info("channelActive------TCP客户端新建连接------clientIp:{}", clientIp);
}
/**
* 客户端与服务端 断连时 执行
*
* @param ctx
* @throws Exception
*/
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception
{
super.channelInactive(ctx);
InetSocketAddress insocket = (InetSocketAddress) ctx.channel().remoteAddress();
String clientIp = insocket.getAddress().getHostAddress();
ctx.close(); //断开连接时,必须关闭,否则造成资源浪费
log.info("channelInactive------TCP客户端断开连接----------clientIp:{}", clientIp);
}
}
复制代码
BootNettyClient客户端
@Slf4j
public class BootNettyClient
{
public void connect(String host,int port)
{
/**
* 客户端的NIO线程组
*
*/
EventLoopGroup group = new NioEventLoopGroup();
try
{
/**
* Bootstrap 是一个启动NIO服务的辅助启动类 客户端的
*/
Bootstrap bootstrap = new Bootstrap();
/**
* 设置group
*/
bootstrap = bootstrap.group(group);
/**
* 关联客户端通道
*/
bootstrap = bootstrap.channel(NioSocketChannel.class).option(ChannelOption.TCP_NODELAY, true);
/**
* 设置 I/O处理类,主要用于网络I/O事件,记录日志,编码、解码消息
*/
bootstrap = bootstrap.handler(new ChannelInitializer<SocketChannel>()
{
@Override
protected void initChannel(SocketChannel channel) throws Exception
{
ChannelPipeline pipeline = channel.pipeline();
// 添加自定义协议的编解码工具
pipeline.addLast(new MyDecoder());
pipeline.addLast(new MyEncoder());
/**
* 自定义ChannelInboundHandlerAdapter
*/
pipeline.addLast(new BootNettyClientChannelInboundHandlerAdapter());
}
});
/**
* 连接服务端
*/
ChannelFuture f = bootstrap.connect(host, port).sync();
log.info("TCP客户端连接成功, 地址是: " + host + ":" + port);
/**
* 等待连接端口关闭
*/
f.channel().closeFuture().sync();
}
catch (Exception e)
{
log.error("启动netty client失败:", e);
}
finally
{
/**
* 退出,释放资源
*/
group.shutdownGracefully();
}
}
}
复制代码
NettyClientApplication程序启动类
@SpringBootApplication
public class NettyClientApplication implements CommandLineRunner {
public static void main(String[] args) {
SpringApplication.run(NettyClientApplication.class, args);
}
@Override
public void run(String... args) throws Exception {
new BootNettyClient().connect("172.16.1.100", 6000);
}
}
复制代码
测试
利用网络调试助手工具,开启一个服务端,模拟发送数据
发送一个完整的包(43 59 52 43 00 01 10 02 00 00 0f),如下图,客户端完整接收数据。
半包测试数据(43 59 52 43 00 01 10 02 00),无日志打印,说明客户端没有接收该不完整数据。
粘包数据测试,两个包一起发送(43 59 52 43 00 01 10 02 00 00 0f 43 59 52 43 00 01 10 02 00 00 0f),如下图,客户端同时接收到两条数据。
粘包数据测试,一个半包发送(43 59 52 43 00 01 10 02 00 00 0f 43 59 52 43 00 01 10 02),如下图,可以看出,只接收到前面完整包的数据,后面的半包数据被忽略。
业务代码编写
业务代码,无非就是将收到的数据进行一些逻辑处理,数据的解析。编写一个接收消息处理类即可。示例如下
通信参数对象:
序号 | 名称 | 字节数 | 取值范围 | 备注 |
1 | 对象标识 | 1 | 3 | 对象标识号:3 |
2 | IP地址 | 4 | 每个字节表示一段地址值(A.B.C.D,第一字节对应A,依次类推) | |
3 | 端口 | 2 | ||
4 | 标志 | 1 | [0-1] | 0:网络通信,1: 485通信(端口赋波特率,IP赋值0) |
@Slf4j
public class ClientService {
/** 接收边缘盒子消息 */
public void receiveData(MyProtocol myProtocol) {
try {
byte[] data = myProtocol.getContent();
int type = data[1];
int objId = data[4];
// 心跳应答
if (type == PackageTypeConstant.HEART_BEAT_REPLY) {
log.info("--------收到心跳回复----------");
}
// 查询应答
else if (type == PackageTypeConstant.QUERY_RESULT) {
switch (objId) {
case ObjectIdConstant.SIGNAL:
{
reSignalParameter(data); // 接收通信参数
break;
}
//.....
default:
{
break;
}
}
}
} catch (Exception e) {
log.error("错误的消息指令..", e);
}
}
private void reSignalParameter(byte[] data) {
EventCenterService.getInstance()
.submitEvent(
new IEvent() {
@Override
public void execute() {
try {
SignalParameter sp = new SignalParameter();
int idx = 5;
byte temp1;
byte temp2;
int a = (data[idx++] & 0xFF);
int b = (data[idx++] & 0xFF);
int c = (data[idx++] & 0xFF);
int d = (data[idx++] & 0xFF);
String ip = a + "." + b + "." + c + "." + d;
temp1 = data[idx++];
temp2 = data[idx++];
// 端口号
int port = ((char) (temp1 & 0xFF) << 8) | (char) (temp2 & 0xFF);
int sign = data[idx];
sp.setIp(ip);
sp.setPort(port);
sp.setSign(sign);
DataConfig.signalParameterList.add(sp);
} catch (Exception e) {
log.error("通信参数解析出错:", e);
}
}
});
}
}
复制代码
后记
工作中,利用netty开发网络通信服务,数据的编解码处理好了,后面的业务代码相对就很容易了。
本篇文章,是我在工作中的一些实战经验,希望对netty感兴趣的小伙伴有点帮助。关于netty的原理这篇文章就不做过多介绍了,前面的文章也讲了很多,后面时间主要讲讲netty实际的运用。
相关推荐
- 什么是Java中的继承?如何实现继承?
-
什么是继承?...
- Java 继承与多态:从基础到实战的深度解析
-
在面向对象编程(OOP)的三大支柱中,继承与多态是构建灵活、可复用代码的核心。无论是日常开发还是框架设计,这两个概念都扮演着至关重要的角色。本文将从基础概念出发,结合实例与图解,带你彻底搞懂Java...
- Java基础教程:Java继承概述_java的继承
-
继承概述假如我们要定义如下类:学生类,老师类和工人类,分析如下。学生类属性:姓名,年龄行为:吃饭,睡觉老师类属性:姓名,年龄,薪水行为:吃饭,睡觉,教书班主任属性:姓名,年龄,薪水行为:吃饭,睡觉,管...
- java4个技巧:从继承和覆盖,到最终的类和方法
-
日复一日,我们编写的大多数Java只使用了该语言全套功能的一小部分。我们实例化的每个流以及我们在实例变量前面加上的每个@Autowired注解都足以完成我们的大部分目标。然而,有些时候,我们必须求助于...
- java:举例说明继承的概念_java继承的理解
-
在现实生活中,继承一般指的是子女继承父辈的财产。在程序中,继承描述的是事物之间的所属关系,通过继承可以使多种事物之间形成一种关系体系。例如猫和狗都属于动物,程序中便可以描述为猫和狗继承自动物,同理,...
- 从零开始构建一款开源的 Vibe Coding 产品 Week1Day4:业界调研之 Agent 横向对比
-
前情回顾前面两天我们重点调研了了一下Cursor的原理和Cursor中一个关键的工具edit_file的实现,但是其他CodingAgent也需要稍微摸一下底,看看有没有优秀之处,下...
- 学会这几个插件,让你的Notepad++使用起来更丝滑
-
搞程序开发的小伙伴相信对Notepad++都不会陌生,是一个占用空间少、打开启动快的文件编辑器,很多程序员喜欢使用Notepad++进行纯文本编辑或者脚本开发,但是Notepad++的功能绝不止于此,...
- 将 node_modules 目录放入 Git 仓库的优点
-
推荐一篇文章Whyyoushouldcheck-inyournodedependencies[1]...
- 再度加码AI编程,腾讯发布AI CLI并宣布CodeBuddy IDE开启公测
-
“再熬一年,90%的程序员可能再也用不着写for循环。”凌晨两点半,王工还在公司敲键盘。他手里那份需求文档写了足足六页,产品经理反复改了三次。放在过去,光数据库建表、接口对接、单元测试就得写两三天。现...
- git 如何查看stash的内容_git查看ssh key
-
1.查看Stash列表首先,使用gitstashlist查看所有已保存的stash:...
- 6万星+ Git命令懒人必备!lazygit 终端UI神器,效率翻倍超顺手!
-
项目概览lazygit是一个基于终端的Git命令可视化工具,通过简易的TUI(文本用户界面)提升Git操作效率。开发者无需记忆复杂命令,即可完成分支管理、提交、合并等操作。...
- 《Gemini CLI 实战系列》(一)Gemini CLI 入门:AI 上命令行的第一步
-
谷歌的Gemini模型最近热度很高,而它的...
- deepin IDE新版发布:支持玲珑构建、增强AI智能化
-
IT之家8月7日消息,深度操作系统官方公众号昨日(8月6日)发布博文,更新推出新版deepin集成开发环境(IDE),重点支持玲珑构建。支持玲珑构建deepinIDE在本次重磅更...
- 狂揽82.7k的star,这款开源可视化神器,轻松创建流程图和图表
-
再不用Mermaid,你的技术文档可能已经在悄悄“腐烂”——图表版本对不上、同事改完没同步、评审会上被一句“这图哪来的”问得哑口无言。这不是危言耸听。GitHub2025年开发者报告显示,63%的新仓...
- 《Gemini CLI 实战系列》(五)打造专属命令行工具箱
-
在前几篇文章中,我们介绍了GeminiCLI的基础用法、效率提升、文件处理和与外部工具结合。今天我们进入第五篇...
- 一周热门
-
-
C# 13 和 .NET 9 全知道 :13 使用 ASP.NET Core 构建网站 (1)
-
程序员的开源月刊《HelloGitHub》第 71 期
-
详细介绍一下Redis的Watch机制,可以利用Watch机制来做什么?
-
如何将AI助手接入微信(打开ai手机助手)
-
SparkSQL——DataFrame的创建与使用
-
假如有100W个用户抢一张票,除了负载均衡办法,怎么支持高并发?
-
Java面试必考问题:什么是乐观锁与悲观锁
-
redission YYDS spring boot redission 使用
-
如何利用Redis进行事务处理呢? 如何利用redis进行事务处理呢英文
-
一文带你了解Redis与Memcached? redis与memcached的区别
-
- 最近发表
-
- 什么是Java中的继承?如何实现继承?
- Java 继承与多态:从基础到实战的深度解析
- Java基础教程:Java继承概述_java的继承
- java4个技巧:从继承和覆盖,到最终的类和方法
- java:举例说明继承的概念_java继承的理解
- 从零开始构建一款开源的 Vibe Coding 产品 Week1Day4:业界调研之 Agent 横向对比
- 学会这几个插件,让你的Notepad++使用起来更丝滑
- 将 node_modules 目录放入 Git 仓库的优点
- 再度加码AI编程,腾讯发布AI CLI并宣布CodeBuddy IDE开启公测
- git 如何查看stash的内容_git查看ssh key
- 标签列表
-
- git pull (33)
- git fetch (35)
- mysql insert (35)
- mysql distinct (37)
- concat_ws (36)
- java continue (36)
- jenkins官网 (37)
- mysql 子查询 (37)
- python元组 (33)
- mybatis 分页 (35)
- vba split (37)
- redis watch (34)
- python list sort (37)
- nvarchar2 (34)
- mysql not null (36)
- hmset (35)
- python telnet (35)
- python readlines() 方法 (36)
- munmap (35)
- docker network create (35)
- redis 集合 (37)
- python sftp (37)
- setpriority (34)
- c语言 switch (34)
- git commit (34)