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 控件訊息。 WSASendMs g 傳回之後,傳輸時間戳可能無法立即使用。 當傳輸時間戳變成可用時,它們會放入每個套接字緩衝區中。 使用 SIO_GET_TX_TIMESTAMP IOCTL 依標識符輪詢時間戳。 如果時間戳可用,則會從緩衝區中移除並傳回時間戳。 如果時間戳無法使用,WSAGetLastError 會傳回 WSAEWOULDBLOCK。 如果在緩衝區已滿時產生傳輸時間戳,則會捨棄新的時間戳。
SO_TIMESTAMP_ID (0x300B) 定義於 mstcpip.h
中。 您應該提供控制項訊息資料做為 UINT32。
時間戳會以64位計數器值表示。 計數器的頻率取決於時間戳的來源。 對於軟體時間戳,計數器是 QueryPerformanceCounter (QPC) 值,而且您可以透過 queryPerformanceFrequency來判斷其頻率。 針對 NIC 硬體時間戳,計數器頻率取決於 NIC 硬體,而且您可以使用 captureInterfaceHardwareCrossTimestamp 所提供的額外資訊來判斷它。 若要判斷時間戳的來源,請使用 GetInterfaceActiveTimestampCapabilities 和 GetInterfaceSupportedTimestampCapabilities 函式。
除了使用 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 一律為非封鎖作業。 即使以重疊方式呼叫 IOCTL,如果目前沒有可用的傳輸時間戳,則會立即傳回 WSAEWOULDBLOCK。 由於 WSASendMsg 傳回之後,傳輸時間戳可能無法立即使用,因此您的應用程式必須輪詢 IOCTL,直到時間戳可用為止。 傳輸時間戳保證可在成功 WSASendMsg 呼叫之後使用,因為傳輸時間戳緩衝區未滿。