EchoServer
EchoServer 是基于 RFC 862 标准定义的一种简单网络服务协议。它的核心功能是接收客户端发送的数据,并将这些数据原样返回(即"回显")。该协议支持 TCP 和 UDP 两种传输方式,通常运行在端口 7 上。EchoServer 的设计目的是提供一个轻量级、高效的测试工具,用于验证网络连接性和数据完整性。
详细用途
EchoServer 的主要应用场景包括以下几个方面,这些用途使其在网络调试和测试中非常实用:
-
网络连接测试:
- 通过向 EchoServer 发送数据包并接收回显,可以快速检查网络路径是否通畅。例如,管理员使用工具如
telnet或nc(netcat) 连接 EchoServer,如果数据能原样返回,说明网络连接正常;否则,表明存在连接问题。 - 用途示例:验证防火墙规则或路由器配置是否允许数据双向传输。
- 通过向 EchoServer 发送数据包并接收回显,可以快速检查网络路径是否通畅。例如,管理员使用工具如
-
延迟和性能测量:
- EchoServer 可以用于测量网络延迟(ping time)或带宽。客户端发送一个时间戳数据包,服务器回显后,客户端计算往返时间(RTT)。这有助于诊断网络拥塞或性能瓶颈。
- 用途示例:在数据中心部署时,监控不同节点间的响应时间,确保服务质量。
-
数据完整性验证:
- 当数据传输过程中可能发生错误时(如位翻转),EchoServer 的回显机制允许客户端比较发送和接收的数据是否一致。这能检测硬件故障或协议错误。
- 用途示例:测试新开发的网络设备驱动程序,确保数据在传输中没有被篡改。
实现
业务逻辑:把收到的数据原封不动地发回客户端, 网络库会帮我们管理发送缓冲区
1 连接建立 accept / connect onConnection 创建连接对象
cpp
void onConnection(const TcpConnection::TcpConnectionPtr& conn)
{
LOG_TRACE << conn->peerAddress().toIpPort() << " -> "
<< conn->localAddress().toIpPort() << " is "
<< (conn->connected() ? "UP" : "DOWN");
LOG_INFO << conn->getTcpInfoString();
conn->send(StringPiece("hello\n"));
}
2 消息到达 read onMessage 数据接收与处理
cpp
void onMessage(const TcpConnection::TcpConnectionPtr& conn, Buffer* buf, Timestamp time)
{
string msg(buf->retrieveAllAsString());
LOG_TRACE << conn->name() << " recv " << msg.size() << " bytes at " << time.toString();
if (msg == "exit\n")
{
conn->send(StringPiece("bye\n"));
conn->shutdown();
}
if (msg == "quit\n")
{
loop_->quit();
}
conn->send(StringPiece(msg));
}
DISCARD 协议
DISCARD 协议的详细规范基于 RFC 863 标准(1983 年发布)。以下是其核心约定(specifications),包括工作方式、端口使用和行为规则:
-
协议类型和端口:
- DISCARD 协议同时支持 TCP 和 UDP 传输层协议。
- 默认端口号为 9(TCP/UDP 均适用)。如果端口被占用,可配置其他端口,但标准实现始终使用端口 9。
-
工作流程:
- 客户端行为:客户端可以向服务器发送任意数据包(例如文本、二进制数据或空包)。数据格式无限制,协议不要求特定编码。
- 服务器行为:服务器监听端口 9,接收到数据后立即丢弃所有内容,不进行任何处理、存储或响应。服务器不会返回确认消息、错误代码或任何反馈。
-
典型应用场景:
- 网络诊断:测试防火墙规则或网络连通性(例如,验证数据是否能到达目标端口)。
- 服务模拟:作为"哑终端"服务,帮助开发者调试客户端应用(如检查超时处理)。
- 基准测试:测量网络吞吐量(通过发送大量数据并观察丢弃速度)。
实现
cpp
void DiscardServer::onConnection(const TcpConnectionPtr& conn)
{
LOG_INFO << "DiscardServer - " << conn->peerAddress().toIpPort() << " -> "
<< conn->localAddress().toIpPort() << " is "
<< (conn->connected() ? "UP" : "DOWN");
}
void DiscardServer::onMessage(const TcpConnectionPtr& conn,
Buffer* buf,
Timestamp time)
{
string msg(buf->retrieveAllAsString());
LOG_INFO << conn->name() << " discards " << msg.size()<<msg.c_str() << " bytes received at " << time.toString();
}
Daytime协议
Daytime协议定义在RFC 867文档中,是一个简单的网络时间协议。它通过TCP或UDP提供可读的日期和时间字符串服务,主要设计用于网络调试和基本时间同步需求。该协议运行在13号端口,采用ASCII字符格式返回时间信息。
详细约定说明
1. 服务模式
-
TCP模式:
- 客户端建立TCP连接到服务器的13端口
- 服务器立即返回当前日期时间字符串(ASCII格式)
- 传输完成后立即关闭连接
- 示例响应:
Tuesday, February 22, 2023 14:30:45-CST
-
UDP模式:
- 客户端向服务器13端口发送空UDP数据包
- 服务器返回包含日期时间的UDP数据包
- 不需要建立持久连接
2. 数据格式要求
- 必须使用ASCII字符集
- 时间字符串需包含完整日期、时间和时区
- 推荐格式:
星期, 月份 日期, 年份 时:分:秒-时区
例如:
Wednesday, August 16, 2023 09:15:22-PST - 禁止返回二进制或机器可读格式(区别于NTP协议)
3. 行为规范
-
服务器端:
- 必须监听13端口
- TCP模式下收到连接即响应数据
- UDP模式下仅响应空数据包请求
- 返回时间必须基于服务器本地时钟
-
客户端:
- 不发送任何数据内容(TCP/UDP均适用)
- 在UDP模式下必须发送空数据包
- 应忽略响应中的换行符等格式差异
4. 协议特性
| 特性 | 说明 |
|---|---|
| 精度 | 秒级(不提供毫秒/微秒精度) |
| 时区处理 | 必须包含时区标识 |
| 错误处理 | 无重传机制,不保证可靠性 |
| 适用场景 | 网络调试、基础时间参考 |
⚠️ 注意事项 :
该协议已逐步被NTP取代,现代系统通常仅保留兼容支持。因其无加密和认证机制,不适合安全敏感场景。
实现
cpp
void DaytimeServer::onConnection(const TcpConnectionPtr &conn) {
LOG_INFO << "DaytimeServer - " << conn->peerAddress().toIpPort() << " -> "
<< conn->localAddress().toIpPort() << " is "
<< (conn->connected() ? "UP" : "DOWN");
if (conn->connected()) {
conn->send(Timestamp::now().toFormattedString() + "\n");
conn->shutdown();
}
}
void DaytimeServer::onMessage(const TcpConnectionPtr &conn,
Buffer *buf,
Timestamp time) {
string msg(buf->retrieveAllAsString());
LOG_INFO << conn->name() << " discards " << msg.size()
<< " bytes received at " << time.toString();
}
TcpConnection::shutdown() 实现的是一个"优雅关闭"(graceful shutdown)。它的逻辑是:
- 调用 shutdown() 会将连接状态设置为 kDisconnecting,并安排在 I/O 线程中执行 shutdownInLoop()。
- shutdownInLoop() 会检查当前是否正在发送数据(通过 channel_->isWriting() 判断)。
- 如果应用层缓冲区(outputBuffer_)中还有数据待发送 ,channel_->isWriting() 会返回
true,那么 socket_->shutdownWrite() 不会被立即调用。- I/O 线程会继续通过 handleWrite() 发送剩余数据。
- 当 outputBuffer_ 中的数据全部发送完毕后,handleWrite() 会发现连接状态是 kDisconnecting,于是再次调用 shutdownInLoop()。
- 此时,因为数据已发完,channel_->isWriting() 为
false,socket_->shutdownWrite() 才会被执行,向对端发送FIN包。这个设计的目的是确保所有你调用 send() 的数据都成功写入内核缓冲区之后,再关闭连接的写端。这对于很多协议是必要的,可以防止数据丢失。
如果待发送的数据量非常大,
shutdownWrite()的调用确实会被推迟,直到所有数据都发送完成。如果你希望无论如何都立即关闭连接(可能会丢失 outputBuffer_ 中未发送的数据),应该使用 forceClose() 方法。forceClose() 会直接在 I/O 循环中调用 handleClose(),这会关闭文件描述符并清理连接资源。
but normally the timestamp is very short that kernel space can be enough to write, then close it is fine.
时间协议(RFC 868)简介与详细约定
时间协议(Time Protocol)由RFC 868定义,是一种简单的网络协议,用于在计算机之间传输时间信息。它设计于1983年,主要用于早期互联网环境,允许客户端从服务器获取准确的UTC时间。该协议因其简洁性而易于实现,但现代系统已逐渐被更先进的协议(如NTP)取代。下面我将逐步介绍其核心内容,并详细说明其约定。
1. 协议简介
- 目的:时间协议允许客户端通过网络查询服务器的时间,以同步本地时钟。服务器返回一个时间戳,表示从特定参考点开始的秒数。
- 工作原理:客户端连接到服务器的指定端口,发送一个空请求;服务器响应一个32位整数,该整数编码了时间信息。协议支持TCP和UDP两种传输方式。
- 历史背景:RFC 868发布于1983年,是互联网工程任务组(IETF)的标准文档,旨在提供基本的时间服务。它常用于旧式系统或嵌入式设备。
2. 详细约定
协议约定定义了数据格式、传输规则和行为要求,确保兼容性和可靠性。以下是关键约定的逐步说明:
-
端口号:
- 服务器必须监听端口号37(TCP或UDP)。客户端通过此端口发起连接或发送数据报。
-
请求格式:
- 客户端发送的请求为空(即无数据内容)。对于TCP,客户端建立连接后立即发送空数据包;对于UDP,客户端发送一个空数据报。
- 约定:服务器必须忽略任何非空请求数据,以避免错误处理。
-
响应格式:
- 服务器响应一个32位无符号整数(4字节),表示从参考时间点起的秒数。
- 时间原点:参考点为1900年1月1日00:00:00 UTC。范围从0到约136年。
- 字节顺序:数据必须使用网络字节顺序(大端序)。例如,时间戳值 123456789 的字节序列为
0x07 0x5B 0xCD 0x15。
-
传输行为:
- TCP模式 :
- 客户端建立TCP连接后,发送空请求(0字节数据)。
- 服务器立即响应一个4字节的时间戳,然后关闭连接。
- 约定:服务器必须在收到请求后立即响应,超时或错误时应关闭连接而不发送数据。
- UDP模式 :
- 客户端发送一个空UDP数据报到端口37。
- 服务器响应一个UDP数据报,包含4字节时间戳。
- 约定:服务器不保证可靠传输;客户端应处理丢包或延迟,可能通过重试机制。
- TCP模式 :
总结
RFC 868时间协议是一个轻量级解决方案,强调简单性而非精度(它不处理毫秒级时间或网络延迟补偿)。其约定确保了跨平台兼容性,但现代应用中,它已被更精确的协议取代。
实现
cpp
void TimeServer::onConnection(const muduo::net::TcpConnectionPtr& conn)
{
LOG_INFO << "TimeServer - " << conn->peerAddress().toIpPort() << " -> "
<< conn->localAddress().toIpPort() << " is "
<< (conn->connected() ? "UP" : "DOWN");
if (conn->connected())
{
time_t now = ::time(NULL);
int32_t be32 = sockets::hostToNetwork32(static_cast<int32_t>(now));
conn->send(&be32, sizeof be32);
conn->shutdown();
}
}
void TimeServer::onMessage(const muduo::net::TcpConnectionPtr& conn,
muduo::net::Buffer* buf,
muduo::Timestamp time)
{
string msg(buf->retrieveAllAsString());
LOG_INFO << conn->name() << " discards " << msg.size()
<< " bytes received at " << time.toString();
}
Chargen 协议(RFC 864)简介
Chargen (Character Generator Protocol)是定义于 RFC 864 的应用层协议,设计用于生成连续的字符流。其核心功能是提供可预测的字符序列,常用于网络设备调试、测试终端设备或打印机性能等场景。
协议关键约定
-
服务端口
- TCP 模式 :默认监听 19 号端口
客户端连接后,服务器持续发送任意字符流,直到连接关闭。 - UDP 模式 :默认监听 19 号端口
服务器收到任意 UDP 报文后,回复一个 ≤ 512 字节 的随机字符数据包。
- TCP 模式 :默认监听 19 号端口
-
数据生成规则
-
字符集为 ASCII 可打印字符(如字母、数字、符号)。
-
数据流无固定模式,但需确保输出连续不间断(TCP 模式)。
-
典型响应示例(部分):
!"#$%&'()*+,-./0123456789:;<=>?@ABCDEF...
-
-
原始设计用途
- 终端设备测试:验证终端滚屏、换行等功能。
- 打印机调试:检测打印机走纸和字符渲染。
- 网络带宽测试:通过持续数据流评估链路吞吐量。
-
现代安全性警告
⚠️ 重要风险提示
Chargen 协议因缺乏访问控制,易被滥用为 DDoS 反射攻击 工具:
- 攻击者伪造源 IP 向 Chargen 服务器发送请求。
- 服务器向受害者 IP 反射大量字符数据包(放大攻击)。
- 最佳实践:现代网络应禁用 Chargen 服务(关闭端口 19)。
总结
Chargen 作为早期网络调试工具,因 协议简单 和 数据可预测性 被广泛应用,但现代环境中其 安全缺陷 远超实用价值。管理员应确保关闭相关服务,避免成为网络攻击的跳板。
实现
它发送数据的速度不能快过客户端接收的速度,因此需要关注"三个半事件"中的半个"消息/数据发送完毕"事件(onWriteComplete), also On connection should send once
cpp
void ChargenServer::onConnection(const TcpConnectionPtr& conn)
{
LOG_INFO << "ChargenServer - " << conn->peerAddress().toIpPort() << " -> "
<< conn->localAddress().toIpPort() << " is "
<< (conn->connected() ? "UP" : "DOWN");
if (conn->connected())
{
conn->setTcpNoDelay(true);
conn->send(message_); // On connection send once
}
}
void ChargenServer::onMessage(const TcpConnectionPtr& conn,
Buffer* buf,
Timestamp time)
{
string msg(buf->retrieveAllAsString());
LOG_INFO << conn->name() << " discards " << msg.size()
<< " bytes received at " << time.toString();
}
void ChargenServer::onWriteComplete(const TcpConnectionPtr& conn)
{
transferred_ += message_.size();
conn->send(message_); // After last send ok , then send next.
}