目前企业级主流的穿透技术是PHTunnel和Wangooe Tunnel技术

目前国内内网穿透企业级的服务商有花生壳和神卓互联,我接触过很多公司在用,花生壳的穿透技术是PHTunnel ,神卓互联用的是Wangooe Tunnel技术,都是应用于企业级的,虽然我本人的水平还达不大企业级的水平,也不会提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率,零拷贝,TCP连接池,内存池,Reactor等技术,但是自己想试一下从头开始写一个仿神卓互联内网穿透的项目练练手,虽然网上有开源的FRP,毕竟是开源Go语言写的,性能上不适合商用,于是自己开始写,只是简单的demo,自己测试可以,相信我会越来越强大。

看一下我设计的拓扑图
需要云端有一台服务器,客户端有一个对接转发的客户端,用来转发用户发过来的请求,然后转发给应用服务器,应用服务器再将结果返回给用户

为了让公网能访问子网服务,需要使用一台公网服务器做代理服务器proxyServer(这里的代理是个人叫法),假设某一client需要访问server(在子网subnet里面部署的服务),但是client不能直接与server连接。但是client能连接位于公网的proxyServer,之后如果proxyServer能将client的消息传给server,再将server的数据返回给client的话,在client看来,就相当于访问的服务就是proxyServer提供的,所以可以认为该方法可认为对应用层是透明的。

以上是假设proxyServer实现了client与server的数据转发之后,client认为自己访问了server。但是,因为server在子网,所以proxyServer是不能直接向server发起连接的。同时,server主要是接收请求,而不是主动发起连接,所以server也不会主动去连接proxyServer。这样,server和proxyServer是不会直接连接的,两个都是服务,都想别人来连接,而不是主动去连接别人。为了缓解尴尬,就要再来一个代理客户端proxyClient,让他去主动连接proxyServer和server,然后转发他们之间的消息。
这里将proxyServer和proxyClient看作代理proxy,其主要作用是在传输层转发client和server的信息。proxy只负责转发消息,至于信息的含义是什么,proxy一无所知。所以说proxy只工作在传输层,对上层使用什么信息,传输的内容是什么完全不知道。而client发送给proxyServer信息之后,proxyServer返回的信息与client直接访问server返回的信息是一致的,client认为proxySercer提供了服务,把他看成server,他对于传输层数据经过什么处理完全不知道,所以可以说该方法对应用层是透明的。对proxyServer和proxyClient,二者协同共同完成以上的工作,缺一不可。
实现思路
通过以上分析可知,主要目标是实现proxy,而proxy由proxyServer和proxyClient组成,所以这里将需要两个对象。同时,proxy主要是实现传输层数据转发,在具体编程实现时,实际上转发的是两个socket之间的数据。为了方便管理socket之间的关联关系,即应该将当前socket的数据转发给哪个socket,这里将定义一个转发器节点TranslatorNode类,用于存储源socket和目的socket,当当前socket是这个转发器节点的源/目的socket是,只需要将数据转发给目的/源socket就行了。

    在proxyClient和proxyServer的功能中,有一些相同的功能,如转发数据等,也有一些类似的属性,如TranslatorNode对象等。为此,定义一个NetProxy类,用于实现这些共同的部分,再让proxyClient和proxyServer继承他就可以不用到处复制代码了。

考虑具体实现时,proxyClient和proxyServer之间首先要建立通信,如果proxyServer转发多个client的数据时,都用同一个proxyClient与自己建立的连接(socket),那proxyClient从server那里获取的数据都会再通过这个连接返回,这个时候,有很多个client等着拿数据,当前返回的数据到底给谁呢,一种办法就是在转发的数据之前加上一些控制信息,控制信息指明这个数据给谁。但是这会存在一个问题,如果接受数据的buffer比较大,某一次返回的数据可能是转给两个client的,结果混在一起了,这就会导致转发错误的消息给client(未实践证明,个人理解)。而且控制信息的管理也不太方便。为了保证传输数据的纯净,就是proxy不在转发的数据之上再添加额外的信息,这里采用另外一种方法:为每一个client建立一个专门的连接,凡是client的数据都通过为他建立的连接转发给proxyClient,proxyClient发到这个连接的数据都返回给对应的client。这样管理起来就比较方便了。当有一个client连接到proxyServer时,proxyServer和proxyClient要建立一条新的连接,但是proxyServer又不能主动建立连接,因此由proxyClient建立,为了让proxyClient知道什么时候该建立连接,就需要proxyClient与proxyServer有一条专门用来通信的连接proxy_communication_conn。

    注意到就proxyServer而言,他并不知道连接到自己的到底是client还是proxyClient,所以这里将定义一些指令,用于指明proxyClient的身份,说明自己是proxyClient。在proxyClient连接到proxyServer之后,就需要发送特定消息,这就要求proxyServer和proxyClient约定好了什么消息代表什么含义。这里将用一个指令Command类来定义具体的指令,且指令不可被修改,指令尽可能为client不可能发送的消息,不然proxyServer会误把client当成proxyClient。
    总结一下,需要实现TranslatorNode,Command,NetProxy,NetProxyServer(即proxyserver),NetProxyClient(即proxyClient),NetProxy。下面将一一介绍实现细节。

NetProxyServer和NetProxyClient用来实现proxyServer和proxyClient的功能,他们继承自NetProxy,但是NetProxy具体要实现哪些功能,需要先分析了NetProxyClient和NetProxyServer之后,总结其共同部分才能得到,所以这里先这两个类作介绍。这里通过一个client访问server的过程说明proxyServer和proxyClient的功能。

    为了让proxyServer和proxyClient互通消息,应该先建立proxy_communication_conn,所以在proxyClient启动之后,应先去连接proxyServer,并跟他说明这个连接是用来通信的,所以两者约定一个通信连接指令,当proxyServer接收到一个连接之后,如果这个连接发送了这个指令给自己,那就把这个连接标识为proxy_communication_conn,当然,proxyClient发送这条指令所用的连接也标识为proxy_communication_conn,当这个连接收到消息时,需要解析其含义,而其他连接则只需转发消息。NetProxy可以定义一个变量proxy_communication_conn,这样在继承以后NetProxyClient和NetProxyServer不用重复定义这个变量。
    在client连接到proxyServer之后,发送消息,proxyServer收到消息,判断不是与proxyClient约定的指令,就看下这个连接有没有对应的转发连接。那怎么知道是否存在呢?这里需要用一个translator_node_pool来存储,他是一个列表,元素为TranslatorNode,每一个TranslatorNode有两个连接,表示从一个连接收到的消息要发送到另一个连接,这样就实现了连接之间的关联关系。当然,client可能在proxyClient跟自己建立连接之前已经给自己发送消息,这个时候并不知道消息要发给谁,直接丢掉又太直接了,所以考虑在TranslatorNode里面增加一个列表元素,用来存储关联连接之前client发送过来的消息。为了建立这个关联连接,那就要proxyClient主动跟自己再创建一个连接,然后再将这个连接关联到client的连接。为了识别出是proxyClient建立的用来关联的连接,二者需要约定一个指令,用来表明当前连接是proxyClient主动连接的,需要关联到client的连接。
    proxyClient也需要一个translator_node_pool,用来转发与client关联的连接与server的连接之间的消息。为了简单管理,在proxyServer通知自己添加一个跟client连接关联的连接之后,与proxyServer建立连接并发送指定指令,然后还需要建立一个到server的连接,再用TranslatorNode将这两个连接关联,之后一个连接接收到消息之后,就知道应该把消息转发到哪里了。
    同样,可以在NetProxy定义一个translator_node_pool,同时可以定义一个Command类,因为proxyClient和proxyServer都通过他来获取大家约定的指令。另外,转发消息也是他们都需要实现的方法,也可以写到NetProxy里面。考虑到断开连接时执行的操作也基本一致,可以也将该方法加入进去。
    还要注意的一点是接收消息时设置的buffersize,如果过大则会占用过多内存,但是设置过小的话,在下载大文件的时候,下载速度直接受到buffersize的限制。为此,可考虑动态调整buffersize,对TranslatorNode添加一个buffersize属性,用于指示下次的buffersize应该要设多大。为了避免recv消息时阻塞,可以结合select实现非阻塞接收消息。


代码小熊
2 声望0 粉丝