李林超博客
首页
归档
留言
友链
动态
关于
归档
留言
友链
动态
关于
首页
Java
正文
04.Netty进阶之协议设计与解析
Leefs
2022-06-16 PM
716℃
0条
[TOC] ### 前言 + **小故事** > 很久很久以前,一位私塾先生到一家任教。双方签订了一纸协议:“无鸡鸭亦可无鱼肉亦可白菜豆腐不可少不得束修金”。此后,私塾先生虽然认真教课,但主人家则总是给私塾先生以白菜豆腐为菜,丝毫未见鸡鸭鱼肉的款待。私塾先生先是很不解,可是后来也就想通了:主人把鸡鸭鱼肉的钱都会换为束修金的,也罢。至此双方相安无事。 > > 年关将至,一个学年段亦告结束。私塾先生临行时,也不见主人家为他交付束修金,遂与主家理论。然主家亦振振有词:“有协议为证——无鸡鸭亦可,无鱼肉亦可,白菜豆腐不可少,不得束修金。这白纸黑字明摆着的,你有什么要说的呢?” > > 私塾先生据理力争:“协议是这样的——无鸡,鸭亦可;无鱼,肉亦可;白菜豆腐不可,少不得束修金。” > > 双方唇枪舌战,你来我往,真个是不亦乐乎! > > 这里的束修金,也作“束脩”,应当是泛指教师应当得到的报酬 ### 一、协议作用 TCP/IP 中消息传输基于流的方式,没有边界。 **协议的目的就是划定消息的边界,制定通信双方要共同遵守的通信规则** 例如HTTP协议、redis通信协议、websocket协议等等。 **如何设计协议呢?** 其实就是给网络传输的信息加上“标点符号”。但通过分隔符来断句不是很好,因为分隔符本身如果用于传输,那么必须加以区分。因此,下面一种协议较为常用 > 定长字节表示内容长度 + 实际内容 ### 二、Redis协议 要发送消息给redis,需要遵从其协议。 **示例** > 向Redis发送如下命令 ```sh set name Leefs ``` 首先要发送整个命令的长度,然后分别发送命令每个位置的长度 ```sh *3 //命令长度 $3 //set长度 set //命令内容 $4 //key的长度 name //key的内容 $3 //value的长度 Leefs //value的内容 ``` + `*3`:首先需要让你发送数组的长度`*`表示的是命令的数量,3则是命令组成的长度。 + `$3`:$表示的是某个命令参数的长度,3表示该命令参数长度为3。 + 每个命令参数都由`\r\n`来进行分割 **代码示例** > 使用redis协议模拟与redis服务端进行通信,执行一条set、get命令。 > > + set name Leefs > + get name ```java import io.netty.bootstrap.Bootstrap; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.channel.ChannelInitializer; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioSocketChannel; import io.netty.handler.logging.LoggingHandler; import lombok.extern.slf4j.Slf4j; import java.nio.charset.Charset; /** * @author lilinchao * @date 2022/6/16 * @description 模拟Redis客户端来向redis服务端发送命令 **/ @Slf4j public class RedisDemo { public static void main(String[] args) { NioEventLoopGroup worker = new NioEventLoopGroup(); //两个字节:13表示回车,10表示换行 byte[] LINE = {13,10}; try { Bootstrap bootstrap = new Bootstrap(); bootstrap.channel(NioSocketChannel.class); bootstrap.group(worker); bootstrap.handler(new ChannelInitializer
() { @Override protected void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast(new LoggingHandler()); ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){ // 会在连接 channel 建立成功后,会触发 active 事件 @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { set(ctx); get(ctx); } //向redis发送get命令 //get name private void get(ChannelHandlerContext ctx) { ByteBuf buf = ctx.alloc().buffer(); buf.writeBytes("*2".getBytes()); buf.writeBytes(LINE); buf.writeBytes("$3".getBytes()); buf.writeBytes(LINE); buf.writeBytes("get".getBytes()); buf.writeBytes(LINE); buf.writeBytes("$4".getBytes()); buf.writeBytes(LINE); buf.writeBytes("name".getBytes()); buf.writeBytes(LINE); ctx.writeAndFlush(buf); } //向redis发送set命令 //set name Leefs private void set(ChannelHandlerContext ctx){ ByteBuf buf = ctx.alloc().buffer(); buf.writeBytes("*3".getBytes()); buf.writeBytes(LINE); buf.writeBytes("$3".getBytes()); buf.writeBytes(LINE); buf.writeBytes("set".getBytes()); buf.writeBytes(LINE); buf.writeBytes("$4".getBytes()); buf.writeBytes(LINE); buf.writeBytes("name".getBytes()); buf.writeBytes(LINE); buf.writeBytes("$5".getBytes()); buf.writeBytes(LINE); buf.writeBytes("Leefs".getBytes()); buf.writeBytes(LINE); ctx.writeAndFlush(buf); } @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { ByteBuf buf = (ByteBuf) msg; System.out.println(buf.toString(Charset.defaultCharset())); } }); } }); ChannelFuture channelFuture = bootstrap.connect("localhost",6379).sync(); channelFuture.channel().closeFuture().sync(); } catch (InterruptedException e) { log.error("client error",e); } finally { worker.shutdownGracefully(); } } } ``` **运行结果** ``` 11:47:43.495 [nioEventLoopGroup-2-1] DEBUG io.netty.handler.logging.LoggingHandler - [id: 0x13f4d8f0, L:/127.0.0.1:61803 - R:localhost/127.0.0.1:6379] WRITE: 34B +-------------------------------------------------+ | 0 1 2 3 4 5 6 7 8 9 a b c d e f | +--------+-------------------------------------------------+----------------+ |00000000| 2a 33 0d 0a 24 33 0d 0a 73 65 74 0d 0a 24 34 0d |*3..$3..set..$4.| |00000010| 0a 6e 61 6d 65 0d 0a 24 35 0d 0a 4c 65 65 66 73 |.name..$5..Leefs| |00000020| 0d 0a |.. | +--------+-------------------------------------------------+----------------+ 11:47:43.496 [nioEventLoopGroup-2-1] DEBUG io.netty.handler.logging.LoggingHandler - [id: 0x13f4d8f0, L:/127.0.0.1:61803 - R:localhost/127.0.0.1:6379] FLUSH 11:47:43.501 [nioEventLoopGroup-2-1] DEBUG io.netty.handler.logging.LoggingHandler - [id: 0x13f4d8f0, L:/127.0.0.1:61803 - R:localhost/127.0.0.1:6379] WRITE: 23B +-------------------------------------------------+ | 0 1 2 3 4 5 6 7 8 9 a b c d e f | +--------+-------------------------------------------------+----------------+ |00000000| 2a 32 0d 0a 24 33 0d 0a 67 65 74 0d 0a 24 34 0d |*2..$3..get..$4.| |00000010| 0a 6e 61 6d 65 0d 0a |.name.. | +--------+-------------------------------------------------+----------------+ 11:47:43.501 [nioEventLoopGroup-2-1] DEBUG io.netty.handler.logging.LoggingHandler - [id: 0x13f4d8f0, L:/127.0.0.1:61803 - R:localhost/127.0.0.1:6379] FLUSH 11:47:43.540 [nioEventLoopGroup-2-1] DEBUG io.netty.handler.logging.LoggingHandler - [id: 0x13f4d8f0, L:/127.0.0.1:61803 - R:localhost/127.0.0.1:6379] READ: 16B +-------------------------------------------------+ | 0 1 2 3 4 5 6 7 8 9 a b c d e f | +--------+-------------------------------------------------+----------------+ |00000000| 2b 4f 4b 0d 0a 24 35 0d 0a 4c 65 65 66 73 0d 0a |+OK..$5..Leefs..| +--------+-------------------------------------------------+----------------+ +OK $5 Leefs ``` **查看Redis中结果** ![04.Netty进阶之协议设计与解析01.jpg](https://lilinchao.com/usr/uploads/2022/06/1919506909.jpg) ### 三、HTTP协议 HTTP协议在请求行请求头中都有很多的内容,自己实现较为困难,可以使用`HttpServerCodec`作为**服务器端的解码器与编码器,来处理HTTP请求** ```java // HttpServerCodec 中既有请求的解码器 HttpRequestDecoder 又有响应的编码器 HttpResponseEncoder // Codec(CodeCombine) 一般代表该类既作为 编码器 又作为 解码器 public final class HttpServerCodec extends CombinedChannelDuplexHandler
implements HttpServerUpgradeHandler.SourceCodec ``` **使用方式** ```java ch.pipeline().addLast(new HttpServerCodec()); ``` 浏览器发送一次请求(无论什么方法请求)实际上会解析成两部分: 若是重写`channelRead`方法,那么一个http请求就会走两次该`handler`方法,每次执行方法其中的`Object msg`分别为不同部分的解析对象 + `DefaultHttpRequest`:解析出来请求行和请求头。 + `LastHttpContent$1`:表示请求体。(即便是get请求,请求体内容为空也会专门解析一个请求体对象) **情况一** > 若是想区分请求头、请求体中handler那么就需要写一个简单的判断 ```java ch.pipeline().addLast(new ChannelInboundHandlerAdapter() { @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { //DefaultHttpRequest实现了HttpRequest接口 if (msg instanceof HttpRequest){ System.out.println("请求行、头"); //LastHttpContent实现了HttpContent接口 }else if (msg instanceof HttpContent){ System.out.println("请求体"); } super.channelRead(ctx, msg); } }); ``` **情况二** > 若是只对某个特定类型感兴趣的话,例如只对解析出来的`DefaultHttpRequest`请求体对象感兴趣,可以实现一个`SimpleChannelInboundHandler` ```java //若是只对HTTP请求的请求头感兴趣,那么实现SimpleChannelInboundHandler实例,指明感兴趣的请求对象为HttpRequest(实际就是DefaultHttpRequest) ch.pipeline().addLast(new SimpleChannelInboundHandler
() { @Override protected void channelRead0(ChannelHandlerContext ctx, HttpRequest msg) throws Exception { log.debug("解析对象类型:{}", msg.getClass()); log.debug(msg.uri()); //进行响应返回 //构建响应对象 final DefaultFullHttpResponse response = new DefaultFullHttpResponse(msg.protocolVersion(), HttpResponseStatus.OK); // 响应内容 final byte[] content = "
Hello,world!
".getBytes(); //设置响应头:content-length:内容长度。不设置的话浏览器就不能够知道确切的响应内容大小则会造成一直没有处理完的现象 response.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, content.length); response.content().writeBytes(content); //写会响应 ctx.writeAndFlush(response); } }); ``` **代码示例** > 服务器响应http请求并返回hello,world标签 ```java import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInitializer; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioServerSocketChannel; import io.netty.handler.codec.http.DefaultFullHttpResponse; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http.HttpServerCodec; import io.netty.handler.logging.LogLevel; import io.netty.handler.logging.LoggingHandler; import lombok.extern.slf4j.Slf4j; import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_LENGTH; /** * @author lilinchao * @date 2022/6/16 * @description http协议编解码 **/ @Slf4j public class HttpDemo { public static void main(String[] args) { NioEventLoopGroup boss = new NioEventLoopGroup(); NioEventLoopGroup worker = new NioEventLoopGroup(); try { ServerBootstrap serverBootstrap = new ServerBootstrap(); serverBootstrap.channel(NioServerSocketChannel.class); serverBootstrap.group(boss,worker); serverBootstrap.childHandler(new ChannelInitializer
() { @Override protected void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG)); //netty提供的对HTTP协议编解码处理器类 ch.pipeline().addLast(new HttpServerCodec()); //若是只对HTTP请求的请求头感兴趣,那么实现SimpleChannelInboundHandler实例 //指明感兴趣的请求对象为HttpRequest(实际就是DefaultHttpRequest) ch.pipeline().addLast(new SimpleChannelInboundHandler
() { @Override protected void channelRead0(ChannelHandlerContext ctx, HttpRequest msg) throws Exception { //获得请求uri log.debug(msg.uri()); //返回响应 //构建响应对象,设置版本号与状态码 DefaultFullHttpResponse response = new DefaultFullHttpResponse(msg.protocolVersion(), HttpResponseStatus.OK); //响应内容 byte[] bytes = "
Hello, world!
".getBytes(); //设置响应头:content-length:内容长度。不设置的话浏览器就不能够知道确切的响应内容大小,则会造成一直没有处理完的现象 response.headers().setInt(CONTENT_LENGTH,bytes.length); //设置响应体 response.content().writeBytes(bytes); //写回响应 ctx.writeAndFlush(response); } }); } }); ChannelFuture channelFuture = serverBootstrap.bind(8080).sync(); channelFuture.channel().closeFuture().sync(); } catch (InterruptedException e) { log.error("server error",e); } finally { boss.shutdownGracefully(); worker.shutdownGracefully(); } } } ``` **运行结果** ``` 13:49:40.645 [nioEventLoopGroup-3-1] DEBUG io.netty.handler.logging.LoggingHandler - [id: 0x5ac5a1ea, L:/0:0:0:0:0:0:0:1:8080 - R:/0:0:0:0:0:0:0:1:49775] READ: 685B +-------------------------------------------------+ | 0 1 2 3 4 5 6 7 8 9 a b c d e f | +--------+-------------------------------------------------+----------------+ |00000000| 47 45 54 20 2f 20 48 54 54 50 2f 31 2e 31 0d 0a |GET / HTTP/1.1..| |00000010| 48 6f 73 74 3a 20 6c 6f 63 61 6c 68 6f 73 74 3a |Host: localhost:| |00000020| 38 30 38 30 0d 0a 43 6f 6e 6e 65 63 74 69 6f 6e |8080..Connection| ............................ 13:49:40.736 [nioEventLoopGroup-3-1] DEBUG com.lilinchao.netty.protocol.HttpDemo - / 13:49:40.752 [nioEventLoopGroup-3-1] DEBUG io.netty.handler.logging.LoggingHandler - [id: 0x5ac5a1ea, L:/0:0:0:0:0:0:0:1:8080 - R:/0:0:0:0:0:0:0:1:49775] WRITE: 61B +-------------------------------------------------+ | 0 1 2 3 4 5 6 7 8 9 a b c d e f | +--------+-------------------------------------------------+----------------+ |00000000| 48 54 54 50 2f 31 2e 31 20 32 30 30 20 4f 4b 0d |HTTP/1.1 200 OK.| |00000010| 0a 63 6f 6e 74 65 6e 74 2d 6c 65 6e 67 74 68 3a |.content-length:| |00000020| 20 32 32 0d 0a 0d 0a 3c 68 31 3e 48 65 6c 6c 6f | 22....
Hello| |00000030| 2c 20 77 6f 72 6c 64 21 3c 2f 68 31 3e |, world!
| +--------+-------------------------------------------------+----------------+ ``` **浏览器** ![04.Netty进阶之协议设计与解析02.jpg](https://lilinchao.com/usr/uploads/2022/06/2584353729.jpg) *附参考文章:* *《黑马程序员Netty教程》*
标签:
Netty
非特殊说明,本博所有文章均为博主原创。
如若转载,请注明出处:
https://lilinchao.com/archives/2169.html
上一篇
03.Netty进阶之长度域解码器
下一篇
05.Netty进阶之自定义协议
取消回复
评论啦~
提交评论
栏目分类
随笔
2
Java
326
大数据
229
工具
31
其它
25
GO
47
标签云
Jquery
递归
人工智能
Java
Redis
JavaWEB项目搭建
MyBatis-Plus
Hive
Thymeleaf
JavaWeb
高并发
并发编程
机器学习
Spark SQL
查找
Golang
SpringCloud
gorm
Scala
稀疏数组
序列化和反序列化
Stream流
数据结构和算法
链表
MyBatisX
Sentinel
散列
Python
Jenkins
FastDFS
友情链接
申请
范明明
庄严博客
Mx
陶小桃Blog
虫洞