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.