全网整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:400-708-3566

利用Golang实现TCP连接的双向拷贝详解

前言

本文主要给大家介绍了关于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

一个普遍的观点是Goroutine是无法被中断的。当一个Goroutine在做conn.Read时,这个协程就被阻塞在那里了。实际上并不是毫无办法的,我们可以通过conn.Close来中断Goroutine。但是在连接池的情况下,又无法Close链接。另外一种做法就是通过SetDeadline为一个过去的时间戳来中断当前正在进行的阻塞读或者阻塞写。

var pool = make(chan net.Conn, 100)

type client struct {
 conn net.Conn
 inUse *sync.WaitGroup
}

func borrow() (clt *client, err error) {
 var conn net.Conn
 select {
 case conn = <- pool:
 default:
 conn, err = net.Dial("tcp", "127.0.0.1:18849")
 }
 if err != nil {
 return nil, err
 }
 clt = &client{
 conn: conn,
 inUse: &sync.WaitGroup{},
 }
 return
}

func release(clt *client) error {
 clt.conn.SetDeadline(time.Now().Add(-time.Second))
 clt.inUse.Done()
 clt.inUse.Wait()
 select {
 case pool <- clt.conn:
 // returned to pool
 return nil
 default:
 // pool is overflow
 return clt.conn.Close()
 }
}

func handle(server *net.TCPConn) {
 defer server.Close()
 clt, err := borrow()
 if err != nil {
 fmt.Print(err)
 return
 }
 clt.inUse.Add(1)
 defer release(clt)
 go func() {
 clt.inUse.Add(1)
 defer server.Close()
 defer release(clt)
 buf := make([]byte, 2048)
 io.CopyBuffer(server, clt.conn, buf)
 }()
 buf := make([]byte, 2048)
 io.CopyBuffer(clt.conn, server, buf)
}

通过SetDeadline实现了goroutine的中断,然后通过sync.WaitGroup来保证这些使用方都退出了之后再归还给连接池。否则一个连接被复用的时候,之前的使用方可能还没有退出。

连接有效性

为了保证在归还给pool之前,连接仍然是有效的。连接在被读写的过程中如果发现了error,我们就要标记这个连接是有问题的,会释放之后直接close掉。但是SetDeadline必然会导致读取或者写入的时候出现一次timeout的错误,所以还需要把timeout排除掉。

var pool = make(chan net.Conn, 100)

type client struct {
 conn net.Conn
 inUse *sync.WaitGroup
 isValid int32
}

const maybeValid = 0
const isValid = 1
const isInvalid = 2

func (clt *client) Read(b []byte) (n int, err error) {
 n, err = clt.conn.Read(b)
 if err != nil {
 if !isTimeoutError(err) {
 atomic.StoreInt32(&clt.isValid, isInvalid)
 }
 } else {
 atomic.StoreInt32(&clt.isValid, isValid)
 }
 return
}

func (clt *client) Write(b []byte) (n int, err error) {
 n, err = clt.conn.Write(b)
 if err != nil {
 if !isTimeoutError(err) {
 atomic.StoreInt32(&clt.isValid, isInvalid)
 }
 } else {
 atomic.StoreInt32(&clt.isValid, isValid)
 }
 return
}

type timeoutErr interface {
 Timeout() bool
}

func isTimeoutError(err error) bool {
 timeoutErr, _ := err.(timeoutErr)
 if timeoutErr == nil {
 return false
 }
 return timeoutErr.Timeout()
}

func borrow() (clt *client, err error) {
 var conn net.Conn
 select {
 case conn = <- pool:
 default:
 conn, err = net.Dial("tcp", "127.0.0.1:18849")
 }
 if err != nil {
 return nil, err
 }
 clt = &client{
 conn: conn,
 inUse: &sync.WaitGroup{},
 isValid: maybeValid,
 }
 return
}

func release(clt *client) error {
 clt.conn.SetDeadline(time.Now().Add(-time.Second))
 clt.inUse.Done()
 clt.inUse.Wait()
 if clt.isValid == isValid {
 return clt.conn.Close()
 }
 select {
 case pool <- clt.conn:
 // returned to pool
 return nil
 default:
 // pool is overflow
 return clt.conn.Close()
 }
}

func handle(server *net.TCPConn) {
 defer server.Close()
 clt, err := borrow()
 if err != nil {
 fmt.Print(err)
 return
 }
 clt.inUse.Add(1)
 defer release(clt)
 go func() {
 clt.inUse.Add(1)
 defer server.Close()
 defer release(clt)
 buf := make([]byte, 2048)
 io.CopyBuffer(server, clt, buf)
 }()
 buf := make([]byte, 2048)
 io.CopyBuffer(clt, server, buf)
}

判断 error 是否是 timeout 需要类型强转来实现。

对于连接池里的conn是否仍然是有效的,如果用后台不断ping的方式来实现成本比较高。因为不同的协议要连接保持需要不同的ping的方式。一个最简单的办法就是下次用的时候试一下。如果连接不好用了,则改成新建一个连接,避免连续拿到无效的连接。通过这种方式把无效的连接给淘汰掉。

关于正确性

本文在杭州机场写成,完全不保证内容的正确性

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对的支持。


# go  # tcp  # 连接  # golang  # 双向  # golang之tcp自动重连实现方法  # golang 实现tcp转发代理的方法  # Golang 实现Socket服务端和客户端使用TCP协议通讯  # golang中net的tcp服务使用  # Golang通过包长协议处理TCP粘包的问题解决  # Golang 编写Tcp服务器的解决方案  # golang实现简单的tcp数据传输  # Golang实现自己的Redis(TCP篇)实例探究  # Golang TCP网络编程的具体实现  # 另外一个  # 连接池  # 仍然是  # 出了  # 问题是  # 显而易见  # 最简单  # 来实现  # 复用  # 就会  # 还没有  # 相关内容  # 是有  # 说了  # 毫无办法  # 已有  # 不多  # 较高  # 有一定  # 我们可以 


相关文章: ,交易猫的商品怎么发布到网站上去?  如何通过免费商城建站系统源码自定义网站主题与功能?  平台云上自助建站如何快速打造专业网站?  高端智能建站公司优选:品牌定制与SEO优化一站式服务  学生网站制作软件,一个12岁的学生写小说,应该去什么样的网站?  如何快速生成橙子建站落地页链接?  大连网站制作公司哪家好一点,大连买房网站哪个好?  如何快速辨别茅台真假?关键步骤解析  江苏网站制作公司有哪些,江苏书法考级官方网站?  如何在七牛云存储上搭建网站并设置自定义域名?  建站主机选购指南与交易推荐:核心配置解析  建站之星如何快速更换网站模板?  如何用已有域名快速搭建网站?  高防服务器租用如何选择配置与防御等级?  专业公司网站制作公司,用什么语言做企业网站比较好?  独立制作一个网站多少钱,建立网站需要花多少钱?  文字头像制作网站推荐软件,醒图能自动配文字吗?  网页设计与网站制作内容,怎样注册网站?  香港服务器网站卡顿?如何解决网络延迟与负载问题?  建站DNS解析失败?如何正确配置域名服务器?  高端网站建设与定制开发一站式解决方案 中企动力  建站之星24小时客服电话如何获取?  专业的网站制作设计是什么,如何制作一个企业网站,建设网站的基本步骤有哪些?  非常酷的网站设计制作软件,酷培ai教育官方网站?  定制建站模板如何实现SEO优化与智能系统配置?18字教程  唐山网站制作公司有哪些,唐山找工作哪个网站最靠谱?  韩国代理服务器如何选?解析IP设置技巧与跨境访问优化指南  已有域名和空间如何快速搭建网站?  音响网站制作视频教程,隆霸音响官方网站?  制作网站的软件免费下载,免费制作app哪个平台好?  制作农业网站的软件,比较好的农业网站推荐一下?  网站制作大概要多少钱一个,做一个平台网站大概多少钱?  高性价比服务器租赁——企业级配置与24小时运维服务  子杰智能建站系统|零代码开发与AI生成SEO优化指南  如何通过IIS搭建网站并配置访问权限?  网站制作大概多少钱一个,做一个平台网站大概多少钱?  建站主机是否属于云主机类型?  制作国外网站的软件,国外有哪些比较优质的网站推荐?  存储型VPS适合搭建中小型网站吗?  全景视频制作网站有哪些,全景图怎么做成网页?  如何快速生成凡客建站的专业级图册?  一键制作网站软件下载安装,一键自动采集网页文档制作步骤?  如何通过PHP快速构建高效问答网站功能?  建站之星安装失败:服务器环境不兼容?  如何通过主机屋免费建站教程十分钟搭建网站?  黑客如何通过漏洞一步步攻陷网站服务器?  如何通过可视化优化提升建站效果?  如何快速搭建支持数据库操作的智能建站平台?  教学网站制作软件,学习*后期制作的网站有哪些?  沈阳个人网站制作公司,哪个网站能考到沈阳事业编招聘的信息? 

您的项目需求

*请认真填写需求信息,我们会在24小时内与您取得联系。