6. 协议设计与解析
6.1 为什么需要协议?
TCP/IP 中消息传输基于流的方式,没有边界。
协议的目的就是划定消息的边界,制定通信双方要共同遵守的通信规则
例如:在网络上传输
下雨天留客天留我不留
是中文一句著名的无标点符号句子,在没有标点符号情况下,这句话有数种拆解方式,而意思却是完全不同,所以常被用作讲述标点符号的重要性
一种解读
下雨天留客,天留,我不留
另一种解读
下雨天,留客天,留我不?留
如何设计协议呢?其实就是给网络传输的信息加上“标点符号”。但通过分隔符来断句不是很好,因为分隔符本身如果用于传输,那么必须加以区分。因此,下面一种协议较为常用
定长字节表示内容长度 + 实际内容
例如,假设一个中文字符长度为 3,按照上述协议的规则,发送信息方式如下,就不会被接收方弄错意思了
0f下雨天留客
06天留
09我不留
6.2 redis 协议举例
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;
/**
* @author chenxc
* @date 2021/9/7 22:26
*/
@Slf4j
public class TestRedis {
public static void main(String[] args) {
//回车、换行
NioEventLoopGroup worker = new NioEventLoopGroup();
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) {
ch.pipeline().addLast(new LoggingHandler());
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
// 会在连接 channel 建立成功后,会触发 active 事件
@Override
public void channelActive(ChannelHandlerContext ctx) {
set(ctx);
get(ctx);
}
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("$3".getBytes());
buf.writeBytes(LINE);
buf.writeBytes("aaa".getBytes());
buf.writeBytes(LINE);
ctx.writeAndFlush(buf);
}
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("$3".getBytes());
buf.writeBytes(LINE);
buf.writeBytes("aaa".getBytes());
buf.writeBytes(LINE);
buf.writeBytes("$3".getBytes());
buf.writeBytes(LINE);
buf.writeBytes("bbb".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();
}
}
}
6.3 http 协议举例
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.http.*;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import lombok.extern.slf4j.Slf4j;
import java.nio.charset.StandardCharsets;
/**
* @author chenxc
* @date 2021/9/1 22:48
*/
@Slf4j
public class TestHttp {
public static void main(String[] args){
final NioEventLoopGroup boos = new NioEventLoopGroup(1);
final NioEventLoopGroup worker = new NioEventLoopGroup();
try {
final ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(boos,worker);
bootstrap.channel(NioServerSocketChannel.class);
bootstrap.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
//添加http协议 编、解码器
ch.pipeline().addLast(new HttpServerCodec());
//只有消息的类型为HttpRequest才会进入这个handler中
ch.pipeline().addLast(new SimpleChannelInboundHandler<HttpRequest>() {
@Override
protected void channelRead0(ChannelHandlerContext ctx, HttpRequest msg) throws Exception {
// 获取请求
log.debug("{}",msg.uri()); //请求地址
log.debug("{}",msg.headers()); //请求头
//返回响应
DefaultFullHttpResponse response = new DefaultFullHttpResponse(msg.protocolVersion(), HttpResponseStatus.OK);
final byte[] bytes = "<h1>Hello world</h1>".getBytes(StandardCharsets.UTF_8);
response.headers().setInt(HttpHeaderNames.CONTENT_LENGTH,bytes.length);
response.content().writeBytes(bytes);
ctx.writeAndFlush(response);
}
});
/*ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (msg instanceof HttpRequest){ //请求行 请求头
}else if (msg instanceof HttpContent){ //请求体
}
}
});*/
}
});
ChannelFuture channelFuture = bootstrap.bind(8080).sync();
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
log.error("server error {}", e.getLocalizedMessage());
} finally {
boos.shutdownGracefully();
worker.shutdownGracefully();
}
}
}
6.4 自定义协议要素
- 魔数,用来在第一时间判定是否是无效数据包
- 版本号,可以支持协议的升级
- 序列化算法,消息正文到底采用哪种序列化反序列化方式,可以由此扩展,例如:json、protobuf、hessian、jdk
- 指令类型,是登录、注册、单聊、群聊… 跟业务相关
- 请求序号,为了双工通信,提供异步能力
- 正文长度
- 消息正文
编解码器
根据上面的要素,设计一个登录请求消息和登录响应消息,并使用 Netty 完成收发
import com.itcxc.message.Message;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToMessageCodec;
import lombok.extern.slf4j.Slf4j;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.List;
/**
* 必须和LengthFieldBasedFrameDecoder一起使用,确保接到的byteBuf消息是完整的
* @author chenxc
* @date 2021/9/9 23:35
*/
@Slf4j
@ChannelHandler.Sharable
public class MessageCodecSharable extends MessageToMessageCodec<ByteBuf, Message> {
@Override
protected void encode(ChannelHandlerContext ctx, Message msg, List<Object> outList) throws Exception {
final ByteBuf out = ctx.alloc().buffer();
//1、 5个字节的魔术
out.writeBytes(new byte[]{'i','t','c','x','c'});
//2、 1个字节的版本
out.writeByte(1);
//3、 1个字节的系列化算法 jdk-0 json-1
out.writeByte(0);
//4、 1个字节的指令类型
out.writeByte(msg.getMessageType());
//5、 4个字节的请求序列
out.writeInt(msg.getSequenceId());
// 系列化对象
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(msg);
final byte[] bytes = bos.toByteArray();
//6、 4个字节的正文长度
out.writeInt(bytes.length);
//7、 正文
out.writeBytes(bytes);
outList.add(out);
}
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
byte[] magcNum = new byte[5];
in.readBytes(magcNum,0, 5);
String res = new String(magcNum);
byte version = in.readByte();
byte serializableType = in.readByte();
byte messageType = in.readByte();
int sequenceId = in.readInt();
int length = in.readInt();
byte[] bytes = new byte[length];
in.readBytes(bytes,0, length);
Message msg = null;
if (serializableType == 0){
ByteArrayInputStream bis = new ByteArrayInputStream(bytes);
ObjectInputStream ois = new ObjectInputStream(bis);
msg = (Message) ois.readObject();
}
log.debug("{},{},{},{},{},{}",res,version,serializableType,messageType,sequenceId,length);
log.debug("{}",msg);
out.add(msg);
}
}
测试
import com.itcxc.message.LoginRequestMessage;
import io.netty.buffer.ByteBuf;
import io.netty.channel.embedded.EmbeddedChannel;
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
/**
* @author chenxc
* @date 2021/9/8 22:45
*/
public class TestMessageCodec {
public static void main(String[] args) {
EmbeddedChannel channel = new EmbeddedChannel(
new LoggingHandler(LogLevel.DEBUG),
new LengthFieldBasedFrameDecoder(1024,12,4,0,0),
new MessageCodec());
LoginRequestMessage message = new LoginRequestMessage("shangsan","123");
channel.writeOutbound(message);
ByteBuf buf = channel.readOutbound();
//半包测试
final ByteBuf buf1 = buf.slice(0, 100);
buf1.retain();
final ByteBuf buf2 = buf.slice(100, buf.readableBytes() - 100);
buf2.retain();
channel.writeInbound(buf); //引用指数会减1
channel.writeInbound(buf1);
channel.writeInbound(buf2);
}
}
解读
💡 什么时候可以加 @Sharable
- 当 handler 不保存状态时,就可以安全地在多线程下被共享
- 但要注意对于编解码器类,不能继承 ByteToMessageCodec 或 CombinedChannelDuplexHandler 父类,他们的构造方法对 @Sharable 有限制
- 如果能确保编解码器不会保存状态,可以继承 MessageToMessageCodec 父类
package com.itcxc.protocol;
import com.itcxc.message.Message;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToMessageCodec;
import lombok.extern.slf4j.Slf4j;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.List;
/**
* 必须和LengthFieldBasedFrameDecoder一起使用,确保接到的byteBuf消息是完整的
* @author chenxc
* @date 2021/9/9 23:35
*/
@Slf4j
@ChannelHandler.Sharable
public class MessageCodecSharable extends MessageToMessageCodec<ByteBuf, Message> {
@Override
protected void encode(ChannelHandlerContext ctx, Message msg, List<Object> outList) throws Exception {
final ByteBuf out = ctx.alloc().buffer();
//1、 5个字节的魔术
out.writeBytes(new byte[]{'i','t','c','x','c'});
//2、 1个字节的版本
out.writeByte(1);
//3、 1个字节的系列化算法 jdk-0 json-1
out.writeByte(0);
//4、 1个字节的指令类型
out.writeByte(msg.getMessageType());
//5、 4个字节的请求序列
out.writeInt(msg.getSequenceId());
// 系列化对象
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(msg);
final byte[] bytes = bos.toByteArray();
//6、 4个字节的正文长度
out.writeInt(bytes.length);
//7、 正文
out.writeBytes(bytes);
outList.add(out);
}
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
byte[] magcNum = new byte[5];
in.readBytes(magcNum,0, 5);
String res = new String(magcNum);
byte version = in.readByte();
byte serializableType = in.readByte();
byte messageType = in.readByte();
int sequenceId = in.readInt();
int length = in.readInt();
byte[] bytes = new byte[length];
in.readBytes(bytes,0, length);
Message msg = null;
if (serializableType == 0){
ByteArrayInputStream bis = new ByteArrayInputStream(bytes);
ObjectInputStream ois = new ObjectInputStream(bis);
msg = (Message) ois.readObject();
}
//log.debug("{},{},{},{},{},{}",res,version,serializableType,messageType,sequenceId,length);
//log.debug("{}",msg);
out.add(msg);
}
}