掘金 后端 ( ) • 2024-05-05 17:13

背景

Connection MigrationQUIC协议的特性之一。协议通信一般依赖网络标识比如(IP和port), 如果网络切换,通信需要重新开始,这对于频繁网络切换场景体验很差,比如无线网络使用APP时,网络切换可能需要重新登陆,终端用户可能莫名其妙。QUIC协议设计的时候,使用Connection ID标识对方,网络切换只要保证Connection ID正确就可以。Connection Migration就是描述这一过程交互规范。

细节

Connection Migration需要在Handshake完成后才能执行。发送方可能发送PATH_CHALLANGE帧给对方,对方可能发送PATH_RESPONSE帧回应。发送方也可以不发送probe frame, 直接切换继续通信。

实现

本地实验最好的方式就是变更port,比如从9000变为9001。下面使用ngtcp2库实现Connection Migration的代码片段。

  1. 生成新的UDP socket
struct sockaddr_in source;
source.sin_addr.s_addr = htonl(INADDR_ANY);
source.sin_family = AF_INET;
source.sin_port = htons(9001);
fd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
bind(fd, (struct sockaddr *)&source, sizeof(source));
connect(fd, (struct sockaddr *)(&remote), remote_len);
getsockname(fd, (struct sockaddr *)&local, &local_len)
  1. 使用ngtcp2提供的Connection Migration接口。
int ngtcp2_conn_initiate_immediate_migration(ngtcp2_conn *conn, const ngtcp2_path *path, ngtcp2_tstamp ts)
int ngtcp2_conn_initiate_migration(ngtcp2_conn *conn, const ngtcp2_path *path, ngtcp2_tstamp ts)

ngtcp2提供了两种接口,都会发送PATH_CHALLANGE, 但是前者不会等待对方的PATH_RESPONSE就迁移。后者会等待对端的PATH_RESPONSE帧才迁移本地的网络路径。

ngtcp2_addr addr;
ngtcp2_addr_init(&addr, (struct sockaddr *)&local, local_len);
if (0) // nat rebinding
{
     ngtcp2_conn_set_local_addr(conn, &addr);
     ngtcp2_conn_set_path_user_data(conn, client);
}
else
{
     ngtcp2_path path = {
          addr,
          {
               (struct sockaddr *)&remote,
               remote_len,
          },
          client,
     };
     if ((res = ngtcp2_conn_initiate_immediate_migration(conn, &path, timestamp())) != 0)
     // if ((res = ngtcp2_conn_initiate_migration(conn, &path, timestamp())) != 0)
     {
          fprintf(stderr, "ngtcp2_conn_initiate_immediate_migration: %s\n", ngtcp2_strerror(res));
          return -1;
     }
}
  1. 处理path validation callback,比如对端回复了PATH_RESPONSE
int path_validation(ngtcp2_conn *conn, uint32_t flags, const ngtcp2_path *path,
					const ngtcp2_path *old_path,
					ngtcp2_path_validation_result res, void *user_data)
{
	(void)conn;
	if (old_path)
	{
		get_ip_port((struct sockaddr_storage *)(old_path->local.addr), ip, &port);
		fprintf(stdout, ", old local: %s:%d", ip, port);
	}

	if (flags & NGTCP2_PATH_VALIDATION_FLAG_PREFERRED_ADDR)
	{
		struct client *c = (struct client *)(user_data);
		memcpy(&c->remote_addr, path->remote.addr, path->remote.addrlen);
		c->remote_addrlen = path->remote.addrlen;
	}
	return 0;
}

最后

可以参考ngtcp2的example(https://github.com/ngtcp2/ngtcp2/tree/main/examples), 如果想看简单的,可以参考我的http3 client