推荐两个网络复用相关的 Go pkg: cmux smux
推荐两个网络复用相关的 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-20 和 BenchmarkConnTLS-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的更多相关文章
- linux网络配置相关命令、虚拟网络接口eth0:0
网络接口(interface)是网络硬件设备在操作系统中的表示方法,比如网卡在Linux操作系统中用ethX,是由0开始的正整数,比如eth0.eth1...... ethX.而普通猫和ADSL的接口 ...
- Linux网络配置相关
路由相关 #添加到主机的路由 route add -host 192.168.1.2 dev eth0 route add -host 192.168.1.2 gw 192.168.1.1 注1:添加 ...
- linux网络配置相关文件
网络接口(interface)是网络硬件设备在操作系统中的表示方法,比如网卡在Linux操作系统中用ethX,是由0开始的正整数,比如eth0.eth1...... ethX.而普通猫和ADSL的接口 ...
- 常见的GAN网络的相关原理及推导
常见的GAN网络的相关原理及推导 在上一篇中我们给大家介绍了GAN的相关原理和推导,GAN是VAE的后一半,再加上一个鉴别网络.这样而导致了完全不同的训练方式. GAN,生成对抗网络,主要有两部分构成 ...
- iOS开发UI篇—推荐两个好用的Xcode插件(提供下载链接)
iOS开发UI篇—推荐两个好用的Xcode插件(提供下载链接) 这里推荐两款好用的Xcode插件,并提供下载链接. 一.插件和使用如下: 1.两款插件 对项目中图片提供自动提示功能的插件:KSImag ...
- 推荐两款Xcode插件:KSImageNamed & ColorSense
之前没怎么接触过Xcode插件,最近发现有人给Xcode做了一些方便编程的插件.今天就推荐两个我个人认为比较好的. 1.KSImageNamed 网站地址 KSImageNamed是一款方便填写图片文 ...
- 推荐两个不错的CAD二次开发(.Net)手册
推荐两个不错的CAD二次开发(.Net)手册 http://www.mjtd.com/helpcenter/netguide/index.html http://www.ceesky.com/book ...
- 推荐两个好用的Xcode插件(提供下载链接)
这里推荐两款好用的Xcode插件,并提供下载链接. 一.插件和使用如下: 1.两款插件 对项目中图片提供自动提示功能的插件:KSImageNamed-Xcode-master 提供快速创建自动注释:V ...
- 推荐两款好用的反编译工具(Luyten,Jadx)
使用JD-Gui打开单个.class文件,总是报错// INTERNAL ERROR 但当我用jd-gui反编译前面操作获得的jar文件的时,但有一部分类不能显示出来--constants类,仅仅显示 ...
- 网络营销相关缩写名称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 ...
随机推荐
- [转帖]在 TiDB 中正确使用索引,性能提升 666 倍
https://tidb.net/book/tidb-monthly/2022/2022-04/usercase/index-666 背景 最近在给一个物流系统做TiDB POC测试,这个系统是基于 ...
- [转帖]jmeter实现不写代码把测试结果存入execl
这里使用数据库作为中间件来实现不写代码就把测试结果存入execl,下面是步骤 1.新建一个setup线程组用来设置数据库连接信息和新建数据库,如下图所示,我们使用sqlite数据库来存储信息,因为不需 ...
- [转帖]JMETER结果分析
https://www.cnblogs.com/a00ium/p/10462892.html 我相信你同意:有很多方法可以收集和解释JMeter结果,你会感到迷茫. 嗯,看完这篇文章后,您将了解收集和 ...
- [转帖]NVMe 与 AHCI
https://www.cnblogs.com/zengkefu/p/5634345.html http://elf8848.iteye.com/blog/1731274 AHCI: NCQ技术,60 ...
- Ergonomics JVM 的一种FullGC的说明
https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/ergonomics.html 2 Ergonomics Ergo ...
- [转帖]Python安装模块(包/库)的方法
这里写目录标题 通过pip安装 正常在线安装 pip命令补全 更改下载镜像 离线包安装 库的下载 库的安装 whl的安装 .tar.gz的安装 源码安装 本地安装报错(依赖) Pycharm中安装 手 ...
- SPECJVM2008 再学习
SPECJVM2008 再学习 摘要 昨天的太水了 感觉今天有必要再水一点.. 存在的问题 默认进行启动 sunflow 必定过不去. 一般的解决办法要求进行重新编译 但是我不知道怎么下载源码... ...
- 【图论】CF1508C Complete the MST
Problem Link 有一张 \(n\) 个点的完全图,其中 \(m\) 条边已经标有边权.你需要给剩下的边都标上权值,使得所有边权的异或和为 \(0\),并且整张图的最小生成树边权和最小. \( ...
- 【发现一个问题】VictoriaMetrics中,突然某个时间段新增的data point无法查询出来
写了一个很简单的python程序,使用remote write协议发数据到vm-insert. 可是有个时段突然查询不出来数据了. 百思不得其解. 吃完中午饭,刷新页面又出来了. 还不清楚问题出现在哪 ...
- 【k哥爬虫普法】非法入侵计算机信息系统,获取1500万余条个人信息!
我国目前并未出台专门针对网络爬虫技术的法律规范,但在司法实践中,相关判决已屡见不鲜,K 哥特设了"K哥爬虫普法"专栏,本栏目通过对真实案例的分析,旨在提高广大爬虫工程师的法律意识, ...