golang手把手实现tcp内网穿透代理(3)快速编码测试

差不多一个星期没更新这个系列了,今天难得有点时间,然后就开始写了点代码,上一章节讲了数据模型的定义和数据发送。这些都是一些准备,但是实际上距离真正实现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的服务器来做一个透传代理。

内网服务器准备

这里假设你已经安装python2或者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
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 143,048评论 1 300
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 61,330评论 1 256
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 94,541评论 0 213
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 41,042评论 0 176
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 48,821评论 1 255
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 38,795评论 1 176
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 30,413评论 2 271
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,173评论 0 167
  • 想象着我的养父在大火中拼命挣扎,窒息,最后皮肤化为焦炭。我心中就已经是抑制不住地欢快,这就叫做以其人之道,还治其人...
    爱写小说的胖达阅读 28,994评论 6 229
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 32,525评论 0 213
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,327评论 2 215
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 30,648评论 1 230
  • 白月光回国,霸总把我这个替身辞退。还一脸阴沉的警告我。[不要出现在思思面前, 不然我有一百种方法让你生不如死。]我...
    爱写小说的胖达阅读 24,248评论 0 31
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,142评论 2 214
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 31,521评论 3 204
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,622评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,025评论 0 166
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 33,543评论 2 230
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 33,636评论 2 231

推荐阅读更多精彩内容