接上一篇文章內網穿透服務設計挖的坑,本篇來聊一下內網穿透的實作,
為了方便理解,我們先統一定義使用到的名詞:
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
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/351909.html
標籤:Java
