Winsock 时间戳
介绍
数据包时间戳是许多时钟同步应用程序的关键功能,例如精度时间协议。 时间戳生成越接近于网络适配器硬件接收/发送数据包时,同步应用程序就越准确。
因此,本主题中所述的时间戳 API 为应用程序提供了一种机制,用于报告在应用程序层下方生成的时间戳。 具体而言,微型端口和 NDIS 之间的接口上的软件时间戳,以及 NIC 硬件中的硬件时间戳。 时间戳 API 可以极大地提高时钟同步准确性。 目前,支持范围限定为用户数据报协议 (UDP) 套接字。
接收时间戳
通过 SIO_TIMESTAMPING IOCTL 配置接收时间戳接收。 使用该 IOCTL 启用接收时间戳接收。 使用 LPFN_WSARECVMSG (WSARecvMsg) 函数接收数据报时,其时间戳(如果可用)包含在 SO_TIMESTAMP 控制消息中。
SO_TIMESTAMP(0x300A)在 mstcpip.h
中定义。 控件消息数据以 UINT64的形式返回。
传输时间戳
传输时间戳接收也通过 SIO_TIMESTAMPING IOCTL 进行配置。 使用该 IOCTL 启用传输时间戳接收,并指定系统将缓冲的传输时间戳数。 生成传输时间戳时,它们将添加到缓冲区。 如果缓冲区已满,则会丢弃新的传输时间戳。
发送数据报时,将数据报与 SO_TIMESTAMP_ID 控制消息相关联。 这应包含唯一标识符。 使用 WSASendMsg发送数据报及其 SO_TIMESTAMP_ID 控制消息。 WSASendMsg 返回后,传输时间戳可能不会立即可用。 当传输时间戳可用时,它们将放置在每套接字缓冲区中。 使用 SIO_GET_TX_TIMESTAMP IOCTL 按其 ID 轮询时间戳。 如果时间戳可用,则会将其从缓冲区中删除并返回。 如果时间戳不可用,WSAGetLastError 返回 WSAEWOULDBLOCK。 如果在缓冲区已满时生成传输时间戳,则会丢弃新时间戳。
SO_TIMESTAMP_ID(0x300B)在 mstcpip.h
中定义。 应以 UINT32的形式提供控制消息数据。
时间戳表示为 64 位计数器值。 计数器的频率取决于时间戳的来源。 对于软件时间戳,计数器是 QueryPerformanceCounter (QPC) 值,可以通过 QueryPerformanceFrequency来确定其频率。 对于 NIC 硬件时间戳,计数器频率取决于 NIC 硬件,可以使用 CaptureInterfaceHardwareCrossTimestamp提供的其他信息来确定它。 若要确定时间戳的来源,请使用 GetInterfaceActiveTimestampCapabilities 和 GetInterfaceSupportedTimestampCapabilities 函数。
除了使用 SIO_TIMESTAMPING 套接字选项启用套接字的时间戳接收之外,还需要系统级配置。
估计套接字发送路径的延迟
在本部分中,我们将使用传输时间戳来估计套接字发送路径的延迟。 如果你有一个使用应用程序级 IO 时间戳的现有应用程序(其中时间戳需要尽可能接近实际传输点),则此示例提供了一个量化说明,说明 Winsock 时间戳 API 可以提高应用程序的准确性。
该示例假定系统中只有一个网络接口卡(NIC),interfaceLuid 是该适配器的 LUID。
void QueryHardwareClockFrequency(LARGE_INTEGER* clockFrequency)
{
// Returns the hardware clock frequency. This can be calculated by
// collecting crosstimestamps via CaptureInterfaceHardwareCrossTimestamp
// and forming a linear regression model.
}
void estimate_send_latency(SOCKET sock,
PSOCKADDR_STORAGE addr,
NET_LUID* interfaceLuid,
BOOLEAN hardwareTimestampSource)
{
DWORD numBytes;
INT error;
CHAR data[512];
CHAR control[WSA_CMSG_SPACE(sizeof(UINT32))] = { 0 };
WSABUF dataBuf;
WSABUF controlBuf;
WSAMSG wsaMsg;
ULONG64 appLevelTimestamp;
dataBuf.buf = data;
dataBuf.len = sizeof(data);
controlBuf.buf = control;
controlBuf.len = sizeof(control);
wsaMsg.name = (PSOCKADDR)addr;
wsaMsg.namelen = (INT)INET_SOCKADDR_LENGTH(addr->ss_family);
wsaMsg.lpBuffers = &dataBuf;
wsaMsg.dwBufferCount = 1;
wsaMsg.Control = controlBuf;
wsaMsg.dwFlags = 0;
// Configure tx timestamp reception.
TIMESTAMPING_CONFIG config = { 0 };
config.flags |= TIMESTAMPING_FLAG_TX;
config.txTimestampsBuffered = 1;
error =
WSAIoctl(
sock,
SIO_TIMESTAMPING,
&config,
sizeof(config),
NULL,
0,
&numBytes,
NULL,
NULL);
if (error == SOCKET_ERROR) {
printf("WSAIoctl failed %d\n", WSAGetLastError());
return;
}
// Assign a tx timestamp ID to this datagram.
UINT32 txTimestampId = 123;
PCMSGHDR cmsg = WSA_CMSG_FIRSTHDR(&wsaMsg);
cmsg->cmsg_len = WSA_CMSG_LEN(sizeof(UINT32));
cmsg->cmsg_level = SOL_SOCKET;
cmsg->cmsg_type = SO_TIMESTAMP_ID;
*(PUINT32)WSA_CMSG_DATA(cmsg) = txTimestampId;
// Capture app-layer timestamp prior to send call.
if (hardwareTimestampSource) {
INTERFACE_HARDWARE_CROSSTIMESTAMP crossTimestamp = { 0 };
crossTimestamp.Version = INTERFACE_HARDWARE_CROSSTIMESTAMP_VERSION_1;
error = CaptureInterfaceHardwareCrossTimestamp(interfaceLuid, &crossTimestamp);
if (error != NO_ERROR) {
printf("CaptureInterfaceHardwareCrossTimestamp failed %d\n", error);
return;
}
appLevelTimestamp = crossTimestamp.HardwareClockTimestamp;
}
else { // software source
LARGE_INTEGER t1;
QueryPerformanceCounter(&t1);
appLevelTimestamp = t1.QuadPart;
}
error =
sendmsg(
sock,
&wsaMsg,
0,
&numBytes,
NULL,
NULL);
if (error == SOCKET_ERROR) {
printf("sendmsg failed %d\n", WSAGetLastError());
return;
}
printf("sent packet\n");
// Poll for the socket tx timestamp value. The timestamp may not be available
// immediately.
UINT64 socketTimestamp;
ULONG maxTimestampPollAttempts = 6;
ULONG txTstampRetrieveIntervalMs = 1;
BOOLEAN retrievedTimestamp = FALSE;
for (ULONG i = 0; i < maxTimestampPollAttempts; i++) {
error =
WSAIoctl(
sock,
SIO_GET_TX_TIMESTAMP,
&txTimestampId,
sizeof(txTimestampId),
&socketTimestamp,
sizeof(socketTimestamp),
&numBytes,
NULL,
NULL);
if (error != SOCKET_ERROR) {
ASSERT(numBytes == sizeof(timestamp));
ASSERT(timestamp != 0);
retrievedTimestamp = TRUE;
break;
}
error = WSAGetLastError();
if (error != WSAEWOULDBLOCK) {
printf(“WSAIoctl failed % d\n”, error);
break;
}
Sleep(txTstampRetrieveIntervalMs);
txTstampRetrieveIntervalMs *= 2;
}
if (retrievedTimestamp) {
LARGE_INTEGER clockFrequency;
ULONG64 elapsedMicroseconds;
if (hardwareTimestampSource) {
QueryHardwareClockFrequency(&clockFrequency);
}
else { // software source
QueryPerformanceFrequency(&clockFrequency);
}
// Compute socket send path latency.
elapsedMicroseconds = socketTimestamp - appLevelTimestamp;
elapsedMicroseconds *= 1000000;
elapsedMicroseconds /= clockFrequency.QuadPart;
printf("socket send path latency estimation: %lld microseconds\n",
elapsedMicroseconds);
}
else {
printf("failed to retrieve TX timestamp\n");
}
}
估计套接字接收路径的延迟
下面是接收路径的类似示例。 该示例假定系统中只有一个网络接口卡(NIC),interfaceLuid 是该适配器的 LUID。
void QueryHardwareClockFrequency(LARGE_INTEGER* clockFrequency)
{
// Returns the hardware clock frequency. This can be calculated by
// collecting crosstimestamps via CaptureInterfaceHardwareCrossTimestamp
// and forming a linear regression model.
}
void estimate_receive_latency(SOCKET sock,
NET_LUID* interfaceLuid,
BOOLEAN hardwareTimestampSource)
{
DWORD numBytes;
INT error;
CHAR data[512];
CHAR control[WSA_CMSG_SPACE(sizeof(UINT64))] = { 0 };
WSABUF dataBuf;
WSABUF controlBuf;
WSAMSG wsaMsg;
UINT64 socketTimestamp = 0;
ULONG64 appLevelTimestamp;
dataBuf.buf = data;
dataBuf.len = sizeof(data);
controlBuf.buf = control;
controlBuf.len = sizeof(control);
wsaMsg.name = NULL;
wsaMsg.namelen = 0;
wsaMsg.lpBuffers = &dataBuf;
wsaMsg.dwBufferCount = 1;
wsaMsg.Control = controlBuf;
wsaMsg.dwFlags = 0;
// Configure rx timestamp reception.
TIMESTAMPING_CONFIG config = { 0 };
config.flags |= TIMESTAMPING_FLAG_RX;
error =
WSAIoctl(
sock,
SIO_TIMESTAMPING,
&config,
sizeof(config),
NULL,
0,
&numBytes,
NULL,
NULL);
if (error == SOCKET_ERROR) {
printf("WSAIoctl failed %d\n", WSAGetLastError());
return;
}
error =
recvmsg(
sock,
&wsaMsg,
&numBytes,
NULL,
NULL);
if (error == SOCKET_ERROR) {
printf("recvmsg failed %d\n", WSAGetLastError());
return;
}
// Capture app-layer timestamp upon message reception.
if (hardwareTimestampSource) {
INTERFACE_HARDWARE_CROSSTIMESTAMP crossTimestamp = { 0 };
crossTimestamp.Version = INTERFACE_HARDWARE_CROSSTIMESTAMP_VERSION_1;
error = CaptureInterfaceHardwareCrossTimestamp(interfaceLuid, &crossTimestamp);
if (error != NO_ERROR) {
printf("CaptureInterfaceHardwareCrossTimestamp failed %d\n", error);
return;
}
appLevelTimestamp = crossTimestamp.HardwareClockTimestamp;
}
else { // software source
LARGE_INTEGER t1;
QueryPerformanceCounter(&t1);
appLevelTimestamp = t1.QuadPart;
}
printf("received packet\n");
// Look for socket rx timestamp returned via control message.
BOOLEAN retrievedTimestamp = FALSE;
PCMSGHDR cmsg = WSA_CMSG_FIRSTHDR(&wsaMsg);
while (cmsg != NULL) {
if (cmsg->cmsg_level == SOL_SOCKET && cmsg->cmsg_type == SO_TIMESTAMP) {
socketTimestamp = *(PUINT64)WSA_CMSG_DATA(cmsg);
retrievedTimestamp = TRUE;
break;
}
cmsg = WSA_CMSG_NXTHDR(&wsaMsg, cmsg);
}
if (retrievedTimestamp) {
// Compute socket receive path latency.
LARGE_INTEGER clockFrequency;
ULONG64 elapsedMicroseconds;
if (hardwareTimestampSource) {
QueryHardwareClockFrequency(&clockFrequency);
}
else { // software source
QueryPerformanceFrequency(&clockFrequency);
}
// Compute socket send path latency.
elapsedMicroseconds = appLevelTimestamp - socketTimestamp;
elapsedMicroseconds *= 1000000;
elapsedMicroseconds /= clockFrequency.QuadPart;
printf("RX latency estimation: %lld microseconds\n",
elapsedMicroseconds);
}
else {
printf("failed to retrieve RX timestamp\n");
}
}
限制
Winsock 时间戳 API 的一个限制是调用 SIO_GET_TX_TIMESTAMP 始终是非阻塞作。 即使以重叠方式调用 IOCTL,如果当前没有可用的传输时间戳,则立即返回 WSAEWOULDBLOCK。 由于传输时间戳在 WSASendMsg 返回后可能无法立即可用,因此应用程序必须在时间戳可用之前轮询 IOCTL。 如果传输时间戳缓冲区未满,在成功 WSASendMsg 调用后,将保证传输时间戳可用。