写点什么

推荐两个网络复用相关的 Go pkg: cmux/smux

  • 2024-02-23
    福建
  • 本文字数:5376 字

    阅读完需:约 18 分钟

只写一下如何使用,不对实现进行大量描述,两个库的代码都比较精炼,花一会看一下就行。


  • cmux 对端口进行复用,单端口可以建立不同协议的连接(本质都是 TCP),如 TCP/TLS/HTTP/gRPC 或自定义协议

  • smux 对 TCP 连接复用,单 TCP 连接承载多条 smux stream


适用使用场景


  • cmux 一些对外只提供单端口的服务,比如一个端口同时提供 HTTP/HTTPS 功能,其实还能够更多

  • smux 一些性能敏感的地方,比如大量 TLS 的短连接请求,对于频繁的握手非常消耗 CPU,见性能压测反向连接,将 TCP 客户端抽象为服务端,方便如 HTTP/gRPC 的服务开发


cmux 的使用


借用一些官方示例,使用还是相对简单的,23456 端口同时提供了 gRPC/HTTP/tRPC 复用。


// Create the main listener.l, err := net.Listen("tcp", ":23456")if err != nil {	log.Fatal(err)}
// Create a cmux.m := cmux.New(l)
// Match connections in order:// First grpc, then HTTP, and otherwise Go RPC/TCP.grpcL := m.Match(cmux.HTTP2HeaderField("content-type", "application/grpc"))httpL := m.Match(cmux.HTTP1Fast())trpcL := m.Match(cmux.Any()) // Any means anything that is not yet matched.
// Create your protocol servers.grpcS := grpc.NewServer()grpchello.RegisterGreeterServer(grpcS, &server{})
httpS := &http.Server{ Handler: &helloHTTP1Handler{} }
trpcS := rpc.NewServer()trpcS.Register(&ExampleRPCRcvr{})
// Use the muxed listeners for your servers.go grpcS.Serve(grpcL)go httpS.Serve(httpL)go trpcS.Accept(trpcL)
// Start serving!m.Serve()
复制代码


自定义 Matcher


cmux 的实现上是对 payload 进行匹配,cmux.HTTP1Fast 是一个匹配函数,为内置的集中匹配函数中的一种,这类匹配函数可以同时设置多个。内部将 net.Conn 的数据读入至 buffer 内,依次调用各个匹配函数对这个 buffer 进行分析,如果匹配成功则 httpL 被返回,httpS 服务收到请求。

所以我们可以自定义一些 Matcher,比如一些携带 Magic/字符串 头的数据


const (	PacketMagic = 0x00114514	PacketToken = "xyz_token")
func PacketMagicMatcher(r io.Reader) bool { buf := make([]byte, 4) n, err := io.ReadFull(r, buf) if err != nil { return false } return binary.BigEndian.Uint32(buf[:n]) == PacketMagic}
func PacketTokenMatcher(r io.Reader) bool { buf := make([]byte, len(PacketToken)) n, err := io.ReadFull(r, buf) if err != nil { return false } return string(buf[:n]) == PacketToken}
复制代码


使用上和 cmux.HTTP1Fast 相同,需要注意的是,net.Conn 头部的 Magic/Token 是和连接相关的,和业务数据无关在使用这些数据之前,需要先将其读取出来


tcpMux := cmux.New(lis)magicLis := tcpMux.Match(PacketMagicMatcher)
go func() { conn, _ := magicLis.Accept() buf := make([]byte, 4) io.ReadAtLeast(conn, buf, len(buf)) // Read header magic length // Handle data ...}()
tcpMux.Serve()
复制代码


多 mux 场景


需求:只开放一个端口 12345,需要支持


  • HTTP 协议的包下载

  • 基于 TLS 的 gRPC 服务

  • 基于 TLS 的自定义服务


分析:对于 HTTP 和 TLS 需要使用一个 mux 进行区分,TLS 中的 gRPC 和 自定义服务需要再通过一个 mux 区分


参考的实现(截取了一部分业务代码)


// TCP 分流 http/tlstcpMux := cmux.New(lis)installerL := tcpMux.Match(cmux.HTTP1Fast())anyL := tcpMux.Match(cmux.Any())
// tls.NewListener(anyL, ...)mtlsL, err := mTLSListener(anyL, tlsEnable, tlsCertPath, tlsKeyPath, tlsCAPath)if err != nil { return err}tlsMux := cmux.New(mtlsL)grpcL := tlsMux.Match(cmux.HTTP2())gwL := tlsMux.Match(gw.Matcher)
复制代码


smux 的使用


还是放一些官方的简单示例


func client() {    // Get a TCP connection    conn, err := net.Dial(...)    if err != nil {        panic(err)    }    // Setup client side of smux    session, err := smux.Client(conn, nil)    if err != nil {        panic(err)    }    // Open a new stream    stream, err := session.OpenStream()    if err != nil {        panic(err)    }    // Stream implements io.ReadWriteCloser    stream.Write([]byte("ping"))    stream.Close()    session.Close()}
func server() { // Accept a TCP connection conn, err := listener.Accept() if err != nil { panic(err) } // Setup server side of smux session, err := smux.Server(conn, nil) if err != nil { panic(err) } // Accept a stream stream, err := session.AcceptStream() if err != nil { panic(err) } // Listen for a message buf := make([]byte, 4) stream.Read(buf) stream.Close() session.Close()}
复制代码


smux.Session 和 net.Conn 对应,smux.Stream 实现了 net.Conn 接口,所以使用起来和普通的连接无异。smux.Session 是双向的,Client/Server 的区分仅仅是内部的 Id 区别,这就为反向连接打下了基础


基于 smux 的反向连接


对于一个普通的 TCP 服务而言,A(client) -> B(server)。在 B 上 建立 gRPC/HTTP 服务是一件非常自然的事情。


在某些场景下,比如 A 在公网,B 在内网,不做公网映射的话,只能够 B(client) -> A(server)。但是这个情况下,B 上面的 gRPC/HTTP 的服务就不能直接建立了。


上面说过 smux.Session 再使用上时没有方向的,并且提供了和 net.Listener 相近的接口,如果将 smux.Session 封装实现 net.Listener,加上 smux.Stream 是 net.Conn,那么 B 连接 A 继续在 B 上建立 gRPC/HTTP 服务是可以的,内部感知不到具体的实现细节。


对 smux.Session 的封装如下


type SmuxSession struct{ *smux.Session }
func (s *SmuxSession) Addr() net.Addr { return s.Session.LocalAddr() }func (s *SmuxSession) Accept() (net.Conn, error) { return s.Session.AcceptStream() }func (s *SmuxSession) Close() error { return s.Session.Close() }
复制代码


将 B(tcp:client) -> A(tcp:server) 的场景改为 A(gRPC:client) -> B(gRPC:Server),关键实现如下:


// 忽略错误处理// 在 A 上的实现如下,cc 为后续使用的 gRPC clientfunc handleConn(conn net.Conn) {	sess, _ := smux.Client(conn, nil)	cc, _ := grpc.Dial(		"",		grpc.WithContextDialer(func(ctx context.Context, s string) (net.Conn, error) { return sess.OpenStream() }),		grpc.WithTransportCredentials(insecure.NewCredentials()),	)    // do something.}
// 在 B 的实现如下func dialAndServe() { conn, _ := net.Dial(...) sess, _ := smux.Server(conn, nil) return g.server.Serve(&SmuxSession{Session: sess})}
复制代码


cmux/smux 结合使用


其实 cmux/smux 是两个不同的维度:单端口/单连接,所以只要保证做好 net.Listener/net.Conn 的抽象,使用起来是感知不到的。


比如上面的反向连接中,handleConn 在上面的 gwL := tlsMux.Match(gw.Matcher) 中驱动的


// A 的实现func handleConn(conn net.Conn) {    // 增加的代码:读取 Header	io.CopyN(io.Discard, conn, int64(len(gw.MatcherToken)))
sess, _ := smux.Client(conn, nil) cc, _ := grpc.Dial( "", grpc.WithContextDialer(func(ctx context.Context, s string) (net.Conn, error) { return sess.OpenStream() }), grpc.WithTransportCredentials(insecure.NewCredentials()), )}
// B 的实现func dialAndServe() { conn, _ := net.Dial(...) conn.Write([]byte(gw.MatcherToken)) // 增加的代码:发送一个头 sess, _ := smux.Server(conn, nil) return g.server.Serve(&SmuxSession{Session: sess})}
复制代码


性能压测

性能压测代码见此.

长连接的读写

测试的连接的 case:

  • TCP 连接,作为一个参考基准

  • TLS

  • SmuxTCP,底层协议为 TCP 的情况 TLS,底层协议为 TLS 的情况

  • CmuxTCP,底层协议为 TCP 的多个 MatcherTLS,底层协议为 TLS 的单个 Matcher,复合 mux


$ go test -v -benchtime=10s  -benchmem -run=^$ -bench ^BenchmarkConn .goos: linuxgoarch: amd64pkg: benchmark/connectioncpu: 12th Gen Intel(R) Core(TM) i7-12700BenchmarkConnCmuxBenchmarkConnCmux/MagicMatcherBenchmarkConnCmux/MagicMatcher-20                     997550             11862 ns/op        11049.73 MB/s          0 B/op          0 allocs/opBenchmarkConnCmux/TokenMatcherBenchmarkConnCmux/TokenMatcher-20                     958461             11714 ns/op        11188.94 MB/s          0 B/op          0 allocs/opBenchmarkConnCmux/TLSMatcherBenchmarkConnCmux/TLSMatcher/TLSBenchmarkConnCmux/TLSMatcher/TLS-20                   295111             40471 ns/op        3238.68 MB/s         192 B/op          7 allocs/opBenchmarkConnCmux/TLSMatcher/MagicMatcherBenchmarkConnCmux/TLSMatcher/MagicMatcher-20          296203             39566 ns/op        3312.75 MB/s         192 B/op          7 allocs/opBenchmarkConnCmux/AnyMatcherBenchmarkConnCmux/AnyMatcher-20                       932871             11870 ns/op        11041.90 MB/s          0 B/op          0 allocs/opBenchmarkConnSmuxBenchmarkConnSmux/OverTCPBenchmarkConnSmux/OverTCP-20                          438889             24703 ns/op        5305.97 MB/s        1380 B/op         26 allocs/opBenchmarkConnSmux/OverTLSBenchmarkConnSmux/OverTLS-20                          210336             57345 ns/op        2285.69 MB/s        1596 B/op         36 allocs/opBenchmarkConnTCPBenchmarkConnTCP-20                                   917894             12120 ns/op        10814.60 MB/s          0 B/op          0 allocs/opBenchmarkConnTLSBenchmarkConnTLS-20                                   292843             40310 ns/op        3251.57 MB/s         192 B/op          7 allocs/opPASSok      benchmark/connection    106.287s
复制代码


短连接的读写


较长连接的 case 变化,减少 Cmux 为一个 Matcher,额外引入了 net.HTTP 和 fasthttp 参与 PK。

短连接的测试,包含了连接的建立和关闭的场景。


$ go test -v -benchtime=10s  -benchmem -run=^$ -bench ^BenchmarkEcho .goos: linuxgoarch: amd64pkg: benchmark/connectioncpu: 12th Gen Intel(R) Core(TM) i7-12700BenchmarkEchoCmuxBenchmarkEchoCmux-20                       83162            164356 ns/op         797.49 MB/s       34005 B/op         26 allocs/opBenchmarkEchoFastHTTPBenchmarkEchoFastHTTP-20                  144302             95231 ns/op        1376.36 MB/s       12941 B/op         41 allocs/opBenchmarkEchoNetHTTPBenchmarkEchoNetHTTP-20                    65124            239187 ns/op         547.99 MB/s      370816 B/op         59 allocs/opBenchmarkEchoSmuxBenchmarkEchoSmux/OverTCPBenchmarkEchoSmux/OverTCP-20              153706             70494 ns/op        1859.34 MB/s       79824 B/op         85 allocs/opBenchmarkEchoSmux/OverTLSBenchmarkEchoSmux/OverTLS-20              106585            112120 ns/op        1169.04 MB/s       81776 B/op        102 allocs/opBenchmarkEchoTCPBenchmarkEchoTCP-20                       308125             39266 ns/op        3338.05 MB/s        1078 B/op         23 allocs/opBenchmarkEchoTLSBenchmarkEchoTLS-20                        10000           1988704 ns/op          65.91 MB/s      241188 B/op       1112 allocs/opPASSok      benchmark/connection    112.673s
复制代码


性能压测的结论


对照 TCP 为基准


  • cmu 长连接下对性能的影响很小,接近 TCP,测试有的时候还会比 TCP 高一些短链接下,性能比较低;应该是在 Accept 返回 cmux.MuxConn 之前慢的,多了一次内存拷贝,函数匹配,chan 传递

  • smux 底层协议为 TCP,性能相对 TCP 50% 左右,长连接和短连接表现差不多底层协议为 TLS,性能相对 TCP 25-30% 左右,长连接和短连接表现也接近这个比例

  • TLS 正常的 Read/Write 性能大概在 50% 左右,在短连接的情况下,性能非常差(TLS 握手攻击原理)

  • fasthttp 速度非常快


从性能的角度看,smux 适用于频繁建立 TLS 短连接的场景,将短连接变成了一般的 TLS 长连接,参考 BenchmarkConnSmux/OverTLS-20 和 BenchmarkConnTLS-20 性能只下降了 30% 左右,还是比较能接受的。


文章转载自:小胖西瓜

原文链接:https://www.cnblogs.com/shuqin/p/18027908

体验地址:http://www.jnpfsoft.com/?from=001

用户头像

还未添加个人签名 2023-06-19 加入

还未添加个人简介

评论

发布
暂无评论
推荐两个网络复用相关的 Go pkg: cmux/smux_Go_不在线第一只蜗牛_InfoQ写作社区