Netty類比redis伺服器

來源:互聯網
上載者:User

標籤:

Redis的用戶端與服務端採用叫做 RESP(Redis Serialization Protocol)的網路通訊協定交換資料,QKxue.NET用戶端和伺服器通過 TCP 串連來進行資料互動, 伺服器預設的連接埠號碼為 6379 。用戶端和伺服器發送的命令或資料一律以 \r\n (CRLF)結尾。

RESP支援五種資料類型:

狀態回複(status reply):以“+”開頭,表示正確的狀態資訊,”+”後就是具體資訊?,比如:

redis 127.0.0.1:6379> set ss sdf
OK
其實它真正回複的資料是:+OK\r\n
錯誤回複(error reply):以”-“開頭,表示錯誤的狀態資訊,”-“後就是具體資訊,比如:

redis 127.0.0.1:6379> incr ss
(error) ERR value is not an integer or out of range
整數回複(integer reply):以”:”開頭,表示對某些操作的回複比如DEL, EXISTS, INCR等等

redis 127.0.0.1:6379> incr aa
(integer) 1
批量回複(bulk reply):以”$”開頭,表示下一行的字串長度,具體字串在下一行中

多條批量回複(multi bulk reply):以”*”開頭,表示訊息體總共有多少行(不包括當前行)”*”是具體行數

redis 127.0.0.1:6379> get ss
"sdf"

用戶端->伺服器
*2\r\n
$3\r\n
get\r\n
$2\r\n
ss\r\n
伺服器->用戶端
$3\r\n
sdf\r\n
註:騰雲科技TY300.COM以上寫的都是XX回複,並不是說協議格式只是適用於伺服器->用戶端,用戶端->伺服器端也同樣使用以上協議格式,其實雙端協議格式的統一更加方便擴充

回到正題,我們這裡是通過netty來類比redis伺服器,可以整理一下思路大概分為這麼幾步:

1.需要一個底層的通訊架構,這裡選擇的是netty4.0.25
2.需要對用戶端穿過來的資料進行解碼(Decoder),其實就是分別處理以上5種資料類型
3.解碼以後我們封裝成更加利於理解的命令(Command),比如:set<name> foo hello<params>
4.有了命令以後就是處理命令(execute),其實我們可以去串連正在的redis伺服器,不過這裡只是簡單的類比
5.處理完之後就是封裝回複(Reply),然後編碼(Encoder),需要根據不同的命令分別返回以後5種資料類型
6.測實驗證,通過redis-cli去串連netty類比的redis伺服器,看能否返回正確的結果
以上思路參考github上的一個項目:https://github.com/spullara/redis-protocol,測試代碼也是在此基礎上做了一個簡化

第一步:通訊架構netty

<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.0.25.Final</version>
</dependency>
第二步:資料類型解碼

public class RedisCommandDecoder extends ReplayingDecoder<Void> {

public static final char CR = ‘\r‘;
public static final char LF = ‘\n‘;

public static final byte DOLLAR_BYTE = ‘$‘;
public static final byte ASTERISK_BYTE = ‘*‘;

private byte[][] bytes;
private int arguments = 0;

@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in,
List<Object> out) throws Exception {
if (bytes != null) {
int numArgs = bytes.length;
for (int i = arguments; i < numArgs; i++) {
if (in.readByte() == DOLLAR_BYTE) {
int l = RedisReplyDecoder.readInt(in);
if (l > Integer.MAX_VALUE) {
throw new IllegalArgumentException(
"Java only supports arrays up to "
+ Integer.MAX_VALUE + " in size");
}
int size = (int) l;
bytes[i] = new byte[size];
in.readBytes(bytes[i]);
if (in.bytesBefore((byte) CR) != 0) {
throw new RedisException("Argument doesn‘t end in CRLF");
}
// Skip CRLF(\r\n)
in.skipBytes(2);
arguments++;
checkpoint();
} else {
throw new IOException("Unexpected character");
}
}
try {
out.add(new Command(bytes));
} finally {
bytes = null;
arguments = 0;
}
} else if (in.readByte() == ASTERISK_BYTE) {
int l = RedisReplyDecoder.readInt(in);
if (l > Integer.MAX_VALUE) {
throw new IllegalArgumentException(
"Java only supports arrays up to " + Integer.MAX_VALUE
+ " in size");
}
int numArgs = (int) l;
if (numArgs < 0) {
throw new RedisException("Invalid size: " + numArgs);
}
bytes = new byte[numArgs][];
checkpoint();
decode(ctx, in, out);
} else {
in.readerIndex(in.readerIndex() - 1);
byte[][] b = new byte[1][];
b[0] = in.readBytes(in.bytesBefore((byte) CR)).array();
in.skipBytes(2);
out.add(new Command(b, true));
}
}

}
首先通過接受到以“*”開頭的多條批量類型初始化二維數組byte[][] bytes,以讀取到第一個以\r\n結尾的資料作為數組的長度,然後再處理以“$”開頭的批量類型。
以上除了處理我們熟悉的批量和多條批量類型外,還處理了沒有任何標識的資料,其實有一個專門的名字叫Inline命令:
有些時候僅僅是telnet串連Redis服務,或者是僅僅向Redis服務發送一個命令進行檢測(勤快學QKXue.NET)。雖然Redis協議可以很容易的實現,但是使用Interactive sessions 並不理想,而且redis-cli也不總是可以使用。基於這些原因,Redis支援特殊的命令來實現上面描述的情況。這些命令的設計是很人性化的,被稱作Inline 命令。

第三步:封裝command對象

由第二步中可以看到不管是commandName還是params都統一放在了位元組二維數組裡面,最後封裝在command對象裡面

public class Command {
public static final byte[] EMPTY_BYTES = new byte[0];

private final Object name;
private final Object[] objects;
private final boolean inline;

public Command(Object[] objects) {
this(null, objects, false);
}

public Command(Object[] objects, boolean inline) {
this(null, objects, inline);
}

private Command(Object name, Object[] objects, boolean inline) {
this.name = name;
this.objects = objects;
this.inline = inline;
}

public byte[] getName() {
if (name != null)
return getBytes(name);
return getBytes(objects[0]);
}

public boolean isInline() {
return inline;
}

private byte[] getBytes(Object object) {
byte[] argument;
if (object == null) {
argument = EMPTY_BYTES;
} else if (object instanceof byte[]) {
argument = (byte[]) object;
} else if (object instanceof ByteBuf) {
argument = ((ByteBuf) object).array();
} else if (object instanceof String) {
argument = ((String) object).getBytes(Charsets.UTF_8);
} else {
argument = object.toString().getBytes(Charsets.UTF_8);
}
return argument;
}

public void toArguments(Object[] arguments, Class<?>[] types) {
for (int position = 0; position < types.length; position++) {
if (position >= arguments.length) {
throw new IllegalArgumentException(
"wrong number of arguments for ‘"
+ new String(getName()) + "‘ command");
}
if (objects.length - 1 > position) {
arguments[position] = objects[1 + position];
}
}
}

}
所有的資料都放在了Object數組裡面,而且可以通過getName方法知道Object[0]就是commandName

第四步:執行命令

在經曆瞭解碼和封裝之後,下面需要實現handler類,用來處理訊息

public class RedisCommandHandler extends SimpleChannelInboundHandler<Command> {

private Map<String, Wrapper> methods = new HashMap<String, Wrapper>();

interface Wrapper {
Reply<?> execute(Command command) throws RedisException;
}

public RedisCommandHandler(final RedisServer rs) {
Class<? extends RedisServer> aClass = rs.getClass();
for (final Method method : aClass.getMethods()) {
final Class<?>[] types = method.getParameterTypes();
methods.put(method.getName(), new Wrapper() {
@Override
public Reply<?> execute(Command command) throws RedisException {
Object[] objects = new Object[types.length];
try {
command.toArguments(objects, types);
return (Reply<?>) method.invoke(rs, objects);
} catch (Exception e) {
return new ErrorReply("ERR " + e.getMessage());
}
}
});
}
}

@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.flush();
}

@Override
protected void channelRead0(ChannelHandlerContext ctx, Command msg)
throws Exception {
String name = new String(msg.getName());
Wrapper wrapper = methods.get(name);
Reply<?> reply;
if (wrapper == null) {
reply = new ErrorReply("unknown command ‘" + name + "‘");
} else {
reply = wrapper.execute(msg);
}
if (reply == StatusReply.QUIT) {
ctx.close();
} else {
if (msg.isInline()) {
if (reply == null) {
reply = new InlineReply(null);
} else {
reply = new InlineReply(reply.data());
}
}
if (reply == null) {
reply = ErrorReply.NYI_REPLY;
}
ctx.write(reply);
}
}
}
在執行個體化handler的時候傳入了一個RedisServer對象,這個方法是真正用來處理redis命令的,理論上這個對象應該支援redis的所有命令,不過這裡只是測試所有只提供了2個方法:

public interface RedisServer {

public BulkReply get(byte[] key0) throws RedisException;

public StatusReply set(byte[] key0, byte[] value1) throws RedisException;

}
在channelRead0方法中我們可以拿到之前封裝好的command方法,然後通過命令名稱執行操作,這裡的RedisServer也很簡單,只是用簡單的hashmap進行臨時的儲存資料。

第五步:封裝回複

第四步種我們可以看到處理完命令之後,返回了一個Reply對象

public interface Reply<T> {

byte[] CRLF = new byte[] { RedisReplyDecoder.CR, RedisReplyDecoder.LF };

T data();

void write(ByteBuf os) throws IOException;
}
根據上面提到的5種類型再加上一個inline命令,根據不同的資料格式進行拼接,比如StatusReply:

public void write(ByteBuf os) throws IOException {
os.writeByte(‘+‘);
os.writeBytes(statusBytes);
os.writeBytes(CRLF);
}
所以對應Decoder的Encoder就很簡單了:

public class RedisReplyEncoder extends MessageToByteEncoder<Reply<?>> {
@Override
public void encode(ChannelHandlerContext ctx, Reply<?> msg, ByteBuf out)
throws Exception {
msg.write(out);
}
}
只需要將封裝好的Reply返回給用戶端就行了

最後一步:測試

啟動類:

public class Main {
private static Integer port = 6379;

public static void main(String[] args) throws InterruptedException {
final RedisCommandHandler commandHandler = new RedisCommandHandler(
new SimpleRedisServer());

ServerBootstrap b = new ServerBootstrap();
final DefaultEventExecutorGroup group = new DefaultEventExecutorGroup(1);
try {
b.group(new NioEventLoopGroup(), new NioEventLoopGroup())
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 100).localAddress(port)
.childOption(ChannelOption.TCP_NODELAY, true)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch)
throws Exception {
ChannelPipeline p = ch.pipeline();
p.addLast(new RedisCommandDecoder());
p.addLast(new RedisReplyEncoder());
p.addLast(group, commandHandler);
}
});

ChannelFuture f = b.bind().sync();
f.channel().closeFuture().sync();
} finally {
group.shutdownGracefully();
}
}
}
ChannelPipeline分別添加了RedisCommandDecoder、RedisReplyEncoder和RedisCommandHandler,同時我們啟動的連接埠和Redis伺服器連接埠是一樣的也是6379

開啟redis-cli程式:

redis 127.0.0.1:6379> get dsf
(nil)
redis 127.0.0.1:6379> set dsf dsfds
OK
redis 127.0.0.1:6379> get dsf
"dsfds"
redis 127.0.0.1:6379>
從結果可以看出和正常使用redis伺服器沒有差別

總結

這樣做的意義其實就是可以把它當做一個redis代理,由這個Proxy 伺服器去進行sharding處理,用戶端不直接存取redis伺服器,對用戶端來說,後台redis叢集是完全透明的。

Netty類比redis伺服器

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.