原文地址: haifeiWu的博客
博客地址:www.hchstudio.cn
欢迎转载,转载请注明作者及出处,谢谢!

近期一直在做网络协议相关的工作,所以博客也就与之相关的比较多,今天楼主结合 Redis的协议 RESP 看看在 Netty 源码中是如何实现的。

RESP 协议

RESP 是 Redis 序列化协议的简写。它是一种直观的文本协议,优势在于实现非常简单,解析性能极好。

Redis 协议将传输的结构数据分为 5 种最小单元类型,单元结束时统一加上回车换行符号\r\n,来表示该单元的结束。

  1. 单行字符串 以 + 符号开头。
  2. 多行字符串 以 $ 符号开头,后跟字符串长度。
  3. 整数值 以 : 符号开头,后跟整数的字符串形式。
  4. 错误消息 以 - 符号开头。
  5. 数组 以 * 号开头,后跟数组的长度。

关于 RESP 协议的具体介绍感兴趣的小伙伴请移步楼主的另一篇文章Redis协议规范(译文)

Netty 中 RESP 协议的定义

如下面代码中所表示的,Netty中使用对应符号的ASCII码来表示,感兴趣的小伙伴可以查一下ASCII码表来验证一下。


public enum RedisMessageType {
// 以 + 开头的单行字符串
SIMPLE_STRING((byte)43, true),
// 以 - 开头的错误信息
ERROR((byte)45, true),
// 以 : 开头的整型数据
INTEGER((byte)58, true),
// 以 $ 开头的多行字符串
BULK_STRING((byte)36, false),
// 以 * 开头的数组
ARRAY_HEADER((byte)42, false),
ARRAY((byte)42, false); private final byte value;
private final boolean inline; private RedisMessageType(byte value, boolean inline) {
this.value = value;
this.inline = inline;
} public byte value() {
return this.value;
} public boolean isInline() {
return this.inline;
} public static RedisMessageType valueOf(byte value) {
switch(value) {
case 36:
return BULK_STRING;
case 42:
return ARRAY_HEADER;
case 43:
return SIMPLE_STRING;
case 45:
return ERROR;
case 58:
return INTEGER;
default:
throw new RedisCodecException("Unknown RedisMessageType: " + value);
}
}
}

Netty 中 RESP 解码器实现

解码器,顾名思义,就是将服务器返回的数据根据协议反序列化成易于阅读的信息。RedisDecoder 就是根据 RESP 将服务端返回的信息反序列化出来。下面是指令的编码格式

SET key value => *3\r\n$5\r\nSET\r\n$1\r\nkey\r\n$1\r\nvalue\r\n

指令是一个字符串数组,编码一个字符串数组,首先需要编码数组长度*3\r\n。然后依次编码各个字符串参数。编码字符串首先需要编码字符串的长度$5\r\n。然后再编码字符串的内容SET\r\n。Redis 消息以\r\n作为分隔符,这样设计其实挺浪费网络传输流量的,消息内容里面到处都是\r\n符号。但是这样的消息可读性会比较好,便于调试。RESP 协议是牺牲性能换取可读,易于实现的一个经典例子。

指令解码器的实现,网络字节流的读取存在拆包问题。所拆包问题是指一次Read调用从套件字读到的字节数组可能只是一个完整消息的一部分。而另外一部分则需要发起另外一次Read调用才可能读到,甚至要发起多个Read调用才可以读到完整的一条消息。对于拆包问题感兴趣的小伙伴可以查看楼主的另一篇文章TCP 粘包问题浅析及其解决方案

如果我们拿部分消息去反序列化成输入消息对象肯定是要失败的,或者说生成的消息对象是不完整填充的。这个时候我们需要等待下一次Read调用,然后将这两次Read调用的字节数组拼起来,尝试再一次反序列化。

问题来了,如果一个输入消息对象很大,就可能需要多个Read调用和多次反序列化操作才能完整的解包出一个输入对象。那这个反序列化的过程就会重复了多次。

针对这个问题,Netty 中很巧妙的解决了这个问题,如下所示,Netty 中通过 state 属性来保存当前序列化的状态,然后下次反序列化的时候就可以从上次记录的 state 直接继续反序列化。这样就避免了重复的问题。

    // 保持当前序列化状态的字段
private RedisDecoder.State state; public RedisDecoder() {
this(65536, FixedRedisMessagePool.INSTANCE);
} public RedisDecoder(int maxInlineMessageLength, RedisMessagePool messagePool) {
this.toPositiveLongProcessor = new RedisDecoder.ToPositiveLongProcessor();
// 默认初始化状态为,反序列化指令类型
this.state = RedisDecoder.State.DECODE_TYPE;
if (maxInlineMessageLength > 0 && maxInlineMessageLength <= 536870912) {
this.maxInlineMessageLength = maxInlineMessageLength;
this.messagePool = messagePool;
} else {
throw new RedisCodecException("maxInlineMessageLength: " + maxInlineMessageLength + " (expected: <= " + 536870912 + ")");
}
} // 解码器的主要业务逻辑
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
try {
// 循环读取信息,将信息完成的序列化
while(true) {
switch(this.state) {
case DECODE_TYPE:
if (this.decodeType(in)) {
break;
} return;
case DECODE_INLINE:
if (this.decodeInline(in, out)) {
break;
} return;
case DECODE_LENGTH:
if (this.decodeLength(in, out)) {
break;
} return;
case DECODE_BULK_STRING_EOL:
if (this.decodeBulkStringEndOfLine(in, out)) {
break;
} return;
case DECODE_BULK_STRING_CONTENT:
if (this.decodeBulkStringContent(in, out)) {
break;
} return;
default:
throw new RedisCodecException("Unknown state: " + this.state);
}
}
} catch (RedisCodecException var5) {
this.resetDecoder();
throw var5;
} catch (Exception var6) {
this.resetDecoder();
throw new RedisCodecException(var6);
}
}

下面代码中,是针对每种数据类型进行反序列化的具体业务逻辑。有小伙伴可能会想,没有看到解码胡数组类型的逻辑呢?实际上在 RESP 协议中数组就是其他类型的组合,所以完全可以循环读取,按照单个元素解码。

// 解码消息类型
private boolean decodeType(ByteBuf in) throws Exception {
if (!in.isReadable()) {
return false;
} else {
this.type = RedisMessageType.valueOf(in.readByte());
this.state = this.type.isInline() ? RedisDecoder.State.DECODE_INLINE : RedisDecoder.State.DECODE_LENGTH;
return true;
}
} // 解码单行字符串,错误信息,或者整型数据类型
private boolean decodeInline(ByteBuf in, List<Object> out) throws Exception {
ByteBuf lineBytes = readLine(in);
if (lineBytes == null) {
if (in.readableBytes() > this.maxInlineMessageLength) {
throw new RedisCodecException("length: " + in.readableBytes() + " (expected: <= " + this.maxInlineMessageLength + ")");
} else {
return false;
}
} else {
out.add(this.newInlineRedisMessage(this.type, lineBytes));
this.resetDecoder();
return true;
}
} // 解码消息长度
private boolean decodeLength(ByteBuf in, List<Object> out) throws Exception {
ByteBuf lineByteBuf = readLine(in);
if (lineByteBuf == null) {
return false;
} else {
long length = this.parseRedisNumber(lineByteBuf);
if (length < -1L) {
throw new RedisCodecException("length: " + length + " (expected: >= " + -1 + ")");
} else {
switch(this.type) {
case ARRAY_HEADER:
out.add(new ArrayHeaderRedisMessage(length));
this.resetDecoder();
return true;
case BULK_STRING:
if (length > 536870912L) {
throw new RedisCodecException("length: " + length + " (expected: <= " + 536870912 + ")");
} this.remainingBulkLength = (int)length;
return this.decodeBulkString(in, out);
default:
throw new RedisCodecException("bad type: " + this.type);
}
}
}
} // 解码多行字符串
private boolean decodeBulkString(ByteBuf in, List<Object> out) throws Exception {
switch(this.remainingBulkLength) {
case -1:
out.add(FullBulkStringRedisMessage.NULL_INSTANCE);
this.resetDecoder();
return true;
case 0:
this.state = RedisDecoder.State.DECODE_BULK_STRING_EOL;
return this.decodeBulkStringEndOfLine(in, out);
default:
out.add(new BulkStringHeaderRedisMessage(this.remainingBulkLength));
this.state = RedisDecoder.State.DECODE_BULK_STRING_CONTENT;
return this.decodeBulkStringContent(in, out);
}
}

Netty 中 RESP 编码器实现

编码器,顾名思义,就是将对象根据 RESP 协议序列化成字节流发送到服务端。编码器的实现非常简单,不用考虑拆包等问题,就是分配一个ByteBuf,然后将将消息输出对象序列化的字节数组塞到ByteBuf中输出就可以了。

下面代码中就是 encode 方法直接调用 writeRedisMessage 方法,根据消息类型进行写buffer操作。

    @Override
protected void encode(ChannelHandlerContext ctx, RedisMessage msg, List<Object> out) throws Exception {
try {
writeRedisMessage(ctx.alloc(), msg, out);
} catch (CodecException e) {
throw e;
} catch (Exception e) {
throw new CodecException(e);
}
} private void writeRedisMessage(ByteBufAllocator allocator, RedisMessage msg, List<Object> out) {
// 判断消息类型,然后调用写相应消息的方法。
if (msg instanceof InlineCommandRedisMessage) {
writeInlineCommandMessage(allocator, (InlineCommandRedisMessage) msg, out);
} else if (msg instanceof SimpleStringRedisMessage) {
writeSimpleStringMessage(allocator, (SimpleStringRedisMessage) msg, out);
} else if (msg instanceof ErrorRedisMessage) {
writeErrorMessage(allocator, (ErrorRedisMessage) msg, out);
} else if (msg instanceof IntegerRedisMessage) {
writeIntegerMessage(allocator, (IntegerRedisMessage) msg, out);
} else if (msg instanceof FullBulkStringRedisMessage) {
writeFullBulkStringMessage(allocator, (FullBulkStringRedisMessage) msg, out);
} else if (msg instanceof BulkStringRedisContent) {
writeBulkStringContent(allocator, (BulkStringRedisContent) msg, out);
} else if (msg instanceof BulkStringHeaderRedisMessage) {
writeBulkStringHeader(allocator, (BulkStringHeaderRedisMessage) msg, out);
} else if (msg instanceof ArrayHeaderRedisMessage) {
writeArrayHeader(allocator, (ArrayHeaderRedisMessage) msg, out);
} else if (msg instanceof ArrayRedisMessage) {
writeArrayMessage(allocator, (ArrayRedisMessage) msg, out);
} else {
throw new CodecException("unknown message type: " + msg);
}
}

下面代码主要是实现对应消息按照 RESP 协议 进行序列化操作,具体就是上面楼主说的,分配一个ByteBuf,然后将将消息输出对象序列化的字节数组塞到ByteBuf中输出即可。

private static void writeInlineCommandMessage(ByteBufAllocator allocator, InlineCommandRedisMessage msg,
List<Object> out) {
writeString(allocator, RedisMessageType.INLINE_COMMAND, msg.content(), out);
} private static void writeSimpleStringMessage(ByteBufAllocator allocator, SimpleStringRedisMessage msg,
List<Object> out) {
writeString(allocator, RedisMessageType.SIMPLE_STRING, msg.content(), out);
} private static void writeErrorMessage(ByteBufAllocator allocator, ErrorRedisMessage msg, List<Object> out) {
writeString(allocator, RedisMessageType.ERROR, msg.content(), out);
} private static void writeString(ByteBufAllocator allocator, RedisMessageType type, String content,
List<Object> out) {
ByteBuf buf = allocator.ioBuffer(type.length() + ByteBufUtil.utf8MaxBytes(content) +
RedisConstants.EOL_LENGTH);
type.writeTo(buf);
ByteBufUtil.writeUtf8(buf, content);
buf.writeShort(RedisConstants.EOL_SHORT);
out.add(buf);
} private void writeIntegerMessage(ByteBufAllocator allocator, IntegerRedisMessage msg, List<Object> out) {
ByteBuf buf = allocator.ioBuffer(RedisConstants.TYPE_LENGTH + RedisConstants.LONG_MAX_LENGTH +
RedisConstants.EOL_LENGTH);
RedisMessageType.INTEGER.writeTo(buf);
buf.writeBytes(numberToBytes(msg.value()));
buf.writeShort(RedisConstants.EOL_SHORT);
out.add(buf);
} private void writeBulkStringHeader(ByteBufAllocator allocator, BulkStringHeaderRedisMessage msg, List<Object> out) {
final ByteBuf buf = allocator.ioBuffer(RedisConstants.TYPE_LENGTH +
(msg.isNull() ? RedisConstants.NULL_LENGTH :
RedisConstants.LONG_MAX_LENGTH + RedisConstants.EOL_LENGTH));
RedisMessageType.BULK_STRING.writeTo(buf);
if (msg.isNull()) {
buf.writeShort(RedisConstants.NULL_SHORT);
} else {
buf.writeBytes(numberToBytes(msg.bulkStringLength()));
buf.writeShort(RedisConstants.EOL_SHORT);
}
out.add(buf);
} private static void writeBulkStringContent(ByteBufAllocator allocator, BulkStringRedisContent msg,
List<Object> out) {
out.add(msg.content().retain());
if (msg instanceof LastBulkStringRedisContent) {
out.add(allocator.ioBuffer(RedisConstants.EOL_LENGTH).writeShort(RedisConstants.EOL_SHORT));
}
} private void writeFullBulkStringMessage(ByteBufAllocator allocator, FullBulkStringRedisMessage msg,
List<Object> out) {
if (msg.isNull()) {
ByteBuf buf = allocator.ioBuffer(RedisConstants.TYPE_LENGTH + RedisConstants.NULL_LENGTH +
RedisConstants.EOL_LENGTH);
RedisMessageType.BULK_STRING.writeTo(buf);
buf.writeShort(RedisConstants.NULL_SHORT);
buf.writeShort(RedisConstants.EOL_SHORT);
out.add(buf);
} else {
ByteBuf headerBuf = allocator.ioBuffer(RedisConstants.TYPE_LENGTH + RedisConstants.LONG_MAX_LENGTH +
RedisConstants.EOL_LENGTH);
RedisMessageType.BULK_STRING.writeTo(headerBuf);
headerBuf.writeBytes(numberToBytes(msg.content().readableBytes()));
headerBuf.writeShort(RedisConstants.EOL_SHORT);
out.add(headerBuf);
out.add(msg.content().retain());
out.add(allocator.ioBuffer(RedisConstants.EOL_LENGTH).writeShort(RedisConstants.EOL_SHORT));
}
} /**
* Write array header only without body. Use this if you want to write arrays as streaming.
*/
private void writeArrayHeader(ByteBufAllocator allocator, ArrayHeaderRedisMessage msg, List<Object> out) {
writeArrayHeader(allocator, msg.isNull(), msg.length(), out);
} /**
* Write full constructed array message.
*/
private void writeArrayMessage(ByteBufAllocator allocator, ArrayRedisMessage msg, List<Object> out) {
if (msg.isNull()) {
writeArrayHeader(allocator, msg.isNull(), RedisConstants.NULL_VALUE, out);
} else {
writeArrayHeader(allocator, msg.isNull(), msg.children().size(), out);
for (RedisMessage child : msg.children()) {
writeRedisMessage(allocator, child, out);
}
}
} private void writeArrayHeader(ByteBufAllocator allocator, boolean isNull, long length, List<Object> out) {
if (isNull) {
final ByteBuf buf = allocator.ioBuffer(RedisConstants.TYPE_LENGTH + RedisConstants.NULL_LENGTH +
RedisConstants.EOL_LENGTH);
RedisMessageType.ARRAY_HEADER.writeTo(buf);
buf.writeShort(RedisConstants.NULL_SHORT);
buf.writeShort(RedisConstants.EOL_SHORT);
out.add(buf);
} else {
final ByteBuf buf = allocator.ioBuffer(RedisConstants.TYPE_LENGTH + RedisConstants.LONG_MAX_LENGTH +
RedisConstants.EOL_LENGTH);
RedisMessageType.ARRAY_HEADER.writeTo(buf);
buf.writeBytes(numberToBytes(length));
buf.writeShort(RedisConstants.EOL_SHORT);
out.add(buf);
}
}

小结

对于 Netty 源码,楼主一直是一种敬畏的态度,没想到今天竟然从另一个方面对 Netty 的冰山一角展开解读,毕竟万事开头难,有了这一次希望之后可以更顺利,在技术成长的道路上一起加油。

参考链接

Netty 源码中对 Redis 协议的实现的更多相关文章

  1. netty学习--netty源码中的部分util方法

    io.netty.buffer.AbstractByteBuf#calculateNewCapacity  申请内存空间 private int calculateNewCapacity(int mi ...

  2. netty源码解解析(4.0)-20 ChannelHandler: 自己实现一个自定义协议的服务器和客户端

    本章不会直接分析Netty源码,而是通过使用Netty的能力实现一个自定义协议的服务器和客户端.通过这样的实践,可以更深刻地理解Netty的相关代码,同时可以了解,在设计实现自定义协议的过程中需要解决 ...

  3. Netty源码分析第8章(高性能工具类FastThreadLocal和Recycler)---->第4节: recycler中获取对象

    Netty源码分析第八章: 高性能工具类FastThreadLocal和Recycler 第四节: recycler中获取对象 这一小节剖析如何从对象回收站中获取对象: 我们回顾上一小节demo的ma ...

  4. Netty 源码(一)Netty 组件简介

    Netty 源码(一)Netty 组件简介 Netty 系列目录(https://www.cnblogs.com/binarylei/p/10117436.html) 一.Netty 架构 Core: ...

  5. 阿里P7终于讲完了JDK+Spring+mybatis+Dubbo+SpringMvc+Netty源码

    前言 这里普及一下,每个公司都有职别定级系统,阿里也是,技术岗以 P 定级,一般校招 P5, 社招 P6 起.其实阅读源码也是有很多诀窍的,这里分享几点心得: 首先要会用.你要知道这个库是干什么的,掌 ...

  6. 脚踏实地的Netty源码研究笔记——开篇

    1. 脚踏实地的Netty源码研究笔记--开篇 1.1. Netty介绍 Netty是一个老牌的高性能网络框架.在众多开源框架中都有它的身影,比如:grpc.dubbo.seata等. 里面有着非常多 ...

  7. Netty源码阅读(一) ServerBootstrap启动

    Netty源码阅读(一) ServerBootstrap启动 转自我的Github Netty是由JBOSS提供的一个java开源框架.Netty提供异步的.事件驱动的网络应用程序框架和工具,用以快速 ...

  8. Netty 源码 ChannelHandler(四)编解码技术

    Netty 源码 ChannelHandler(四)编解码技术 Netty 系列目录(https://www.cnblogs.com/binarylei/p/10117436.html) 一.拆包与粘 ...

  9. Netty 源码 ChannelHandler(三)概述

    Netty 源码 ChannelHandler(三)概述 Netty 系列目录(https://www.cnblogs.com/binarylei/p/10117436.html) 一.Channel ...

随机推荐

  1. 获取验证码效果和后台代码(js+html+cs)

    客户端js+html代码 <script type="text/javascript"> var tcode = 0;//定时器返回代码 //获得验证码 functio ...

  2. Apache虚拟主机/端口多开

    Apache就是强大啊,简单配置一下就可以再开启另一个端口的web服务. 笔者最近使用XAMPP架设php服务端.有一些特别的需求:同样的代码,需要开始不同的端口, 协议类型提供web服务给客户端(h ...

  3. program by the way......

    ostrich birds fruit apple constructor height weight method overload override base sub inherit extend ...

  4. [django] Deploy Django Applications Using uWSGI and Nginx on Ubuntu 14.04

    关键点1:chmod-socket=666 (mysite_uwsgi.ini) 关键点2 : 工程目录和虚拟环境目录搞清楚 几个参考: http://uwsgi-docs.readthedocs.i ...

  5. delphi 数据库技术沉浮录--谨给成为历史的BDE

    2014年9月,delphi xe7 出来了,这次在数据库技术方面,彻底抛掉了从1995 年 delphi 1.0 就自带的(Borland Database Engine)数据库访问技术.从而宣告了 ...

  6. servlet-cookie

    /**  * Cookie学习;  *         作用:解决了发送的不同请求的数据共享问题  *         使用:  *         1.Cookie的创建和存储  *         ...

  7. 2018.09.07 loj#10166 数字游戏(数位dp)

    传送门 数位dp板子题. f[i][mod]" role="presentation" style="position: relative;"> ...

  8. [GO]kafka的生产者和消费者

    生产者: package main import ( "github.com/Shopify/sarama" "fmt" "time" ) ...

  9. js数组合并(一个数组添加到另一个数组里面)方法

    js定义两个数组. var arrA=[1,2,3]; var arrB=[4,5,6]; 要实现[1,2,3,4,5,6],如果直接arrA.push(arrB); 则arrB只会作为了arrA的一 ...

  10. python编码(七)

    本文中,以'哈'来解释作示例解释所有的问题,“哈”的各种编码如下: 1. UNICODE (UTF8-16),C854:2. UTF-8,E59388:3. GBK,B9FE. 一.python中的s ...