一、功能描述:
客戶端通過訪問外網服務器上指定端口,間接訪問自已本地的內網服務。
二、原理圖如下:

三、實現代碼如下:
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,代碼僅供參考,切勿用於生產環境。
