一、功能描述:
客戶端通過訪問外網服務器上指定端口,間接訪問自已本地的內網服務。
二、原理圖如下:
三、實現代碼如下:
server.go代碼:
package main; import ( "net" "fmt" "flag" "os" ) type MidServer struct { //客戶端監聽 clientLis *net.TCPListener; //后端服務連接 transferLis *net.TCPListener; //所有通道 channels map[int]*Channel; //當前通道ID curChannelId int; } type Channel struct { //通道ID id int; //客戶端連接 client net.Conn; //后端服務連接 transfer net.Conn; //客戶端接收消息 clientRecvMsg chan []byte; //后端服務發送消息 transferSendMsg chan []byte; } //創建一個服務器 func New() *MidServer { return &MidServer{ channels: make(map[int]*Channel), curChannelId: 0, }; } //啟動服務 func (m *MidServer) Start(clientPort int, transferPort int) error { addr, err := net.ResolveTCPAddr("tcp", fmt.Sprintf(":%d", clientPort)); if err != nil { return err; } m.clientLis, err = net.ListenTCP("tcp", addr); if err != nil { return err; } addr, err = net.ResolveTCPAddr("tcp", fmt.Sprintf(":%d", transferPort)); if err != nil { return err; } m.transferLis, err = net.ListenTCP("tcp", addr); if err != nil { return err; } go m.AcceptLoop(); return nil; } //關閉服務 func (m *MidServer) Stop() { m.clientLis.Close(); m.transferLis.Close(); //循環關閉通道連接 for _, v := range m.channels { v.client.Close(); v.transfer.Close(); } } //刪除通道 func (m *MidServer) DelChannel(id int) { chs := m.channels; delete(chs, id); m.channels = chs; } //處理連接 func (m *MidServer) AcceptLoop() { transfer, err := m.transferLis.Accept(); if err != nil { return; } for { //獲取連接 client, err := m.clientLis.Accept(); if err != nil { continue; } //創建一個通道 ch := &Channel{ id: m.curChannelId, client: client, transfer: transfer, clientRecvMsg: make(chan []byte), transferSendMsg: make(chan []byte), }; m.curChannelId++; //把通道加入channels中 chs := m.channels; chs[ch.id] = ch; m.channels = chs; //啟一個goroutine處理客戶端消息 go m.ClientMsgLoop(ch); //啟一個goroutine處理后端服務消息 go m.TransferMsgLoop(ch); go m.MsgLoop(ch); } } //處理客戶端消息 func (m *MidServer) ClientMsgLoop(ch *Channel) { defer func() { fmt.Println("ClientMsgLoop exit"); }(); for { select { case data, isClose := <-ch.transferSendMsg: { //判斷channel是否關閉,如果是則返回 if !isClose { return; } _, err := ch.client.Write(data); if err != nil { return; } } } } } //處理后端服務消息 func (m *MidServer) TransferMsgLoop(ch *Channel) { defer func() { fmt.Println("TransferMsgLoop exit"); }(); for { select { case data, isClose := <-ch.clientRecvMsg: { //判斷channel是否關閉,如果是則返回 if !isClose { return; } _, err := ch.transfer.Write(data); if err != nil { return; } } } } } //客戶端與后端服務消息處理 func (m *MidServer) MsgLoop(ch *Channel) { defer func() { //關閉channel,好讓ClientMsgLoop與TransferMsgLoop退出 close(ch.clientRecvMsg); close(ch.transferSendMsg); m.DelChannel(ch.id); fmt.Println("MsgLoop exit"); }(); buf := make([]byte, 1024); for { n, err := ch.client.Read(buf); if err != nil { return; } ch.clientRecvMsg <- buf[:n]; n, err = ch.transfer.Read(buf); if err != nil { return; } ch.transferSendMsg <- buf[:n]; } } func main() { //參數解析 localPort := flag.Int("localPort", 8080, "客戶端訪問端口"); remotePort := flag.Int("remotePort", 8888, "服務訪問端口"); flag.Parse(); if flag.NFlag() != 2 { flag.PrintDefaults(); os.Exit(1); } ms := New(); //啟動服務 ms.Start(*localPort, *remotePort); //循環 select {}; }
client.go代碼:
package main; import ( "net" "fmt" "flag" "os" ) func handler(r net.Conn, localPort int) { buf := make([]byte, 1024); for { //先從遠程讀數據 n, err := r.Read(buf); if err != nil { continue; } data := buf[:n]; //建立與本地80服務的連接 local, err := net.Dial("tcp", fmt.Sprintf(":%d", localPort)); if err != nil { continue; } //向80服務寫數據 n, err = local.Write(data); if err != nil { continue; } //讀取80服務返回的數據 n, err = local.Read(buf); //關閉80服務,因為本地80服務是http服務,不是持久連接 //一個請求結束,就會自動斷開。所以在for循環里我們要不斷Dial,然后關閉。 local.Close(); if err != nil { continue; } data = buf[:n]; //向遠程寫數據 n, err = r.Write(data); if err != nil { continue; } } } func main() { //參數解析 host := flag.String("host", "127.0.0.1", "服務器地址"); remotePort := flag.Int("remotePort", 8888, "服務器端口"); localPort := flag.Int("localPort", 80, "本地端口"); flag.Parse(); if flag.NFlag() != 3 { flag.PrintDefaults(); os.Exit(1); } //建立與服務器的連接 remote, err := net.Dial("tcp", fmt.Sprintf("%s:%d", *host, *remotePort)); if err != nil { fmt.Println(err); } go handler(remote, *localPort); select {}; }
四、測試
1、先把server.go上傳到外網服務器上,安裝GO環境,並編譯,然后運行server
> ./server -localPort 8080 -remotePort 8888
2、在本地編譯client.go,運行client
> client.exe -host 外網服務器IP -localPort 80 -remotePort 8888
3、瀏覽器訪問外網服務器8080端口
當我瀏覽器訪問時,外網服務器的server會打印兩次MsgLoop exit,這是因為谷歌瀏覽器會多一個favicon.ico請求,不知道其他瀏覽器會不會。
注意,上面的server.go和client.go代碼不排除會有BUG,代碼僅供參考,切勿用於生產環境。