差不多一個星期沒更新這個系列了,今天難得有點時間,然後就開始寫了點代碼,上一章節講了數據模型的定義和數據發送。這些都是一些準備,但是實際上距離真正實現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
"
))
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
"
)
}
}

這裡大家思考一下,如果真的有請求來了,也就是訪問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
即可

........
addConnMathAccept(tcpConn)
sendMessage("new
"
)
}
}
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(
)
if err != nil || err == io.EOF {
break
} else {
//接收到new的指令的時候,新建一個tcp連接
if s == "new
"
{
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()
}

最後把服務端和客戶端運行起來看看效果,訪問8007,而不是訪問8000


推薦閱讀:
相关文章