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

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

  • 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/tls
tcpMux := 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 client
func 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
  • Smux
    • TCP,底层协议为 TCP 的情况
    • TLS,底层协议为 TLS 的情况
  • Cmux
    • TCP,底层协议为 TCP 的多个 Matcher
    • TLS,底层协议为 TLS 的单个 Matcher,复合 mux
$ go test -v -benchtime=10s  -benchmem -run=^$ -bench ^BenchmarkConn .
goos: linux
goarch: amd64
pkg: benchmark/connection
cpu: 12th Gen Intel(R) Core(TM) i7-12700
BenchmarkConnCmux
BenchmarkConnCmux/MagicMatcher
BenchmarkConnCmux/MagicMatcher-20 997550 11862 ns/op 11049.73 MB/s 0 B/op 0 allocs/op
BenchmarkConnCmux/TokenMatcher
BenchmarkConnCmux/TokenMatcher-20 958461 11714 ns/op 11188.94 MB/s 0 B/op 0 allocs/op
BenchmarkConnCmux/TLSMatcher
BenchmarkConnCmux/TLSMatcher/TLS
BenchmarkConnCmux/TLSMatcher/TLS-20 295111 40471 ns/op 3238.68 MB/s 192 B/op 7 allocs/op
BenchmarkConnCmux/TLSMatcher/MagicMatcher
BenchmarkConnCmux/TLSMatcher/MagicMatcher-20 296203 39566 ns/op 3312.75 MB/s 192 B/op 7 allocs/op
BenchmarkConnCmux/AnyMatcher
BenchmarkConnCmux/AnyMatcher-20 932871 11870 ns/op 11041.90 MB/s 0 B/op 0 allocs/op
BenchmarkConnSmux
BenchmarkConnSmux/OverTCP
BenchmarkConnSmux/OverTCP-20 438889 24703 ns/op 5305.97 MB/s 1380 B/op 26 allocs/op
BenchmarkConnSmux/OverTLS
BenchmarkConnSmux/OverTLS-20 210336 57345 ns/op 2285.69 MB/s 1596 B/op 36 allocs/op
BenchmarkConnTCP
BenchmarkConnTCP-20 917894 12120 ns/op 10814.60 MB/s 0 B/op 0 allocs/op
BenchmarkConnTLS
BenchmarkConnTLS-20 292843 40310 ns/op 3251.57 MB/s 192 B/op 7 allocs/op
PASS
ok benchmark/connection 106.287s

短连接的读写

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

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

$ go test -v -benchtime=10s  -benchmem -run=^$ -bench ^BenchmarkEcho .
goos: linux
goarch: amd64
pkg: benchmark/connection
cpu: 12th Gen Intel(R) Core(TM) i7-12700
BenchmarkEchoCmux
BenchmarkEchoCmux-20 83162 164356 ns/op 797.49 MB/s 34005 B/op 26 allocs/op
BenchmarkEchoFastHTTP
BenchmarkEchoFastHTTP-20 144302 95231 ns/op 1376.36 MB/s 12941 B/op 41 allocs/op
BenchmarkEchoNetHTTP
BenchmarkEchoNetHTTP-20 65124 239187 ns/op 547.99 MB/s 370816 B/op 59 allocs/op
BenchmarkEchoSmux
BenchmarkEchoSmux/OverTCP
BenchmarkEchoSmux/OverTCP-20 153706 70494 ns/op 1859.34 MB/s 79824 B/op 85 allocs/op
BenchmarkEchoSmux/OverTLS
BenchmarkEchoSmux/OverTLS-20 106585 112120 ns/op 1169.04 MB/s 81776 B/op 102 allocs/op
BenchmarkEchoTCP
BenchmarkEchoTCP-20 308125 39266 ns/op 3338.05 MB/s 1078 B/op 23 allocs/op
BenchmarkEchoTLS
BenchmarkEchoTLS-20 10000 1988704 ns/op 65.91 MB/s 241188 B/op 1112 allocs/op
PASS
ok 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-20BenchmarkConnTLS-20 性能只下降了 30% 左右,还是比较能接受的。

参考

smux, A Stream Multiplexing Library for golang with least memory usage(TDMA)。

cmux, Connection multiplexer for GoLang: serve different services on the same port!

kcptun开发小记, smux 作者在知乎上的一篇文章,里面提到了 smux 被开发的原因。

benchmark for connections,各种连接的压测代码和结果

推荐两个网络复用相关的 Go pkg: cmux smux的更多相关文章

  1. linux网络配置相关命令、虚拟网络接口eth0:0

    网络接口(interface)是网络硬件设备在操作系统中的表示方法,比如网卡在Linux操作系统中用ethX,是由0开始的正整数,比如eth0.eth1...... ethX.而普通猫和ADSL的接口 ...

  2. Linux网络配置相关

    路由相关 #添加到主机的路由 route add -host 192.168.1.2 dev eth0 route add -host 192.168.1.2 gw 192.168.1.1 注1:添加 ...

  3. linux网络配置相关文件

    网络接口(interface)是网络硬件设备在操作系统中的表示方法,比如网卡在Linux操作系统中用ethX,是由0开始的正整数,比如eth0.eth1...... ethX.而普通猫和ADSL的接口 ...

  4. 常见的GAN网络的相关原理及推导

    常见的GAN网络的相关原理及推导 在上一篇中我们给大家介绍了GAN的相关原理和推导,GAN是VAE的后一半,再加上一个鉴别网络.这样而导致了完全不同的训练方式. GAN,生成对抗网络,主要有两部分构成 ...

  5. iOS开发UI篇—推荐两个好用的Xcode插件(提供下载链接)

    iOS开发UI篇—推荐两个好用的Xcode插件(提供下载链接) 这里推荐两款好用的Xcode插件,并提供下载链接. 一.插件和使用如下: 1.两款插件 对项目中图片提供自动提示功能的插件:KSImag ...

  6. 推荐两款Xcode插件:KSImageNamed & ColorSense

    之前没怎么接触过Xcode插件,最近发现有人给Xcode做了一些方便编程的插件.今天就推荐两个我个人认为比较好的. 1.KSImageNamed 网站地址 KSImageNamed是一款方便填写图片文 ...

  7. 推荐两个不错的CAD二次开发(.Net)手册

    推荐两个不错的CAD二次开发(.Net)手册 http://www.mjtd.com/helpcenter/netguide/index.html http://www.ceesky.com/book ...

  8. 推荐两个好用的Xcode插件(提供下载链接)

    这里推荐两款好用的Xcode插件,并提供下载链接. 一.插件和使用如下: 1.两款插件 对项目中图片提供自动提示功能的插件:KSImageNamed-Xcode-master 提供快速创建自动注释:V ...

  9. 推荐两款好用的反编译工具(Luyten,Jadx)

    使用JD-Gui打开单个.class文件,总是报错// INTERNAL ERROR 但当我用jd-gui反编译前面操作获得的jar文件的时,但有一部分类不能显示出来--constants类,仅仅显示 ...

  10. 网络营销相关缩写名称CPM CPT CPC CPA CPS SEM SEO解析

    网络营销相关缩写名称CPM CPT CPC CPA CPS SEM SEO解析 CPM CPT CPC CPA CPS SEM SEO在网络营销中是什么意思?SEO和SEM的区别是? CPM(Cost ...

随机推荐

  1. [转帖]在 TiDB 中正确使用索引,性能提升 666 倍

    https://tidb.net/book/tidb-monthly/2022/2022-04/usercase/index-666 背景​ 最近在给一个物流系统做TiDB POC测试,这个系统是基于 ...

  2. [转帖]jmeter实现不写代码把测试结果存入execl

    这里使用数据库作为中间件来实现不写代码就把测试结果存入execl,下面是步骤 1.新建一个setup线程组用来设置数据库连接信息和新建数据库,如下图所示,我们使用sqlite数据库来存储信息,因为不需 ...

  3. [转帖]JMETER结果分析

    https://www.cnblogs.com/a00ium/p/10462892.html 我相信你同意:有很多方法可以收集和解释JMeter结果,你会感到迷茫. 嗯,看完这篇文章后,您将了解收集和 ...

  4. [转帖]NVMe 与 AHCI

    https://www.cnblogs.com/zengkefu/p/5634345.html http://elf8848.iteye.com/blog/1731274 AHCI: NCQ技术,60 ...

  5. Ergonomics JVM 的一种FullGC的说明

    https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/ergonomics.html 2 Ergonomics Ergo ...

  6. [转帖]Python安装模块(包/库)的方法

    这里写目录标题 通过pip安装 正常在线安装 pip命令补全 更改下载镜像 离线包安装 库的下载 库的安装 whl的安装 .tar.gz的安装 源码安装 本地安装报错(依赖) Pycharm中安装 手 ...

  7. SPECJVM2008 再学习

    SPECJVM2008 再学习 摘要 昨天的太水了 感觉今天有必要再水一点.. 存在的问题 默认进行启动 sunflow 必定过不去. 一般的解决办法要求进行重新编译 但是我不知道怎么下载源码... ...

  8. 【图论】CF1508C Complete the MST

    Problem Link 有一张 \(n\) 个点的完全图,其中 \(m\) 条边已经标有边权.你需要给剩下的边都标上权值,使得所有边权的异或和为 \(0\),并且整张图的最小生成树边权和最小. \( ...

  9. 【发现一个问题】VictoriaMetrics中,突然某个时间段新增的data point无法查询出来

    写了一个很简单的python程序,使用remote write协议发数据到vm-insert. 可是有个时段突然查询不出来数据了. 百思不得其解. 吃完中午饭,刷新页面又出来了. 还不清楚问题出现在哪 ...

  10. 【k哥爬虫普法】非法入侵计算机信息系统,获取1500万余条个人信息!

    我国目前并未出台专门针对网络爬虫技术的法律规范,但在司法实践中,相关判决已屡见不鲜,K 哥特设了"K哥爬虫普法"专栏,本栏目通过对真实案例的分析,旨在提高广大爬虫工程师的法律意识, ...