Timestamping Winsock
Introduction
Les horodatages de paquets sont une fonctionnalité cruciale pour de nombreuses applications de synchronisation d’horloge, par exemple, le protocole Precision Time Protocol. Plus la génération d’horodatage est proche de lorsqu’un paquet est reçu/envoyé par le matériel de carte réseau, plus l’application de synchronisation peut être précise.
Ainsi, les API d’horodatage décrites dans cette rubrique fournissent à votre application un mécanisme pour signaler les horodatages générés bien en dessous de la couche application. Plus précisément, un horodatage logiciel à l’interface entre le miniport et NDIS, et un horodatage matériel dans le matériel de la carte réseau. L’API d’horodatage peut améliorer considérablement la précision de la synchronisation des horloges. Actuellement, la prise en charge est limitée aux sockets UDP (User Datagram Protocol).
Horodatages de réception
Vous configurez la réception de l’horodatage de réception via le SIO_TIMESTAMPING IOCTL. Utilisez ce IOCTL pour activer la réception de l’horodatage de réception. Lorsque vous recevez un datagramme à l’aide de la fonction LPFN_WSARECVMSG (WSARecvMsg), son horodatage (le cas échéant) est contenu dans le message de contrôle SO_TIMESTAMP .
SO_TIMESTAMP (0x300A) est défini dans mstcpip.h
. Les données de message de contrôle sont retournées en tant que UINT64.
Timestamps de transmission
La réception de l’horodatage de transmission est également configurée via le SIO_TIMESTAMPING IOCTL. Utilisez cet IOCTL pour activer la réception de l’horodatage de transmission et spécifiez le nombre d’horodatages de transmission que le système mettra en mémoire tampon. À mesure que les horodatages de transmission sont générés, ils sont ajoutés à la mémoire tampon. Si la mémoire tampon est pleine, les nouveaux horodatages de transmission sont ignorés.
Lors de l’envoi d’un datagramme, associez-le à un message de contrôle SO_TIMESTAMP_ID . Celui-ci doit contenir un identificateur unique. Envoyez le datagramme, ainsi que son message de contrôle SO_TIMESTAMP_ID , à l’aide de WSASendMsg. Les horodatages de transmission peuvent ne pas être immédiatement disponibles après le retour de WSASendMsg . À mesure que les horodatages de transmission deviennent disponibles, ils sont placés dans une mémoire tampon par socket. Utilisez le SIO_GET_TX_TIMESTAMP IOCTL pour interroger l’horodatage par son ID. Si l’horodatage est disponible, il est supprimé de la mémoire tampon et retourné. Si l’horodatage n’est pas disponible, WSAGetLastError retourne WSAEWOULDBLOCK. Si un horodatage de transmission est généré alors que la mémoire tampon est pleine, le nouvel horodatage est ignoré.
SO_TIMESTAMP_ID (0x300B) est défini dans mstcpip.h
. Vous devez fournir les données du message de contrôle sous la forme d’un UINT32.
Les horodatages sont représentés sous la forme d’une valeur de compteur 64 bits. La fréquence du compteur dépend de la source de l’horodatage. Pour les horodatages logiciels, le compteur est une valeur QueryPerformanceCounter (QPC) et vous pouvez déterminer sa fréquence via QueryPerformanceFrequency. Pour les horodatages du matériel de carte réseau, la fréquence du compteur dépend du matériel de carte réseau, et vous pouvez la déterminer avec des informations supplémentaires fournies par CaptureInterfaceHardwareCrossTimestamp. Pour déterminer la source des horodatages, utilisez les fonctions GetInterfaceActiveTimestampCapabilities et GetInterfaceSupportedTimestampCapabilities .
En plus de la configuration au niveau du socket utilisant l’option de socket SIO_TIMESTAMPING pour activer la réception de l’horodatage pour un socket, une configuration au niveau du système est également nécessaire.
Estimation de la latence du chemin d’envoi du socket
Dans cette section, nous allons utiliser des horodatages de transmission pour estimer la latence du chemin d’envoi du socket. Si vous avez une application existante qui consomme des horodatages d’E/S au niveau de l’application, où l’horodatage doit être aussi proche que possible du point de transmission réel, cet exemple fournit une description quantitative de la mesure dans laquelle les API d’horodatage Winsock peuvent améliorer la précision de votre application.
L’exemple suppose qu’il n’y a qu’une seule interface réseau carte (NIC) dans le système, et que interfaceLuid est le LUID de cette carte.
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");
}
}
Estimation de la latence du chemin de réception du socket
Voici un exemple similaire pour le chemin de réception. L’exemple suppose qu’il n’y a qu’une seule interface réseau carte (NIC) dans le système, et que interfaceLuid est le LUID de cette carte.
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");
}
}
Une limitation
L’une des limitations des API d’horodatage Winsock est que l’appel de SIO_GET_TX_TIMESTAMP est toujours une opération non bloquante. Même l’appel de l’IOCTL d’une façon CHEVAUCHÉE entraîne un retour immédiat de WSAEWOULDBLOCK s’il n’y a actuellement aucun horodatage de transmission disponible. Étant donné que les horodatages de transmission peuvent ne pas être disponibles immédiatement après le retour de WSASendMsg , votre application doit interroger le IOCTL jusqu’à ce que l’horodatage soit disponible. Un horodatage de transmission est garanti pour être disponible après un appel WSASendMsg réussi, étant donné que la mémoire tampon d’horodatage de transmission n’est pas saturée.