QuantumTunnel:Netty實現
- 2021 年 11 月 7 日
- 筆記
- QuantumTunnel, 內網穿透
接上一篇文章內網穿透服務設計挖的坑,本篇來聊一下內網穿透的實現。
為了方便理解,我們先統一定義使用到的名詞:
UserClient
:用戶客戶端,真實的請求發起方;UserServer
:內網穿透-用戶服務端,接收用戶客戶端發起的請求;並將請求轉發給代理服務端;ProxyServer
:內網穿透-代理服務端,與代理客戶端保持一個連接通道用於傳輸數據;ProxyClient
:內網穿透-代理客戶端,從通道中接收來自代理服務端的請求數據,並且發起真正的請求。拿到請求結果後再通過該通道寫回到代理服務端;TargetServer
:目標伺服器目標伺服器,即被代理的伺服器;UserChannel
:用戶客戶端 -> 內網穿透服務端,用戶連接通道;QuantumTunnel
:內網穿透服務端 -> 內網穿透客戶端,量子通道;ProxyChannel
:內網穿透客戶端 -> 目標伺服器,代理通道。
需要關注一下最後的UserChannel、QuantumChannel和ProxyChannel這3個通道,內網穿透的本質就是數據流量在這三個網路連接通道中流轉。
流程圖
進行開發之前,我們再梳理一下內網穿透的流程。
在上篇文章的基礎上,對流程圖進行了更詳細的補充。這個流程圖非常重要,所有程式碼都是圍繞這個流程圖進行實現的
。對全局有了掌控,程式碼實現的時候才心中有數。
具體實現
內網穿透的前提條件是網路之間建立一個網路傳輸通道,我稱之為QuantumTunnel
,進行網路打通。我們來看看這部分是怎麼實現的。
為了方便理解代理,這裡對Netty開發流程簡單說明一下。
- Netty開發編程中,
Channel
是一個很核心的概念,代表的是一個網路連接通道,負責數據傳輸; - Netty接收到對端傳輸過來的數據後,交由
Handler
來執行具體的業務流程,也就是說我們的業務邏輯幾乎都在Handler裡面; - 實際開發過程中會有很多Handler了,
Pipeline
則負責將Handler組織起來,就一個流水線,前一個Handler執行完成後交給後面的Handler繼續執行。
如果小夥伴對Netty開發不太熟悉可以了解相關教程資料,本文不展開討論。
管理QuantumTunnel連接
ProxyServerHandler
QuantumTunnel由ProxyServer和ProxyClient維護,這是ProxyServerHandler的程式碼:
public class ProxyServerHandler extends QuantumCommonHandler {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
QuantumMessage message = (QuantumMessage) msg;
if (message.getMessageType() == QuantumMessageType.REGISTER) {
processRegister(ctx, message);
} else if (message.getMessageType() == QuantumMessageType.PROXY_DISCONNECTED) {
processProxyDisconnected(message);
} else if (message.getMessageType() == QuantumMessageType.DATA) {
processData(message);
} else {
ctx.channel().close();
throw new RuntimeException("Unknown MessageType: " + message.getMessageType());
}
}
}
程式碼中對ProxyClient過來的數據進行了類型判斷並進行處理,總共有三種事件類型:
- 註冊事件:接收ProxyClient的註冊請求,打開QuantumTunnel
- 數據傳輸事件:接收ProxyClient返回的數據,並發送給UserChannel
- ProxyChannel斷開事件:ProxyChannel斷開後需要同步斷開UserChannel
ProxyClientHandler
public class ProxyClientHandler extends QuantumCommonHandler {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
log.info("準備註冊通道");
QuantumMessage quantumMessage = new QuantumMessage();
quantumMessage.setClientId("localTest");
quantumMessage.setMessageType(QuantumMessageType.REGISTER);
ctx.writeAndFlush(quantumMessage);
super.channelActive(ctx);
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
QuantumMessage quantumMessage = (QuantumMessage) msg;
if (quantumMessage.getMessageType() == QuantumMessageType.USER_DISCONNECTED) {
processUserChannelDisconnected(quantumMessage);
} else if (quantumMessage.getMessageType() == QuantumMessageType.DATA) {
processData(ctx, quantumMessage);
} else {
throw new RuntimeException("Unknown type: " + quantumMessage.getMessageType());
}
}
}
ProxyClientHandler主要有三個邏輯,與ProxyServerHandler的三個事件類型相呼應:
- 向ProxyServer發起註冊請求,打開QuantumTunnel;
- 處理QuantumTunnel過來的數據,向目標服務發起真正的請求並返回結果;
- 處理UserChannel連接斷開事件。
對流量進行內網穿透
當QuantumTunnel通道建立完成以後,便可以對外提供內網穿透服務了。
假設現在要代理UserClient的Http請求,那麼UserClient應該把請求打到UserServer,再由UserServer對流量進行轉發。
綜上,UserServer的功能有兩個:
- 管理UserChannel連接;
- 解析數據流量包的路由資訊,進行轉發。
UserServerHandler
public class UserServerHandler extends QuantumCommonHandler {
//userChannel標識
private String userChannelId;
//內網標識,即流量要轉發到哪個網路
private String clientId;
//被代理的真實伺服器內網地址
private String proxyHost;
//被代理服務的埠
private String proxyPort;
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
QuantumMessage message = new QuantumMessage();
byte[] bytes = (byte[]) msg;
message.setData(bytes);
//解析路由資訊
if (clientId == null || proxyHost == null || proxyPort == null) {
String s = new String(bytes);
clientId = getHeaderValue(s, "clientId");
proxyHost = getHeaderValue(s, "proxyHost");
proxyPort = getHeaderValue(s, "proxyPort");
}
if (clientId == null || proxyHost == null || proxyPort == null) {
log.info("缺少參數,clientId={},proxyHost={},proxyPort={}", clientId, proxyHost, proxyPort);
ctx.channel().close();
}
message.setClientId(clientId);
message.setMessageType(QuantumMessageType.DATA);
message.setChannelId(userChannelId);
message.setProxyHost(proxyHost);
message.setProxyPort(Integer.parseInt(proxyPort));
//封裝QuantumMessage並寫入QuantumTunnel,轉發到對應的內部網路
boolean success = writeMessage(message);
if (!success) {
log.info("寫入數據失敗,clientId={},proxyHost={},proxyPort={}", clientId, proxyHost, proxyPort);
ctx.channel().close();
}
}
}
ProxyClient#doProxyRequest
當UserClient的Http請求被UserServer通過QuantumTunnel轉發到了UserClient,那麼最後便是發起真正的請求,拿到請求結果。
這裡我之前想,如果有很多不同的應用之前協議,如Http,WebSocket等,是不是要全部都適配呢?仔細思考後發現是不需要的,因為UserClient拿到的數據包是已經封裝好的應用層數據包,直接轉發到對應的埠即可。
想通了以後,這個環節就比較簡單了:利用Netty打開指定host+port的Channel,往裡面寫數據就好了。
private void doProxyRequest(ChannelHandlerContext ctx, QuantumMessage quantumMessage) throws InterruptedException {
Channel proxyChannel = user2ProxyChannelMap.get(quantumMessage.getChannelId());
ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer(quantumMessage.getData().length);
//將byte數組轉換成ByteBuf
buffer.writeBytes(quantumMessage.getData());
if (proxyChannel == null) {
try {
Bootstrap b = new Bootstrap();
b.group(WORKER_GROUP);
b.channel(NioSocketChannel.class);
b.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ChannelPipeline pipeline = ch.pipeline();
//在ProxyRequestHandler中處理被代理服務返回的數據
pipeline.addLast(new ProxyRequestHandler(ctx, quantumMessage.getChannelId()));
}
});
//打開Channel
Channel channel = b.connect(quantumMessage.getProxyHost(), quantumMessage.getProxyPort()).sync().channel();
//把數據寫入Channel
channel.writeAndFlush(buffer);
} catch (Exception e) {
throw e;
}
} else {
proxyChannel.writeAndFlush(buffer);
}
}
運行結果
QuantumTunnel主要工作在傳輸層,理論上可以代理所有的應用層協議
。唯一需要依賴應用層協議的地方是解析路由資訊這部分,得益於Netty的責任鏈開發模式,只需要針對特定的應用層協議開發對應的解析路由資訊的Handler即可(可以參考UserServerHandler實現)。
這裡展示一下WebSocket(雙向通訊)的內網穿透效果,http內網穿透效果可以上一篇文章
最後
遇到的問題
實現過程中遇到最大的問題便是路由資訊的解析,比如
- Netty的拆包:消息體過大或者過小時,會出現粘包和半包的問題;
- WebSocket的路由轉發:如何獲取數據幀的路由資訊。
以及UserChannel和ProxyChannel連接的管理等,這些問題我會在下一篇文章和大家一起分析。
倉庫地址
歡迎一起共建致力於Java領域最好的內網穿透工具:QuantumTunnel
- Gitee:樂天派 / quantum-tunnel
- GitHub:liumian97/quantum-tunnel