暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

如何优雅的处理Golang socket读写事件关闭

技术随想录 2021-12-01
5296

最近的事情挺杂的,开发的工作量也就小了很多,只能记录一些零碎的知识点。

给Easegress写TCP
代理(PR: add tcp/udp proxy feature
)的时候,我将socket的读写分离至两个goroutine中,通过for-select进行数据的读写操作,以下简称read-loop以及write-loop。

此时,当socket需要关闭的时候,例如read-loop中读取到io.EOF
时,需要调用conn.Close()
关闭读写连接。问题出现了,此时write-loop中会报异常use of closed network connection
。最开始的处理措施是,当遇到异常(除了超时的所有异常)时,判断当前连接是否已经被关闭(通过closed
原子变量),若连接已关闭,则直接退出循环(不处理异常,是因为无法有效区分正常关闭和异常关闭,只会徒增日志量)。

然而博民提出质疑:

(1) write-loop写操作抛出异常,尝试处理异常;
(2) read-loop读取到io.EOF,关闭连接,设置closed
原子变量;
(3) write-loop处理异常,发现closed
变量被设置了,直接退出循环。

在这个场景下,岂不是错误被吞没了。
那么有没有办法通知read-loop以及write-loop同时退出呢?貌似分别在read-loop以及write-loop中设置SetReadDeadline
以及SetWriteDeadline
是个不错的方法。不去强制关闭资源,每次读取超时异常处理时,判断当前连接是否已经被关闭(仅设置closed
原子变量),若关闭则退出read-loop以及write-loop。

老实话,刚开始我确实是如此想的。
但是随之而来的问题是,如果合理的设置读写的超时时间呢?设置时间长了,socket资源无法被清理,造成资源占用浪费;设置时间短了,在goroutine数量较多的场景下,又会造成性能负担。
那么Golang官方有没有办法给socket连接设置stopped标志量的API呢?在GitHub上搜寻良久,有相似想法的issue很多,但是官方的态度都是坚决拒绝(例:proposal: io: add Context parameter to Reader, etc.)。此路不通,我们得想个别的辙了。

万幸,看到一个很巧妙的解决方案,代码如下所示:

type ReadDeadliner interface {
        SetReadDeadline(t time.Time) error
}

func SetReadDeadlineOnCancel(ctx context.Context, d ReadDeadliner) {
        go func() {
                <-ctx.Done()
                d.SetReadDeadline(time.Now())
        }()
}

func handleConnection(ctx context.Context, conn net.Conn) {
    ctx, cancel := context.WithCancel(ctx)
    defer cancel()
    SetReadDeadlineOnCancel(ctx, conn)

    // rest of code goes here, skeleton only shown
    for {
        n, err := conn.Read(buf)   // blocking here waiting for a message
        if err != nil {
            ... see below
        }
        ...
        conn.Write(...)
    }
}

以上代码是结合Context处理socket关闭。当需要关闭socket连接时,关闭context,SetReadDeadlineOnCancel
中的goroutine启动,给socket连接SetReadDeadline
。此时socket连接会立即报超时错误。处理超时错误时,观察context,会发现context已经被cancel了,则优雅退出事件循环。

挺好的,Google的mtail项目也是这样处理的(Commit: Simplify pipestream by correctly interrupting a read on context cancel)。

那么我的处理逻辑也相应的修改为(列举一个场景):

(1) 假如当read-loop读取到io.EOF,更新closed
原子变量,调用conn.SetReadDeadline(time.Now())
;
(2) write-loop遇到超时异常,检查closed
变量,发现连接已被关闭,退出循环。

改动的具体代码可见[tcpproxy] notify read/write loop to exit by connection timeout。
这里依然存在问题,即在更新closed
变量以及调用conn.SetReadDeadline(time.Now())
之间write-loop发生异常,应该如何处理?老实话,我没有想到很好的处理方式,总不可能在几个goroutine之间使用锁保持同步吧。那么是否可以将closed
类型从bool
变更为chan struct{}
,答案是否定的。

以read-loop中的for-select为例,当连接进入read
阻塞调用时,此时closed chan
关闭,也只能等待read
读取完成或者读取超时,才能执行下一轮for-select循环。因此,只能安慰自己有些异常,报出来或者默默的吞掉就可以,也是种解决方案。

怀念C++,使用muduo
的话,我已经有比较不错的解决方案了。

最后,欢迎大家来找茬,PR: add tcp/udp proxy feature
,争取让代码早日合并进去(2400多行代码都快成为我的心病了)。

感谢来自于Pexels 上的 Lisa 拍摄的封面图片,虽然截取之后不是那么完美了


文章转载自技术随想录,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论