04.Netty进阶之协议设计与解析
前言
- 小故事
很久很久以前,一位私塾先生到一家任教。双方签订了一纸协议:“无鸡鸭亦可无鱼肉亦可白菜豆腐不可少不得束修金”。此后,私塾先生虽然认真教课,但主人家则总是给私塾先生以白菜豆腐为菜,丝毫未见鸡鸭鱼肉的款待。私塾先生先是很不解,可是后来也就想通了:主人把鸡鸭鱼肉的钱都会换为束修金的,也罢。至此双方相安无事。
年关将至,一个学年段亦告结束。私塾先生临行时,也不见主人家为他交付束修金,遂与主家理论。然主家亦振振有词:“有协议为证——无鸡鸭亦可,无鱼肉亦可,白菜豆腐不可少,不得束修金。这白纸黑字明摆着的,你有什么要说的呢?”
私塾先生据理力争:“协议是这样的——无鸡,鸭亦可;无鱼,肉亦可;白菜豆腐不可,少不得束修金。”
双方唇枪舌战,你来我往,真个是不亦乐乎!
这里的束修金,也作“束脩”,应当是泛指教师应当得到的报酬
一、协议作用
TCP/IP 中消息传输基于流的方式,没有边界。
协议的目的就是划定消息的边界,制定通信双方要共同遵守的通信规则
例如HTTP协议、redis通信协议、websocket协议等等。
如何设计协议呢?
其实就是给网络传输的信息加上“标点符号”。但通过分隔符来断句不是很好,因为分隔符本身如果用于传输,那么必须加以区分。因此,下面一种协议较为常用
定长字节表示内容长度 + 实际内容
二、Redis协议
要发送消息给redis,需要遵从其协议。
示例
向Redis发送如下命令
set name Leefs
首先要发送整个命令的长度,然后分别发送命令每个位置的长度
*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
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;
@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<SocketChannel>() {
@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中结果
三、HTTP协议
HTTP协议在请求行请求头中都有很多的内容,自己实现较为困难,可以使用HttpServerCodec
作为服务器端的解码器与编码器,来处理HTTP请求
// HttpServerCodec 中既有请求的解码器 HttpRequestDecoder 又有响应的编码器 HttpResponseEncoder
// Codec(CodeCombine) 一般代表该类既作为 编码器 又作为 解码器
public final class HttpServerCodec extends CombinedChannelDuplexHandler<HttpRequestDecoder, HttpResponseEncoder>
implements HttpServerUpgradeHandler.SourceCodec
使用方式
ch.pipeline().addLast(new HttpServerCodec());
浏览器发送一次请求(无论什么方法请求)实际上会解析成两部分:
若是重写channelRead
方法,那么一个http请求就会走两次该handler
方法,每次执行方法其中的Object msg
分别为不同部分的解析对象
DefaultHttpRequest
:解析出来请求行和请求头。LastHttpContent$1
:表示请求体。(即便是get请求,请求体内容为空也会专门解析一个请求体对象)
情况一
若是想区分请求头、请求体中handler那么就需要写一个简单的判断
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
//若是只对HTTP请求的请求头感兴趣,那么实现SimpleChannelInboundHandler实例,指明感兴趣的请求对象为HttpRequest(实际就是DefaultHttpRequest)
ch.pipeline().addLast(new SimpleChannelInboundHandler<HttpRequest>() {
@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 = "<h1>Hello,world!</h1>".getBytes();
//设置响应头:content-length:内容长度。不设置的话浏览器就不能够知道确切的响应内容大小则会造成一直没有处理完的现象
response.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, content.length);
response.content().writeBytes(content);
//写会响应
ctx.writeAndFlush(response);
}
});
代码示例
服务器响应http请求并返回hello,world标签
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;
@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<SocketChannel>() {
@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<HttpRequest>() {
@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 = "<h1>Hello, world!</h1>".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....<h1>Hello|
|00000030| 2c 20 77 6f 72 6c 64 21 3c 2f 68 31 3e |, world!</h1> |
+--------+-------------------------------------------------+----------------+
浏览器
| 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!
|
±-------±------------------------------------------------±---------------+
**浏览器**
[外链图片转存中...(img-DJGDevnl-1682232353668)]