다음을 통해 공유


Winsock 타임스탬프

소개

패킷 타임스탬프는 많은 클록 동기화 애플리케이션(예: 정밀 시간 프로토콜)에 중요한 기능입니다. 타임스탬프 생성이 네트워크 어댑터 하드웨어에서 패킷을 받거나 보낼 때에 가까울수록 동기화 애플리케이션이 더 정확할 수 있습니다.

따라서 이 항목에 설명된 타임스탬프 API는 애플리케이션 계층보다 훨씬 아래에 생성된 타임스탬프를 보고하는 메커니즘을 애플리케이션에 제공합니다. 특히 미니포트와 NDIS 간의 인터페이스에 있는 소프트웨어 타임스탬프와 NIC 하드웨어의 하드웨어 타임스탬프입니다. 타임스탬프 API는 클록 동기화 정확도를 크게 향상시킬 수 있습니다. 현재 지원 범위는 UDP(사용자 데이터그램 프로토콜) 소켓으로 지정됩니다.

타임스탬프 받기

SIO_TIMESTAMPING IOCTL을 통해 수신 타임스탬프 수신을 구성합니다. 해당 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을 사용하여 ID로 타임스탬프를 폴링합니다. 타임스탬프를 사용할 수 있는 경우 버퍼에서 제거되고 반환됩니다. 타임스탬프를 사용할 수 없는 경우 WSAGetLastErrorWSAEWOULDBLOCK을 반환합니다. 버퍼가 가득 찬 동안 전송 타임스탬프가 생성되면 새 타임스탬프가 삭제됩니다.

SO_TIMESTAMP_ID (0x300B)은 에 정의되어 있습니다 mstcpip.h. 컨트롤 메시지 데이터를 UINT32로 제공해야 합니다.

타임스탬프는 64비트 카운터 값으로 표시됩니다. 카운터의 빈도는 타임스탬프의 원본에 따라 달라집니다. 소프트웨어 타임스탬프의 경우 카운터는 QPC(QueryPerformanceCounter ) 값이며 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 호출이 성공한 후에 전송 타임스탬프를 사용할 수 있습니다.