Share via


Winsock 時間戳記

簡介

封包時間戳記是許多時鐘同步處理應用程式的重要功能,例如精確度時間通訊協定。 時間戳記產生越接近于網路介面卡硬體接收/傳送封包時,同步處理應用程式就越精確。

因此,本主題所述的時間戳記 API 會提供一種機制來報告應用層下方產生的時間戳記。 具體而言,迷你埠與 NDIS 之間的介面上的軟體時間戳記,以及 NIC 硬體中的硬體時間戳記。 時間戳記 API 可以大幅改善時鐘同步處理精確度。 目前支援的範圍是使用者資料包通訊協定 (UDP) 通訊端。

接收時間戳記

您可以透過 IOCTL 設定 接收時間戳記接收SIO_TIMESTAMPING。 使用該 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,依識別碼輪詢時間戳記。 如果時間戳記可用,則會從緩衝區中移除並傳回時間戳記。 如果時間戳記無法使用, 則 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 時,傳輸時間戳記保證可供使用,因為傳輸時間戳記緩衝區未滿。