内容简介:差不多一个星期没更新这个系列了,今天难得有点时间,然后就开始写了点代码,上一章节讲了数据模型的定义和数据发送。这些都是一些准备,但是实际上距离真正实现tcp内网穿透代理还有些距离。所以今天的章节是快速写一个例子,来测试一下tcp内网穿透代理。然后再规范代码,因为快速的demo测试,可以立马看到效果,当然我们的一些设计还是有用的。我自己对于写代码的理解是:
差不多一个星期没更新这个系列了,今天难得有点时间,然后就开始写了点代码,上一章节讲了数据模型的定义和数据发送。这些都是一些准备,但是实际上距离真正实现tcp内网穿透代理还有些距离。
所以今天的章节是快速写一个例子,来测试一下tcp内网穿透代理。然后再规范代码,因为快速的demo测试,可以立马看到效果,当然我们的一些设计还是有用的。
我自己对于写代码的理解是:
分为以下几步
1.首先需要规划,设计,考虑要做的事情,比如接口怎么定义,数据结构是怎么样的,代码逻辑,业务逻辑,流程是怎样的,这些都是需要梳理清楚的。
2.那就是根据这些东西先来个快速的编码,先不要管细节,能越快实现就越好。这一步就是不需要过度的按照设计去实现东西。
3.最后就是根据我们快速编码实现的效果,去改进优化。实现的更加优雅,通用。
今天要做的就是第二步,来快速实现一个tcp内网穿透代理。
首先还是我们需要一个http服务器,这个http服务器是我们的内网的服务器,也就是说我们需要在外网访问到这个位于内网的http服务器。假设我们内网的ip是127.0.0.1,分配的局域网ip是192.168.1.10,然后http端口是8080
那么显而易见,我们在同一内网环境是可以访问的,直接使用192.168.1.10:8000即可访问到服务器
但是如果不在同一局域网的机器就不行了,需要借助一台公网ip的服务器来做一个透传代理。
内网服务器准备
这里假设你已经安装 python 2或者python3,打开我们的mac终端或者windows cmd
在python2下输入python -m SimpleHTTPServer
python3下输入python -m http.server
这样我们可以快速得到一台http服务器,打开浏览器输入127.0.0.1:8000可以发现是一个文件浏览的http服务器
我们不需要很复杂的http服务器,仅仅用来做测试而已,所以这样是足够的了
服务端代码
控制客户端的监听代码
1.这里选择监听在8009端口,这个tcp服务,主要用来接受客户端的连接请求的,然后发送控制指令给到客户端,请求建立隧道连接的。这里只接受一个客户端的连接请求,如果有多余的会close掉
一旦有客户端连接到8009端口,这个tcp连接是一直保持的,为什么呢?
因为服务端需要发送控制指令给客户端,所以tcp连接必须一直保持。
然后服务端会每隔两秒发送 hi 这个消息给到客户端,客户端可以直接忽略掉,因为这个hi只是类似心跳机制的保证。
var cache *net.TCPConn = nil func makeControl() { var tcpAddr *net.TCPAddr tcpAddr, _ = net.ResolveTCPAddr("tcp", "127.0.0.1:8009") //打开一个tcp断点监听 tcpListener, err := net.ListenTCP("tcp", tcpAddr) if err != nil { panic(err) } fmt.Println("控制端口已经监听") for { tcpConn, err := tcpListener.AcceptTCP() if err != nil { panic(err) } fmt.Println("新的客户端连接到控制端服务进程:" + tcpConn.RemoteAddr().String()) if cache != nil { fmt.Println("已经存在一个客户端连接!") //直接关闭掉多余的客户端请求 tcpConn.Close() } else { cache = tcpConn } go control(tcpConn) } func control(conn *net.TCPConn) { go func() { for { //一旦有客户端连接到服务端的话,服务端每隔2秒发送hi消息给到客户端 //如果发送不出去,则认为链路断了,清除cache连接 _, e := conn.Write(([]byte)("hi\n")) if e != nil { cache = nil } time.Sleep(time.Second * 2) } }() }
对外访问的服务端口监听
假设端口是8007,这里的对外访问的服务端口监听,也就是说假设我们服务器ip是10.18.10.1的话,那么 访问10.18.10.1的端口8007,就等于请求内网的127.0.0.1:8000 127.0.0.1:8000就是上面的python服务器
和上面的代码看起来很像,但是用处不一样,上面那个主要目的是控制客户端,要求它建立请求
这里的目的主要是 提供真正需要tcp代理透传的服务!
func makeAccept() { var tcpAddr *net.TCPAddr tcpAddr, _ = net.ResolveTCPAddr("tcp", "127.0.0.1:8007") tcpListener, err := net.ListenTCP("tcp", tcpAddr) if err != nil { panic(err) } defer tcpListener.Close() for { tcpConn, err := tcpListener.AcceptTCP() if err != nil { fmt.Println(err) continue } fmt.Println("A client connected 8007:" + tcpConn.RemoteAddr().String()) addConnMathAccept(tcpConn) sendMessage("new\n") } }
这里大家思考一下,如果真的有请求来了,也就是访问8007了,我们怎么办呢?显然我们需要 把进来的流量发给127.0.0.1:8000,让它去处理就行了。
这么一想好像很简单的样子,但是好像有问题,那就是我的10.18.10.1是公网ip啊,大家都知道, 只有非公网可以主动访问公网 ,非公网主动访问公网的意思就是好像我们日常访问百度一样。 公网是不可以直接跟非公网建立tcp连接的。
那么怎么解决呢?
那就是我们需要先记录下这个进来的8007的tcp连接,然后上面不是说到我们有个tcp连接是一直hold住的,那就是8009那个,服务器每隔2秒发送hi给客户端的。
那么我们可以通过这个8009的tcp链路发送一条消息给客户端,告诉客户端赶紧和我建立一个新的tcp请求吧,为了方便描述, 我把"告诉客户端赶紧和我建立一个新的tcp请求"这个新的请求标记为8008链路
这时候就可以把8007的tcp流量发送到这个新建立的tcp链路上。然后把这个新建立的tcp链路的请求发送回去,建立一个读写传输链路即可。
注意这里不能使用8009的tcp链路,8009只是我们用来沟通的链路。
理清楚后,开始编码吧
记录进来的8007的tcp连接,使用一个结构体来存储,这个结构体需要记录accept的tcp连接,也就是8007的tcp链路,和请求的时间,以及8008的链路
刚开始记录的时候8008的链路肯定是nil的,所以设置为nil即可
把它添加到map里面。使用unixNano作为临时key
type ConnMatch struct { accept *net.TCPConn //8007 tcp链路 accept acceptAddTime int64 //接受请求的时间 tunnel *net.TCPConn //8008 tcp链路 tunnel } var connListMap = make(map[string]*ConnMatch) var lock = sync.Mutex{} func addConnMathAccept(accept *net.TCPConn) { //加锁防止竞争读写map lock.Lock() defer lock.Unlock() now := time.Now().UnixNano() connListMap[strconv.FormatInt(now, 10)] = &ConnMatch{accept, time.Now().Unix(), nil} }
告诉客户端赶紧和我建立一个新的tcp请求
这里直接用上面那个cache的tcp链路发送消息即可,不需要太复杂,这里简单定义为new\n即可
........ addConnMathAccept(tcpConn) sendMessage("new\n") } }
func sendMessage(message string) { fmt.Println("send Message " + message) if cache != nil { _, e := cache.Write([]byte(message)) if e != nil { fmt.Println("消息发送异常") fmt.Println(e.Error()) } } else { fmt.Println("没有客户端连接,无法发送消息") } }
转发的tcp监听服务
这里我们来创建前面提到的8008tcp连接了,这里的8008端口,也就是前面说的 发送new这个消息告诉客户端来和这个8008连接吧
func makeForward() { var tcpAddr *net.TCPAddr tcpAddr, _ = net.ResolveTCPAddr("tcp", "127.0.0.1:8008") tcpListener, err := net.ListenTCP("tcp", tcpAddr) if err != nil { panic(err) } defer tcpListener.Close() fmt.Println("Server ready to read ...") for { tcpConn, err := tcpListener.AcceptTCP() if err != nil { fmt.Println(err) continue } fmt.Println("A client connected 8008 :" + tcpConn.RemoteAddr().String()) configConnListTunnel(tcpConn) } }
然后把8008链路分配到ConnMatch,这两个tcp链路是配对的
var connListMapUpdate = make(chan int) func configConnListTunnel(tunnel *net.TCPConn) { //加锁解决竞争问题 lock.Lock() used := false for _, connMatch := range connListMap { //找到tunnel为nil的而且accept不为nil的connMatch if connMatch.tunnel == nil && connMatch.accept != nil { //填充tunnel链路 connMatch.tunnel = tunnel used = true //这里要break,是防止这条链路被赋值到多个connMatch! break } } if !used { //如果没有被使用的话,则说明所有的connMatch都已经配对好了,直接关闭多余的8008链路 fmt.Println(len(connListMap)) _ = tunnel.Close() fmt.Println("关闭多余的tunnel") } lock.Unlock() //使用channel机制来告诉另一个方法已经就绪 connListMapUpdate <- UPDATE }
tcp 转发,这里读取connListMapUpdate这个chain,说明map有更新,需要建立tcpForward隧道
func tcpForward() { for { select { case <-connListMapUpdate: lock.Lock() for key, connMatch := range connListMap { //如果两个都不为空的话,建立隧道连接 if connMatch.tunnel != nil && connMatch.accept != nil { fmt.Println("建立tcpForward隧道连接") go joinConn2(connMatch.accept, connMatch.tunnel) //从map中删除 delete(connListMap, key) } } lock.Unlock() } } } func joinConn2(conn1 *net.TCPConn, conn2 *net.TCPConn) { f := func(local *net.TCPConn, remote *net.TCPConn) { //defer保证close defer local.Close() defer remote.Close() //使用io.Copy传输两个tcp连接, _, err := io.Copy(local, remote) if err != nil { fmt.Println(err.Error()) return } fmt.Println("join Conn2 end") } go f(conn2, conn1) go f(conn1, conn2) }
最后增加一个超时机制,因为会存在这种情况,就是当用户请求8007端口的时候, 迟迟等不到配对的connMatch的tunnel链路啊 ,因为有可能client端挂掉了,导致server不管怎么发送"new"请求,client都无动于衷。
在浏览器看来表现就是一直转着,但是我们不能这样子。
所以当超时的时候,我们主动断掉connMatch中的accept链路即可,设置为5秒
func releaseConnMatch() { for { lock.Lock() for key, connMatch := range connListMap { //如果在指定时间内没有tunnel的话,则释放该连接 if connMatch.tunnel == nil && connMatch.accept != nil { if time.Now().Unix()-connMatch.acceptAddTime > 5 { fmt.Println("释放超时连接") err := connMatch.accept.Close() if err != nil { fmt.Println("释放连接的时候出错了:" + err.Error()) } delete(connListMap, key) } } } lock.Unlock() time.Sleep(5 * time.Second) } }
最后把所有方法整合起来
func main() { //监听控制端口8009 go makeControl() //监听服务端口8007 go makeAccept() //监听转发端口8008 go makeForward() //定时释放连接 go releaseConnMatch() //执行tcp转发 tcpForward() }
客户端代码
连接到服务器的8009控制端口,随时接受服务器的控制请求,随时待命
func connectControl() { var tcpAddr *net.TCPAddr //这里在一台机测试,所以没有连接到公网,可以修改到公网ip tcpAddr, _ = net.ResolveTCPAddr("tcp", "127.0.0.1:8009") conn, err := net.DialTCP("tcp", nil, tcpAddr) if err != nil { fmt.Println("Client connect error ! " + err.Error()) return } fmt.Println(conn.LocalAddr().String() + " : Client connected!8009") reader := bufio.NewReader(conn) for { s, err := reader.ReadString('\n') if err != nil || err == io.EOF { break } else { //接收到new的指令的时候,新建一个tcp连接 if s == "new\n" { go combine() } if s == "hi" { //忽略掉hi的请求 } } } }
combine方法的代码,整合local和remote的tcp连接
func combine() { local := connectLocal() remote := connectRemote() if local != nil && remote != nil { joinConn(local, remote) } else { if local != nil { err := local.Close() if err!=nil{ fmt.Println("close local:" + err.Error()) } } if remote != nil { err := remote.Close() if err!=nil{ fmt.Println("close remote:" + err.Error()) } } } } func joinConn(local *net.TCPConn, remote *net.TCPConn) { f := func(local *net.TCPConn, remote *net.TCPConn) { defer local.Close() defer remote.Close() _, err := io.Copy(local, remote) if err != nil { fmt.Println(err.Error()) return } fmt.Println("end") } go f(local, remote) go f(remote, local) }
connectLocal 连接到python的8000端口!
func connectLocal() *net.TCPConn { var tcpAddr *net.TCPAddr tcpAddr, _ = net.ResolveTCPAddr("tcp", "127.0.0.1:8000") conn, err := net.DialTCP("tcp", nil, tcpAddr) if err != nil { fmt.Println("Client connect error ! " + err.Error()) return nil } fmt.Println(conn.LocalAddr().String() + " : Client connected!8000") return conn }
connectRemote 连接到服务端的8008端口!
func connectRemote() *net.TCPConn { var tcpAddr *net.TCPAddr tcpAddr, _ = net.ResolveTCPAddr("tcp", "127.0.0.1:8008") conn, err := net.DialTCP("tcp", nil, tcpAddr) if err != nil { fmt.Println("Client connect error ! " + err.Error()) return nil } fmt.Println(conn.LocalAddr().String() + " : Client connected!8008") return conn; }
全部整合起来就是
func main() { connectControl() }
最后把服务端和客户端运行起来看看效果
5d0e5308f402d_preview.gif
以上所述就是小编给大家介绍的《golang手把手实现tcp内网穿透代理(3)快速编码测试》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Rework
Jason Fried、David Heinemeier Hansson / Crown Business / 2010-3-9 / USD 22.00
"Jason Fried and David Hansson follow their own advice in REWORK, laying bare the surprising philosophies at the core of 37signals' success and inspiring us to put them into practice. There's no jarg......一起来看看 《Rework》 这本书的介绍吧!