本章将介绍如何用muduo网络库完成常见的TCP网络编程任务。内容如下:
1.[UNP]中的五个简单协议,包括echo、daytime、time、discard、chargen等。
2.文件传输,示范非阻塞TCP网络程序中如何完整地发送数据。
3.Boost.Asio中的示例,包括timer2~6、chat等。chat实现了TCP封包与拆包(codec)。
4.muduo Buffer class的设计与使用。
5.Protobuf编码解码器(codec)与消息分发器(dispatcher)。
6.限制服务器的最大并发连接数。
7.Java Netty中的示例,包括discard、echo、uptime等,其中的discard和echo带流量统计功能。
8.用于测试两台机器的往返延迟的roundtrip。
9.用timing wheel踢掉空闲连接。
10.一个基于TCP的应用层广播hub。
11.云风的串并转换连接服务器multiplexer,及其自动化测试。
12.socks4a代理服务器,包括简单的TCP中继(relay)。
13.一个提供短址服务的httpd服务器。
14.与其他库的集成,包括UDNS、c-ares DNS、curl等等。
这些例子都比较简单,逻辑不复杂,代码也很短,适合摘取关键部分放到博客上。其中一些有一定的代表性与针对性,比如“如何传输完整的文件”估计是网络编程的初学者经常遇到的问题。请注意,muduo是设计来开发内网的网络程序,它没有做任何安全方面的加强措施,如果用在公网上可能会受到攻击,在后面的例子中我会谈到这一点。
7.1 五个简单TCP示例
本节将介绍五个简单TCP网络服务程序,包括echo(RFC 862)、discard(RFC 863)、chargen(RFC 864)、daytime(RFC 867)、time(RFC 868)这五个协议,以及time协议的客户端。各程序的协议简介如下。
● discard:丢弃所有收到的数据。
● daytime:服务端accept连接之后,以字符串形式发送当前时间,然后主动断开连接。
● time:服务端accept连接之后,以二进制形式发送当前时间(从Epoch到现在的秒数),然后主动断开连接;我们需要一个客户程序来把收到的时间转换为字符串。
● echo:回显服务,把收到的数据发回客户端。
● chargen:服务端accept连接之后,不停地发送测试数据。
以上五个协议使用不同的端口,可以放到同一个进程中实现,且不必使用多线程。完整的代码见muduo/examples/simple。
discard
discard恐怕算是最简单的长连接TCP应用层协议,它只需要关注“三个半事件”中的“消息/数据到达”事件(6.4.1节),事件处理函数如下:
void DiscardServer::onMessage(const TcpConnectionPtr& conn,
Buffer* buf, Timestamp time)
{
string msg(buf->retrieveAllAsString());
LOG_INFO << conn->name() << " discards " << msg.size()
<< " bytes received at " << time.toString();
}
与前面p.139的echo服务相比,除了省略namespace外,关键的区别在于少了L40:将收到的数据发回客户端。
剩下的都是例行公事的代码,此处从略,读者可对比参考echo服务。
-------分隔线------------------------------------
笔记:
//DiscardServer.h 丢弃所有收到的数据
#ifndef EXAMPLE_SIMPLE_DISCARD_H
#define EXAMPLE_SIMPLE_DISCARD_H
#include "muduo/net/TcpServer.h"
using namespace muduo;
using namespace muduo::net;
//RFC 863
class DiscardServer
{
public:
DiscardServer(EventLoop* loop, const InetAddress& listenAddr);
void start();
private:
void onConnection(const TcpConnectionPtr& conn);
void onMessage(const TcpConnectionPtr& conn,
Buffer* buf, Timestamp time);
TcpServer server_;
};
#endif //EXAMPLE_SIMPLE_DISCARD_H
DiscardServer.cpp文件
//DiscardServer.cpp
#include "DiscardServer.h"
#include "muduo/base/Logging.h"
DiscardServer::DiscardServer(EventLoop* loop,
const InetAddress& listenAddr)
:server_(loop, listenAddr, "DiscardServer")
{
server_.setConnectionCallback(std::bind(&DiscardServer::onConnection, this, _1));
server_.setMessageCallback(std::bind(&DiscardServer::onMessage, this, _1, _2, _3));
}
void DiscardServer::start()
{
server_.start();
}
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()
<< " bytes received at " << time.toString();
}
main.cpp主函数
//main.cpp DiscardServer的主函数
#include "DiscardServer.h"
#include "muduo/net/EventLoop.h"
#include "muduo/base/Logging.h"
#include <unistd.h>
#include <iostream>
int main()
{
LOG_INFO << "pid = " << getpid();
EventLoop loop;
InetAddress listenAddr(2009);
DiscardServer server(&loop, listenAddr);
server.start();
std::cout << "DiscardServer start" << std::endl;
loop.loop();
return 0;
}
编译运行:

客户端

-------分隔线------------------------------------
daytime
daytime是短连接协议,在发送完当前时间后,由服务端主动断开连接。它只需要关注“三个半事件”中的“连接已建立”事件,事件处理函数如下:
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");//L34发送时间字符串
conn->shutdown();//L35主动断开连接
}
}
L34发送时间字符串,L35主动断开连接。剩下的都是例行公事的代码,为节省篇幅,此处从略。
用netcat扮演客户端,运行结果如下:

-------分隔线------------------------------------
笔记:
//DaytimeServer.h 在发送完当前时间后,由服务端主动断开连接
#ifndef EXAMPLE_SIMPLE_DAYTIME_H
#define EXAMPLE_SIMPLE_DAYTIME_H
#include "muduo/net/TcpServer.h"
using namespace muduo;
using namespace muduo::net;
//RFC 867
class DaytimeServer
{
public:
DaytimeServer(EventLoop* loop, const InetAddress& listenAddr);
void start();
private:
void onConnection(const TcpConnectionPtr& conn);
void onMessage(const TcpConnectionPtr& conn,
Buffer* buf, Timestamp time);
TcpServer server_;
};
#endif // EXAMPLE_SIMPLE_DAYTIME_H
DaytimeServer.cpp文件:
//DaytimeServer.cpp
#include "DaytimeServer.h"
#include "muduo/base/Logging.h"
DaytimeServer::DaytimeServer(EventLoop* loop,
const InetAddress& listenAddr)
:server_(loop, listenAddr, "DaytimeServer")
{
server_.setConnectionCallback(std::bind(&DaytimeServer::onConnection, this, _1));
server_.setMessageCallback(std::bind(&DaytimeServer::onMessage, this, _1, _2, _3));
}
void DaytimeServer::start()
{
server_.start();
}
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();
}
main.cpp文件
//main.cpp DaytimeServer的主函数
#include "DaytimeServer.h"
#include "muduo/net/EventLoop.h"
#include "muduo/base/Logging.h"
#include <unistd.h>
#include <iostream>
int main()
{
LOG_INFO << "pid = " << getpid();
EventLoop loop;
InetAddress listenAddr(2013);
DaytimeServer server(&loop, listenAddr);
server.start();
std::cout << "DaytimeServer start" << std::endl;
loop.loop();
return 0;
}
编译运行:

客户端连接成功后,马上就收到断开的信号

-------分隔线------------------------------------
time
time协议与daytime极为类似,只不过它返回的不是日期时间字符串,而是一个32-bit整数,表示从1970-01-0100:00:00Z到现在的秒数。当然,这个协议有“2038年问题”。服务端只需要关注“三个半事件”中的“连接已建立”事件,事件处理函数如下:
void TimeServer::onConnection(const 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();
}
}
L34、L35取当前时间并转换为网络字节序(Big Endian),L36发送32-bit整数,L37主动断开连接。剩下的都是例行公事的代码,为节省篇幅,此处从略。
用netcat扮演客户端,并用hexdump来打印二进制数据,运行结果如下:

-------分隔线------------------------------------
笔记:
//TimeServer.h 在发送完当前时间戳(32位)后,由服务端主动断开连接
#ifndef EXAMPLE_SIMPLE_TIME_H
#define EXAMPLE_SIMPLE_TIME_H
#include "muduo/net/TcpServer.h"
using namespace muduo;
using namespace muduo::net;
//RFC 868
class TimeServer
{
public:
TimeServer(EventLoop* loop, const InetAddress& listenAddr);
void start();
private:
void onConnection(const TcpConnectionPtr& conn);
void onMessage(const TcpConnectionPtr& conn,
Buffer* buf, Timestamp time);
TcpServer server_;
};
#endif // EXAMPLE_SIMPLE_TIME_H
TimeServer.cpp文件:
//TimeServer.cpp
#include "TimeServer.h"
#include "muduo/base/Logging.h"
#include "muduo/net/Endian.h"
TimeServer::TimeServer(EventLoop* loop,
const InetAddress& listenAddr)
:server_(loop, listenAddr, "TimeServer")
{
server_.setConnectionCallback(std::bind(&TimeServer::onConnection, this, _1));
server_.setMessageCallback(std::bind(&TimeServer::onMessage, this, _1, _2, _3));
}
void TimeServer::start()
{
server_.start();
}
void TimeServer::onConnection(const 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 TcpConnectionPtr& conn,
Buffer* buf, Timestamp time)
{
string msg(buf->retrieveAllAsString());
LOG_INFO << conn->name() << " discards " << msg.size()
<< " bytes received at " << time.toString();
}
main.cpp
//main.cpp TimeServer的主函数
#include "TimeServer.h"
#include "muduo/net/EventLoop.h"
#include "muduo/base/Logging.h"
#include <unistd.h>
#include <iostream>
int main()
{
LOG_INFO << "pid = " << getpid();
EventLoop loop;
InetAddress listenAddr(2037);
TimeServer server(&loop, listenAddr);
server.start();
std::cout << "TimeServer start" << std::endl;
loop.loop();
return 0;
}
编译运行:

-------分隔线------------------------------------
time客户端
因为time服务端发送的是二进制数据,不便直接阅读,我们编写一个客户端来解析并打印收到的4个字节数据。这个程序只需要关注“三个半事件”中的“消息/数据到达”事件,事件处理函数如下:
void onMessage(const TcpConnectionPtr& conn, Buffer* buf, Timestamp receiveTime)
{
if (buf->readableBytes() >= sizeof(int32_t))
{
const void* data = buf->peek();
int32_t be32 = *static_cast<const int32_t*>(data);
buf->retrieve(sizeof(int32_t));//取数据
time_t time = sockets::networkToHost32(be32);
Timestamp ts(implicit_cast<uint64_t>(time) * Timestamp::kMicroSecondsPerSecond);
LOG_INFO << "Server time = " << time << " , " << ts.toFormattedString();
}
else
{
LOG_INFO << conn->name() << " no enough data " << buf->readableBytes()
<< " at " << receiveTime.toFormattedString();
}
}
注意其中考虑到了如果数据没有一次性收全,已经收到的数据会累积在Buffer里(在else分支里没有调用Buffer::retrieve*系列函数),以等待后续数据到达,程序也不会阻塞。这样即便服务器一个字节一个字节地发送数据,代码还是能正常工作,这也是非阻塞网络编程必须在用户态使用接收缓冲的主要原因。
这是我们第一次用到TcpClient class,完整的代码如下:
//timeclient.cpp 客户端
#include "muduo/base/Logging.h"
#include "muduo/net/Endian.h"
#include "muduo/net/EventLoop.h"
#include "muduo/net/InetAddress.h"
#include "muduo/net/TcpClient.h"
#include <utility>
#include <unistd.h>
#include <stdio.h>
using namespace muduo;
using namespace muduo::net;
class TimeClient : noncopyable
{
public:
TimeClient(EventLoop* loop, const InetAddress& serverAddr)
:loop_(loop),
client_(loop, serverAddr, "TimeClient")
{
client_.setConnectionCallback(std::bind(&TimeClient::onConnection, this, _1));
client_.setMessageCallback(std::bind(&TimeClient::onMessage, this, _1, _2, _3));
}
void connect()
{
client_.connect();
}
private:
EventLoop* loop_;
TcpClient client_;
void onConnection(const TcpConnectionPtr& conn)
{
LOG_INFO << conn->localAddress().toIpPort() << " -> "
<< conn->peerAddress().toIpPort() << " is "
<< (conn->connected() ? "UP" : "DOWN");
if (!conn->connected())
{
loop_->quit(); //这行如果连接断开,就退出main()函数的loop.loop()事件循环,程序也就终止了,
}
}
void onMessage(const TcpConnectionPtr& conn, Buffer* buf, Timestamp receiveTime)
{
if (buf->readableBytes() >= sizeof(int32_t))
{
const void* data = buf->peek();
int32_t be32 = *static_cast<const int32_t*>(data);
buf->retrieve(sizeof(int32_t));
time_t time = sockets::networkToHost32(be32);
Timestamp ts(implicit_cast<uint64_t>(time) * Timestamp::kMicroSecondsPerSecond);
LOG_INFO << "Server time = " << time << " , " << ts.toFormattedString();
}
else
{
LOG_INFO << conn->name() << " no enough data " << buf->readableBytes()
<< " at " << receiveTime.toFormattedString();
}
}
};
//以上L42表示如果连接断开,就退出事件循环(L75),程序也就终止了。
int main(int argc, char* argv[])
{
LOG_INFO << "pid = " << getpid();
if (argc > 1)
{
EventLoop loop;
InetAddress serverAddr(argv[1], 2037);
TimeClient timeClient(&loop, serverAddr);
timeClient.connect();
loop.loop();
}
else
{
printf("Usage: %s host_ip\n", argv[0]);
}
return 0;
}
注意TcpConnection对象表示“一次”TCP连接,连接断开之后不能重建。TcpClient重试之后新建的连接会是另一个TcpConnection对象。
程序的运行结果如下(有折行),假设time server运行在本机:

-------分隔线------------------------------------
笔记:
QT测试端设置命令行参数:

运行结果:

-------分隔线------------------------------------
echo
前面几个协议都是单向接收或发送数据,echo是我们遇到的第一个双向的协议:服务端把客户端发过来的数据原封不动地传回去。它只需要关注“三个半事件”中的“消息/数据到达”事件,事件处理函数已在p.139列出,这里复制一遍。
void EchoServer::onMessage(const TcpConnectionPtr& conn,
Buffer* buf, Timestamp time)
{
muduo::string msg(buf->retrieveAllAsString());//取走多有字符数据
LOG_INFO << conn->name() << " discards " << msg.size()
<< " bytes received at " << time.toString();
conn->send(msg);
}
这段代码实现的不是行回显(line echo)服务,而是有一点数据就发送一点数据。这样可以避免客户端恶意地不发送换行字符,而服务端又必须缓存已经收到的数据,导致服务器内存暴涨。但这个程序还是有一个安全漏洞,即如果客户端故意不断发送数据,但从不接收,那么服务端的发送缓冲区会一直堆积,导致内存暴涨。解决办法可以参考下面的chargen协议,或者在发送缓冲区累积到一定大小时主动断开连接。一般来说,非阻塞网络编程中正确处理数据发送比接收数据要困难,因为要应对对方接收缓慢的情况。
练习1:修改EchoServer::onMessage(),实现大小写互换。
练习2:修改EchoServer::onMessage(),实现ROT13加密[注1:http://en.wikipedia.org/wiki/ROT13]。
-------分隔线------------------------------------
笔记:
//EchoServer.h 服务端把客户端发过来的数据原封不动地传回去。
#ifndef EXAMPLE_SIMPLE_ECHO_H
#define EXAMPLE_SIMPLE_ECHO_H
#include "muduo/net/TcpServer.h"
using namespace muduo;
using namespace muduo::net;
//RFC 862
class EchoServer
{
public:
EchoServer(EventLoop* loop, const InetAddress& listenAddr);
void start(); // calls server_.start();
private:
void onConnection(const TcpConnectionPtr& conn);
void onMessage(const TcpConnectionPtr& conn,
Buffer* buf, Timestamp time);
TcpServer server_;
};
#endif // EXAMPLE_SIMPLE_ECHO_H
EchoServer.cpp文件:
//EchoServer.cpp
#include "EchoServer.h"
#include "muduo/base/Logging.h"
#include "muduo/net/Endian.h"
EchoServer::EchoServer(EventLoop* loop,
const InetAddress& listenAddr)
:server_(loop, listenAddr, "TimeServer")
{
server_.setConnectionCallback(std::bind(&EchoServer::onConnection, this, _1));
server_.setMessageCallback(std::bind(&EchoServer::onMessage, this, _1, _2, _3));
}
void EchoServer::start()
{
server_.start();
}
void EchoServer::onConnection(const TcpConnectionPtr& conn)
{
LOG_INFO << "EchoServer - " << conn->peerAddress().toIpPort() << " -> "
<< conn->localAddress().toIpPort() << " is "
<< (conn->connected() ? "UP" : "DOWN");
}
void EchoServer::onMessage(const TcpConnectionPtr& conn,
Buffer* buf, Timestamp time)
{
muduo::string msg(buf->retrieveAllAsString());//取走多有字符数据
LOG_INFO << conn->name() << " discards " << msg.size()
<< " bytes received at " << time.toString();
//把收到的数据再发回客户端
conn->send(msg); //如果客户端不接收,只发送,服务端的发送缓冲区就一直占着?
}
main.cpp文件:
//main.cpp EchoServer的主函数
#include "EchoServer.h"
#include "muduo/net/EventLoop.h"
#include "muduo/base/Logging.h"
#include <unistd.h>
#include <iostream>
int main()
{
LOG_INFO << "pid = " << getpid();
EventLoop loop;
InetAddress listenAddr(2007);
EchoServer server(&loop, listenAddr);
server.start();
std::cout << "EchoServer start" << std::endl;
loop.loop();
return 0;
}
编译运行:

-------分隔线------------------------------------
chargen
Chargen协议很特殊,它只发送数据,不接收数据。而且,它发送数据的速度不能快过客户端接收的速度,因此需要关注“三个半事件”中的半个“消息/数据发送完毕”事件(onWriteComplete),事件处理函数如下:
//ChargenServer.cpp
#include "ChargenServer.h"
#include "muduo/base/Logging.h"
#include "muduo/net/EventLoop.h"
#include <stdio.h>
using namespace muduo;
using namespace muduo::net;
ChargenServer::ChargenServer(EventLoop* loop,
const InetAddress& listenAddr,
bool print)
:server_(loop, listenAddr, "ChargenServer"),
transferred_(0),
startTime_(Timestamp::now())
{
server_.setConnectionCallback(std::bind(&ChargenServer::onConnection, this, _1));
server_.setMessageCallback(std::bind(&ChargenServer::onMessage, this, _1, _2, _3));
server_.setWriteCompleteCallback(std::bind(&ChargenServer::onWriteComplete, this, _1));
if (print)
{
// 每3秒打印吞吐量
loop->runEvery(3.0, std::bind(&ChargenServer::printThroughput, this));
}
// 初始化要发送的数据
string line;
for (int i = 33; i < 127; ++i)
{
line.push_back(char(i));
}
line += line;
for (size_t i = 0; i < 127 - 33; ++i)
{
message_ += line.substr(i, 72) + '\n';
}
}
void ChargenServer::start()
{
server_.start();
}
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_);
}
}
void ChargenServer::onMessage(const TcpConnectionPtr& conn,
Buffer* buf, Timestamp time)
{
muduo::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_);
}
void ChargenServer::printThroughput()
{
Timestamp endTime = Timestamp::now();
double time = timeDifference(endTime, startTime_);
printf("%4.3f MiB/s\n", static_cast<double>(transferred_) / time / 1024 / 1024);
transferred_ = 0;
startTime_ = endTime;
}
L57在连接建立时发送第一次数据;L73继续发送数据。剩下的都是例行公事的代码,为节省篇幅,此处从略。
完整的chargen服务端还带流量统计功能,用到了定时器,我们会在§ 7.8介绍定时器的使用,到时候再回头来看相关代码。
用netcat扮演客户端,运行结果如下:
$ nc localhost 2019|head
-------分隔线------------------------------------
笔记:
//ChargenServer.h 服务端accept连接之后,不停地发送测试数据。
#ifndef EXAMPLE_SIMPLE_CHARGEN_H
#define EXAMPLE_SIMPLE_CHARGEN_H
#include "muduo/net/TcpServer.h"
using namespace muduo;
using namespace muduo::net;
//RFC 864
class ChargenServer
{
public:
ChargenServer(EventLoop* loop, const InetAddress& listenAddr,
bool print = false);
void start();
private:
void onConnection(const TcpConnectionPtr& conn);
void onMessage(const TcpConnectionPtr& conn,
Buffer* buf, Timestamp time);
void onWriteComplete(const TcpConnectionPtr& conn);
void printThroughput();
TcpServer server_;
muduo::string message_;
int64_t transferred_;
Timestamp startTime_;
};
#endif // EXAMPLE_SIMPLE_CHARGEN_H
main.cpp文件:
//main.cpp ChargenServer的主函数
#include "ChargenServer.h"
#include "muduo/net/EventLoop.h"
#include "muduo/base/Logging.h"
#include <unistd.h>
#include <iostream>
int main()
{
LOG_INFO << "pid = " << getpid();
EventLoop loop;
InetAddress listenAddr(2019);
ChargenServer server(&loop, listenAddr, true);
server.start();
std::cout << "ChargenServer start" << std::endl;
loop.loop();
return 0;
}
编译运行:

-------分隔线------------------------------------
五合一
前面五个程序都用到了EventLoop。这其实是个Reactor,用于注册和分发IO事件。muduo遵循one loop per thread模型,多个服务端(TcpServer)和客户端(TcpClient)可以共享同一个EventLoop,也可以分配到多个EventLoop上以发挥多核多线程的好处。这里我们把五个服务端用同一个EventLoop跑起来,程序还是单线程的,功能却强大了很多:
//main.cpp 5合一服务器
#include "chargen/ChargenServer.h"
#include "daytime/DaytimeServer.h"
#include "discard/DiscardServer.h"
#include "echo/EchoServer.h"
#include "time/TimeServer.h"
#include "muduo/base/Logging.h"
#include "muduo/net/EventLoop.h"
#include <unistd.h>
#include <iostream>
int main()
{
LOG_INFO << "pid = " << getpid();
EventLoop loop; // one loop shared by multiple servers
ChargenServer chargenServer(&loop, InetAddress(2019));
chargenServer.start();
DaytimeServer daytimeServer(&loop, InetAddress(2013));
daytimeServer.start();
DiscardServer discardServer(&loop, InetAddress(2009));
discardServer.start();
EchoServer echoServer(&loop, InetAddress(2007));
echoServer.start();
TimeServer timeServer(&loop, InetAddress(2037));
timeServer.start();
std::cout << "All in One Server start" << std::endl;
loop.loop();
return 0;
}
这个例子充分展示了Reactor模式复用线程的能力,让一个单线程程序同时具备多个网络服务功能。一个容易想到的例子是httpd同时侦听80端口和443端口,另一个例子是程序中有多个TcpClient,分别和数据库、Redis、Sudoku Solver等后台服务打交道。对于初次接触这种编程模型的读者,值得跟踪代码运行的详细过程,弄清楚每个事件每个回调发生的时机与条件。
以上几个协议的消息格式都非常简单,没有涉及TCP网络编程中常见的分包处理,在后文§ 7.3讲Boost.Asio的聊天服务器时我们再来讨论这个问题。
7.2 文件传输
本节用发送文件的例子来说明TcpConnection::send()的使用。到目前为止,我们用到了TcpConnection::send()的两个重载,分别是send(const string&)[注2:p.139 L40.]和send(const void*message,size_t len)[注3:p.179,time示例中的L36.]。
TcpConnection目前提供了三个send()重载函数,原型如下。
// Tcp connection, for both client and server usage.
class TcpConnection:boost::noncopyable,
public boost:enable_shared_from_this<TcpConnection>
{
public:
void send(const void* message, size_t len);
void send(const StringPiece& message);
void send(Buffer* message); // this one might swap data without copying
// void send(Buffer&& message); //C++11
// void send(string&& message); //C++11
在非阻塞网络编程中,发送消息通常是由网络库完成的,用户代码不会直接调用write(2)或send(2)等系统调用。原因见p.205“TcpConnection必须要有output buffer”。在使用TcpConnection::send()时值得注意的有几点:
● send()的返回类型是void,意味着用户不必关心调用send()时成功发送了多少字节,muduo库会保证把数据发送给对方。
● send()是非阻塞的。意味着客户代码只管把一条消息准备好,调用send()来发送,即便TCP的发送窗口满了,也绝对不会阻塞当前调用线程。
● send()是线程安全、原子的。多个线程可以同时调用send(),消息之间不会混叠或交织。但是多个线程同时发送的消息(s)的先后顺序是不确定的,muduo只能保证每个消息本身的完整性[注4:假设两个线程同时各自发送了一条任意长度的消息,那么这两条消息a、b的发送顺序要么是先a后b,要么是先b后a,不会出现“a的前一半,b,a的后一半”这种交织情况。]。另外,send()在多线程下仍然是非阻塞的。
● send(const void*message,size_t len)这个重载最平淡无奇,可以发送任意字节序列。
● send(const StringPiece&message)这个重载可以发送std::string和const char*,其中StringPiece [注5:代码位于muduo/base/StringPiece.h.]是Google发明的专门用于传递字符串参数的class,这样程序里就不必为const char*和const std::string&提供两份重载了。
● send(Buffer*)有点特殊,它以指针为参数,而不是常见的const引用,因为函数中可能用Buffer::swap()来高效地交换数据,避免内存拷贝[注6:目前的实现尚未照此办理。],起到类似C++右值引用的效果。
● 如果将来支持C++11,那么可以增加对右值引用的重载,这样可以用move语义来避免内存拷贝。
下面我们来实现一个发送文件的命令行小工具,这个工具的协议很简单,在启动时通过命令行参数指定要发送的文件,然后在2021端口侦听,每当有新连接进来,就把文件内容完整地发送给对方。
如果不考虑并发,那么这个功能用netcat加重定向就能实现。这里展示的版本更加健壮,比方说发送100MB的文件,支持上万个并发客户连接;内存消耗只与并发连接数有关,跟文件大小无关;任何连接可以在任何时候断开,程序不会有内存泄漏或崩溃[注7:我用Java实现了压力测试,代码位于examples/filetransfer/loadtest.]。
我一共写了三个版本,代码位于examples/filetransfer。
1.一次性把文件读入内存,一次性调用send(const string&)发送完毕。这个版本满足除了“内存消耗只与并发连接数有关,跟文件大小无关”之外的健壮性要求。
2.一块一块地发送文件,减少内存使用,用到了WriteCompleteCallback。这个版本满足了上述全部健壮性要求。
3.同2,但是采用shared_ptr来管理FILE*,避免手动调用::fclose(3)。
版本一
在建立好连接之后,把文件的全部内容读入一个string,一次性调用TcpConnection::send()发送。不用担心文件发送不完整。也不用担心send()之后立刻shutdown()会有什么问题,见下一节的说明。
-------分隔线------------------------------------
笔记:
//download.cpp 文件传输版本一
#include "muduo/base/Logging.h"
#include "muduo/net/EventLoop.h"
#include "muduo/net/TcpServer.h"
#include <stdio.h>
#include <unistd.h>
using namespace muduo;
using namespace muduo::net;
const char* g_file = NULL;
//FIXME: use FileUtil::readFile()
string readFile(const char* filename)
{
string content;
FILE* fp = ::fopen(filename, "rb");
if (fp)
{
// inefficient!!! 低效
const int kBufSize = 1024 * 1024;
char iobuf[kBufSize];
::setbuffer(fp, iobuf, sizeof(iobuf));
char buf[kBufSize];
size_t nread = 0;
while ((nread = ::fread(buf, 1, sizeof(buf), fp)) > 0)
{
content.append(buf, nread);
}
::fclose(fp);
}
return content;
}
void onHighWaterMark(const TcpConnectionPtr& conn, size_t len)
{
LOG_INFO << "HighWaterMark " << len;
}
void onConnection(const TcpConnectionPtr& conn)
{
LOG_INFO << "FileServer - " << conn->peerAddress().toIpPort() << " -> "
<< conn->localAddress().toIpPort() << " is "
<< (conn->connected() ? "UP" : "DOWN");
if (conn->connected())
{
LOG_INFO << "FileServer - Sending file " << g_file
<< " to " << conn->peerAddress().toIpPort();
conn->setHighWaterMarkCallback(onHighWaterMark, 64 * 1024);
string fileContent = readFile(g_file);
conn->send(fileContent);
conn->shutdown();//发送完成后,断开连接
LOG_INFO << "FileServer - done";
}
}
int main(int argc, char* argv[])
{
LOG_INFO << "pid = " << getpid();
if (argc > 1)
{
g_file = argv[1];
EventLoop loop;
InetAddress listenAddr(2021);
TcpServer server(&loop, listenAddr, "FileServer");
server.setConnectionCallback(onConnection);
server.start();
printf("File Server start\n");
loop.loop();
}
else
{
fprintf(stderr, "Usage:%s file_for _downloading\n", argv[0]);
}
return 0;
}
编译运行,这里用test1文件测试,只有一行数据,服务端发送完数据后,断开连接
$ ./chapter7.out /home/scott/trunk/test1

客户端接收数据,然后收到断开信号

-------分隔线------------------------------------
注意每次建立连接的时候我们都去重新读一遍文件,这是考虑到文件有可能被其他程序修改。如果文件是immutable的,整个程序就可以共享同一个fileContent对象。
这个版本有一个明显的缺陷,即内存消耗与(并发连接数×文件大小)成正比,文件越大内存消耗越多,如果文件大小上GB,那几乎就是灾难了。只需要建立少量并发连接就能把服务器的内存耗尽,因此我们有了版本二。
版本二
为了解决版本一占用内存过多的问题,我们采用流水线的思路,当新建连接时,先发送文件的前64KiB数据,等这块数据发送完毕时再继续发送下64KiB数据,如此往复直到文件内容全部发送完毕。代码中使用了TcpConnection::setContext()和getContext()来保存TcpConnection的用户上下文(这里是FILE*),因此不必使用额外的std::map<TcpConnectionPtr,FILE*>来记住每个连接的当前文件位置。
-------分隔线------------------------------------
笔记:
//download2.cpp 文件传输版本二
#include "muduo/base/Logging.h"
#include "muduo/net/EventLoop.h"
#include "muduo/net/TcpServer.h"
#include <stdio.h>
#include <unistd.h>
using namespace muduo;
using namespace muduo::net;
const int kBufSize = 64 * 1024; //版本2
const char* g_file = NULL;
void onHighWaterMark(const TcpConnectionPtr& conn, size_t len)
{
LOG_INFO << "HighWaterMark " << len;
}
void onConnection(const TcpConnectionPtr& conn)
{
LOG_INFO << "FileServer2 - " << conn->peerAddress().toIpPort() << " -> "
<< conn->localAddress().toIpPort() << " is "
<< (conn->connected() ? "UP" : "DOWN");
if (conn->connected())
{
LOG_INFO << "FileServer - Sending file " << g_file
<< " to " << conn->peerAddress().toIpPort();
conn->setHighWaterMarkCallback(onHighWaterMark, 64 * 1024);
FILE* fp = ::fopen(g_file, "rb");
if (fp)
{
conn->setContext(fp);
char buf[kBufSize];
size_t nread = ::fread(buf, 1, sizeof(buf), fp);
conn->send(buf, static_cast<int>(nread));
}
else
{
conn->shutdown();
LOG_INFO << "FileServer2 - no such file";
}
}
else
{
if (!conn->getContext().empty())
{
FILE* fp = boost::any_cast<FILE*>(conn->getContext());
if (fp)
{
::fclose(fp);
}
}
}
}
// 在onWriteComplete()回调函数中读取下一块文件数据,继续发送。
void onWriteComplete(const TcpConnectionPtr& conn)
{
FILE* fp = boost::any_cast<FILE*>(conn->getContext());
char buf[kBufSize];
size_t nread = ::fread(buf, 1, sizeof(buf), fp);
if (nread > 0)
{
conn->send(buf, static_cast<int>(nread));
}
else
{
::fclose(fp);
fp = NULL;
conn->setContext(fp);
conn->shutdown();
LOG_INFO << "FileServer2 - done";
}
}
int main(int argc, char* argv[])
{
LOG_INFO << "pid = " << getpid();
if (argc > 1)
{
g_file = argv[1];
EventLoop loop;
InetAddress listenAddr(2021);
TcpServer server(&loop, listenAddr, "FileServer2");
server.setConnectionCallback(onConnection);
server.setWriteCompleteCallback(onWriteComplete);
server.start();
printf("File Server start\n");
loop.loop();
}
else
{
fprintf(stderr, "Usage:%s file_for _downloading\n", argv[0]);
}
return 0;
}
Qt写的客户端接收数据保存到文件。
void MainWindow::slotRecvSaveAsFile()
{
if (!outputFile.isOpen()) {
// 第一次接收,创建文件
outputFile.setFileName("received_file.txt");
if (!outputFile.open(QIODevice::WriteOnly)) {
qDebug() << "Failed to open file for writing";
return;
}
// 读取文件大小(假设前8字节是文件大小)
// if (socket->bytesAvailable() >= 8) {
// QByteArray sizeData = socket->read(8);
// totalSize = *reinterpret_cast<const qint64*>(sizeData.constData());
// }
}
QByteArray data = socket->readAll();
outputFile.write(data);
receivedSize += data.size();
qDebug() << "slotRecvSaveAsFile =======================" << receivedSize;
outputFile.close();
// if (receivedSize >= totalSize && totalSize > 0) {
// // 文件接收完成
// outputFile.close();
// qDebug() << "File received successfully";
// }
}
编译运行:

客户端接保存为文件:

-------分隔线------------------------------------
注意每次建立连接的时候我们都去重新打开那个文件,使得程序中文件描述符的数量翻倍(每个连接占一个socket fd和一个file fd),这是考虑到文件有可能被其他程序修改。如果文件是immutable的,一种改进措施是:整个程序可以共享同一个文件描述符,然后每个连接记住自己当前的偏移量,在onWriteComplete()回调函数里用pread(2)来读取数据。
这个版本也存在一个问题,如果客户端故意只发起连接,不接收数据,那么要么把服务器进程的文件描述符耗尽,要么占用很多服务端内存(因为每个连接有64KiB的发送缓冲区)。解决办法可参考后文§ 7.7“限制服务器的最大并发连接数”和§ 7.10“用timing wheel踢掉空闲连接”。必须说明的是,muduo并不是设计来编写面向公网的网络服务程序,这种服务程序需要在安全性方面下很多工夫,我个人对此不在行,我更关心实现内网(不一定是局域网)的高效服务程序。
版本三
用shared_ptr的custom deleter来减轻资源管理负担,使得FILE*的生命期和TcpConnection一样长,代码也更简单了。
-------分隔线------------------------------------
笔记:
//download3.cpp 文件传输版本三
#include "muduo/base/Logging.h"
#include "muduo/net/EventLoop.h"
#include "muduo/net/TcpServer.h"
#include <stdio.h>
#include <unistd.h>
using namespace muduo;
using namespace muduo::net;
const int kBufSize = 64 * 1024;
const char* g_file = NULL;
typedef std::shared_ptr<FILE> FilePtr; //版本3
void onHighWaterMark(const TcpConnectionPtr& conn, size_t len)
{
LOG_INFO << "HighWaterMark " << len;
}
void onConnection(const TcpConnectionPtr& conn)
{
LOG_INFO << "FileServer3 - " << conn->peerAddress().toIpPort() << " -> "
<< conn->localAddress().toIpPort() << " is "
<< (conn->connected() ? "UP" : "DOWN");
if (conn->connected())
{
LOG_INFO << "FileServer3 - Sending file " << g_file
<< " to " << conn->peerAddress().toIpPort();
conn->setHighWaterMarkCallback(onHighWaterMark, 64 * 1024);
FILE* fp = ::fopen(g_file, "rb");
if (fp)
{
FilePtr ctx(fp, ::fclose);
conn->setContext(ctx);
char buf[kBufSize];
size_t nread = ::fread(buf, 1, sizeof(buf), fp);
conn->send(buf, static_cast<int>(nread));
}
else
{
conn->shutdown();
LOG_INFO << "FileServer3 - no such file";
}
}
}
// 在onWriteComplete()回调函数中读取下一块文件数据,继续发送。
void onWriteComplete(const TcpConnectionPtr& conn)
{
const FilePtr& fp = boost::any_cast<const FilePtr&>(conn->getContext());
char buf[kBufSize];
size_t nread = ::fread(buf, 1, sizeof(buf), get_pointer(fp));
if (nread > 0)
{
conn->send(buf, static_cast<int>(nread));
}
else
{
conn->shutdown();
LOG_INFO << "FileServer3 - done";
}
}
int main(int argc, char* argv[])
{
LOG_INFO << "pid = " << getpid();
if (argc > 1)
{
g_file = argv[1];
EventLoop loop;
InetAddress listenAddr(2021);
TcpServer server(&loop, listenAddr, "FileServer3");
server.setConnectionCallback(onConnection);
server.setWriteCompleteCallback(onWriteComplete);
server.start();
printf("File Server3 start\n");
loop.loop();
}
else
{
fprintf(stderr, "Usage:%s file_for _downloading\n", argv[0]);
}
return 0;
}
编译运行:

客户端接收保存文件

-------分隔线------------------------------------
以上代码体现了现代C++的资源管理思路,即无须手动释放资源,而是通过将资源与对象生命期绑定,在对象析构的时候自动释放资源,从而把资源管理转换为对象生命期管理,而后者是早已解决了的问题。这正是C++最重要的编程技法:RAII。
为什么TcpConnection::shutdown()没有直接关闭TCP连接
我曾经收到一位网友的来信:“在simple的daytime示例中,服务端主动关闭时调用的是如下函数序列,这不是只是关闭了连接上的写操作吗,怎么是关闭了整个连接?”
void DaytimeServer::onConnection(const muduo::net::TcpConnectionPtr& conn)
{
if(conn->connected())
{
conn->send(Timestamp::now().toFormattedString() + "\n);
conn->shutdown(); //调用TcpConnection::shutdown()
}
}
void TcpConnection::shutdown()
{
if(state_ == kConnected)
{
setState(kDisconnecting);
// 调用 TcpConnection::shutdownInLoop()
loop_->runInLoop(boost::bind(&TcpConnection::shutdownInLoop, this));
}
}
void TcpConnection::shutdownInLoop()
{
loop_->assertInLoopThread();
if(!channel_->isWriting()) //如果当前没有发送数据
{
// we are not writing
socket_->shutdownWrite(); //调用Socket::shutdownWrite()
}
}
void Socket::shutdownWrite()
{
sockets::shutdownWrite(sockfd_);
}
void sockets::shutdownWrite(int sockfd)
{
int ret = ::shutdown(sockfd, SHUT_WR);
// 检查错误
}
笔者答复如下:
muduo TcpConnection没有提供close(),而只提供shutdown(),这么做是为了收发数据的完整性。
TCP是一个全双工协议,同一个文件描述符既可读又可写,shutdownWrite()关闭了“写”方向的连接,保留了“读”方向,这称为TCP half-close。如果直接close(socket_fd),那么socket_fd就不能读或写了。
用shutdown而不用close的效果是,如果对方已经发送了数据,这些数据还“在路上”,那么muduo不会漏收这些数据。换句话说,muduo在TCP这一层面解决了“当你打算关闭网络连接的时候,如何得知对方是否发了一些数据而你还没有收到?”这一问题。当然,这个问题也可以在上面的协议层解决,双方商量好不再互发数据,就可以直接断开连接。
也就是说muduo把“主动关闭连接”这件事情分成两步来做,如果要主动关闭连接,它会先关本地“写”端,等对方关闭之后,再关本地“读”端。
练习:阅读代码,回答“如果被动关闭连接,muduo的行为如何?”[注8:提示:在read(2)返回0的时候会回调connection callback,这样客户代码就知道对方断开连接了。]
另外,如果当前output buffer里还有数据尚未发出的话,muduo也不会立刻调用shutdownWrite,而是等到数据发送完毕再shutdown,可以避免对方漏收数据。
void TcpConnection::handleWrite()
{
loop_->assertInLoopThread();
if(channel_->isWriting())
{
ssize_t n = sockets::write(channel_->fd(), //思考:为什么只写一次?
outputBuffer_.peer(),
outputBuffer_.readableBytes());
if(n > 0)
{
outputBuffer_.retrieve(n);
if(outputBuffer_.readableBytes() == 0)
{
channel_->disableWriting();
if(writeCompleteCallback_)
{
loop_->queueInLoop(boost::bind(writeCompleteCallback_,
shared_from_this()));
}
if(state_ == kDisconnecting)
{
shutdownInLoop();
}
}
}
}
}
muduo这种关闭连接的方式对对方也有要求,那就是对方read()到0字节之后会主动关闭连接(无论shutdownWrite()还是close()),一般的网络程序都会这样,不是什么问题。当然,这么做有一个潜在的安全漏洞,万一对方故意不关闭连接,那么muduo的连接就一直半开着,消耗系统资源。必要时可以调用TcpConnection::handleClose()来强行关闭连接,这需要将handleClose()改为public成员函数。
完整的流程见图7-1。我们发完了数据,于是shutdownWrite,发送TCP FIN分节,对方会读到0字节,然后对方通常会关闭连接。这样muduo会读到0字节,然后muduo关闭连接。(思考题:在shutdown()之后,muduo回调connection callback的时间间隔大约是一个round-trip time,为什么?)

图7-1
如果有必要,对方可以在read()返回0之后继续发送数据,这是直接利用了half-close TCP连接。muduo不会漏收这些数据。
那么muduo什么时候真正close socket呢?在TcpConnection对象析构的时候。TcpConnection持有一个Socket对象,Socket是一个RAII handler,它的析构函数会close(sockfd_)。这样,如果发生TcpConnection对象泄漏,那么我们从/proc/pid/fd/就能找到没有关闭的文件描述符,便于查错。
muduo在read()返回0的时候会回调connection callback,TcpServer或Tcp-Client把TcpConnection的引用计数减一。如果引用计数降到零,则表明用户代码也不持有TcpConnection,它就会析构了。
参考:《TCP/IP详解》[TCPv1]18.5节“TCP Half-Close”和《UNIX网络编程(第3版)》[UNP]6.6节“shutdown()函数”。
在网络编程中,应用程序发送数据往往比接收数据简单(实现非阻塞网络库正相反,发送比接收难),下一节我们再谈接收并解析消息的要领。
7.3 Boost.Asio的聊天服务器
本节将介绍一个与Boost.Asio的示例代码中的聊天服务器功能类似的网络服务程序,包括客户端与服务端的muduo实现。这个例子的主要目的是介绍如何处理分包,并初步涉及muduo的多线程功能。本文的代码位于examples/asio/chat/。
7.3.1 TCP分包
§ 7.1“五个简单TCP示例”中处理的协议没有涉及分包,在TCP这种字节流协议上做应用层分包是网络编程的基本需求。分包指的是在发生一个消息(message)或一帧(frame)数据时,通过一定的处理,让接收方能从字节流中识别并截取(还原)出一个个消息。“粘包问题”是个伪问题。
对于短连接的TCP服务,分包不是一个问题,只要发送方主动关闭连接,就表示一条消息发送完毕,接收方read()返回0,从而知道消息的结尾。例如§ 7.1里的daytime和time协议。
对于长连接的TCP服务,分包有四种方法:
1.消息长度固定,比如muduo的roundtrip示例就采用了固定的16字节消息。
2.使用特殊的字符或字符串作为消息的边界,例如HTTP协议的headers以“\r\n”为字段的分隔符。
3.在每条消息的头部加一个长度字段,这恐怕是最常见的做法,本文的聊天协议也采用这一办法。
4.利用消息本身的格式来分包,例如XML格式的消息中<root>...</root>的配对,或者JSON格式中的{...}的配对。解析这种消息格式通常会用到状态机(state machine)。
在后文的代码讲解中还会仔细讨论用长度字段分包的常见陷阱。
聊天服务
本节实现的聊天服务非常简单,由服务端程序和客户端程序组成,协议如下:
● 服务端程序在某个端口侦听(listen)新的连接。
● 客户端向服务端发起连接。
● 连接建立之后,客户端随时准备接收服务端的消息并在屏幕上显示出来。
● 客户端接受键盘输入,以回车为界,把消息发送给服务端。
● 服务端接收到消息之后,依次发送给每个连接到它的客户端;原来发送消息的客户端进程也会收到这条消息。
● 一个服务端进程可以同时服务多个客户端进程。当有消息到达服务端后,每个客户端进程都会收到同一条消息,服务端广播发送消息的顺序是任意的,不一定哪个客户端会先收到这条消息。
●(可选)如果消息A先于消息B到达服务端,那么每个客户端都会先收到A再收到B。
这实际上是一个简单的基于TCP的应用层广播协议,由服务端负责把消息发送给每个连接到它的客户端。参与“聊天”的既可以是人,也可以是程序。在后文§7.11中,我将介绍一个稍微复杂一点的例子hub,它有“聊天室”的功能,客户端可以注册特定的topic(s),并往某个topic发送消息,这样代码更有意思。
我在“谈一谈网络编程学习经验”(附录A)中把聊天服务列为“最主要的三个例子”之一,其与前面的“五个简单TCP协议”不同,聊天服务的特点是“连接之间的数据有交流,从a连接收到的数据要发给b连接。这样对连接管理提出了更高的要求:如何用一个程序同时处理多个连接?fork()-per-connection似乎是不行的。如何防止串话?b有可能随时断开连接,而新建立的连接c可能恰好复用了b的文件描述符,那么a会不会错误地把消息发给c?”muduo的这个例子充分展示了解决以上问题的手法。
7.3.2 消息格式
本聊天服务的消息格式非常简单,“消息”本身是一个字符串,每条消息有一个4字节的头部,以网络序存放字符串的长度。消息之间没有间隙,字符串也不要求以'\0'结尾。比方说有两条消息“hello”和“chenshuo”,那么打包后的字节流共有21字节:

打包的代码 这段代码把string message打包为muduo::net::Buffer,并通过conn发送。由于这个codec的代码位于头文件中,因此反复出现了muduo::net namespace。
void send(muduo::net::TcpConnection* conn,
const muduo::StringPiece& message)
{
muduo::net::Buffer buf;
buf.append(message.data(), message.size());
int32_t len = static_cast<int32_t>(message.size());
int32_t be32 = muduo::net::sockets::hostToNetwork32(len);
buf.prepend(&be32, sizeof(be32)); // L62行
conn->send(&buf);
}
muduo Buffer有一个很好的功能,它在头部预留了8个字节的空间,这样L62的prepend()操作就不需要移动已有的数据,效率较高。
分包的代码 解析数据往往比生成数据更复杂,分包、打包也不例外。
void onMessage(const muduo::net::TcpConnectionPtr& conn,
muduo::net::Buffer* buf,
muduo::Timestamp receiveTime)
{
while(buf->readableBytes() >= kHeaderLen) // kHeaderLen == 4
{
//FIXME: use Buffer::peekInt32()
const void* data = buf->peek();
int32_t be32 = *static_cast<const int32_t*>(data); //SIGBUS L32
const int32_t len = muduo::net::sockets::networkToHost32(be32);
if(len > 65536 || len < 0)
{
LOG_ERROR << "Invalid length " << len;
conn->shutdown(); // FIXME: disable reading
break;
}
else if(buf->readableBytes() >= len + kkHeaderLen)
{
buf->retrieve(kHeaderLen);
muduo::string message(buf->peek(), len); // L43
messageCallback_(conn, message, receiveTime); //L44
buf->retrieve(len);
}
else
{
break;
}
}
}
onMessage()中L43构造完整的消息,L44通过messageCallback_回调用户代码。L32有潜在的问题,在某些不支持非对齐内存访问的体系结构上会造成SIGBUS core dump,读取消息长度应该改用Buffer::peekInt32()。上面这段代码的L28用了while循环来反复读取数据,直到Buffer中的数据不够一条完整的消息。请读者思考,如果换成if(buf->readableBytes()>=kHeaderLen)会有什么后果。
以前面提到的两条消息的字节流为例:

假设数据最终都全部到达,onMessage()至少要能正确处理以下各种数据到达的次序,每种情况下messageCallback_都应该被调用两次:
1.每次收到一个字节的数据,onMessage()被调用21次;
2.数据分两次到达,第一次收到2个字节,不足消息的长度字段;
3.数据分两次到达,第一次收到4个字节,刚好够长度字段,但是没有body;
4.数据分两次到达,第一次收到8个字节,长度完整,但body不完整;
5.数据分两次到达,第一次收到9个字节,长度完整,body也完整;
6.数据分两次到达,第一次收到10个字节,第一条消息的长度完整、body也完整,第二条消息长度不完整;
7.请自行移动和增加分割点,验证各种情况;一共有超过100万种可能(221−1)。
8.数据一次就全部到达,这时必须用while循环来读出两条消息,否则消息会堆积在Buffer中。
请读者验证onMessage()是否做到了以上几点。这个例子充分说明了non-blocking read必须和input buffer一起使用。而且在写decoder的时候一定要在收到完整的消息(L40)之后再retrieve整条消息(L42和L45),除非接收方使用复杂的状态机来解码。
7.3.3 编解码器LengthHeaderCodec
有人评论muduo的接收缓冲区不能设置回调函数的触发条件[注9:http://www.cnblogs.com/Solstice/archive/2011/02/02/1948839.html#2022206],确实如此。每当socket可读时,muduo的TcpConnection会读取数据并存入input buffer,然后回调用户的函数。不过,一个简单的间接层就能解决问题,让用户代码只关心“消息到达”而不是“数据到达”,如本例中的LengthHeaderCodec所展示的那样。
class LengthHeaderCodec:boost::noncopyable
{
public:
typedef boost::function<void(const muduo::net::TcpConnectionPtr&,
const muduo::string& message,
muduo::Timestamp)> StringMessageCallback;
explicit LengthHeaderCodec(const StringMessageCallback& cb)
:mesageCallback_(cb)
{
}
onMessage()和send()同前
private:
StringMessageCallback messageCallback_;
const static size_t kHeaderLen = sizeof(int32_t);
};
这段代码把以Buffer*为参数的MessageCallback转换成了以const string&为参数的StringMessageCallback,让用户代码不必关心分包操作,具体的调用时序图见p.231图7-29。如果编程语言相同,客户端和服务端可以(应该)共享同一个codec,这样既节省工作量,又避免因对协议理解不一致而导致的错误。
7.3.4 服务端的实现
聊天服务器的服务端代码小于100行,不到asio的一半。
请先阅读p.200 L65~L69的数据成员的定义。除了经常见到的EventLoop和TcpServer,ChatServer还定义了codec_和connections_作为成员,后者存放目前已建立的客户连接。在收到消息之后,服务器会遍历整个容器,把消息广播给其中的每一个TCP连接(onStringMessage())。
首先,在构造函数里注册回调:
class ChatServer : noncopyable
{
public:
ChatServer(EventLoop* loop,
const InetAddress& listenAddr)
:server_(loop, listenAddr, "ChatServer"),
codec_(std::bind(&ChatServer::onStringMessage, this, _1, _2, _3))
{
server_.setConnectionCallback(std::bind(&ChatServer::onConnection, this, _1));
server_.setMessageCallback(std::bind(&LengthHeaderCodec::onMessage, &codec_, _1, _2, _3));
}
void start()
{
server_.start();
}
这里有几点值得注意,在以往的代码里是直接把本class的onMessage()注册给server_;这里我们把LengthHeaderCodec::onMessage()注册给server_,然后向codec_注册了ChatServer::onStringMessage(),等于说让codec_负责解析消息,然后把完整的消息回调给ChatServer。这正是我前面提到的“一个简单的间接层”,在不增加muduo库的复杂度的前提下,提供了足够的灵活性让我们在用户代码里完成需要的工作。
另外,server_.start()绝对不能在构造函数里调用,这么做将来会有线程安全的问题,见§ 1.2的论述。
以下是处理连接的建立和断开的代码,注意它把新建的连接加入到connections_容器中,把已断开的连接从容器中删除。这么做是为了避免内存和资源泄漏,TcpConnectionPtr是boost::shared_ptr<TcpConnection>,是muduo里唯一一个默认采用shared_ptr来管理生命期的对象。§ 4.7谈了这么做的原因。
private:
void onConnection(const TcpConnectionPtr& conn)
{
LOG_INFO << conn->peerAddress().toIpPort() << " -> "
<< conn->localAddress().toIpPort() << " is "
<< (conn->connected() ? "UP" : "DOWN");
if (conn->connected())
{
connections_.insert(conn);
}
else
{
connections_.erase(conn);
}
}
以下是服务端处理消息的代码,它遍历整个connections_容器,把消息打包发送给各个客户连接。
void onStringMessage(const TcpConnectionPtr&,
const string& message, Timestamp)
{
for (ConnectionList::iterator it = connections_.begin();
it != connections_.end(); ++it)
{
codec_.send(get_pointer(*it), message);
}
}
数据成员:
typedef std::set<TcpConnectionPtr> ConnectionList;
EventLoop* loop_;
TcpServer server_;
LengthHeaderCodec codec_;
ConnectionList connections_;
main()函数中是例行公事的代码:
int main(int argc, char* argv[])
{
LOG_INFO << "pid = " << getpid();
if (argc > 1)
{
EventLoop loop;
uint16_t port = static_cast<uint16_t>(atoi(argv[1]));
InetAddress serverAddr(port);
ChatServer server(&loop, serverAddr);
server.start();
loop.loop();
}
else
{
printf("Usage: %s port\n", argv[0]);
}
return 0;
}
如果你读过asio的对应代码,会不会觉得Reactor往往比Proactor容易使用?
7.3.5 客户端的实现
我有时觉得服务端的程序常常比客户端的更容易写,聊天服务器再次验证了我的看法。客户端的复杂性来自于它要读取键盘输入,而EventLoop是独占线程的,所以我用了两个线程:main()函数所在的线程负责读键盘,另外用一个EventLoopThread来处理网络IO。[注10:我暂时没有把标准输入输出融入Reactor的想法,因为服务器程序很少用到stdin和stdout.]
现在来看代码,首先,在构造函数里注册回调,并使用了跟前面一样的Length-HeaderCodec作为中间层,负责打包、分包。
public:
ChatClient(EventLoop* loop, const InetAddress& serverAddr)
:loop_(loop),
client_(loop, serverAddr, "ChatClient"),
codec_(std::bind(&ChatClient::onStringMessage, this, _1, _2, _3))
{
client_.setConnectionCallback(
boost::bind(&ChatClient::onConnection, this, _1));
client_.setMessageCallback(
boost::bind(&LengthHeaderCodec::onMessage, &codec_, _1, _2, _3));
client_.enableRetry();
}
void connect()
{
client_.connect();
}
disconnect()目前为空,客户端的连接由操作系统在进程终止时关闭。
void disconnect()
{
//client_.disconnect();
}
write()会由main线程调用,所以要加锁,这个锁不是为了保护TcpConnection,而是为了保护shared_ptr。
void write(const StringPiece& message)
{
MutexLockGuard lock(mutex_);
if (connection_)
{
codec_.send(get_pointer(connection_), message);
}
}
onConnection()会由EventLoop线程调用,所以要加锁以保护shared_ptr。
private:
void onConnection(const TcpConnectionPtr& conn)
{
LOG_INFO << conn->localAddress().toIpPort() << " -> "
<< conn->peerAddress().toIpPort() << " is "
<< (conn->connected() ? "UP" : "DOWN");
MutexLockGuard lock(mutex_);
if (conn->connected())
{
connection_ = conn;
}
else
{
connection_.reset();
}
}
把收到的消息打印到屏幕,这个函数由EventLoop线程调用,但是不用加锁,因为printf()是线程安全的。注意这里不能用std::cout<<,它不是线程安全的。
void onStringMessage(const TcpConnectionPtr&,
const string& message, Timestamp)
{
printf("<<< %s\n", message.c_str());
}
数据成员:
EventLoop* loop_;
TcpClient client_;
LengthHeaderCodec codec_;
MutexLock mutex_;
TcpConnectionPtr connection_ GUARDED_BY(mutex_);
main()函数里除了例行公事,还要启动EventLoop线程和读取键盘输入。
int main(int argc, char* argv[])
{
LOG_INFO << "pid = " << getpid();
if (argc > 2)
{
EventLoopThread loopThread;
uint16_t port = static_cast<uint16_t>(atoi(argv[2]));
InetAddress serverAddr(argv[1], port);
ChatClient client(loopThread.startLoop(), serverAddr);
client.connect();
std::string line;
while (std::getline(std::cin, line))
{
client.write(line);
}
client.disconnect();
CurrentThread::sleepUsec(1000 * 1000); //wait for disconnect, see ace/logging/client.cc
}
else
{
printf("Usage: %s host_ip port\n", argv[0]);
}
return 0;
}
L92 ChatClient使用EventLoopThread的EventLoop,而不是通常的主线程的EventLoop。L97发送数据行。
简单测试
打开三个命令行窗口,在第一个窗口运行:
$ ./asio_chat_server 3000
在第二个窗口运行:
$ ./asio_chat_client 127.0.0.1 3000
在第三个窗口运行同样的命令:
$ ./asio_chat_client 127.0.0.1 3000
这样就有两个客户端进程参与聊天。在第二个窗口里输入一些字符并回车,字符会出现在本窗口和第三个窗口中。
代码示例中还有另外三个server程序,都是多线程的,详细介绍在p.260。
● server_threaded.cc使用多线程TcpServer,并用mutex来保护共享数据。
● server_threaded_efficient.cc对共享数据以§ 2.8“借shared_ptr实现copy-on-write”的手法来降低锁竞争。
● server_threaded_highperformance.cc采用thread local变量,实现多线程高效转发,这个例子值得仔细阅读理解。
§ 7.8会介绍muduo中的定时器,并实现Boost.Asio教程中的timer2~5示例,以及带流量统计功能的discard和echo服务器(来自Java Netty)。流量等于单位时间内发送或接收的字节数,这要用到定时器功能。
-------分隔线------------------------------------
笔记:
//msg_codec.h 消息的编码与解码
#ifndef EXAMPLE_ASIO_CHAT_MSG_CODEC_H
#define EXAMPLE_ASIO_CHAT_MSG_CODEC_H
#include "muduo/base/Logging.h"
#include "muduo/net/Buffer.h"
#include "muduo/net/Endian.h"
#include "muduo/net/TcpConnection.h"
class LengthHeaderCodec : muduo::noncopyable
{
public:
typedef std::function<void(const muduo::net::TcpConnectionPtr&,
const muduo::string& message,
muduo::Timestamp)> StringMessageCallback;
explicit LengthHeaderCodec(const StringMessageCallback& cb)
:messageCallback_(cb)
{
}
void onMessage(const muduo::net::TcpConnectionPtr& conn,
muduo::net::Buffer* buf,
muduo::Timestamp receiveTime)
{
while (buf->readableBytes() >= kHeaderLen) //kHeaderLen == 4
{
// FIXME: use Buffer::peekInt32() 在某些不支持非对齐内存访问的体系结构上会造成SIGBUS core dump
const void* data = buf->peek();
int32_t be32 = *static_cast<const int32_t*>(data); //SIGBUS
const int32_t len = muduo::net::sockets::networkToHost32(be32);
if (len > 65536 || len < 0)
{
LOG_ERROR << "Invalid length " << len;
conn->shutdown(); //FIXME: disable reading
break;
}
else if (buf->readableBytes() >= len + kHeaderLen)
{
buf->retrieve(kHeaderLen);
muduo::string message(buf->peek(), len);
messageCallback_(conn, message, receiveTime);
buf->retrieve(len);
}
else
{
break;
}
}
}
//FIXME: TcpConnectionPtr
void send(muduo::net::TcpConnection* conn,
const muduo::StringPiece& message)
{
muduo::net::Buffer buf;
buf.append(message.data(), message.size());
int32_t len = static_cast<int32_t>(message.size());
int32_t be32 = muduo::net::sockets::hostToNetwork32(len);
buf.prepend(&be32, sizeof(be32));
conn->send(&buf);
}
private:
StringMessageCallback messageCallback_;
const static size_t kHeaderLen = sizeof(int32_t);
};
#endif // EXAMPLE_ASIO_CHAT_MSG_CODEC_H
服务端文件
//server.cpp
#include "msg_codec.h"
#include "muduo/base/Logging.h"
#include "muduo/base/Mutex.h"
#include "muduo/net/EventLoop.h"
#include "muduo/net/TcpServer.h"
#include <set>
#include <stdio.h>
#include <unistd.h>
using namespace muduo;
using namespace muduo::net;
class ChatServer : noncopyable
{
public:
ChatServer(EventLoop* loop,
const InetAddress& listenAddr)
:loop_(loop),
server_(loop, listenAddr, "ChatServer"),
codec_(std::bind(&ChatServer::onStringMessage, this, _1, _2, _3))
{
server_.setConnectionCallback(std::bind(&ChatServer::onConnection, this, _1));
server_.setMessageCallback(std::bind(&LengthHeaderCodec::onMessage, &codec_, _1, _2, _3));
}
void start()
{
server_.start();
}
private:
void onConnection(const TcpConnectionPtr& conn)
{
LOG_INFO << conn->peerAddress().toIpPort() << " -> "
<< conn->localAddress().toIpPort() << " is "
<< (conn->connected() ? "UP" : "DOWN");
if (conn->connected())
{
connections_.insert(conn);
}
else
{
connections_.erase(conn);
}
}
void onStringMessage(const TcpConnectionPtr&,
const string& message, Timestamp)
{
for (ConnectionList::iterator it = connections_.begin();
it != connections_.end(); ++it)
{
codec_.send(get_pointer(*it), message);
}
}
typedef std::set<TcpConnectionPtr> ConnectionList;
EventLoop* loop_;
TcpServer server_;
LengthHeaderCodec codec_;
ConnectionList connections_;
};
int main(int argc, char* argv[])
{
LOG_INFO << "pid = " << getpid();
if (argc > 1)
{
EventLoop loop;
int nPort = atoi(argv[1]);
printf("port ======= %d\n", nPort);
uint16_t port = static_cast<uint16_t>(atoi(argv[1]));
InetAddress serverAddr(port);
ChatServer server(&loop, serverAddr);
server.start();
loop.loop();
}
else
{
printf("Usage: %s port\n", argv[0]);
}
return 0;
}
客户端文件:
//client.cpp
#include "msg_codec.h"
#include "muduo/base/Logging.h"
#include "muduo/base/Mutex.h"
#include "muduo/net/EventLoopThread.h"
#include "muduo/net/TcpClient.h"
#include <iostream>
#include <stdio.h>
#include <unistd.h>
using namespace muduo;
using namespace muduo::net;
class ChatClient : noncopyable
{
public:
ChatClient(EventLoop* loop, const InetAddress& serverAddr)
:client_(loop, serverAddr, "ChatClient"),
codec_(std::bind(&ChatClient::onStringMessage, this, _1, _2, _3))
{
client_.setConnectionCallback(std::bind(&ChatClient::onConnection, this, _1));
client_.setMessageCallback(std::bind(&LengthHeaderCodec::onMessage, &codec_, _1, _2, _3));
client_.enableRetry();
}
void connect()
{
client_.connect();
}
void disconnect()
{
client_.disconnect();
}
void write(const StringPiece& message)
{
MutexLockGuard lock(mutex_);
if (connection_)
{
codec_.send(get_pointer(connection_), message);
}
}
private:
void onConnection(const TcpConnectionPtr& conn)
{
LOG_INFO << conn->localAddress().toIpPort() << " -> "
<< conn->peerAddress().toIpPort() << " is "
<< (conn->connected() ? "UP" : "DOWN");
MutexLockGuard lock(mutex_);
if (conn->connected())
{
connection_ = conn;
}
else
{
connection_.reset();
}
}
void onStringMessage(const TcpConnectionPtr&,
const string& message, Timestamp)
{
printf("<<< %s\n", message.c_str());
}
TcpClient client_;
LengthHeaderCodec codec_;
MutexLock mutex_;
TcpConnectionPtr connection_ GUARDED_BY(mutex_);
};
int main(int argc, char* argv[])
{
LOG_INFO << "pid = " << getpid();
if (argc > 2)
{
EventLoopThread loopThread;
uint16_t port = static_cast<uint16_t>(atoi(argv[2]));
InetAddress serverAddr(argv[1], port);
ChatClient client(loopThread.startLoop(), serverAddr);
client.connect();
std::string line;
while (std::getline(std::cin, line))
{
client.write(line);
}
client.disconnect();
CurrentThread::sleepUsec(1000 * 1000); //wait for disconnect, see ace/logging/client.cc
}
else
{
printf("Usage: %s host_ip port\n", argv[0]);
}
return 0;
}
编译运行,先启动服务端

启动客户端连接并发送消息

-------分隔线------------------------------------

4万+

被折叠的 条评论
为什么被折叠?



