MPTCP: 一个在 Go 1.21中的被忽略的新特性

Go 1.21 再有两三个月就发布了,很多同学都已经总结了Go 1.21的新特性了,为新的Go版本的到来造势,但是我还没看到有同学专门介绍Go 1.21为网络库新增加的一个特性,所以我专门新开一篇专门来介绍。

关于MPTCP这个新特性,专门有一个issue (#56539)跟进和讨论。它是一个对TCP的单路径的扩展,由RFC8684规范来定义。

多路径传输控制协议(Multipath TCP,简称MPTCP)是一种在传输层的协议,旨在增强传统的单路径TCP协议,使其能够在多个网络路径上同时传输数据。MPTCP允许同时利用多条路径进行数据传输,提供更高的带宽、更好的负载均衡和更高的可靠性。
传统的TCP协议是为单路径设计的,它通过在端到端之间的单个连接上进行数据传输。而MPTCP通过引入额外的功能,使得一个TCP连接可以同时在多个网络路径上运行。
MPTCP的工作原理如下:

  1. 建立连接:MPTCP的连接建立过程与传统的TCP类似,但在初始握手时,双方会交换能力选项,以确定是否支持MPTCP。
  2. 子流建立:一旦MPTCP连接建立,它可以启动多个子流(subflow),每个子流通过不同的网络路径传输数据。这些子流可以通过不同的IP地址和端口号来标识。
  3. 路径管理:MPTCP使用路径管理机制来选择和管理多个网络路径。它可以根据路径的质量、延迟、带宽等指标进行路径选择,并根据网络条件动态地调整路径的使用。
  4. 数据传输: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() { // 默认mptcp是禁用的
panic("MultipathTCP should be off by default")
}
lc.SetMultipathTCP(true) // 主动启用mptcp
ln, err := lc.Listen(context.Background(), "tcp", *addr) // 正常tcp监听
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() // 检查连接是否支持了mptcp
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() { // 已经设置dial的时候使用mptcp
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() // 建立的连接是否真的支持mptcp
if err != nil {
panic(err)
}
fmt.Printf("outgoing connection from %s with mptcp: %t\n", *addr, mptcp)
if !mptcp { // 不支持mptcp, panic
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)
}
}