Skip to content

Commit f6a3c72

Browse files
committed
feat:使用魔数来进行协议嗅探->TCP = 0xAABBCCDD,HTTP/2 = 0x50524920
1 parent 57b736d commit f6a3c72

2 files changed

Lines changed: 131 additions & 3 deletions

File tree

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
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+
}

rpc-transport-netty/src/main/java/com/xiaoyu/rpc/core/transport/netty/NettyTransportServer.java

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import com.xiaoyu.rpc.core.config.RpcConfig;
44
import com.xiaoyu.rpc.core.protocol.Protocol;
5+
import com.xiaoyu.rpc.core.protocol.ProtocolDetectHandler;
56
import com.xiaoyu.rpc.core.protocol.ProtocolFactory;
67
import com.xiaoyu.rpc.core.server.NettyRpcHandler;
78
import com.xiaoyu.rpc.core.transport.TransportServer;
@@ -36,10 +37,15 @@ public void start() throws InterruptedException {
3637
.childHandler(new ChannelInitializer<SocketChannel>() {
3738
@Override
3839
protected void initChannel(SocketChannel ch) {
39-
// 协议实现通过配置切换,服务端业务处理统一复用 NettyRpcHandler
4040
String protocolName = RpcConfig.getInstance().getProtocol();
41-
Protocol protocol = ProtocolFactory.getProtocol(protocolName);
42-
protocol.config(ch.pipeline(), true, new NettyRpcHandler());
41+
if ("auto".equalsIgnoreCase(protocolName)) {
42+
// 自动嗅探模式:先放嗅探器,连接建立后根据首字节判断协议
43+
ch.pipeline().addLast(new ProtocolDetectHandler());
44+
} else {
45+
// 指定协议模式:保持原有行为
46+
Protocol protocol = ProtocolFactory.getProtocol(protocolName);
47+
protocol.config(ch.pipeline(), true, new NettyRpcHandler());
48+
}
4349
}
4450
});
4551

0 commit comments

Comments
 (0)