Метка времени Winsock

Введение

Метки времени пакетов являются важной функцией для многих приложений синхронизации часов, например протокола точного времени. Чем ближе поколение метки времени к моменту получения или отправки пакета оборудованием сетевого адаптера, тем точнее приложение синхронизации.

Поэтому API меток времени, описанные в этом разделе, предоставляют вашему приложению механизм для создания меток времени, которые создаются значительно ниже уровня приложения. В частности, программная метка времени на интерфейсе между мини-портом и NDIS и метка времени оборудования в оборудовании сетевой карты. API меток времени может значительно повысить точность синхронизации часов. В настоящее время поддержка распространяется на сокеты UDP.

Получение меток времени

Вы настраиваете получение меток времени через SIO_TIMESTAMPING IOCTL. Используйте этот IOCTL, чтобы включить прием меток времени получения. При получении датаграммы с помощью функции LPFN_WSARECVMSG (WSARecvMsg) ее метка времени (если она доступна) содержится в сообщении управления SO_TIMESTAMP .

SO_TIMESTAMP (0x300A) определяется в mstcpip.h. Данные сообщения элемента управления возвращаются в виде UINT64.

Передача меток времени

Прием меток времени передачи также настраивается с помощью SIO_TIMESTAMPING IOCTL. Используйте этот IOCTL, чтобы включить прием меток времени передачи и указать количество меток времени передачи, которые система будет буферистивать. При создании меток времени передачи они добавляются в буфер. Если буфер заполнен, новые метки времени передачи удаляются.

При отправке датаграммы свяжите ее с сообщением SO_TIMESTAMP_ID управления. Он должен содержать уникальный идентификатор. Отправьте датаграмму вместе с сообщением управления SO_TIMESTAMP_ID с помощью WSASendMsg. Метки времени передачи могут быть недоступны сразу после возврата WSASendMsg . По мере того как метки времени передачи становятся доступными, они помещаются в буфер для каждого сокета. Используйте SIO_GET_TX_TIMESTAMP IOCTL для опроса метки времени по ее идентификатору. Если метка времени доступна, она удаляется из буфера и возвращается. Если метка времени недоступна, WSAGetLastError возвращает WSAEWOULDBLOCK. Если при заполнении буфера создается метка времени передачи, новая метка времени удаляется.

SO_TIMESTAMP_ID (0x300B) определяется в mstcpip.h. Данные сообщения элемента управления следует предоставить в виде UINT32.

Метки времени представлены в виде 64-разрядного значения счетчика. Частота счетчика зависит от источника метки времени. Для меток времени программного обеспечения счетчик является значением QueryPerformanceCounter (QPC), и его частоту можно определить с помощью QueryPerformanceFrequency. Для меток времени оборудования сетевой карты частота счетчика зависит от оборудования сетевой карты, и вы можете определить ее с помощью дополнительных сведений, предоставленных CaptureInterfaceHardwareCrossTimestamp. Чтобы определить источник меток времени, используйте функции GetInterfaceActiveTimestampCapabilities и GetInterfaceSupportedTimestampCapabilities .

В дополнение к конфигурации на уровне сокета с использованием параметра SIO_TIMESTAMPING сокета для включения получения меток времени для сокета также требуется конфигурация системного уровня.

Оценка задержки пути отправки сокета

В этом разделе мы будем использовать метки времени передачи для оценки задержки пути отправки сокета. Если у вас есть приложение, использующее метки времени ввода-вывода на уровне приложения, где метка времени должна быть как можно ближе к фактической точке передачи, этот пример предоставляет количественное описание того, насколько API-интерфейсы меток времени Winsock могут повысить точность приложения.

В примере предполагается, что в системе есть только один сетевой интерфейс карта и что 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");
    }
}

Оценка задержки пути получения сокета

Ниже приведен аналогичный пример для пути получения. В примере предполагается, что в системе есть только один сетевой интерфейс карта и что 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");
    }
}

Ограничение

Одно из ограничений API-интерфейсов меток времени Winsock заключается в том, что вызов SIO_GET_TX_TIMESTAMP всегда является неблокирующей операцией. Даже вызов IOCTL в режиме OVERLAPPED приводит к немедленному возврату WSAEWOULDBLOCK , если в настоящее время нет доступных меток времени передачи. Так как метки времени передачи могут быть недоступны сразу после возврата WSASendMsg , приложение должно опрашивает IOCTL до тех пор, пока не станет доступна метка времени. Метка времени передачи гарантированно будет доступна после успешного вызова WSASendMsg , если буфер меток времени передачи не заполнен.