Skip to content

通信协议解析

1. 为什么需要协议

TCP/IP 中消息传输基于流的方式,没有边界。协议的目的就是划定消息的边界,制定通信双方要共同遵守的通信规则。例如:在网络上传输:

sh
下雨天留客天留我不留

这句话有数种拆解方式,而意思却是完全不同。
如何设计协议呢?其实就是给网络传输的信息加上“标点符号”。但通过分隔符来断句不是很好,因为分隔符本身如果用于传输,那么必须加以区分。因此,下面一种协议较为常用:

sh
定长字节表示内容长度 + 实际内容

2. 和redis通信

Netty提供了现有的netty-codec-redis的模块中,这里基于原始的byte数组和redis通信,实现基本功能set key value, redis要求内容格式:

sh
# 比如执行set name zhangsan:       
*3          三个字符串组成
$3          第一个字符串长度3
set         内容set
$4          第二个字符串长度4
name        内容name
$8          第三个字符串长度8
zhangsan    内容zhangsan

编写redis客户端实现保存字符串类型数据功能:

java
public static void main(String[] args) {
    NioEventLoopGroup group = new NioEventLoopGroup();
    Bootstrap bootstrap = new Bootstrap();
    bootstrap.group(group);
    bootstrap.channel(NioSocketChannel.class);
    bootstrap.handler(new ChannelInitializer<NioSocketChannel>() {
        @Override
        protected void initChannel(NioSocketChannel ch) throws Exception {
            ch.pipeline().addLast(new LoggingHandler(LogLevel.INFO));
            ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){
                @Override
                public void channelActive(ChannelHandlerContext ctx) throws Exception {
                    ByteBuf buffer = ctx.alloc().buffer();
                    generateData(buffer, "set name zhangsan");
                    ctx.writeAndFlush(buffer);
                }
                @Override
                public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                    ByteBuf byteBuf = (ByteBuf) msg;
                    System.out.println(byteBuf.toString(Charset.defaultCharset()));
                }
            });
        }
    });
    bootstrap.connect(new InetSocketAddress("127.0.0.1", 6379));
}

private static void generateData(ByteBuf buffer, String str) {
    final byte[] nextLine = {13, 10}; // 回车换行符
    String[] commandArray = str.split(" ");
    buffer.writeBytes(("*"+commandArray.length).getBytes());
    // 每写入数据就添加回车换行符
    buffer.writeBytes(nextLine);
    for (int i = 0; i < commandArray.length; i++) {
        String command = commandArray[i];
        buffer.writeBytes(("$"+command.length()).getBytes());
        // 每写入数据就添加回车换行符
        buffer.writeBytes(nextLine);
        buffer.writeBytes(command.getBytes());
        // 每写入数据就添加回车换行符
        buffer.writeBytes(nextLine);
    }
}

运行结果:

sh
17:19:39.309 [nioEventLoopGroup-2-1] INFO  io.netty.handler.logging.LoggingHandler - [id: 0xea217111] REGISTERED
17:19:39.311 [nioEventLoopGroup-2-1] INFO  io.netty.handler.logging.LoggingHandler - [id: 0xea217111] CONNECT: /127.0.0.1:6379
17:19:39.314 [nioEventLoopGroup-2-1] INFO  io.netty.handler.logging.LoggingHandler - [id: 0xea217111, L:/127.0.0.1:49930 - R:/127.0.0.1:6379] ACTIVE
17:19:39.330 [nioEventLoopGroup-2-1] INFO  io.netty.handler.logging.LoggingHandler - [id: 0xea217111, L:/127.0.0.1:49930 - R:/127.0.0.1:6379] WRITE: 37B
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 2a 33 0d 0a 24 33 0d 0a 73 65 74 0d 0a 24 34 0d |*3..$3..set..$4.|
|00000010| 0a 6e 61 6d 65 0d 0a 24 38 0d 0a 7a 68 61 6e 67 |.name..$8..zhang|
|00000020| 73 61 6e 0d 0a                                  |san..           |
+--------+-------------------------------------------------+----------------+
17:19:39.331 [nioEventLoopGroup-2-1] INFO  io.netty.handler.logging.LoggingHandler - [id: 0xea217111, L:/127.0.0.1:49930 - R:/127.0.0.1:6379] FLUSH
17:19:39.333 [nioEventLoopGroup-2-1] INFO  io.netty.handler.logging.LoggingHandler - [id: 0xea217111, L:/127.0.0.1:49930 - R:/127.0.0.1:6379] READ: 5B
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 2b 4f 4b 0d 0a                                  |+OK..           |
+--------+-------------------------------------------------+----------------+
+OK

2. 和http通信

由于Http通信协议比较复杂,Netty里面可以直接使用HttpServerCodec类, 下面是编写Http服务端代码:

java
public static void main(String[] args) throws IOException, ExecutionException, InterruptedException {
    ServerBootstrap bootstrap = new ServerBootstrap();
    NioEventLoopGroup group = new NioEventLoopGroup();
    bootstrap.group(group);
    bootstrap.channel(NioServerSocketChannel.class);
    bootstrap.childHandler(new ChannelInitializer<NioSocketChannel>() {
        @Override
        protected void initChannel(NioSocketChannel ch) throws Exception {
            ch.pipeline().addLast(new LoggingHandler(LogLevel.INFO));
            ch.pipeline().addLast(new HttpServerCodec());
            ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){
                @Override
                public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                    /**
                    * HttpServerCodec 会将请求头和请求体进行传递
                    * 对msg处理之前需要对msg进行判断
                    * 1. 判断是否是 HttpRequest
                    * 2. 判断是否是 HttpContent
                    */
                    if (msg instanceof HttpRequest){
                        logger.info("HttpRequest:{}", msg);
                    }else if (msg instanceof HttpContent){
                        logger.info("HttpContent:{}", msg);
                    }
                    // 响应消息使用DefaultFullHttpResponse,要求指定http协议版本和http状态
                    DefaultFullHttpResponse httpResponse = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
                    httpResponse.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html");
                    byte[] contentBytes = "<html><body><h1>hello netty!</h1></body></html>".getBytes();
                    // 浏览器如果没有收到内容长度,会一直处于转圈状态,认为数据还没接收完毕
                    httpResponse.headers().set(HttpHeaderNames.CONTENT_LENGTH, contentBytes.length);
                    httpResponse.content().writeBytes(contentBytes);
                    ctx.writeAndFlush(httpResponse);
                }
            });
        }
    });
    bootstrap.bind("127.0.0.1", 8080);
}

运行结果:

sh
19:39:33.966 [nioEventLoopGroup-2-5] INFO  io.netty.handler.logging.LoggingHandler - [id: 0x144955e3, L:/127.0.0.1:8080 - R:/127.0.0.1:56628] READ: 824B
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 47 45 54 20 2f 69 6e 64 65 78 2e 68 74 6d 6c 20 |GET /index.html |
|00000010| 48 54 54 50 2f 31 2e 31 0d 0a 48 6f 73 74 3a 20 |HTTP/1.1..Host: |
|00000020| 6c 6f 63 61 6c 68 6f 73 74 3a 38 30 38 30 0d 0a |localhost:8080..|
|00000030| 43 6f 6e 6e 65 63 74 69 6f 6e 3a 20 6b 65 65 70 |Connection: keep|
|00000040| 2d 61 6c 69 76 65 0d 0a 43 61 63 68 65 2d 43 6f |-alive..Cache-Co|
|00000050| 6e 74 72 6f 6c 3a 20 6d 61 78 2d 61 67 65 3d 30 |ntrol: max-age=0|
|00000060| 0d 0a 73 65 63 2d 63 68 2d 75 61 3a 20 22 4e 6f |..sec-ch-ua: "No|
|00000070| 74 20 41 28 42 72 61 6e 64 22 3b 76 3d 22 39 39 |t A(Brand";v="99|
|00000080| 22 2c 20 22 47 6f 6f 67 6c 65 20 43 68 72 6f 6d |", "Google Chrom|
|00000090| 65 22 3b 76 3d 22 31 32 31 22 2c 20 22 43 68 72 |e";v="121", "Chr|
|000000a0| 6f 6d 69 75 6d 22 3b 76 3d 22 31 32 31 22 0d 0a |omium";v="121"..|
|000000b0| 73 65 63 2d 63 68 2d 75 61 2d 6d 6f 62 69 6c 65 |sec-ch-ua-mobile|
|000000c0| 3a 20 3f 30 0d 0a 73 65 63 2d 63 68 2d 75 61 2d |: ?0..sec-ch-ua-|
|000000d0| 70 6c 61 74 66 6f 72 6d 3a 20 22 57 69 6e 64 6f |platform: "Windo|
|000000e0| 77 73 22 0d 0a 55 70 67 72 61 64 65 2d 49 6e 73 |ws"..Upgrade-Ins|
|000000f0| 65 63 75 72 65 2d 52 65 71 75 65 73 74 73 3a 20 |ecure-Requests: |
|00000100| 31 0d 0a 55 73 65 72 2d 41 67 65 6e 74 3a 20 4d |1..User-Agent: M|
|00000110| 6f 7a 69 6c 6c 61 2f 35 2e 30 20 28 57 69 6e 64 |ozilla/5.0 (Wind|
|00000120| 6f 77 73 20 4e 54 20 31 30 2e 30 3b 20 57 69 6e |ows NT 10.0; Win|
|00000130| 36 34 3b 20 78 36 34 29 20 41 70 70 6c 65 57 65 |64; x64) AppleWe|
|00000140| 62 4b 69 74 2f 35 33 37 2e 33 36 20 28 4b 48 54 |bKit/537.36 (KHT|
|00000150| 4d 4c 2c 20 6c 69 6b 65 20 47 65 63 6b 6f 29 20 |ML, like Gecko) |
|00000160| 43 68 72 6f 6d 65 2f 31 32 31 2e 30 2e 30 2e 30 |Chrome/121.0.0.0|
|00000170| 20 53 61 66 61 72 69 2f 35 33 37 2e 33 36 0d 0a | Safari/537.36..|
|00000180| 41 63 63 65 70 74 3a 20 74 65 78 74 2f 68 74 6d |Accept: text/htm|
|00000190| 6c 2c 61 70 70 6c 69 63 61 74 69 6f 6e 2f 78 68 |l,application/xh|
|000001a0| 74 6d 6c 2b 78 6d 6c 2c 61 70 70 6c 69 63 61 74 |tml+xml,applicat|
|000001b0| 69 6f 6e 2f 78 6d 6c 3b 71 3d 30 2e 39 2c 69 6d |ion/xml;q=0.9,im|
|000001c0| 61 67 65 2f 61 76 69 66 2c 69 6d 61 67 65 2f 77 |age/avif,image/w|
|000001d0| 65 62 70 2c 69 6d 61 67 65 2f 61 70 6e 67 2c 2a |ebp,image/apng,*|
|000001e0| 2f 2a 3b 71 3d 30 2e 38 2c 61 70 70 6c 69 63 61 |/*;q=0.8,applica|
|000001f0| 74 69 6f 6e 2f 73 69 67 6e 65 64 2d 65 78 63 68 |tion/signed-exch|
|00000200| 61 6e 67 65 3b 76 3d 62 33 3b 71 3d 30 2e 37 0d |ange;v=b3;q=0.7.|
|00000210| 0a 53 65 63 2d 46 65 74 63 68 2d 53 69 74 65 3a |.Sec-Fetch-Site:|
|00000220| 20 6e 6f 6e 65 0d 0a 53 65 63 2d 46 65 74 63 68 | none..Sec-Fetch|
|00000230| 2d 4d 6f 64 65 3a 20 6e 61 76 69 67 61 74 65 0d |-Mode: navigate.|
|00000240| 0a 53 65 63 2d 46 65 74 63 68 2d 55 73 65 72 3a |.Sec-Fetch-User:|
|00000250| 20 3f 31 0d 0a 53 65 63 2d 46 65 74 63 68 2d 44 | ?1..Sec-Fetch-D|
|00000260| 65 73 74 3a 20 64 6f 63 75 6d 65 6e 74 0d 0a 41 |est: document..A|
|00000270| 63 63 65 70 74 2d 45 6e 63 6f 64 69 6e 67 3a 20 |ccept-Encoding: |
|00000280| 67 7a 69 70 2c 20 64 65 66 6c 61 74 65 2c 20 62 |gzip, deflate, b|
|00000290| 72 2c 20 7a 73 74 64 0d 0a 41 63 63 65 70 74 2d |r, zstd..Accept-|
|000002a0| 4c 61 6e 67 75 61 67 65 3a 20 7a 68 2d 43 4e 2c |Language: zh-CN,|
|000002b0| 7a 68 3b 71 3d 30 2e 39 0d 0a 43 6f 6f 6b 69 65 |zh;q=0.9..Cookie|
|000002c0| 3a 20 5f 67 61 3d 47 41 31 2e 31 2e 32 39 30 37 |: _ga=GA1.1.2907|
|000002d0| 39 30 30 37 31 2e 31 37 33 39 38 34 36 37 37 38 |90071.1739846778|
|000002e0| 3b 20 75 73 65 72 54 6f 6b 65 6e 3d 67 72 72 75 |; userToken=grru|
|000002f0| 6d 6e 6e 77 77 65 75 34 37 70 7a 64 33 73 74 67 |mnnwweu47pzd3stg|
|00000300| 3b 20 49 64 65 61 2d 36 66 31 31 61 62 31 33 3d |; Idea-6f11ab13=|
|00000310| 37 32 37 63 30 33 66 36 2d 34 35 36 65 2d 34 39 |727c03f6-456e-49|
|00000320| 39 33 2d 38 66 35 35 2d 61 65 62 31 38 35 34 63 |93-8f55-aeb1854c|
|00000330| 35 32 66 65 0d 0a 0d 0a                         |52fe....        |
+--------+-------------------------------------------------+----------------+
19:39:33.967 [nioEventLoopGroup-2-5] INFO  com.rocket.demo.test.MyTest08 - HttpRequest:DefaultHttpRequest(decodeResult: success, version: HTTP/1.1)
GET /index.html HTTP/1.1
Host: localhost:8080
Connection: keep-alive
Cache-Control: max-age=0
sec-ch-ua: "Not A(Brand";v="99", "Google Chrome";v="121", "Chromium";v="121"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate, br, zstd
Accept-Language: zh-CN,zh;q=0.9
Cookie: _ga=GA1.1.290790071.1739846778; userToken=grrumnnwweu47pzd3stg; Idea-6f11ab13=727c03f6-456e-4993-8f55-aeb1854c52fe
19:39:33.967 [nioEventLoopGroup-2-5] INFO  io.netty.handler.logging.LoggingHandler - [id: 0x144955e3, L:/127.0.0.1:8080 - R:/127.0.0.1:56628] WRITE: 111B
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 48 54 54 50 2f 31 2e 31 20 32 30 30 20 4f 4b 0d |HTTP/1.1 200 OK.|
|00000010| 0a 63 6f 6e 74 65 6e 74 2d 74 79 70 65 3a 20 74 |.content-type: t|
|00000020| 65 78 74 2f 68 74 6d 6c 0d 0a 63 6f 6e 74 65 6e |ext/html..conten|
|00000030| 74 2d 6c 65 6e 67 74 68 3a 20 34 37 0d 0a 0d 0a |t-length: 47....|
|00000040| 3c 68 74 6d 6c 3e 3c 62 6f 64 79 3e 3c 68 31 3e |<html><body><h1>|
|00000050| 68 65 6c 6c 6f 20 6e 65 74 74 79 21 3c 2f 68 31 |hello netty!</h1|
|00000060| 3e 3c 2f 62 6f 64 79 3e 3c 2f 68 74 6d 6c 3e    |></body></html> |
+--------+-------------------------------------------------+----------------+
19:39:33.967 [nioEventLoopGroup-2-5] INFO  io.netty.handler.logging.LoggingHandler - [id: 0x144955e3, L:/127.0.0.1:8080 - R:/127.0.0.1:56628] FLUSH
19:39:33.968 [nioEventLoopGroup-2-5] INFO  com.rocket.demo.test.MyTest08 - HttpContent:EmptyLastHttpContent

可以看到浏览器发来的信息仅仅请求头就带了大量信息,所以尽量使用Netty自带的Http协议解析类,另外如果只想处理请求体数据的话,可以使用SimpleChannelInboundHandler<?>,通过泛型指定需要关注的数据类型, 从而只处理该类型数据:

java
ch.pipeline().addLast(new SimpleChannelInboundHandler<HttpContent>() {
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, HttpContent msg) throws Exception {

    }
});