目录
背景
最近在弄Tensorflow[1],想自己找开源项目自己跑一下模型实现目标检测,但是没有钱去租服务器的,只好自己买个显卡装到家里电脑来训练。在跑模型的时候,需要访问tensorboard看看当前的状态。由于家里是动态IP上网的,在外面无法直接访问到家里的tensorboard服务,于是我用Golang实现了一个TCP代理工具(项目名:bridge),把家里tensorboard服务端口的数据转发到服务器上。
完整项目代码可以在 https://github.com/juxuny/bridge 里找到
自定义通信协议
开始标记或结束标记:FlagStart=0xE0转义标记:FlagEsc = 0xF0转义后的开始(结束)标记:FlagEscStart = 0xEF转义后的转义标记:FlagEscEsc = 0xFF协议定义如下:
[开始标记(1)| 指令(2)|数据长度(4)|数据(n)|结束标记(1)]
Note:其中小括号后面表示字节数
数据默认都采用大端模式[2]
来自百度百科:下面以unsigned int value = 0x为例,分别看看在两种字节序下其存储情况,我们可以用unsigned char buf[4]来表示valueBig-Endian: 低地址存放高位,如下:高地址 --------------- buf[3] (0x78) -- 低位 buf[2] (0x56) buf[1] (0x34) buf[0] (0x12) -- 高位 ---------------协议指令(CMD)定义如下:
Auth: 认证连接。客户端连接上之后,首先要把自己的token发送到服务器,服务器通过配置文件获取到这个token所对应的端口,验证通过之后服务端开始监听对应的端口。(整个工作流程会在后面详细说明)Data: 数据转发包Msg: 文本消息包,收到这个指令就把数据字段转换成string显示到terminalConnect: 发送连接请求Close: 断开连接以发送认证包为例子:
cmd=1: [0x00 0x01]token: [0x0A 0x0A 0x0A 0x0A]length = 4: [0x00 0x00 0x00 0x04]所以最后打包发送的字节数组为:
[0xE0 0x00 0x01 0x00 0x00 0x00 0x04 0x0A 0x0A 0x0A 0x0A 0xE0]
接收端是按先后顺序读取的,遇到第一个0xE0之前,都是无效数据,可以直接过滤掉。通过循环把主要部分截取出来:0x00 0x01 0x00 0x00 0x00 0x04 0x0A 0x0A 0x0A 0x0A。根据前面的协议定义,前面两个字节是cmd,后面4个字节是length,剩下的就是数据下文部分,即[0x0A 0x0A 0x0A 0x0A]就是下文。
另外,前面还提到了一个转义字符,主要用来解决起始标记冲突问题。下面举另一个例子:
token 改为 [0x0A 0x0A 0xE0 0x0A]
其它字段都不变,按协议打包之后得到以下字节数组:
[0xE0 0x00 0x01 0x00 0x00 0x00 0x04 0x0A 0x0A 0xE0 0x0A 0xE0]
接收端读取数据的时候,读到第一个 0xE0 开始解释数据,第二个 0xE0 就结束,则截取到下面这个数组:
[0xE0 0x00 0x01 0x00 0x00 0x00 0x04 0x0A 0x0A 0xE0]
缓冲里还会剩下 [0x0A 0xE0]
按照截取到的数组解释得到token = [0x0A 0x0A],而length明明是4,所以解释就错误了。或者你又可能说直接根据length来读取指定长度的数据就好,但是如果length刚好也是 0xE0呢?数据包就是这样子的:[0xE0 0x00 0x01 0x00 0x00 0x00 0xE0 0x0A ...... 0x0A 0xE0 0x0A 0xE0]。
截取出来的数据是这样子的:[0x00 0x01 0x00 0x00 0x00 ](还是解释错误)
所以这里就需要用转义字符,把特殊符号替换掉。
0xE0 => 0xF0 0xEF0xF0 => 0xF0 0xFF回到上面说的例子,token = [0x0A 0x0A 0xE0 0x0A],打包出来的数据应该是:
[0xE0 0x00 0x01 0x00 0x00 0x00 0x04 0x0A 0x0A 0xF0 0xEF 0x0A 0xE0]
接收端就遇到0xF0字符的时候就按下面规则转换:
0xF0 0xEF => 0xE0 0xF0 0xFF => 0xF0这就可以解释出正确的数据包:[0x00 0x01 0x00 0x00 0x00 0x04 0x0A 0x0A 0xE0 0x0A]
整体设计
主要组成部分分别为:
bridge-server:
slave manager: bridge-client也叫做slave,这里为了区分好bridge-client 和 web server,所以代码里就写成了slaveManagerconnection manager: listener manager:监听客户端连接,所有connection都交给connection manager处理token manager:根据bridge-client提交的token获取对应的main listener:默认监听9090等待bridge-client连接,连接成功之后把connection交给connection manager管理bridge-client:
connection managermain connection下面用个图片来展示一下各组件之间如何工作的,其中图2为系统简化模型。
图1图2具体实现
上面已经提到了共有5类数据包发送,分别为:CmdAuth, CmdData, CmdMsg, CmdConnect, CmdClose。不同的指令,对应的Data部分也是有不同含意的:
CmdAuth(请求认证):
字段:
token: 传给bridge-server然后通过配置文件获取要代理的端口假设token为[0x0A 0x0A 0x0A 0x0A],要发送的数据为应该:
[0xE0 0x00 0x01 0x00 0x00 0x00 0x04 0x0A 0x0A 0x0A 0x0A 0xE0]
CmdData(数据包):
字段:
fromAddress: 客户端地址(IP+port),8字节toAddress: 目标地址(保留),8字节data: 二进制数据,根据具体数据长度定义字节数由于toAddress字段保留,这里可以直接传 [0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00],表示地址 0.0.0.0:0
例如:[0xE0 0x00 0x02 0x00 0x00 0x00 0x14 0x0A 0x00 0x00 0x01 0x00 0x00 0x03 0xE8 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x01 0x02 0x03 0x04 0xE0]
解包得到:
cmd=2, length = 20, fromAddress = 10.0.0.1:1000, toAddress = 0.0.0.0:0, data = [0x01 0x02 0x03 0x04]
CmdMsg(消息提示):
字段:
msg: 消息文本,(n字节长)例如:[0xE0 0x00 0x03 0x00 0x00 0x00 0x07 0x73 0x75 0x63 0x63 0x65 0x73 0x73 0xE0]
解包得到:
cmd = 3, length = 7, msg = [0x73 0x75 0x63 0x63 0x65 0x73 0x73],转换为string类型得到文本:"success"
CmdConnect(创建连接):
fromAddress: 客户端地址(IP+port),8字节例如:[0xE0 0x00 0x04 0x00 0x00 0x00 0x08 0x0A 0x00 0x00 0x01 0x00 0x00 0x03 0xE8 0xE0]
CmdClose(关闭连接):
fromAddress: 客户端地址(IP+port),8字节例如:[0xE0 0x00 0x05 0x00 0x00 0x00 0x08 0x0A 0x00 0x00 0x01 0x00 0x00 0x03 0xE8 0xE0]
指令常量定义如下:
const ( FlagStart= 0xE0 FlagEscStart = 0xEF FlagEsc= 0xF0 FlagEscEsc = 0xFF )为了更方便地解释数据包,并且一个数据包一个数据读取,特意定义了一个DataReader处理bridge-server 与 bridge-client 之间的TCP通信。
type DataReader struct { conn net.Conn buffer *bytes.Buffer } func NewDataReader(conn net.Conn) (ret *DataReader) { ret = new(DataReader) ret.conn = conn ret.buffer = bytes.NewBuffer(nil) return } func (t *DataReader) ReadOne() (ret Data, isEnd bool, e error) { startCount := 0 block := make([]byte, blockSize) retBuf := bytes.NewBuffer(nil) var n int for startCount < 2 { if t.buffer.Len() == 0 { n, e = t.conn.Read(block) if e != nil { isEnd = true return } t.buffer.Write(block[:n]) } //debug("buffer data:", t.buffer.Len(), t.buffer.Bytes()) b, err := t.buffer.ReadByte() if err != nil { continue } if b == FlagStart { startCount += 1 continue } retBuf.WriteByte(b) } //debug("finished:", retBuf.Bytes()) e = ret.Unpack(retBuf.Bytes()) debug(ret) return }conn 是连接bridge-server的TCP连接
buffer表示网络数据缓存,为空的时候就从connection里读一块数据
每次读取数据包的时候startCount都重新计数,大于等于2的时候说明已经读到了第二个开始标记了,表示已经获取了一个完整的数据包到retBuf,使用Data的Unpack方法就可以解释这个数据包。Data结构定义如下:
type Data struct { Cmdint16 Data []byte } func (t *Data) Pack() (ret []byte, e error) { length := len(t.Data) ret = make([]byte, length+2+4) //cmd ret[0] |= byte(t.Cmd >> 8) ret[1] |= byte(t.Cmd) //length ret[2] |= byte(length >> 24) ret[3] |= byte(length >> 16) ret[4] |= byte(length >> 8) ret[5] |= byte(length) //copy data tmp := ret[2+4:] copy(tmp, t.Data) return } func (t *Data) Unpack(data []byte) (e error) { in := bytes.NewBuffer(data) out := bytes.NewBuffer(nil) for b, err := in.ReadByte(); err == nil; b, err = in.ReadByte() { if b == FlagEsc { b, err = in.ReadByte() if err != nil { break } if b == FlagEscStart { out.WriteByte(FlagStart) } else if b == FlagEscEsc { out.WriteByte(FlagEsc) } } else { out.WriteByte(b) } } data = out.Bytes() //cmd t.Cmd = 0 t.Cmd |= int16(data[0] << 8) t.Cmd |= int16(data[1]) //length length := 0 length |= int(data[2]) << 24 length |= int(data[3]) << 16 length |= int(data[4]) << 8 length |= int(data[5]) if out.Len() - 6 != length { return fmt.Errorf("invalid package, get data: %d, read length: %d", out.Len(), length) } t.Data = data[6:] return } //把打包后的字节流通过TCP socket发送出去 func (t *Data) Write(out io.Writer) (e error) { data, e := t.Pack() if e != nil { return } buffer := bytes.NewBuffer(nil) buffer.WriteByte(FlagStart) for i := 0; i < len(data); i++ { if data[i] == FlagStart { buffer.WriteByte(FlagEsc) buffer.WriteByte(FlagEscStart) } else if data[i] == FlagEsc { buffer.WriteByte(FlagEsc) buffer.WriteByte(FlagEscEsc) } else { buffer.WriteByte(data[i]) } } buffer.WriteByte(FlagStart) debug("send data:", len(buffer.Bytes()), buffer.Bytes()) _, e = out.Write(buffer.Bytes()) return }由于在代理的时间,浏览器可能会创建多个连接到bridge-server,最后bridge-client也会创建相同数量的连接到指定的Web Server。为了更方便管理各个连接,这里创建了ConnManager:
type ConnManager struct { connSet sync.Map } func (t *ConnManager) GetConn(addr string) (conn net.Conn, ok bool) { v, ok := t.connSet.Load(addr) if ok { conn, ok = v.(net.Conn) } return } func (t *ConnManager) Close(addr string) { conn, ok := t.GetConn(addr) if !ok { return } conn.Close() t.connSet.Delete(addr) } func (t *ConnManager) AddConn(addr string, conn net.Conn) { t.connSet.Store(addr, conn) } func (t *ConnManager) SendData(addr string, data []byte) (disconnected bool, err error) { conn, ok := t.GetConn(addr) if !ok { err = fmt.Errorf("conn not found: %s", addr) return } _, err = conn.Write(data) if err != nil { disconnected = true } return }实质上,ConnManager就是给远程地址和TCP socket做了一个映射,让bridge-server或者bridge-client可以通过远程地址查询对应的socket。当浏览器连接到bridge-sever的时候,假设连接地址是10.0.0.1:50001,bridge-sever可以通过下面代码获取到对应的地址:
addr := conn.RemoteAddr() //var conn net.ConnConnManager就负责把addr与conn绑定,所以bridge-server可以通过ConnManager给指定客户返回数据,bridge-client也可以通过ConnManager把数据转发给最终的Web Server。
bridge-server为每个bridge-client创建了一个listener,用于接收Browser连接,每个token对应一个port,listenerManager把port与listener绑定,bridge-client断开连接的时候,可以通过对应的port清理listener,例如调用Remove方法。listenerManager定义如下:
type listenerManager struct { lnSet sync.Map } func (t *listenerManager) GetListener(port int) (l net.Listener, ok bool) { v, ok := t.lnSet.Load(port) if ok { l, ok = v.(net.Listener) return } return } func (t *listenerManager) Add(port int, listener net.Listener) (err error) { _, found := t.GetListener(port) if found { err = fmt.Errorf("listener exists, listening port: %d", port) return } t.lnSet.Store(port, listener) return } func (t *listenerManager) Remove(port int) (err error) { debug("remove listener, port:", port) l, found := t.GetListener(port) if found { t.lnSet.Delete(port) err = l.Close() return } return }另外还需要一个slaveManager管理所有bridge-client:
type slaveManager struct { set sync.Map mutex *sync.Mutex } func newSlaveManager() (ret *slaveManager) { ret = new(slaveManager) ret.mutex = new(sync.Mutex) return } func (t *slaveManager) GetConn(port int) (conn net.Conn, ok bool) { var v interface{} v, ok = t.set.Load(port) if !ok { return } conn, ok = v.(net.Conn) return } func (t *slaveManager) Bind(port int, conn net.Conn) { t.set.Store(port, conn) } func (t *slaveManager) Unbind(port int) { conn, ok := t.GetConn(port) if ok { _ = conn.Close() } t.set.Delete(port) } func (t *slaveManager) sendMsg(port int, msg string) (disconnected bool, e error) { conn, ok := t.GetConn(port) if !ok { disconnected = true e = fmt.Errorf("connection not found") return } m := newMsg(msg) e = m.Write(conn) return } func (t *slaveManager) sendConnect(port int, addr string) (disconnected bool, e error) { conn, ok := t.GetConn(port) if !ok { disconnected = true e = fmt.Errorf("slave not found") return } d := Data{Cmd: CmdConnect} d.Data, e = hostToBytes(addr) if e != nil { return } e = d.Write(conn) if e != nil { disconnected = true } return } func (t *slaveManager) sendClose(port int, addr string) (disconnected bool, e error) { disconnected = false conn, found := t.GetConn(port) if !found { disconnected = true e = fmt.Errorf("slave not found, listen port: %s", port) return } d := Data{} d.Cmd = CmdClose d.Data, e = hostToBytes(addr) if e != nil { return } e = d.Write(conn) if e != nil { disconnected = true } return } func (t *slaveManager) WriteData(port int, fromAddr, toAddr string, data []byte) (disconnected bool, e error) { conn, ok := t.GetConn(port) if !ok { disconnected = true e = fmt.Errorf("connection not found") return } fromBytes, e := hostToBytes(fromAddr) if e != nil { return } toBytes, e := hostToBytes(toAddr) if e != nil { return } debug("slaveManager: write data, from:", fromBytes, " to:", toBytes, " data:", data) d := Data{} d.Cmd = CmdData d.Data = make([]byte, len(fromBytes)+len(toBytes)+len(data)) k := 0 for i := 0; i < len(fromBytes); i++ { d.Data[k] = fromBytes[i] k++ } for i := 0; i < len(toBytes); i++ { d.Data[k] = toBytes[i] k++ } for i := 0; i < len(data); i++ { d.Data[k] = data[i] k++ } e = d.Write(conn) return }其中sendMsg, sendConnect, sendClose, WriteData等方法用于对指定的bridge-client发送不同的数据包。因为每个bridge-client都会有一个特定的token,一个token和bridge-server 监听的端口唯一对应,所以slaveManager可以通过port参数找到对应的bridge-client,然后进行发数据。
最后bridge-server通过一个struct, Server,整合各个manager struct,包括:ConnManager, listenerManager, TokenManager(这个可以参考源码里的定义),slaveManager:
type Server struct { slaves *slaveManager tokenManager *TokenManager config ServerConfig ln net.Listener connMgr *ConnManager lnMgr *listenerManager } func NewServer(c ServerConfig) (ret *Server) { // do something ... return } func (t *Server) Start() { var e error t.ln, e = net.Listen("tcp", ":"+fmt.Sprint(t.config.Port)) if e != nil { panic(e) } _, _ = log("listen on :", t.config.Port) for { conn, e := t.ln.Accept() if e != nil { fmt.Println(e) continue } debug("accept from:", conn.RemoteAddr()) go t.checkAuthorization(conn) } } func (t *Server) bindPort(port int, conn net.Conn) { t.slaves.Bind(port, conn) } func (t *Server) unbindPort(port int) { t.slaves.Unbind(port) } func (t *Server) checkAuthorization(conn net.Conn) { //do something ... } mh func (t *Server) sendMsg(port int, msg string) (disconnected bool, e error){ //do something ... return } func (t *Server) handleMsg(d Data) { log(string(d.Data)) } func (t *Server) handleClose(d Data) { //do something ... } func (t *Server) handleData(d Data) { //do something ... } func (t *Server) handle(d Data) { //do something ... func (t *Server) serveConn(port int, conn net.Conn) { //do something ... } func (t *Server) serveSlave(port int, reader *DataReader) { // do something... }由于篇幅太长,Server struct的源码只能到github上看了,点这里:server.go
安装与配置
安装go(略)go get -u github.com/juxuny/bridgego install github.com/juxuny/bridge/cmd/bridge-servergo install github.com/juxuny/bridge/cmd/bridge-client如果$GOPATH/bin已经加到了PATH变量里的话,可以直接执行bridge-server和bridge-client命令了,否则还需要:export PATH=$PATH:$GOPATH/bin假设外网服务器IP为10.0.0.1,bridge-server监听端口是9090,家里外网的动态IP为10.0.0.x,家里运行的bridge-client的设备是192.168.0.1,内部提供Web 服务的设备是192.168.0.2,网页端口是80。现在要将10.0.0.1:10001的数据转发到家里内部网络的192.168.0.2:8888上。
server.json:
{ "port": 9090, "tokenConf": "token.conf" }token.conf:
#token|aes-key | port nooF2nXPawgx1LUsKTnrtvdlxY2OZgN3sOuv80kduWpVZOl66uSehv9CGnoL8CH5 uNH8RGSW86rRt85b 10001client.json:
{ "token": "nooF2nXPawgx1LUsKTnrtvdlxY2OZgN3sOuv80kduWpVZOl66uSehv9CGnoL8CH5", "key": "uNH8RGSW86rRt85b", "host": "10.0.0.1:9090", "local": "192.168.0.2:8888" }Note:由于每次传输都执行aes加密解密很影响性能,后来把加密代码去掉了
在10.0.0.1服务器上执行:
bridge-server -c=server.json在192.168.0.1设备上执行:
bridge-client -c=client.json性能测试
自定义Web Server,直接获取某个json数据,浏览器访问得到如下结果:
不使用代理访问使用代理访问根据上面的安装配置,8888端口表示源服务,10001端口是bridge-server,使用ab压力测试得到如下结果:
(1)测试100次请求,执行命令:
ab -n 100 -c 100 :8888/Index/TestSum输出结果:
Server Software: Server Hostname:127.0.0.1 Server Port:8888 Document Path:/Index/TestSum Document Length:10 bytes Concurrency Level:100 Time taken for tests: 0.053 seconds Complete requests:100 Failed requests:0 Total transferred:22400 bytes HTML transferred: 1000 bytes Requests per second:1888.25 [#/sec] (mean) Time per request: 52.959 [ms] (mean) Time per request: 0.530 [ms] (mean, across all concurrent requests) Transfer rate:413.06 [Kbytes/sec] received Connection Times (ms) minmean[+/-sd] median max Connect:33 0.43 4 Processing: 3 3811.2 4448 Waiting:3 3711.5 4448 Total:7 4111.2 4752通过代理访问:
ab -n 100 -c 100 :10001/Index/TestSum输出结果:
Server Software: Server Hostname:127.0.0.1 Server Port:10001 Document Path:/Index/TestSum Document Length:10 bytes Concurrency Level:100 Time taken for tests: 0.931 seconds Complete requests:100 Failed requests:0 Total transferred:22400 bytes HTML transferred: 1000 bytes Requests per second:107.40 [#/sec] (mean) Time per request: 931.130 [ms] (mean) Time per request: 9.311 [ms] (mean, across all concurrent requests) Transfer rate:23.49 [Kbytes/sec] received Connection Times (ms) minmean[+/-sd] median max Connect: 14 17 1.0 1718 Processing:37491 256.1522 901 Waiting: 37489 256.1521 901 Total: 54507 255.6540 915由上面的压力测试结果可以得出,不使用代理性能比使用代理快17倍有多(),还有很多优化的空间,但终究实现了穿透防火墙功能。