基于 Netty 的 Lettuce 居然是這樣解析RESP協(xié)議的
今天來分享 Lettuce —— 基于 Netty 實(shí)現(xiàn),Springboot2 中默認(rèn)的 redis 客戶端。
那它是不是直接用 Netty 中的那幾個(gè) handler 來處理 RESP 協(xié)議的呢?一起看看吧。
可以看到這里并沒有 codec-redis 模塊,所以 Lettuce 并沒有使用 Netty 提供的 redis 模塊。
圖片
(⊙﹏⊙),問題解決得太快了,那就再來思考下,它是怎么做的呢?
既然 Lettuce 基于 Netty 實(shí)現(xiàn),那么它必然在 ChannelHandler 上動(dòng)手腳,直接搜索可以發(fā)現(xiàn)有 9 個(gè)實(shí)現(xiàn)類。
圖片
這里我關(guān)心的就是它怎么編解碼,所以直接來看 CommandEncoder 和 CommandHandler 。
打上斷點(diǎn),使用測(cè)試?yán)又苯?debug。
代碼
@Test
void redisTest() {
// 創(chuàng)建 redis 客戶端
RedisClient redisClient = RedisClient.create("redis://123456@192.168.200.128:6379/0");
// 創(chuàng)建 channel
StatefulRedisConnection<String, String> connection = redisClient.connect();
// 使用 sync 同步命令
RedisCommands<String, String> syncCommands = connection.sync();
String name = syncCommands.get("name");
System.out.println(name);
// syncCommands.set("key", "Hello, Redis!");
connection.close();
redisClient.shutdown();
}
剛開始時(shí),要和服務(wù)器建立連接,發(fā)送數(shù)據(jù),涉及到 encode 流程。
CommandHandler
圖片
如圖,直接來到 nioEventLoop 線程,并調(diào)用了 write 方法。
write:382, CommandHandler (io.lettuce.core.protocol)
從右邊可以看到,發(fā)了一個(gè) HELLO 的命令出去,其中 CommandArgs 如下:
CommandArgs [buffer=$1
3
$4
AUTH
$7
default
$6
123456
]
CommandArgs?
直接來到 toString 方法,可以發(fā)現(xiàn) encode 方法。
圖片
如圖,有 4 個(gè) SingularArgument:
圖片
看看他們是怎么 encode 的 。
ProtocolKeywordArgument
圖片
StringArgument
圖片
對(duì)比 Netty
圖片
貌似沒啥大的區(qū)別,可以看到 Lettuce 中,對(duì) ByteBuf 的使用比較粗一些,Netty 中會(huì)計(jì)算這個(gè) ByteBuf 的初始容量,而 Lettuce 就簡(jiǎn)單些處理,直接 singularArguments.size() * 10 。
還有一個(gè) 大小端序 的處理,只能說 Netty 太細(xì)了。
圖片
CommandEncoder
直接 F9 來到這一個(gè)斷點(diǎn)。
圖片
繼續(xù) debug ,會(huì)來到 Command 類,在這里完成對(duì)發(fā)送數(shù)據(jù)的 encode。
圖片
解析下要發(fā)送的數(shù)據(jù)。
圖片
小結(jié)
那么到了這里,我們就了解完 encode 的實(shí)現(xiàn)了。
核心:CommandArgs 中的各種 SingularArgument
圖片
下面就是接受服務(wù)器數(shù)據(jù),進(jìn)行 decode 的流程了。
CommandHandler
來到 channelRead 。
圖片
decode 時(shí),會(huì)調(diào)用到 RedisStateMachine 的 decode ,它是這個(gè)流程的核心。
圖片
RedisStateMachine?
Redis 狀態(tài)機(jī):
圖片
這里我直接 copy 了一份 。
static class State {
// Callback interface to handle a {@link State}.
@FunctionalInterface
interface StateHandler {
Result handle(RedisStateMachine rsm, State state, ByteBuf buffer, CommandOutput<?, ?, ?> output,
Consumer<Exception> errorHandler);
}
enum Type implements StateHandler {
SINGLE('+', RedisStateMachine::handleSingle),
ERROR('-', RedisStateMachine::handleError),
INTEGER(':', RedisStateMachine::handleInteger),
// 下面開始都是 @since 6.0/RESP3
FLOAT(',', RedisStateMachine::handleFloat),
BOOLEAN('#', RedisStateMachine::handleBoolean),
BULK_ERROR('!', RedisStateMachine::handleBulkError),
VERBATIM('=', RedisStateMachine::handleBulkAndVerbatim), VERBATIM_STRING('=', RedisStateMachine::handleVerbatim),
BIG_NUMBER('(', RedisStateMachine::handleBigNumber),
MAP('%', RedisStateMachine::handleMap),
SET('~', RedisStateMachine::handleSet),
ATTRIBUTE('|', RedisStateMachine::handleAttribute),
PUSH('>', RedisStateMachine::handlePushAndMulti),
HELLO_V3('@', RedisStateMachine::handleHelloV3),
NULL('_', RedisStateMachine::handleNull),
BULK('$', RedisStateMachine::handleBulkAndVerbatim),
MULTI('*', RedisStateMachine::handlePushAndMulti), BYTES('*', RedisStateMachine::handleBytes);
final byte marker;
private final StateHandler behavior;
Type(char marker, StateHandler behavior) {
this.marker = (byte) marker;
this.behavior = behavior;
}
@Override
public Result handle(RedisStateMachine rsm, State state, ByteBuf buffer, CommandOutput<?, ?, ?> output,
Consumer<Exception> errorHandler) {
return behavior.handle(rsm, state, buffer, output, errorHandler);
}
}
enum Result {
NORMAL_END, BREAK_LOOP, CONTINUE_LOOP
}
Type type = null;
int count = NOT_FOUND;
@Override
public String toString() {
final StringBuffer sb = new StringBuffer();
sb.append(getClass().getSimpleName());
sb.append(" [type=").append(type);
sb.append(", count=").append(count);
sb.append(']');
return sb.toString();
}
}
繼續(xù) debug,會(huì)來到 doDecode 方法。
這里有兩個(gè)核心步驟:
- 根據(jù)讀取到的第一個(gè)字節(jié),判斷是不是 RESP3。
- 調(diào)用 狀態(tài)機(jī) 中的 State.Type 枚舉類,處理 handle。
這里先手動(dòng)解析下服務(wù)器返回的數(shù)據(jù)。
ByteBufUtil.decodeString(buffer,0,146, Charset.defaultCharset());
%7
$6
server
$5
redis
$7
version
$6
6.0.12
$5
proto
:3
$2
id
:74
$4
mode
$10
standalone
$4
role
$6
master
$7
modules
*0
handleMap
%7 對(duì)應(yīng)的 handler 處理。
圖片
后面就進(jìn)入 狀態(tài)機(jī) 流程判斷了,上面我們拿到的數(shù)據(jù)要循環(huán)好久,就不一一列舉出來了。
$6 對(duì)應(yīng)的 handler 處理。
圖片
最后解析出來剛好 7 個(gè),可以對(duì)比上面手動(dòng)解析的結(jié)果驗(yàn)證下。
圖片
小結(jié)
到了這里,decode 的流程也完畢了,畫個(gè)圖總結(jié)下??。
圖片
結(jié)尾
Lettuce 的 decode 依賴于 狀態(tài)機(jī) RedisStateMachine 實(shí)現(xiàn),encode 靠 SingularArgument 實(shí)現(xiàn)。
圖片
這次我做了兩種嘗試:
- 按以往的方式,從測(cè)試?yán)娱_始 debug。
- 思考下框架的特性,直奔主題。
兩種方式都收獲頗豐,但第二種嘗試得比較少,以后可以多多實(shí)踐,站在不同的角度去思考問題。