前言
本文主要给大家介绍了关于Golang实现TCP连接的双向拷贝的相关内容,分享出来供大家参考学习,下面话不多说了,来一起看看详细的介绍吧。
最简单的实现
每次来一个Server的连接,就新开一个Client的连接。用一个goroutine从server拷贝到client,再用另外一个goroutine从client拷贝到server。任何一方断开连接,双向都断开连接。
func main() {
runtime.GOMAXPROCS(1)
listener, err := net.Listen("tcp", "127.0.0.1:8848")
if err != nil {
panic(err)
}
for {
conn, err := listener.Accept()
if err != nil {
panic(err)
}
go handle(conn.(*net.TCPConn))
}
}
func handle(server *net.TCPConn) {
defer server.Close()
client, err := net.Dial("tcp", "127.0.0.1:8849")
if err != nil {
fmt.Print(err)
return
}
defer client.Close()
go func() {
defer server.Close()
defer client.Close()
buf := make([]byte, 2048)
io.CopyBuffer(server, client, buf)
}()
buf := make([]byte, 2048)
io.CopyBuffer(client, server, buf)
}
一个值得注意的地方是io.Copy的默认buffer比较大,给一个小的buffer可以支持更多的并发连接。
这两个goroutine并序在一个退出之后,另外一个也退出。这个的实现是通过关闭server或者client的socket来实现的。因为socket被关闭了,io.CopyBuffer 就会退出。
Client端实现连接池
一个显而易见的问题是,每次Server的连接进来之后都需要临时去建立一个新的Client的端的连接。这样在代理的总耗时里就包括了一个tcp连接的握手时间。如果能够让Client端实现连接池复用已有连接的话,可以缩短端到端的延迟。
var pool = make(chan net.Conn, 100)
func borrow() (net.Conn, error) {
select {
case conn := <- pool:
return conn, nil
default:
return net.Dial("tcp", "127.0.0.1:8849")
}
}
func release(conn net.Conn) error {
select {
case pool <- conn:
// returned to pool
return nil
default:
// pool is overflow
return conn.Close()
}
}
func handle(server *net.TCPConn) {
defer server.Close()
client, err := borrow()
if err != nil {
fmt.Print(err)
return
}
defer release(client)
go func() {
defer server.Close()
defer release(client)
buf := make([]byte, 2048)
io.CopyBuffer(server, client, buf)
}()
buf := make([]byte, 2048)
io.CopyBuffer(client, server, buf)
}
这个版本的实现是显而易见有问题的。因为连接在归还到池里的时候并不能保证是还保持连接的状态。另外一个更严重的问题是,因为client的连接不再被关闭了,当server端关闭连接时,从client向server做io.CopyBuffer的goroutine就无法退出了。
所以,有以下几个问题要解决:
如何在一个goroutine时退出时另外一个goroutine也退出? 怎么保证归还给pool的连接是有效的? 怎么保持在pool中的连接仍然是一直有效的?通过SetDeadline中断Goroutine









