本文代码存放在muduo\examples\asio\chat目录下
实现的功能:任何一个Client给Server发送消息后,Server都会将该消息回射给连接上来的所有Client
muduo实现一个聊天室服务器,客户发送的消息将广播到连入的所有客户(包括自己)。
消息的字节流定义成这种形式 0xXX 0xXX 0xXX 0xXX XXXXXX,前面4个字节表示消息的长度,后面是消息实体。
muduo作者选择自己编写一个工具类:编解码器LengthHeaderCodec,该类只有两个public成员函数,分别为:onMessage、send,这两个函数都是通过回调调用的。
#ifndef MUDUO_EXAMPLES_ASIO_CHAT_CODEC_H
#define MUDUO_EXAMPLES_ASIO_CHAT_CODEC_H
#include
#include
#include
#include
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) //注册callback_:回复所有客户端的
{
}
//解析[包头+包体]
void onMessage(const muduo::net::TcpConnectionPtr& conn,
muduo::net::Buffer* buf,
muduo::Timestamp receiveTime)
{
//判断接收到的数据是否超过了4个字节(包头长度)
while (buf->readableBytes() >= kHeaderLen)
{
//取出(前4个字节),得到包体的有效字符串的长度(len)
//FIXME:use Buffer::peekInt32()
const void* data = buf->peek(); //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);//先移走4个字节的len
muduo::string message(buf->peek(), len);//拷贝len长度的有效字符串到message
//调用回调函数,向所有的客户端回复消息
messageCallback_(conn, message, receiveTime);
buf->retrieve(len);
}
else //未达到一条完整的消息
{
break;
}
}
}
//封装成[包头+包体],发送
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); //包头长度,4字节
};
#endif // MUDUO_EXAMPLES_ASIO_CHAT_CODEC_H
主线程有一个main Reactor负责accept连接,然后把已连接套接字挂在某个sub Reactor中(I/O Thread),至于怎么选择,以达到每个工作线程的“负载均衡”,muduo采用round-robin的方式。
结论:由于mutex的存在,多线程并不能并发执行,而是串行的,分析原因:
#include "codec.h"
#include
#include
#include
#include
#include
#include
#include
using namespace muduo;
using namespace muduo::net;
class ChatServer : noncopyable
{
private:
typedef std::set<TcpConnectionPtr> ConnectionList;
TcpServer server_;
LengthHeaderCodec codec_; //包含工具类成员变量:消息编解码
MutexLock mutex_;
ConnectionList connections_ GUARDED_BY(mutex_); //连接列表,存放着所有连接上来的client
public:
ChatServer(EventLoop* loop,
const InetAddress& listenAddr)
: server_(loop, listenAddr, "ChatServer"),
//给codec_绑定ChatServer::onStringMessage
codec_(std::bind(&ChatServer::onStringMessage, this, _1, _2, _3))
{
server_.setConnectionCallback(
std::bind(&ChatServer::onConnection, this, _1));
server_.setMessageCallback( //注册消息到来时的回调函数LengthHeaderCodec::onMessage
std::bind(&LengthHeaderCodec::onMessage, &codec_, _1, _2, _3));
}
//numThreads sub reactors
void setThreadNum(int numThreads)
{
server_.setThreadNum(numThreads);
}
void start()
{
server_.start();
}
private:
//新的client连接到来,导致可写事件发生,则建立连接
void onConnection(const TcpConnectionPtr& conn)
{
LOG_INFO << conn->localAddress().toIpPort() << " -> "
<< conn->peerAddress().toIpPort() << " is "
<< (conn->connected() ? "UP" : "DOWN");
//不只有一个IO线程,因而这里的connections_需要mutex保护
MutexLockGuard lock(mutex_);
if (conn->connected())
{
connections_.insert(conn);
}
else
{
connections_.erase(conn);
}
}
//当client给服务器发送的数据到来后,服务器将采用RR选择一个IO线程去处理该数据
//处理过程:
//1.在LengthHeaderCodec::onMessage中又会调用onStringMessage
//2.在onStringMessage中又会调用LengthHeaderCodec::send
void onStringMessage(const TcpConnectionPtr&,
const string& message,
Timestamp)
{
//有多个IO线程,因而这里的connections_需要用mutex保护
MutexLockGuard lock(mutex_);
//转发消息给所有的客户端
for (ConnectionList::iterator it = connections_.begin();
it != connections_.end();
++it)
{
codec_.send(get_pointer(*it), message);
}
}
};
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);
if (argc > 2)
{
server.setThreadNum(atoi(argv[2]));
}
server.start();
loop.loop();
}
else
{
printf("Usage: %s port [thread_num]\n", argv[0]);
}
}
两个线程,一个线程用来从标准输入读入发送的消息,另外一个线程用Reactor处理网络I/O,这里用两个线程的原因是因为作者没有把标准输入输出加入到Reactor的想法,在UNP的单线程Reactor中有管理0,1,2(标准输入、标准输出、标准错误)监听读入键盘数据的示例。
#include "codec.h"
#include
#include
#include
#include
#include
#include
#include
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:
//该函数在IO线程中执行,IO线程与主线程不在同一个线程
void onConnection(const TcpConnectionPtr& conn)
{
LOG_INFO << conn->localAddress().toIpPort() << " -> "
<< conn->peerAddress().toIpPort() << " is "
<< (conn->connected() ? "UP" : "DOWN");
//mutex用来保护connection_这个shared_ptr
MutexLockGuard lock(mutex_);
if (conn->connected())
{
connection_ = conn;
}
else
{
connection_.reset();
}
}
//IO线程:接收道数据
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; //创建一个IO线程,用于与Server通信
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]);
}
}