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 提供的其他信息来确定它。 若要确定时间戳的来源,请使用 GetInterfaceActiveTimestampCapabilitiesGetInterfaceSupportedTimestampCapabilities 函数。

除了使用 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 始终是非阻塞操作。 即使以 OVERLAPPED 方式调用 IOCTL,如果当前没有可用的传输时间戳,也会立即返回 WSAEWOULDBLOCK 。 由于 WSASendMsg 返回后传输时间戳可能不会立即可用,因此应用程序必须轮询 IOCTL,直到时间戳可用。 如果传输时间戳缓冲区未满,则保证在成功调用 WSASendMsg 后,传输时间戳可用。