Winsock-Zeitstempel

Einführung

Paketzeitstempel sind ein wichtiges Feature für viele Anwendungen der 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 daher einen Mechanismus zum Melden von Zeitstempeln, die weit unterhalb der Anwendungsschicht generiert werden. Insbesondere ein Softwarezeitstempel an der Schnittstelle zwischen miniport und NDIS und ein 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) ausgerichtet.

Empfangen von Zeitstempeln

Sie konfigurieren den Empfangszeitstempelempfang über die SIO_TIMESTAMPING IOCTL. Verwenden Sie dieses IOCTL, um den Empfangszeitstempelempfang zu aktivieren. Wenn Sie ein Datagramm mit der funktion LPFN_WSARECVMSG (WSARecvMsg) empfangen, ist dessen Zeitstempel (sofern verfügbar) in der SO_TIMESTAMP-Steuernachricht enthalten.

SO_TIMESTAMP (0x300A) wird in mstcpip.hdefiniert. Die Kontrollmeldungsdaten 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 nach der Rückgabe von WSASendMsg möglicherweise nicht sofort verfügbar. Sobald Sendezeitstempel verfügbar werden, werden sie in einen Puffer pro Socket platziert. Verwenden Sie die SIO_GET_TX_TIMESTAMP IOCTL, um den Zeitstempel anhand seiner 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 WSAGetLastErrorWSAEWOULDBLOCK zurü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 Kontrollmeldungsdaten als UINT32 bereitstellen.

Zeitstempel werden als 64-Bit-Zählerwert dargestellt. Die Häufigkeit des Indikators hängt von der Quelle des Zeitstempels ab. Bei Softwarezeitstempeln ist der Indikator ein QPC-Wert ( QueryPerformanceCounter ), und Sie können seine Häufigkeit über QueryPerformanceFrequency bestimmen. Bei NIC-Hardwarezeitstempeln ist die Zählerhäufigkeit von der NIC-Hardware abhängig, und Sie können sie mit zusätzlichen Informationen ermitteln, die von CaptureInterfaceHardwareCrossTimestamp angegeben werden. Verwenden Sie zum Ermitteln der Quelle von Zeitstempeln die Funktionen GetInterfaceActiveTimestampCapabilities und GetInterfaceSupportedTimestampCapabilities .

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 Sockets-Sendepfads

In diesem Abschnitt verwenden wir Sendezeitstempel, um die Latenz des Sockets-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 –, enthält dieses Beispiel eine quantitative Beschreibung, wie stark die Winsock-Zeitstempel-APIs die Genauigkeit Ihrer Anwendung verbessern können.

Im Beispiel wird davon ausgegangen, dass es nur eine Netzwerkschnittstelle Karte (NIC) im System gibt 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 Socketempfangspfads

Hier sehen Sie ein ähnliches Beispiel für den Empfangspfad. Im Beispiel wird davon ausgegangen, dass es nur eine Netzwerkschnittstelle Karte (NIC) im System gibt 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 SIO_GET_TX_TIMESTAMP immer ein nicht blockierender Vorgang ist. Selbst das Aufrufen der IOCTL in überlappender Weise führt zu einer sofortigen Rückgabe von WSAEWOULDBLOCK , wenn derzeit keine Sendezeitstempel verfügbar sind. Da Sendezeitstempel nach der Rückgabe von WSASendMsg möglicherweise nicht sofort verfügbar sind, muss Ihre Anwendung die IOCTL abfragen, bis der Zeitstempel verfügbar ist. Ein Sendezeitstempel ist nach einem erfolgreichen WSASendMsg-Aufruf garantiert verfügbar, da der Sendezeitstempelpuffer nicht voll ist.