我们仿照NTP时间同步的原理,实现一个测量两台机器之间误差的程序,在实现中服务端将收到数据的时间与发送应答的时间抽象为一个时间点,即忽略server端处理数据的机器误差。
tips:clockdiff
命令可以测量本机与目标主机的时间差,例如 clockdiff -o ntp.ntsc.ac.cn
测量本机与上海交通大学网络中心NTP服务器时间的时间差
代码位置:
- UDP, two threads
recipes/tpc/roundtrip_udp.cc - UDP with muduo, single thread
muduo/examples/roundtrip/roundtrip_udp.cc - TCP with muduo
muduo/examples/roundtrip/roundtrip.cc
客户端与服务段发送的消息体是一个16字节的数据,其中前 8 字节的数据表示客户端的发送时间(T1)。后 8 字节数据表示 服务端接收到数据的时间(T2)。消息体被响应传回至客户端的时间为T3 。则可得到三个时间点 T1, T2, T3,从而计算两台机器的时延。
// 消息体,结构体表示
struct Message
{
int64_t request;
int64_t response;
}
节选自 recipes/tpc/roundtrip_udp.cc 代码片段
// 时间获取函数
int64_t now() // unix元年到现在的微秒数
{
struct timeval tv = { 0, 0 };
gettimeofday(&tv, NULL);
return tv.tv_sec * int64_t(1000000) + tv.tv_usec; /* Microseconds. */
}
/* 服务端 */
void runServer()
{
Socket sock(Socket::createUDP()); // 创建UDP socket
sock.bindOrDie(InetAddress(g_port));// 绑定到端口 3123
while (true)
{
Message message = { 0, 0 };
struct sockaddr peerAddr;
bzero(&peerAddr, sizeof peerAddr);
socklen_t addrLen = sizeof peerAddr;
ssize_t nr = ::recvfrom(sock.fd(), &message, sizeof message, 0, &peerAddr, &addrLen); // 读消息
if (nr == sizeof message)
{
message.response = now(); // 设置时间T2,即服务器当前的时间
ssize_t nw = ::sendto(sock.fd(), &message, sizeof message, 0, &peerAddr, addrLen);
if (nw < 0)
{
perror("send Message");
}
else if (nw != sizeof message)
{
printf("sent message of %zd bytes, expect %zd bytes.\n", nw, sizeof message);
}
}
else if (nr < 0)
{
perror("recv Message");
}
else
{
printf("received message of %zd bytes, expect %zd bytes.\n", nr, sizeof message);
}
}
}
/* 客户端 */
void runClient(const char* server_hostname)
{
Socket sock(Socket::createUDP());
InetAddress serverAddr(g_port);
if (!InetAddress::resolve(server_hostname, &serverAddr))
{
printf("Unable to resolve %s\n", server_hostname);
return;
}
/* udp客户端建立了socket后可以直接调用sendto()函数向服务器发送数据,
但是需要在sendto()函数的参数中指定目的地址/端口,但是可以调用connect()
函数先指明目的地址/端口,然后就可以使用send()函数向目的地址发送数据了 */
if (sock.connect(serverAddr) != 0)
{
perror("connect to server");
return;
}
/* 使用线程去处理 */
std::thread thr([&sock] () {
while (true)
{
Message message = { 0, 0 };
message.request = now();
int nw = sock.write(&message, sizeof message);
if (nw < 0)
{
perror("send Message");
}
else if (nw != sizeof message)
{
printf("sent message of %d bytes, expect %zd bytes.\n", nw, sizeof message);
}
::usleep(200*1000); // 200ms,一秒钟发5个包
}
});
while (true)
{
Message message = { 0, 0 };
int nr = sock.read(&message, sizeof message); // 读消息
if (nr == sizeof message)
{
int64_t back = now(); // 设置时间 T3
int64_t mine = (back + message.request) / 2; // 计算时延
printf("now %jd round trip %jd clock error %jd\n",
back, back - message.request, message.response - mine);
}
else if (nr < 0)
{
perror("send Message");
}
else
{
printf("received message of %d bytes, expect %zd bytes.\n", nr, sizeof message);
}
}
}
编译后本机测试效果如下(编译命令 g++ -o roundtrip_udp roundtrip_udp.cc Socket.cc InetAddress.cc -std=c++11 -lpthread
):(图左:服务端,图右:客户端)
当前时间,精确到秒 往返延迟(微秒) 误差:(单位:微秒)
now 1637145421885614 round trip 195 clock error 30
now 1637145422085667 round trip 135 clock error 13
now 1637145422285777 round trip 123 clock error 15
now 1637145422485995 round trip 208 clock error 16
now 1637145422692842 round trip 314 clock error 89
now 1637145422893054 round trip 294 clock error 93
now 1637145423093058 round trip 140 clock error 8
now 1637145423293291 round trip 179 clock error -13
// 时间戳:前10为表示到秒级(例如1637145423 =》2021-11-17 18:37:03)。
秒级时间戳可以使用一些在线的工具转换成标准时间。全部16位时间戳表示精确到微秒级。