Carimbo de data/hora winsock

Introdução

Os carimbos de data/hora de pacote são um recurso crucial para muitos aplicativos de sincronização de relógio, por exemplo, Protocolo de Tempo de Precisão. Quanto mais próxima a geração de carimbo de data/hora for quando um pacote for recebido/enviado pelo hardware do adaptador de rede, mais preciso poderá ser o aplicativo de sincronização.

Portanto, as APIs de carimbo de data/hora descritas neste tópico fornecem ao aplicativo um mecanismo para relatar carimbos de data/hora gerados bem abaixo da camada do aplicativo. Especificamente, um carimbo de data/hora de software na interface entre o miniporto e o NDIS e um carimbo de data/hora de hardware no hardware nic. A API de carimbo de data/hora pode melhorar muito a precisão da sincronização do relógio. Atualmente, o suporte tem como escopo soquetes UDP (User Datagram Protocol).

Receber carimbos de data/hora

Você configura a recepção de carimbo de data/hora de recebimento por meio do SIO_TIMESTAMPING IOCTL. Use esse IOCTL para habilitar a recepção de carimbo de data/hora de recebimento. Quando você recebe um datagrama usando a função LPFN_WSARECVMSG (WSARecvMsg), seu carimbo de data/hora (se disponível) está contido na mensagem de controle SO_TIMESTAMP .

SO_TIMESTAMP (0x300A) é definido em mstcpip.h. Os dados da mensagem de controle são retornados como um UINT64.

Transmitir carimbos de data/hora

A recepção de carimbo de data/hora de transmissão também é configurada por meio do SIO_TIMESTAMPING IOCTL. Use esse IOCTL para habilitar a recepção de carimbo de data/hora de transmissão e especifique o número de carimbos de data/hora de transmissão que o sistema armazenará em buffer. À medida que os carimbos de data/hora de transmissão são gerados, eles são adicionados ao buffer. Se o buffer estiver cheio, novos carimbos de data/hora de transmissão serão descartados.

Ao enviar um datagrama, associe o datagrama a uma mensagem de controle SO_TIMESTAMP_ID . Isso deve conter um identificador exclusivo. Envie o datagrama, juntamente com sua mensagem de controle SO_TIMESTAMP_ID , usando WSASendMsg. Os carimbos de data/hora de transmissão podem não estar disponíveis imediatamente após o retorno de WSASendMsg . À medida que os carimbos de data/hora de transmissão ficam disponíveis, eles são colocados em um buffer por soquete. Use o SIO_GET_TX_TIMESTAMP IOCTL para sondar o carimbo de data/hora por sua ID. Se o carimbo de data/hora estiver disponível, ele será removido do buffer e retornado. Se o carimbo de data/hora não estiver disponível, WSAGetLastError retornará WSAEWOULDBLOCK. Se um carimbo de data/hora de transmissão for gerado enquanto o buffer estiver cheio, o novo carimbo de data/hora será descartado.

SO_TIMESTAMP_ID (0x300B) é definido em mstcpip.h. Você deve fornecer os dados da mensagem de controle como um UINT32.

Carimbos de data/hora são representados como um valor de contador de 64 bits. A frequência do contador depende da origem do carimbo de data/hora. Para carimbos de data/hora de software, o contador é um valor QueryPerformanceCounter (QPC) e você pode determinar sua frequência por meio de QueryPerformanceFrequency. Para carimbos de data/hora de hardware NIC, a frequência do contador depende do hardware nic e você pode determine-o com informações adicionais fornecidas por CaptureInterfaceHardwareCrossTimestamp. Para determinar a origem dos carimbos de data/hora, use as funções GetInterfaceActiveTimestampCapabilities e GetInterfaceSupportedTimestampCapabilities .

Além da configuração no nível do soquete usando a opção de soquete SIO_TIMESTAMPING para habilitar a recepção de carimbo de data/hora para um soquete, a configuração no nível do sistema também é necessária.

Estimando a latência do caminho de envio do soquete

Nesta seção, usaremos carimbos de data/hora de transmissão para estimar a latência do caminho de envio do soquete. Se você tiver um aplicativo existente que consome carimbos de data/hora de E/S no nível do aplicativo, em que o carimbo de data/hora precisa estar o mais próximo possível do ponto real de transmissão, essa amostra fornecerá uma descrição quantitativa sobre o quanto as APIs de carimbo de data/hora winsock podem melhorar a precisão do aplicativo.

O exemplo pressupõe que há apenas um adaptador de rede cartão (NIC) no sistema e que interfaceLuid é o LUID desse adaptador.

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

Estimando a latência do caminho de recebimento do soquete

Aqui está um exemplo semelhante para o caminho de recebimento. O exemplo pressupõe que há apenas um adaptador de rede cartão (NIC) no sistema e que interfaceLuid é o LUID desse adaptador.

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

Uma limitação

Uma limitação das APIs de carimbo de data/hora do Winsock é que chamar SIO_GET_TX_TIMESTAMP é sempre uma operação sem bloqueio. Mesmo chamar o IOCTL de forma OVERLAPPED resultará em um retorno imediato de WSAEWOULDBLOCK se atualmente não houver carimbos de data/hora de transmissão disponíveis. Como os carimbos de data/hora de transmissão podem não estar imediatamente disponíveis após o retorno de WSASendMsg , seu aplicativo deve sondar o IOCTL até que o carimbo de data/hora esteja disponível. É garantido que um carimbo de data/hora de transmissão esteja disponível após uma chamada WSASendMsg bem-sucedida, considerando que o buffer de carimbo de data/hora de transmissão não está cheio.