編寫自己的handler簡單操作redis
阿新 • • 發佈:2021-08-11
RESP是Redis Serialization Protocol的簡稱,也就是專門為redis設計的一套序列化協議。這個協議比較簡單,簡單的說就是傳送請求的時候按Redis 約定的資料格式進行傳送,解析資料的時候按redis規定的響應資料格式進行相應。
1.RedisClient 類
package cn.xm.netty.example.redis3; import io.netty.bootstrap.Bootstrap; import io.netty.channel.*; import io.netty.channel.nio.NioEventLoopGroup; importio.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioSocketChannel; import io.netty.util.concurrent.GenericFutureListener; import java.io.BufferedReader; import java.io.InputStreamReader; public class RedisClient { private static final String HOST = System.getProperty("host", "192.168.145.139");private static final int PORT = Integer.parseInt(System.getProperty("port", "6379")); public static void main(String[] args) throws Exception { EventLoopGroup group = new NioEventLoopGroup(); try { Bootstrap b = new Bootstrap(); b.group(group) .channel(NioSocketChannel.class) .handler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel ch) throws Exception { ChannelPipeline p = ch.pipeline(); p.addLast(new RedisClientHandler()); } }); // Start the connection attempt. Channel ch = b.connect(HOST, PORT).sync().channel(); // Read commands from the stdin. System.out.println("Enter Redis commands (quit to end)"); ChannelFuture lastWriteFuture = null; BufferedReader in = new BufferedReader(new InputStreamReader(System.in)); for (; ; ) { final String input = in.readLine(); final String line = input != null ? input.trim() : null; if (line == null || "quit".equalsIgnoreCase(line)) { // EOF or "quit" ch.close().sync(); break; } else if (line.isEmpty()) { // skip `enter` or `enter` with spaces. continue; } // Sends the received line to the server. lastWriteFuture = ch.writeAndFlush(line); lastWriteFuture.addListener(new GenericFutureListener<ChannelFuture>() { @Override public void operationComplete(ChannelFuture future) throws Exception { if (!future.isSuccess()) { System.err.print("write failed: "); future.cause().printStackTrace(System.err); } } }); } // Wait until all messages are flushed before closing the channel. if (lastWriteFuture != null) { lastWriteFuture.sync(); } } finally { group.shutdownGracefully(); } } }
這個類比較簡單就是一直for 迴圈等待控制檯輸入資料。並且新增的handler 只有一個handler。 也就是輸入輸出都是一個handler。
2.RedisClientHandler
package cn.xm.netty.example.redis3; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelDuplexHandler; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelPromise; import io.netty.util.CharsetUtil; import org.apache.commons.lang3.StringUtils; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; /** * An example Redis client handler. This handler read input from STDIN and write output to STDOUT. */ public class RedisClientHandler extends ChannelDuplexHandler { @Override public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) { // 轉換髮出去的資料格式 msg = rehandleRequest(msg); ctx.writeAndFlush(Unpooled.copiedBuffer(msg.toString(), CharsetUtil.UTF_8)); } /** * 重新處理訊息,處理為 RESP 認可的資料 * set foo bar * 對應下面資料 * *3\r\n$3\r\nSET\r\n$3\r\nfoo\r\n$3\r\nbar\r\n */ private String rehandleRequest(Object msg) { String result = msg.toString().trim(); String[] params = result.split(" "); List<String> allParam = new ArrayList<>(); Arrays.stream(params).forEach(s -> { allParam.add("$" + s.length() + "\r\n" + s + "\r\n"); // 引數前$length\r\n, 引數後增加 \r\n }); allParam.add(0, "*" + allParam.size() + "\r\n"); StringBuilder stringBuilder = new StringBuilder(); allParam.forEach(p -> { stringBuilder.append(p); }); return stringBuilder.toString(); } @Override public void channelRead(ChannelHandlerContext ctx, Object msg) { ByteBuf byteBuf = (ByteBuf) msg; byte[] bytes = new byte[byteBuf.readableBytes()]; byteBuf.readBytes(bytes); String result = new String(bytes); // 轉換接受到的資料格式 result = rehandleResponse(result).toString(); System.out.println(result); } /** * 重新處理響應訊息 */ private Object rehandleResponse(String result) { // 狀態恢復 - “+OK\r\n” if (result.startsWith("+")) { return result.substring(1, result.length() - 2); } // 錯誤回覆(error reply)的第一個位元組是 "-"。例如 `flushallE` 返回的 `-ERR unknown command 'flushallE'\r\n` if (result.startsWith("-")) { return result.substring(1, result.length() - 2); } // 整數回覆(integer reply)的第一個位元組是 ":"。 例如 `llen mylist` 檢視list 大小返回的 `:3\r\n` if (result.startsWith(":")) { return result.substring(1, result.length() - 2); } // 批量回復(bulk reply)的第一個位元組是 "$", 例如: `get foo` 返回的結果為 `$3\r\nbar\r\n` if (result.startsWith("$")) { result = StringUtils.substringAfter(result, "\r\n"); return StringUtils.substringBeforeLast(result, "\r\n"); } // 多條批量回復(multi bulk reply)的第一個位元組是 "*", 例如: *2\r\n$3\r\nfoo\r\n$4\r\nname\r\n if (result.startsWith("*")) { result = StringUtils.substringAfter(result, "\r\n"); String[] split = result.split("\\$\\d\r\n"); List<String> collect = Arrays.stream(split).filter(tmpStr -> StringUtils.isNotBlank(tmpStr)).collect(Collectors.toList()); List<String> resultList = new ArrayList<>(collect.size()); collect.forEach(str1 -> { resultList.add(StringUtils.substringBeforeLast(str1, "\r\n")); }); return resultList; } return "unknow result"; } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { System.err.print("exceptionCaught: "); cause.printStackTrace(System.err); ctx.close(); } }
這個handler 繼承ChannelDuplexHandler 複用處理器,也就是既可以作為入站處理讀取資料,也可以作為出站處理輸出資料。輸出和輸入的時候都是根據redis 的協議標準進行了一下訊息的轉換。
3. 測試
1. 用strace 啟動redis, 監測呼叫指令
[root@localhost redistest]# strace -ff -o out ../redis-5.0.4/src/redis-server ../redis-5.0.4/redis.conf
2. 命令列測試結果如下
3. 從out檔案檢視redis 接收的命令:
[root@localhost redistest]# grep mytest ./* ./out.8384:read(8, "*3\r\n$3\r\nset\r\n$9\r\nmytestkey\r\n$11\r"..., 16384) = 46 ./out.8384:read(8, "*3\r\n$3\r\nttl\r\n$9\r\nmytestkey\r\n$7\r\n"..., 16384) = 41 ./out.8384:read(8, "*3\r\n$6\r\nexpire\r\n$9\r\nmytestkey\r\n$"..., 16384) = 43 ./out.8384:read(8, "*2\r\n$3\r\nttl\r\n$9\r\nmytestkey\r\n", 16384) = 28 ./out.8384:read(8, "*2\r\n$4\r\ntype\r\n$9\r\nmytestkey\r\n", 16384) = 29
注意: redis 接收到的資料必須以\r\n 結尾,否則redis 不會進行響應。
【當你用心寫完每一篇部落格之後,你會發現它比你用程式碼實現功能更有成就感!】