Tanda waktu Winsock
Pengantar
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 dilingkup 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 penerimaan. Saat Anda menerima datagram menggunakan fungsi LPFN_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
Penerimaan tanda waktu transmisi 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 oleh 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-nya , 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 direpresentasikan sebagai nilai penghitung 64-bit. Frekuensi penghitung tergantung pada sumber tanda waktu. Untuk tanda waktu perangkat lunak, penghitung adalah nilai QueryPerformanceCounter (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 fungsi GetInterfaceActiveTimestampCapabilities dan GetInterfaceSupportedTimestampCapabilities .
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 ituLuid 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 ituLuid 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 dalam mode 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 transmisi dijamin tersedia setelah panggilan WSASendMsg yang berhasil mengingat bahwa buffer tanda waktu pengiriman tidak penuh.