Go 1.21 再有两三个月就发布了,很多同学都已经总结了Go 1.21的新特性了,为新的Go版本的到来造势,但是我还没看到有同学专门介绍Go 1.21为网络库新增加的一个特性,所以我专门新开一篇专门来介绍。
关于MPTCP这个新特性,专门有一个issue (#56539)跟进和讨论。它是一个对TCP的单路径的扩展,由RFC8684规范来定义。
多路径传输控制协议(Multipath TCP,简称MPTCP)是一种在传输层的协议,旨在增强传统的单路径TCP协议,使其能够在多个网络路径上同时传输数据。MPTCP允许同时利用多条路径进行数据传输,提供更高的带宽、更好的负载均衡和更高的可靠性。
传统的TCP协议是为单路径设计的,它通过在端到端之间的单个连接上进行数据传输。而MPTCP通过引入额外的功能,使得一个TCP连接可以同时在多个网络路径上运行。
MPTCP的工作原理如下:
- 建立连接:MPTCP的连接建立过程与传统的TCP类似,但在初始握手时,双方会交换能力选项,以确定是否支持MPTCP。
- 子流建立:一旦MPTCP连接建立,它可以启动多个子流(subflow),每个子流通过不同的网络路径传输数据。这些子流可以通过不同的IP地址和端口号来标识。
- 路径管理:MPTCP使用路径管理机制来选择和管理多个网络路径。它可以根据路径的质量、延迟、带宽等指标进行路径选择,并根据网络条件动态地调整路径的使用。
- 数据传输:MPTCP将数据分割成适当大小的数据块,并在不同的子流上发送。接收端会根据数据块的序列号和数据块所属的子流来重新组装数据。
MPTCP的优点包括:
- 带宽增强:MPTCP可以同时利用多个路径的带宽,从而提供更高的总体带宽。
- 负载均衡:MPTCP可以根据路径质量和可用带宽动态地调整数据传输,实现负载均衡,提高网络资源利用率。
- 容错性:由于数据可以通过多个路径传输,MPTCP可以提供更好的容错性。即使某个路径出现故障,数据仍然可以通过其他可用路径进行传输。
- 移动性支持:MPTCP可以在移动设备切换网络时维持连接,无需重新建立连接,提供更平滑的移动体验。
MPTCP已经成为一项标准化的协议,它被广泛应用于多路径传输场景,例如数据中心内部通信、无线网络和移动网络等。
比如apple官方文档指出:
iOS 支持 Multipath TCP (MPTCP),并且允许 iPhone 或 iPad 通过蜂窝数据连接建立与目标主机的备份 TCP 连接。
iPhone 和 iPad 在具有有效的蜂窝数据连接的情况下使用 MPTCP 来建立两个连接:
- 通过 Wi-Fi 的主要 TCP 连接
- 通过蜂窝数据的备用连接
如果 Wi-Fi 不可用或无响应,iOS 会使用蜂窝数据连接。
https://support.apple.com/zh-cn/HT201373
小红帽的官方帮助文档也对MPTCP进行了专门的介绍。
MPTCP在Linux内核中得到广泛支持,并且已经成为Linux内核的一部分。ChatGPT说从Linux内核版本3.6开始,MPTCP就被纳入主线内核,用户可以通过配置和使用MPTCP功能,但是MPTCP社区网站说MPTCP v1是5.6才开始支持。
Linux发行版如Ubuntu、Fedora和Debian等通常默认包含MPTCP支持。自Linux 5.19开始,MPTCP包含以下的特性:
- 支持
socket
系统调用中设置IPPROTO_MPTCP
协议
- 如果对方或中间设备不支持 MPTCP,则从 MPTCP 回退到 TCP
- 使用内核内或用户空间路径管理器进行路径管理
- 同样使用 TCP 套接字的套接字选项
- 调试功能,包括 MIB 计数器、诊断支持(使用ss命令)和跟踪点
经过Go社区和MPTCP社区同学的努力,在Go 1.21版本中终于找到和实现一个方便支持MPTCP方式,总结来说,就是下面的四个方法。
对于TCP client,你可以通过下面的方法设置以及检查是否支持MPTCP:
1 2
| func (*Dialer) SetMultipathTCP(enabled bool) func (*Dialer) MultipathTCP() bool
|
对于TCP server,你可以通过下面的方法设置以及检查是否支持MPTCP:
1 2
| func (*ListenConfig) SetMultipathTCP(enabled bool) func (*ListenConfig) MultipathTCP() bool
|
所以对于一个系统,客户端和服务器端都需要进行设置,才能保证MPTCP启作用。
我以一个简单的例子演示如何使用MPTCP。
服务器端代码如下,为Listener启用mptcp,和客户端建立的连接可能支持mptcp,也可能退化成普通的tcp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
| package main import ( "context" "errors" "flag" "fmt" "io" "net" ) var ( addr = flag.String("addr", ":8080", "service address") ) func main() { flag.Parse() lc := &net.ListenConfig{} if lc.MultipathTCP() { panic("MultipathTCP should be off by default") } lc.SetMultipathTCP(true) ln, err := lc.Listen(context.Background(), "tcp", *addr) if err != nil { panic(err) } for { conn, err := ln.Accept() if err != nil { panic(err) } go func() { defer conn.Close() isMultipathTCP, err := conn.(*net.TCPConn).MultipathTCP() fmt.Printf("accepted connection from %s with mptcp: %t, err: %v\n", conn.RemoteAddr(), isMultipathTCP, err) for { buf := make([]byte, 1024) n, err := conn.Read(buf) if err != nil { if errors.Is(err, io.EOF) { return } panic(err) } if _, err := conn.Write(buf[:n]); err != nil { panic(err) } } }() } }
|
客户端代码如下,我们为dialer启用了mptcp,并且检查建立好的连接是否真的支持mptcp,因为客户端或服务器不支持mptcp的话,就退化成普通的tcp了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
| package main import ( "flag" "fmt" "net" "time" ) var ( addr = flag.String("addr", "127.0.0.1:8080", "service address") ) func main() { flag.Parse() d := &net.Dialer{} if d.MultipathTCP() { panic("MultipathTCP should be off by default") } d.SetMultipathTCP(true) if !d.MultipathTCP() { panic("MultipathTCP is not on after having been forced to on") } c, err := d.Dial("tcp", *addr) if err != nil { panic(err) } defer c.Close() tcp, ok := c.(*net.TCPConn) if !ok { panic("struct is not a TCPConn") } mptcp, err := tcp.MultipathTCP() if err != nil { panic(err) } fmt.Printf("outgoing connection from %s with mptcp: %t\n", *addr, mptcp) if !mptcp { panic("outgoing connection is not with MPTCP") } for { snt := []byte("MPTCP TEST") if _, err := c.Write(snt); err != nil { panic(err) } b := make([]byte, len(snt)) if _, err := c.Read(b); err != nil { panic(err) } fmt.Println(string(b)) time.Sleep(time.Second) } }
|