Tanda waktu Winsock
Perkenalan
Tanda waktu paket adalah fitur penting untuk banyak aplikasi sinkronisasi jam—misalnya, Protokol Waktu Presisi. Semakin dekat pembuatan tanda waktu adalah ketika paket diterima/dikirim oleh perangkat keras adaptor jaringan, semakin akurat aplikasi sinkronisasi.
Jadi API tanda waktu yang dijelaskan dalam topik ini memberi aplikasi Anda mekanisme untuk melaporkan tanda waktu yang dihasilkan jauh di bawah lapisan aplikasi. Secara khusus, tanda waktu perangkat lunak di antarmuka antara miniport dan NDIS, dan tanda waktu perangkat keras di perangkat keras NIC. API tanda waktu dapat sangat meningkatkan akurasi sinkronisasi jam. Saat ini, dukungan dilingkupkan ke soket Protokol Datagram Pengguna (UDP).
Menerima tanda waktu
Anda mengonfigurasi penerimaan tanda waktu melalui SIO_TIMESTAMPING IOCTL. Gunakan IOCTL tersebut untuk mengaktifkan penerimaan tanda waktu. Saat Anda menerima datagram menggunakan fungsiLPFN_WSARECVMSG (WSARecvMsg), tanda waktunya (jika tersedia) terkandung dalam pesan kontrol SO_TIMESTAMP.
SO_TIMESTAMP (0x300A) didefinisikan dalam mstcpip.h
. Data pesan kontrol dikembalikan sebagai UINT64.
Mengirimkan tanda waktu
Mengirimkan penerimaan tanda waktu juga dikonfigurasi melalui SIO_TIMESTAMPING IOCTL. Gunakan IOCTL tersebut untuk mengaktifkan penerimaan tanda waktu pengiriman, dan tentukan jumlah tanda waktu pengiriman yang akan di-buffer sistem. Saat tanda waktu pengiriman dihasilkan, tanda waktu tersebut ditambahkan ke buffer. Jika buffer penuh, tanda waktu pengiriman baru akan dibuang.
Saat mengirim datagram, kaitkan datagram dengan pesan kontrol SO_TIMESTAMP_ID. Ini harus berisi pengidentifikasi unik. Kirim datagram, bersama dengan pesan kontrol SO_TIMESTAMP_ID, menggunakan WSASendMsg. Mengirimkan tanda waktu mungkin tidak segera tersedia setelah WSASendMsg kembali. Saat tanda waktu pengiriman tersedia, tanda waktu tersebut ditempatkan ke dalam buffer per soket. Gunakan SIO_GET_TX_TIMESTAMP IOCTL untuk polling tanda waktu dengan ID-nya. Jika tanda waktu tersedia, maka akan dihapus dari buffer dan dikembalikan. Jika tanda waktu tidak tersedia, maka WSAGetLastError mengembalikan WSAEWOULDBLOCK. Jika tanda waktu pengiriman dihasilkan saat buffer penuh, tanda waktu baru akan dibuang.
SO_TIMESTAMP_ID (0x300B) didefinisikan dalam mstcpip.h
. Anda harus menyediakan data pesan kontrol sebagai UINT32.
Tanda waktu dinyatakan sebagai nilai penghitung 64-bit. Frekuensi penghitung tergantung pada sumber tanda waktu. Untuk tanda waktu perangkat lunak, penghitung adalah nilaiQueryPerformanceCounter (QPC), dan Anda dapat menentukan frekuensinya melalui QueryPerformanceFrequency. Untuk tanda waktu perangkat keras NIC, frekuensi penghitung tergantung pada perangkat keras NIC, dan Anda dapat menentukannya dengan informasi tambahan yang diberikan oleh CaptureInterfaceHardwareCrossTimestamp. Untuk menentukan sumber tanda waktu, gunakan GetInterfaceActiveTimestampCapabilities dan GetInterfaceSupportedTimestampCapabilities fungsi.
Selain konfigurasi tingkat soket menggunakan opsi soket SIO_TIMESTAMPING untuk mengaktifkan penerimaan tanda waktu untuk soket, konfigurasi tingkat sistem juga diperlukan.
Memperkirakan latensi jalur pengiriman soket
Di bagian ini, kita akan menggunakan tanda waktu pengiriman untuk memperkirakan latensi jalur pengiriman soket. Jika Anda memiliki aplikasi yang sudah ada yang menggunakan tanda waktu IO tingkat aplikasi—di mana tanda waktu harus sedekat mungkin dengan titik transmisi aktual—maka sampel ini memberikan deskripsi kuantitatif tentang berapa banyak API tanda waktu Winsock dapat meningkatkan akurasi aplikasi Anda.
Contohnya mengasumsikan bahwa hanya ada satu kartu antarmuka jaringan (NIC) dalam sistem, dan antarmuka adalah LUID adaptor tersebut.
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");
}
}
Memperkirakan latensi jalur penerimaan soket
Berikut adalah sampel serupa untuk jalur penerima. Contohnya mengasumsikan bahwa hanya ada satu kartu antarmuka jaringan (NIC) dalam sistem, dan antarmuka adalah LUID adaptor tersebut.
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");
}
}
Batasan
Salah satu batasan API tanda waktu Winsock adalah bahwa memanggil SIO_GET_TX_TIMESTAMP selalu merupakan operasi non-pemblokiran. Bahkan memanggil IOCTL dengan cara YANG TUMPANG TINDIH menghasilkan pengembalian langsung WSAEWOULDBLOCK jika saat ini tidak ada tanda waktu pengiriman yang tersedia. Karena tanda waktu pengiriman mungkin tidak segera tersedia setelah WSASendMsg kembali, aplikasi Anda harus melakukan polling IOCTL hingga tanda waktu tersedia. Tanda waktu pengiriman dijamin tersedia setelah panggilan WSASendMsg berhasil mengingat bahwa buffer tanda waktu pengiriman tidak penuh.