内容简介:差不多一个星期没更新这个系列了,今天难得有点时间,然后就开始写了点代码,上一章节讲了数据模型的定义和数据发送。这些都是一些准备,但是实际上距离真正实现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)快速编码测试》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Java核心技术及面试指南
金华、胡书敏、周国华、吴倍敏 / 北京大学出版社 / 2018-9-1 / 59.00
本书根据大多数软件公司对高级开发的普遍标准,为在Java 方面零基础和开发经验在3 年以下的初级程序员提供了升级到高级工程师的路径,并以项目开发和面试为导向,精准地讲述升级必备的技能要点。具体来讲,本书围绕项目常用技术点,重新梳理了基本语法点、面向对象思想、集合对象、异常处理、数据库操作、JDBC、IO 操作、反射和多线程等知识点。 此外,本书还提到了对项目开发很有帮助的“设计模式”和“虚拟......一起来看看 《Java核心技术及面试指南》 这本书的介绍吧!