Freigeben über


Winsock-Zeitstempel

Einleitung

Paketzeitstempel sind ein wichtiges Feature für viele Anwendungen für die Uhrsynchronisierung, z. B. Precision Time Protocol. Je näher die Zeitstempelgenerierung liegt, wenn ein Paket von der Netzwerkadapterhardware empfangen/gesendet wird, desto genauer kann die Synchronisierungsanwendung sein.

Die in diesem Thema beschriebenen Zeitstempel-APIs bieten Ihrer Anwendung einen Mechanismus zum Melden von Zeitstempeln, die gut unterhalb der Anwendungsebene generiert werden. Insbesondere ein Softwarezeitstempel an der Schnittstelle zwischen Miniport und NDIS und einem Hardwarezeitstempel in der NIC-Hardware. Die Zeitstempel-API kann die Genauigkeit der Uhrsynchronisierung erheblich verbessern. Derzeit ist die Unterstützung auf UDP-Sockets (User Datagram Protocol) beschränkt.

Empfangen von Zeitstempeln

Sie konfigurieren den Empfang des Zeitstempels über die SIO_TIMESTAMPING IOCTL. Verwenden Sie diese IOCTL, um den Empfang des Zeitstempelempfangs zu ermöglichen. Wenn Sie ein Datagramm mithilfe der LPFN_WSARECVMSG (WSARecvMsg)-Funktion empfangen, ist der Zeitstempel (sofern verfügbar) in der SO_TIMESTAMP-Steuerelementmeldung enthalten.

SO_TIMESTAMP (0x300A) wird in mstcpip.hdefiniert. Die Steuerelementnachrichtendaten werden als UINT64-zurückgegeben.

Sendezeitstempel

Der Sendezeitstempelempfang wird auch über die SIO_TIMESTAMPING IOCTL konfiguriert. Verwenden Sie diese IOCTL, um den Sendezeitstempelempfang zu aktivieren, und geben Sie die Anzahl der Sendezeitstempel an, die das System puffert. Wenn Sendezeitstempel generiert werden, werden sie dem Puffer hinzugefügt. Wenn der Puffer voll ist, werden neue Sendezeitstempel verworfen.

Ordnen Sie beim Senden eines Datagramms das Datagramm einer SO_TIMESTAMP_ID Steuerelementnachricht zu. Dies sollte einen eindeutigen Bezeichner enthalten. Senden Sie das Datagramm zusammen mit der SO_TIMESTAMP_ID Steuerelementnachricht mithilfe von WSASendMsg. Sendezeitstempel sind möglicherweise nicht sofort verfügbar, nachdem WSASendMsg zurückgegeben wurde. Wenn Sendezeitstempel verfügbar werden, werden sie in einen Puffer pro Socket platziert. Verwenden Sie die SIO_GET_TX_TIMESTAMP IOCTL, um den Zeitstempel anhand der ID abzufragen. Wenn der Zeitstempel verfügbar ist, wird er aus dem Puffer entfernt und zurückgegeben. Wenn der Zeitstempel nicht verfügbar ist, gibt WSAGetLastErrorWSAEWOULDBLOCKzurück. Wenn ein Sendezeitstempel generiert wird, während der Puffer voll ist, wird der neue Zeitstempel verworfen.

SO_TIMESTAMP_ID (0x300B) wird in mstcpip.hdefiniert. Sie sollten die Steuerelementnachrichtendaten als UINT32-angeben.

Zeitstempel werden als 64-Bit-Zählerwert dargestellt. Die Häufigkeit des Zählers hängt von der Quelle des Zeitstempels ab. Bei Software-Zeitstempeln ist der Leistungsindikator ein QueryPerformanceCounter (QPC)-Wert, und Sie können die Häufigkeit über QueryPerformanceFrequencybestimmen. Bei NIC-Hardware-Zeitstempeln ist die Zählerhäufigkeit von der NIC-Hardware abhängig, und Sie können diese mit zusätzlichen Informationen ermitteln, die von CaptureInterfaceHardwareCrossTimestampangegeben werden. Um die Quelle von Zeitstempeln zu ermitteln, verwenden Sie die GetInterfaceActiveTimestampCapabilities und GetInterfaceSupportedTimestampCapabilities Funktionen.

Zusätzlich zur Konfiguration auf Socketebene mit der SIO_TIMESTAMPING Socketoption zum Aktivieren des Zeitstempelempfangs für einen Socket ist auch eine Konfiguration auf Systemebene erforderlich.

Schätzen der Latenz des Socket-Sendepfads

In diesem Abschnitt verwenden wir Sendezeitstempel, um die Latenz des Socket-Sendepfads zu schätzen. Wenn Sie über eine vorhandene Anwendung verfügen, die E/A-Zeitstempel auf Anwendungsebene nutzt – wobei der Zeitstempel so nah wie möglich am tatsächlichen Übertragungspunkt liegen muss – stellt dieses Beispiel eine quantitative Beschreibung bereit, wie viel die Winsock-Zeitstempel-APIs die Genauigkeit Ihrer Anwendung verbessern können.

Im Beispiel wird davon ausgegangen, dass nur eine Netzwerkschnittstellenkarte (Network Interface Card, NIC) im System vorhanden ist und dass interfaceLuid die LUID dieses Adapters ist.

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");
    }
}

Schätzen der Latenz des Socket-Empfangspfads

Hier ist ein ähnliches Beispiel für den Empfangspfad. Im Beispiel wird davon ausgegangen, dass nur eine Netzwerkschnittstellenkarte (Network Interface Card, NIC) im System vorhanden ist und dass interfaceLuid die LUID dieses Adapters ist.

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");
    }
}

Eine Einschränkung

Eine Einschränkung der Winsock-Zeitstempel-APIs besteht darin, dass das Aufrufen von SIO_GET_TX_TIMESTAMP immer ein nicht blockierender Vorgang ist. Selbst das Aufrufen der IOCTL in einer ÜBERLAPPTEN Mode führt zu einer sofortigen Rückgabe von WSAEWOULDBLOCK, wenn derzeit keine Sendezeitstempel verfügbar sind. Da Sendezeitstempel möglicherweise nicht sofort verfügbar sind, nachdem WSASendMsg zurückgegeben wurde, muss Ihre Anwendung die IOCTL abfragen, bis der Zeitstempel verfügbar ist. Nach einem erfolgreichen WSASendMsg Aufruf des Sendezeitstempels ist garantiert ein Sendezeitstempel verfügbar.