|
| 1 | +package com.xiaoyu.rpc.core.protocol; |
| 2 | + |
| 3 | +import com.xiaoyu.rpc.core.server.NettyRpcHandler; |
| 4 | +import io.netty.buffer.ByteBuf; |
| 5 | +import io.netty.channel.ChannelHandlerContext; |
| 6 | +import io.netty.handler.codec.ByteToMessageDecoder; |
| 7 | +import org.slf4j.Logger; |
| 8 | +import org.slf4j.LoggerFactory; |
| 9 | + |
| 10 | +import java.util.List; |
| 11 | + |
| 12 | +/** |
| 13 | + * 协议嗅探器 —— 服务端自动识别多种协议 |
| 14 | + * <p> |
| 15 | + * 原理:连接建立后,偷看(peek)入站数据的前几个字节,根据特征判断协议类型: |
| 16 | + * <ul> |
| 17 | + * <li>0xAABBCCDD → TCP 私有协议(NettyProtocol)</li> |
| 18 | + * <li>0x50524920 ("PRI ") → HTTP/2 Connection Preface(Http2Protocol 或 |
| 19 | + * GrpcProtocol)</li> |
| 20 | + * <li>HTTP 方法名(GET / POST / PUT / HEAD / DELETE / OPTIONS / PATCH)→ |
| 21 | + * HTTP/1.1(HttpProtocol)</li> |
| 22 | + * </ul> |
| 23 | + * 识别后动态配置 pipeline 并移除自身,后续按确定的协议处理。 |
| 24 | + * <p> |
| 25 | + * 注意:gRPC 底层也是 HTTP/2 传输,无法在字节层面与普通 HTTP/2 区分。 |
| 26 | + * 通过构造函数的 {@code http2ProtocolName} 参数控制 HTTP/2 连接的处理方式。 |
| 27 | + */ |
| 28 | +public class ProtocolDetectHandler extends ByteToMessageDecoder { |
| 29 | + |
| 30 | + private static final Logger log = LoggerFactory.getLogger(ProtocolDetectHandler.class); |
| 31 | + |
| 32 | + /** TCP 私有协议魔数,与 NettyRpcEncoder/NettyRpcDecoder 一致 */ |
| 33 | + private static final int NETTY_MAGIC = 0xAABBCCDD; |
| 34 | + |
| 35 | + /** HTTP/2 Connection Preface 前 4 字节: "PRI " = 0x50524920 */ |
| 36 | + private static final int HTTP2_MAGIC = 0x50524920; |
| 37 | + |
| 38 | + // 常见 HTTP/1.1 方法的首字母 ASCII 码 |
| 39 | + private static final byte BYTE_G = 'G'; // GET |
| 40 | + private static final byte BYTE_P = 'P'; // POST, PUT, PATCH |
| 41 | + private static final byte BYTE_D = 'D'; // DELETE |
| 42 | + private static final byte BYTE_H = 'H'; // HEAD |
| 43 | + private static final byte BYTE_O = 'O'; // OPTIONS |
| 44 | + private static final byte BYTE_T = 'T'; // TRACE |
| 45 | + private static final byte BYTE_C = 'C'; // CONNECT |
| 46 | + |
| 47 | + /** |
| 48 | + * 当检测到 HTTP/2 Connection Preface 时使用的协议名。 |
| 49 | + * 因为 gRPC 底层也是 HTTP/2,无法在字节层面区分, |
| 50 | + * 所以通过这个参数指定:可以是 "http2" 或 "grpc"。 |
| 51 | + * 默认为 "http2"。 |
| 52 | + */ |
| 53 | + private final String http2ProtocolName; |
| 54 | + |
| 55 | + /** |
| 56 | + * 默认构造函数,HTTP/2 连接使用 Http2Protocol 处理 |
| 57 | + */ |
| 58 | + public ProtocolDetectHandler() { |
| 59 | + this("http2"); |
| 60 | + } |
| 61 | + |
| 62 | + /** |
| 63 | + * 指定 HTTP/2 连接的处理协议 |
| 64 | + * |
| 65 | + * @param http2ProtocolName HTTP/2 连接使用的协议名("http2" 或 "grpc") |
| 66 | + */ |
| 67 | + public ProtocolDetectHandler(String http2ProtocolName) { |
| 68 | + this.http2ProtocolName = http2ProtocolName; |
| 69 | + } |
| 70 | + |
| 71 | + @Override |
| 72 | + protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception { |
| 73 | + // 至少需要 4 字节才能判断协议类型 |
| 74 | + if (in.readableBytes() < 4) { |
| 75 | + return; |
| 76 | + } |
| 77 | + |
| 78 | + // 偷看前 4 字节,不消费(不移动 readerIndex) |
| 79 | + int magic = in.getInt(in.readerIndex()); |
| 80 | + byte firstByte = in.getByte(in.readerIndex()); |
| 81 | + |
| 82 | + if (magic == NETTY_MAGIC) { |
| 83 | + log.info("检测到 TCP 私有协议连接, 远程地址: {}", ctx.channel().remoteAddress()); |
| 84 | + configProtocol(ctx, "netty"); |
| 85 | + } else if (magic == HTTP2_MAGIC) { |
| 86 | + log.info("检测到 HTTP/2 协议连接 (使用 {} 处理), 远程地址: {}", |
| 87 | + http2ProtocolName, ctx.channel().remoteAddress()); |
| 88 | + configProtocol(ctx, http2ProtocolName); |
| 89 | + } else if (isHttpMethod(firstByte)) { |
| 90 | + log.info("检测到 HTTP/1.1 协议连接, 远程地址: {}", ctx.channel().remoteAddress()); |
| 91 | + configProtocol(ctx, "http"); |
| 92 | + } else { |
| 93 | + log.warn("未知协议, 首字节: 0x{}, 关闭连接, 远程地址: {}", |
| 94 | + Integer.toHexString(magic), ctx.channel().remoteAddress()); |
| 95 | + in.clear(); |
| 96 | + ctx.close(); |
| 97 | + } |
| 98 | + } |
| 99 | + |
| 100 | + /** |
| 101 | + * 判断首字节是否可能是 HTTP/1.1 方法名的开头 |
| 102 | + */ |
| 103 | + private boolean isHttpMethod(byte firstByte) { |
| 104 | + return firstByte == BYTE_G // GET |
| 105 | + || firstByte == BYTE_P // POST, PUT, PATCH |
| 106 | + || firstByte == BYTE_D // DELETE |
| 107 | + || firstByte == BYTE_H // HEAD |
| 108 | + || firstByte == BYTE_O // OPTIONS |
| 109 | + || firstByte == BYTE_T // TRACE |
| 110 | + || firstByte == BYTE_C; // CONNECT |
| 111 | + } |
| 112 | + |
| 113 | + /** |
| 114 | + * 根据协议名动态配置 pipeline,然后移除自身 |
| 115 | + */ |
| 116 | + private void configProtocol(ChannelHandlerContext ctx, String protocolName) { |
| 117 | + Protocol protocol = ProtocolFactory.getProtocol(protocolName); |
| 118 | + // 先移除自身,再配置协议的编解码器和 Handler |
| 119 | + ctx.pipeline().remove(this); |
| 120 | + protocol.config(ctx.pipeline(), true, new NettyRpcHandler()); |
| 121 | + } |
| 122 | +} |
0 commit comments