基于TCP的简单Netty自定义协议实现(万字,全篇例子)
前言
有一阵子没写博客了,最近在学习
Netty
写一个实时聊天软件,一个高性能异步事件驱动的网络应用框架
,我们常用的SpringBoot
一般基于Http
协议,而Netty
是没有十分明确的协议的,不过它内置了一些常用的通信协议,当然你也可以自定义协议。
一、要求
接下来的内容默认你已经有了最基本的
Java
、Netty
、Nio
知识,如果还没有这方面的知识的话,可以先去小破站找个视频学习学习。
二、通信协议
*
本文提到的通信协议都是指基于TCP的应用层通信协议
,请勿理解错误。
1、协议基本单位
当数据在两台计算机上传输时,传输的数据以
比特(Bit)
为单位,就像01010100010010101...
这种,但是以比特作为传输单位太过精细、太过底层,所以封装一下它,将8
个bit
封装成一个单位,就成了字节(Byte)
,所以一个协议的基本单位是字节Byte
。同样的,因为字节是其他大多数高级数据类型的基本组成,所以通信协议的基本单位是字节。例如一串字节流可以被解析为视频、图片、字符串等等,它是通用的。
也就是说,我们要自定义一个通信协议,就必须得自己解析字节。在
SpringBoot
框架中,我们在Controller
中能够直接得到字符串、对象的原因是框架已经帮我们将字节解析好了,我们直接用就行,但是如果我们要自定义协议,就必须自力更生,自己定义格式并解析它。
2、协议格式
协议的格式不是固定的,协议只能是一个约定而不是强制要求。
举个例子,假如你在晚自习上睡觉,你提前和同桌约定好,老师来了他就敲两下桌子,班长来了他就敲三下桌子,那么这种约定就可以认定为是一个通信协议,但其并不是固定的,因为明晚、后晚…你可以约定其他方式,例如敲一下变成老师来了,敲两下变成班长来了,踢你一下表示老师来了,踢你两下表示班长来了。并不是固定的。
基于这种思想,我们可以定义一个简单的通信协议,版本号为
V1
:
请求地址 客户端IP 请求正文
基于这个协议,假如我们有一个请求,它请求服务器的
/test
地址,客户端IP是192.168.1.2
,请求正文是hello
,那么这个协议看起来就像:
/test192.168.1.2hello
将它转为字节流就是(没有空格,空格只是为了方便查看加的):
47 116 101 115 116 49 57 50 46 49 54 56 46 49 46 50 104 101 108 108 111
服务器在解析时,就可以解析
[0,5]
个字符串为请求地址[/test]
,解析[6,11]
个字符串为客户端IP[192.168.1.2]
,解析剩下的所有字符串为请求正文。
当然,为了形象一点举了一个不太恰当的简单例子,解析的不是字符串而是字节。
3、TCP的粘包半包
Ⅰ、问题描述
这个问题可能我一时半会解释不清楚,导致粘包半包的原因很多,感兴趣的可以去找找资料。
你只用知道,基于TCP时,数据并不是一次性达到的,而是分段到达的,例如我们上面举的例子,那个协议数据:
/test192.168.1.2hello
,服务器在接收这些数据时它就有可能:
第一次收到:/test19
第二次收到:2.168.
第三次收到:1.2hello
...
它可能不会一次收全,可能要好几次,所以我们上面定义的简单的协议就有一个问题:
它没有消息边界
,就是当客户端多次发送数据时,服务器无法知道哪些数据是哪次请求的。还是刚才的例子:
第一次收到:/test192.168.1.2he
第二次收到:llo/haha192.168.1.2hi
在这两次数据中,客户端分别发送了两次请求:
/test192.168.1.2hello
和/haha192.168.1.2hi
,但是因为粘包半包的问题,服务器不知道哪条是哪条了,就会导致解析出错。
Ⅱ、如何解决
解决这个问题有很多种方法,常见的方法有分隔符、标识请求长度等等。两种方法我都举个例子,你也可以自己想一个方法来解决,都是灵活的,解决方法不是固定的。
分隔符
的方法也很简单:我们在每次请求结束时,都添加一个特殊符号,用于标识这个请求结束了,服务器在解析时,遇到这个特殊符号,就知道这个请求结束了,后面的数据是新请求的了。例如我们以$
为分隔符,服务器:
第一次收到:/test192.168.1.2
第二次收到:hello$/haha192.168.1.2hi
服务器在解析到
$
符时,就知道/test
请求已经结束了,后面的数据是属于/haha
请求的了。但是这么做的话,有一个缺点,就是之后传输的正文数据中不能含有$
符,不然解析依旧出错,你也可以定义复杂一点的符号,例如几个符号拼接也行:@$&...
。不过我要说的是,其实你还可以用标识请求长度的方式解决。
标识请求长度
就是客户端在传输请求之前,先计算好整个请求有多少个字符(为了不复杂先说成字符吧,其实是字节
),再传输数据,服务器在接收到数据后,会去读取这个字段,查看整个请求有多少个字符,然后再根据这个数字读取多少个字符。那这就需要一个字段用来专门存储长度了。
基于这个需求,我们上面定义的协议就得小小的升级一下,变成
V2
:
请求长度 请求地址 客户端IP 请求正文
以后,服务器会先读取开头的长度,再根据长度读取后面的数据,例如我们还是刚才的
/test
请求,那么它将会变成:
21/test192.168.1.2hello
因为
/test192.168.1.2hello
总共是21
个字符,所以一开始就变为了21
,服务器一读取到开头的数字21
,就往下读取21
个字符,读完后,就默认这个请求已经结束了,再往下的就是其他请求了。
当然,你也可以将长度字段包含在内,那就是:
23/test192.168.1.2hello
这个长度可以出现在整个请求体的任何地方(除了正文),只要你在服务器/客户端解析的时候对应解析就行了。
暂时就介绍这个两个简单的方法,其他的方法你可以自己想,想出来了可以自己实现,原则是能解决问题就是好办法。
三、创建协议
1、改正上面的说法
在上面的各个例子中,我为了例子不复杂说的是解析
字符
,其实解析的是字节(Byte)
。
字符是字符,字节是字节,它们不一样,
你
是一个字符,你好
是一个字符串,而-28(十进制)
它是一个字节,-28
、-67
、-96
它们三个字节组成了一个字符你
。
***
在UTF8
编码下,常见的中文字符一般由3
个字节组成,不常见的一般是4
个字节组成。
***
在UTF8
编码下,英文字符一般由1
个字节组成。
***
数字的情况稍微复杂:
1、8
位的数字一般占用1
字节,范围从-128 到 127
2、16
位的数字一般占2
字节,范围从-32,768 到 32,767
3、32
位数字一般占4
字节,范围从-2,147,483,648 到 2,147,483,647
4、64
位数字一般占8
字节,范围从-9,223,372,036,854,775,808 到 9,223,372,036,854,775,807
5、128
位数字一般占16
字节,范围很大,不写了。
例如在Rust
中,i32
占4
字节,它对应的Java
数字类型是int
,i64
占8
字节,对应的Java
类型是long
,以此类推。
JavaScript
的number
类型是64
位的,占8
字节,所以js
要想表达64
位以下的就有点麻烦了。
2、SP协议
解释了上面的错误后,可以开始正式自定义协议了,给这个协议取个名字,就叫
SP协议
吧,Simple Protocol
,译为简单的协议。
Ⅰ、报文长度
首先,粘包半包的问题用长度字段解决,
4
个字节表示的32
位数字就够用了,它的范围是-2,147,483,648 到 2,147,483,647
,负的20亿
到正的20亿
,用来表示数据的话(不算负数):2147483648 / (1024 * 1024) = 2048 MB
,也就是说32
位数字所表示的数字范围(正数)用来表示数据大小的话,可以表示2GB
的数据,一个请求根本不可能达到这么大,所以32
位的数字够用。因为2147483648个字节就是2GB
。
那么协议开头就是:
长度4字节
Ⅱ、魔数
在协议中添加一个魔数,用来标识这个报文是属于
SP协议
的,服务器在网络中读取字节流时,如果在长度字节后没有找到这个魔数,就证明该字节流不是SP协议
的,就可以停止读取接下来的数据了,可以做关闭连接、丢弃数据等操作,就好像,你去坐火车去北京,火车进站时你看第二节车厢上有没有写目的地北京,如果写了,那么就是你要坐的火车,如果没写,那就证明不是你要坐的火车,你可以等下一趟。其实就是为整个协议打一个标记。
魔数用几个字节都行,为了不重复,建议使用
4
字节的32
位数字,那么协议的第二部分应该是:
长度4字节 魔数4字节
Ⅲ、客户端身份
在多个客户端连接时,服务器需要为每个客户端颁发一个标识,用来区分不同的客户端的请求,用几个字节都行,为了不重复,建议使用
32
字节的uuid
作为客户端唯一标识。
那么协议第三部分是:
长度4字节 魔数4字节 客户端标识32位
Ⅳ、请求路径
请求路径这块比较灵活,你可以使用
1
字节的8
位数字表示,也就是-128 到 127
个数字。例如,你可以规定1
就是登录,2
就是注册等等。
我使用的是英文字符串的方式,也就是一个字符一个字节,但是路径长度不是不变的,它会变化。例如
/test
是5
个字节,但是/hi
是3
个字节,不能像刚才一样用固定的长度来标识,那么就需要一个固定的路径长度字段,用来表示后续路径的长度。
于是协议的第四部分就是:
长度4字节 魔数4字节 客户端标识32位 路径长度4字节 路径N字节
Ⅴ、请求正文
到这步后这个简单的协议就基本完成了,后续的正文长度是不定的,但是我们有开头的长度字段表示整个报文的长度,所以这个协议第五部分就是:
长度4字节 魔数4字节 客户端标识32位 路径长度4字节 路径N字节 正文N字节
3、完整协议
协议定义到这后基本完成了,但是这只是一个简单的例子,实际应用中肯定要复杂许多。
基于该协议,模拟一个请求,它请求
/test
路径,使用Java字节码文件同款的魔数0xCAFEBABE
,请求正文是hello
,那么这个协议组装完成应该是这样的:
| 54 | 0xCAFEBABE | 32位的UUID | 5 | /test | hello |
解释一下,首先魔数占了
4
字节,UUID占了32
字节,路径长度占了4
字节,路径占了5
字节,正文占了5
字节,报文长度字段不计算在内,所以总长度是:4
+32
+4
+5
+5
=54
字节,这就是开头54
的由来。
路径
/test
前的5
就是表示/test
所占的5
字节。
至此,协议定义完成,任何只要遵守了这个协议的请求都能够被
Netty
服务器识别。
四、服务器代码实现
协议定义好了,该写服务器代码实现这个协议了。
1、Netty服务器启动流程
首先得先来复习一下
Netty
的启动流程,我们才知道如何实现这个协议。
快速启动一个
Netty
服务器代码:
public static void main(String[] args) {NioEventLoopGroup boss = new NioEventLoopGroup(1);// 处理连接NioEventLoopGroup worker = new NioEventLoopGroup();// 处理业务try {ChannelFuture channelFuture = new ServerBootstrap().group(boss, worker) // 设置线程组.channel(NioServerSocketChannel.class) // 使用NIO通信模式.childHandler(new ChannelInitializer<SocketChannel>() {@Overrideprotected void initChannel(SocketChannel socketChannel) throws Exception {// 在这里添加自定义的处理器}}).bind(8080).sync();// 绑定端口并启动服务器System.out.println("Netty Server is starting...");channelFuture.channel().closeFuture().sync();// 监听关闭} catch (InterruptedException e) {throw new RuntimeException(e);}finally {// 优雅的关闭线程组boss.shutdownGracefully();worker.shutdownGracefully();}
}
要想自定义一个协议,我们的重点在
initChannel()
方法上,它可以为Netty
添加处理器,在TCP
收到的数据传过来的时候,处理原始的字节流数据
2、添加自定义处理器
Ⅰ、解释ChannelInitializer的作用
为了启动看起来清爽,我们可以将
childHandler()
所需的参数抽取出来:
public class CustomHandler extends ChannelInitializer<SocketChannel> {@Overrideprotected void initChannel(SocketChannel socketChannel) throws Exception {}
}
在
childHandler()
中传递.childHandler(new CustomHandler())
原始的字节流数据在达到
Netty
的时候,Netty
内部会在我们自定义的处理器之前先做一些处理,比如说将字节流数据封装成ByteBuf
对象等等,就像SprinigBoot
我们添加自定义拦截器一样,在我们添加的拦截器之前,SpringBoot
就已经添加了许多内部的拦截器先一步处理过数据了。
也就是说,我们自定义处理器接收到的数据,其实是经过
ByteBuf
封装过的字节流缓冲对象,ByteBuf
对象其实就是对Java.Nio
中ByteBuffer
的进一步封装升级。
画个简陋的图,自定义处理器处理数据的整个流程看起来像这样:
我们刚刚自定义的处理器初始化器就是这部分:
它的作用就是往处理器链中添加一个个的自定义处理器,在
ChannelInitializer
中添加处理器也很简单,继承ChannelInitializer
并实现它的initChannel
方法,再通过initChannel
的形参SocketChannel
获取到ChannelPipeline
就可以添加了,代码像这样:
@Override
protected void initChannel(SocketChannel channel) throws Exception {ChannelPipeline pipeline = channel.pipeline();pipeline.addLast(处理器对象);// 添加一个个的处理器pipeline.addLast(处理器对象);// 添加一个个的处理器...
}
Ⅱ、出站(Outbound)和入站(Inbound)
我没打错字,是
出站
和入站
,不是出栈
、入栈
,说白了其实就是数据进入Netty
和数据从Netty
发出,进入Netty
的行为叫入站
,Netty
往外发送数据的行为叫出站
。
所以处理器可以分为三种:
入站处理器
、出站处理器
、入站出站处理器
,入站处理器
专门处理进入Netty
的数据,出站处理器
专门处理从Netty
发送的数据,而入站出站处理器
则两者都可以。
这些处理器看起来像这样:
***
注意,出站处理器的顺序是与入站相反的,出站是从尾巴上为第1个处理器,头为最后一个处理器,处理数据时会按照顺序一个一个进行。
有一个比喻可以很好理解它们之间的关系:
处理器链pipeline
就像两条相反的流水线,pipeline.addLast();
方法就像在流水线上安排一个工人,调用一次就安排一个工人,只不过一些工人专门处理过来的货物,一些工人专门处理过去的货物。
好了,接下来我们开始代码实现处理器了。
Ⅲ、处理器实现
①、处理长度
报文长度字段是我们自定义协议
SP协议
的第一个字段,所以第一个处理器我们先处理长度。
首先,这个处理器肯定是入站处理器,因为是客户端发送来的数据,我们要解析。而入站处理器怎么写呢?
其实
Netty
为我们提供了入站出站处理器的多个模板,我们需要继承并写上自己的实现就行了。
最简单的入站处理器是
SimpleChannelInboundHandler
,源代码我就不讲了,不然又要讲半天。我们新建一个类继承它,这个类就叫CustomLengthHandler
吧:
public class CustomLengthHandler extends SimpleChannelInboundHandler<ByteBuf> {@Overrideprotected void channelRead0(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf) throws Exception {}
}
为什么
SimpleChannelInboundHandler
的泛型是ByteBuf
?其实这里不一定是固定的(不是第一个处理器的情况),你想是什么都可以,取决于上一个处理器传递给当前处理器什么东西,还记得我们上面的那个流程图吗?:
一个一个的处理器处理完数据后,可以继续往下传递数据,传递的数据就是自定义的。例如我从上一个处理器得到
ByteBuf
对象,我将其解析完后,封装成一个对象MyObject
,那么我可以往下传递这个MyObject
对象,下一个处理器就不用再处理一遍ByteBuf
原始数据了,下一个处理器直接处理MyBoject
封装好数据的对象就行了。类比一下,就好像上一个处理器给我当前处理器传递一个JSON字符串
,我当前处理器处理JSON字符串
,将其序列化为对象,并往下传递这个对象,那么下一个处理器就不用再处理原始的JSON字符串
了,就这么个意思。
所以
SimpleChannelInboundHandler
的泛型就是上一个处理器,传递给当前处理器的数据的类型,刚才解释过了,它并不是固定的,上面的CustomLengthHandler
也可以这么写:
public class CustomLengthHandler extends SimpleChannelInboundHandler<String> {@Overrideprotected void channelRead0(ChannelHandlerContext ctx, String str) throws Exception {// 上一个处理器给我传递了一个字符串}
}
也可以:
public class CustomLengthHandler extends SimpleChannelInboundHandler<Integer> {@Overrideprotected void channelRead0(ChannelHandlerContext ctx, Integer itg) throws Exception {// 上一个处理器给我传递了一个数字}
}
并不是固定的。
好了,不说废话了,开始代码实现:
因为我们是第一个入站处理器,上面我们也提到过,
Netty
内部会将数据封装成ByteBuf
,所以我们从上一个处理器接收到的数据其实是一个ByteBuf
对象,所以第一个处理器的泛型必需为ByteBuf
:
public class CustomLengthHandler extends SimpleChannelInboundHandler<ByteBuf> {@Overrideprotected void channelRead0(ChannelHandlerContext ctx, ByteBuf buf) throws Exception {}
}
ByteBuf
是一个字节缓冲区,我们可以从它读取到字节数据,例如:
protected void channelRead0(ChannelHandlerContext ctx, ByteBuf buf) throws Exception {byte b = buf.readByte();// 读取1个字节int i = buf.readInt();// 读取4个字节,因为我们之前说了Java的int是4字节组成的buf.readShort();// 依次类推,读取2字节buf.readLong();String str = buf.readBytes(5).toString(StandardCharsets.UTF_8);// 读取5个字节并转为字符串,注意编码为UTF8
}
还记得吗,在我们的
SP协议
中,我们定义前四个字节是报文长度,所以一开始我们先读取4
字节:
protected void channelRead0(ChannelHandlerContext ctx, ByteBuf buf) throws Exception {int msgLength = buf.readInt();// 报文长度
}
在得到这个报文长度字段后,我们需要对
ByteBuf
的长度做一下判断,如果它的长度小于报文长度,那就说明数据还未全部到达,那我们先不做处理,等完全到达后再做处理,代码像这样:
protected void channelRead0(ChannelHandlerContext ctx, ByteBuf buf) throws Exception {int msgLength = buf.readInt();if (buf.readableBytes() < msgLength){ // 缓冲区中的数据不足 msgLength 个,暂不处理return;}// 读取 msgLength 个字节,也就是整个报文长度的字节,它得到的就是整个报文的完整字节缓冲区ByteBuf bufNew = buf.readBytes(msgLength);// 读取 msgLength 个字节,不包含 msgLength 占用的4字节// 为了效率也可以写为:// ByteBuf bufNew = buf.readSlice(msgLength);ctx.fireChannelRead(bufNew);// 传递给下一个处理器
}
为什么要这样写?还记得一开始我提到的
TCP粘包半包
吗?因为数据并不是一次完整到达的,所以我们必需处理数据部分达到的情况。ByteBuf
就像一个蓄水池,从管道中一开始流进来一些水,但是这些水没有达到蓄水池该有的蓄水量,所以不管它,等它满足了蓄水量,我们再处理。
buf.readBytes(msgLength);
就是一次性从蓄水池(ByteBuf)中获取msgLength
量的水(字节),并将它放到一个新的水池(ByteBuf bufNew)中,这个新的水池,包含了完整的水量(报文所有字节),接着往下传递这个新的水池ctx.fireChannelRead(bufNew);
定义完处理器后,还需要将它添加进处理器链中,还记得我们上面一开始定义的
public class CustomHandler extends ChannelInitializer<SocketChannel>
吗?在其中添加:
public class CustomHandler extends ChannelInitializer<SocketChannel> {@Overrideprotected void initChannel(SocketChannel channel) throws Exception {ChannelPipeline pipeline = channel.pipeline();pipeline.addLast(new CustomLengthHandler());// 我们自定义的第一个长度处理器,它也是入站处理器1}
}
到此为止,这个超级简单的报文长度处理器就写完了,当然,这个处理器有很多的问题,它只作为演示,实际使用会有很多Bug,因为实际使用中要处理的情况有点复杂,好在
Netty
给我们提供了一个开箱即用的报文长度处理器,这也是为什么我写得这么简单的原因,因为只需了解简单的原理而不需要深入探索,Netty
有现成的。
这个处理器就是
LengthFieldBasedFrameDecoder
,它的构造函数常用且重要的有5个参数,类型都是int
,我们一个一个来看:
1、第一个参数maxFrameLength
,是整个报文最大长度,说白了就是限制报文大小的,你的报文不可能无限大。
2、第二个参数lengthFieldOffset
,是你的长度字段是从第几个字节开始的,我们的SP协议
定义了一开始就是长度字段,所以这个参数我们可以填0。
3、第三个参数lengthFieldLength
,是你的长度字段占几个字节,我们定义的SP协议
指明了长度字段占4
个字节,所以填4就行。
4、第四个参数lengthAdjustment
,有点绕,是指没有计算进长度,但是在报文中存在的数据的长度。例如你有数据:5ab
,因为长度字段5
占用4
个字节,b
占用1
个字节,但是没有把a
占用的1
个字节算进来,所以这个例子中,lengthAdjustment
就得填1
,如果是6ab
,那么lengthAdjustment
就得填0
,因为你将a
占用的1
字节算进来了。
5、第五个参数initialBytesToStrip
,是指最终得到的数据要跳过几个字节,在我们的SP协议
中,如果接下来的数据你不想要长度字段,那就可以跳过长度字段的4字节,initialBytesToStrip
就可以填4
,那么得到的数据中就不包含长度了。
基于我们的
SP协议
,最终得到的处理器应该是:
public class CustomHandler extends ChannelInitializer<SocketChannel> {@Overrideprotected void initChannel(SocketChannel channel) throws Exception {ChannelPipeline pipeline = channel.pipeline();// 长度处理器,它也是入站处理器1pipeline.addLast(new LengthFieldBasedFrameDecoder((1024 * 1024) * 50, // 限制最大报文长度为50MB0, 4, 0, 0));// 长度是从0开始的,长度字段4字节,偏移量为0,不跳过字节}
}
②、魔数校验
长度处理完了,现在
TCP粘包半包
所带来的问题我们解决了,接下来就是校验魔数,新增一个入站处理器:
public class CustomMagicNumberHandler extends SimpleChannelInboundHandler<ByteBuf> {@Overrideprotected void channelRead0(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf) throws Exception {}
}
从
LengthFieldBasedFrameDecoder
中传递过来的数据依旧是ByteBuf
,所以泛型我们依旧写成ByteBuf
,到达这里的数据,其实还是原始的报文数据,只不过经过前面的处理它一定是完整的。
做一下简单的魔数校验:
public class CustomMagicNumberHandler extends SimpleChannelInboundHandler<ByteBuf> {@Overrideprotected void channelRead0(ChannelHandlerContext ctx, ByteBuf buf) throws Exception {buf.readInt();// 跳过开头的4字节长度字段int magicNumber = buf.readInt();if (magicNumber != 0xCAFEBABE){ctx.close();// 魔数不正确,直接关闭连接}ctx.fireChannelRead(buf);}
}
将处理器添加进处理器链:
public class CustomHandler extends ChannelInitializer<SocketChannel> {@Overrideprotected void initChannel(SocketChannel channel) throws Exception {ChannelPipeline pipeline = channel.pipeline();// 长度处理器,它也是入站处理器1pipeline.addLast(new LengthFieldBasedFrameDecoder((1024 * 1024) * 50, // 限制最大报文长度为50MB0, 4,0, 0));// 长度是从0开始的,长度字段4字节,偏移量为0,不跳过字节pipeline.addLast(new CustomMagicNumberHandler());// 魔数处理器,入站处理器2}
}
③、为客户端生成唯一值UUID或校验客户端的UUID是否存在
这里我就不写了,其实就是简单的颁发身份证明和校验身份证明而已,生成一个唯一值,然后存储到服务器上,这里判断UUID是否存在在报文中,如果不存在为其生成一个UUID并存储,如果存在,从服务器存储的UUID中找看能不能找得到。
后面的代码可以根据协议定义的规则解析。
④、其他规则实现
…
Ⅳ、需要注意的点
①、ByteBuf的读取
ByteBuf
在读取的时候是不可回退的,就像迭代器
,迭代到下一个就不能再回去读上一个了,要想回去重新读,必需得重置读取:
buf.resetReaderIndex();
然后又从最开头开始读取。
ByteBuf
中数据的基本单位是字节,readInt()
、readLong()
等方法实际上读取的都是字节,只不过封装了一下,将多个字节转为对应Java类型了。
②、字符编码
注意,解析协议时,客户端与服务器都要使用相同的字符编码,否则解析字节会对不上,因为有些字符编码使用的字节数可能不太一样。
③、业务逻辑处理
协议解析完后,将数据传递到业务逻辑时,可以使用
Netty
服务器启动时的:
NioEventLoopGroup worker = new NioEventLoopGroup();
worker
来处理业务逻辑,worker
的本质其实是一个线程池。
其他的注意事项我想起来了后续会加,有什么问题可以评论区留言,看到会回复。
五、简单封装的框架
根据以上代码的思路,我封装了一个简单的开源框架,主要处理
SP协议
的加强版,它包含了长度处理
、魔数
、客户端标识
、路径处理
、数据加密
等操作(暂未做数据验证)。
源代码链接是:simple-netty-core,丢在
gitee
上了,为什么不是GitHub
?因为我的电脑不科学上网的话,始终访问不到GitHub
,即使修改了host
文件也访问不到,所以干脆就将源代码丢在gitee
上了。
这个框架是我学习
Netty
时写的,比较简单,基本能使用,感兴趣的可以参考一下,也欢迎贡献。
写在最后
最后叠个甲吧:
以上内容是我个人理解,不保证全部正确,如有遗漏、错误等后续我会回来更新这篇博客,欢迎评论区指正。