TFO 已经不是个新技术了,哪怕是计算机网络方面最守旧的 Windows 如今也支持了 TFO(虽然只支持客户端,而且疑似不符合规范并且很多 bug)。最近正好做项目时用到了 TFO,因此总结一下 .NET Core 使用 TFO 的方法。
服务端
目前只有 Linux 对 TFO 服务端支持较好,因此我们暂时也只用考虑 Linux。
服务端对应用程序而言支持起来也是最简单的,只需要调用一次setsockopt
就可以启用。
可以写一个扩展方法
const int TCP_FASTOPEN_LINUX = 23;
[DllImport("libc.so.6", SetLastError = true)]
static extern unsafe int setsockopt(int sockfd, int level, int optname, void* optval, int optlen);
public static unsafe void EnableLinuxFastOpenServer(this Socket socket)
{
int qlen = 5;
int result = setsockopt(
(int)socket.Handle,
6 /* SOL_TCP */,
TCP_FASTOPEN_LINUX,
&qlen, sizeof(int));
if (result == -1)
{
throw new SocketException(Marshal.GetLastWin32Error());
}
}
对 Socket 调用即可,以 TcpListener 为例
var listener = new TcpListener(endpoint);
listener.Server.EnableLinuxFastOpenServer();
listener.Start();
调用前需要检查 /proc/sys/net/ipv4/tcp_fastopen 的值是否包含 0x2 这个flag。如果不包含则会抛异常。
客户端
Windows
Windows 的 TCP Fast Open 客户端实现是设置 Socket option 之后,调用 Windows Server 2003 / Windows Vista 加入的 ConnectEx,在 lpSendBuffer 中送入初始 buffer。
.NET 并没有任何文档说明如何调用 ConnectEx,不过阅读 CoreFX 和 .NET Framework Reference Source 可以发现, Socket.ConnectAsync(SocketAsyncEventArgs)
使用了 ConnectEx,并且将传入的SocketAsyncEventArgs的buffer传递给了ConnectEx。
这样一来我们可以编写一个简单的async扩展方法,实现 Windows 上的 TFO。
const int TCP_FASTOPEN_WINDOWS = 15;
public static void EnableWindowsFastOpenClient(this Socket socket)
{
socket.SetSocketOption(
SocketOptionLevel.Tcp,
(SocketOptionName)TCP_FASTOPEN_WINDOWS,
1);
}
public static Task ConnectAsync(this Socket socket, EndPoint remoteEndPoint, ArraySegment initialData)
{
var tcs = new TaskCompletionSource<object>();
var eventArgs = new SocketAsyncEventArgs();
eventArgs.Completed += (s, e) =>
{
if (e.SocketError == SocketError.Success)
{
tcs.TrySetResult(null);
}
else
{
tcs.TrySetException(new SocketException((int)e.SocketError));
}
};
eventArgs.RemoteEndPoint = remoteEndPoint;
eventArgs.UserToken = tcs;
eventArgs.SetBuffer(initialData.Array, initialData.Offset, initialData.Count);
if (socket.ConnectAsync(eventArgs))
{
return tcs.Task;
}
else
{
return Task.CompletedTask;
}
}
先对Socket调用EnableWindowsFastOpenClient
,再调用ConnectAsync
即可。如果使用的是低版本的Windows,或用户关闭了 TFO,则EnableWindowsFastOpenClient
会抛出异常。
Linux
Linux 有两种 TFO API,一种是使用 sendto+MSG_FASTOPEN 建立连接,支持 3.7+的内核;另一种是使用 TCP_FASTOPEN_CONNECT,支持 4.11+的内核。
.NET 的 Socket 在 Linux 上是一层比较厚的封装,因为参数都是原封不动地从 Winsock 复制站贴,在 Linux 上需要进行转换,自然也不支持调用 sendto 传入 MSG_FASTOPEN;而直接把 fd 拿出来调用原生 sendto 也很麻烦,会破坏一些 managed Socket 的状态,比如私有的 _connected 等,为后续使用带来麻烦。类似的问题还有 macOS 的 TFO,需要使用 connectx()
建立连接,只能等 .NET Core 支持。
相比之下 TCP_FASTOPEN_CONNECT 则是对 Socket 调用者而言完全透明的,它的原理是调用 connect()
时不立刻发送 SYN,而是在第一次调用 send 时发送 SYN (TFO=C) 和 data,此时才真正建立连接。
因此需要做的事情跟 Server 端一样,使用以下扩展方法完成
const int TCP_FASTOPEN_CONNECT = 30;
public static unsafe void EnableLinuxFastOpenConnect(this Socket socket)
{
int val = 1;
int result = setsockopt(
(int)socket.Handle,
6 /*SOL_TCP*/,
TCP_FASTOPEN_CONNECT,
&val, sizeof(int));
if (result == -1)
{
throw new SocketException(Marshal.GetLastWin32Error());
}
}
使用EnableLinuxFastOpenConnect()
之后照常使用 Socket API 即可。需要注意的是 Connect
之后一定要保证调用 SendAsync
至少一次,否则连接不会真正建立,此时调用 ReceiveAsync
是不会返回的。
调用 EnableLinuxFastOpenConnect()
之前需要检查 /proc/sys/net/ipv4/tcp_fastopen 的值是否包含 0x1 flag;除此之外还需要检查内核版本是否符合要求(>4.11),可以通过Environment.OSVersion.Version > new Version(4, 11, 0, 0) 判断。