Ngrok4j
背景
ngrok是一个go语言编写的内网穿透代理软件,之前的开源的版本是 2.1.7,也已经很多年没有更新了,作者也已经放弃这个版本的维护了,因为这个版本存在一个很严重的bug,内存泄漏。导致很多使用者望而止步,一旦用来下载大文件,内存将会一直占用,不会释放。本来想基于go重写一版的,奈何还没熟悉,只能先用吃饭的Java来熟悉下协议流程。因此创建了ngrok4j的工程,基于netty框架来实现,像很多例如Dubbo、RocketMQ、Hadoop都是使用了netty,正好学习这种异步的网络处理框架。
协议分析
ngrok的协议还是比较简单,一个数据包包括数据长度和内容两部分,注意的是长度占了8字节且是小端,关于大小端的解释
Big-Endian和Little-Endian的定义如下:
- Little-Endian就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。
低地址 -----------------> 高地址
0x12 | 0x34 | 0x56 | 0x78
- Big-Endian就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。
低地址 ------------------> 高地址
0x78 | 0x56 | 0x34 | 0x12
包内容
数据内容是用网络交互常见的json,json的结构也是极其的简单,等会再解释几种的数据类型。
{
"Type":"数据的类型",
"Payload":"数据的内容"
}
比如一个最简单的心跳包
{"Type":"Pong","Payload":{}}
一共是28个长度
那么前面8位将会是
数据长度 | 数据内容
1c 00 00 00 00 00 00 00 | 7b 22 54 79 70 65 22 3a 22 50 6f 6e 67 22 2c 22 50 61 79 6c 6f 61 64 22 3a 7b 7d 7d
0x1c = 28
数据类型
- 心跳包(
Ping/Pong):用于客户端和服务端的确认,每过段时间必须发一次,确保连接不被服务端断掉。
{"Type":"Pong","Payload":{}}
{"Type":"Ping","Payload":{}}
- 认证包(
Auth/AuthResponse):用于客户端请求建立连接的clientId
{
"Type":"Auth",
"Payload":{
"Version":"2",
"MmVersion":"1.7",
"User":"",
"Password":"",
"Arch":"amd64",
"ClientId":"",
"OS":"Windows 10"
}
}
{
"Type":"AuthResp",
"Payload":{
"Version":"2",
"MmVersion":"1.7",
"ClientId":"75cffec60f18026fc5d9f0e5ca0dc76b",
"Error":""
}
}
- 隧道请求(
ReqTunnel/NewTunnel):用于隧道的建立
{
"Type":"ReqTunnel",
"Payload":{
"ReqId":"bb813b73",
"Protocol":"http",
"Hostname":null,
"Subdomain":"local-nginx",
"HttpAuth":null,
"RemotePort":0
}
}
{
"Type":"NewTunnel",
"Payload":{
"ReqId":"bb813b73",
"Url":"http://local-nginx.vaiwan.com",
"Protocol":"http",
"Error":""
}
}
- 代理请求(
RegProxy/ReqProxy):用于代理的建立
{"Type":"RegProxy","Payload":{"ClientId":"0bb68e355aed1ee8722cc4bf09c2f791"}}
{"Type":"ReqProxy","Payload":{}}
- 代理开始(
StartProxy):服务端将会发送这个标志,表示开始代理真实数据的交互。
{
"Type":"StartProxy",
"Payload":{
"Url":"http://local-nginx.vaiwan.com",
"ClientAddr":"110.87.21.142:7618"
}
}
时序分析
首先最开始ngrok客户端需要建立一个control的连接即图中的黑色交互线,建立连接后,需要定时发送Ping
- 发送
auth获取到clientId, - 发送需要建立隧道的请求
ReqTunnel - 等待服务器发送建立代理
ReqProxy
然后ngrok客户端需要开始代理准备,即图中蓝色线
- 建立新的一条和服务端的连接
- 建立一条和本地服务端的连接
- 等待客户端的请求
StartProxy - 开始数据的传输交互即图中的橙色线
代码地址
具体的代码逻辑实现已经提交到 github (备用地址coding)上,可能不够完善。使用了钉钉的免费穿透服务器来测试。下载速度还是可以的,同时下载了大文件,没有出现内存溢出。但是不熟悉netty,中间遇到了堆外内存溢出的情况。虽然不影响请求访问,但是log里还是不停的弹异常信息,不过还是花了几天的时间终于处理掉这个问题。后面会继续分享netty使用中遇到的情况和解决方案。