muduo的这个聊天服务器达到的基本要求为:
聊天服务
本文实现的聊天服务非常简单,由服务端程序和客户端程序组成,协议如下:
(1)服务端程序中某个端口侦听 (listen) 新的连接;
(2) 客户端向服务端发起连接;
(3)连接建立之后,客户端随时准备接收服务端的消息并在屏幕上显示出来;
(4)客户端接受键盘输入,以回车为界,把消息发送给服务端;
(5)服务端接收到消息之后,依次发送给每个连接到它的客户端;原来发送消息的客户端进程也会收到这条消息;
(6)一个服务端进程可以同时服务多个客户端进程,当有消息到达服务端后,每个客户端进程都会收到同一条消息,服务端广播发送消息的顺序是任意的,不一定哪个客户端会先收到这条消息。
(7)(可选)如果消息 A 先于消息 B 到达服务端,那么每个客户端都会先收到 A 再收到 B。
然后我们针对muduo源码进行解析,我们从数据的流向对源码就行解析。首先服务端的消息来自于客户端的键盘输入,所以我们首先看看客户端这个类。
class ChatClient : boost::noncopyable
{
public:
ChatClient(EventLoop* loop, const InetAddress& serverAddr)
: client_(loop, serverAddr, "ChatClient"),
codec_(boost::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();
}
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_;
};
我们直接从数据入手,客户端发送数据代码为:
while (std::getline(std::cin, line))
{
client.write(line);
}
void write(const StringPiece& message)
{
MutexLockGuard lock(mutex_);
if (connection_)
{
codec_.send(get_pointer(connection_), message);
}
}
getline 首先每次读取一行,然后调用client.write将这个消息发送给服务器。
所以这里的关键代码是:
codec_.send(get_pointer(connection_), message);
注意connection_是shared_ptr,这时候调用codec_的send函数。
所以我们得看看codec_这个对象。
#ifndef MUDUO_EXAMPLES_ASIO_CHAT_CODEC_H
#define MUDUO_EXAMPLES_ASIO_CHAT_CODEC_H
#include <muduo/base/Logging.h>
#include <muduo/net/Buffer.h>
#include <muduo/net/Endian.h>
#include <muduo/net/TcpConnection.h>
#include <boost/function.hpp>
#include <boost/noncopyable.hpp>
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)
: 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()
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 // MUDUO_EXAMPLES_ASIO_CHAT_CODEC_H
我们看看send函数的参数
void send(muduo::net::TcpConnection* conn,const muduo::StringPiece& message)
得到一个字符串,然后封装成muduo的buffer对象,这里我仔细解析下这个函数的代码。
muduo::net::Buffer buf;
buf.append(message.data(), message.size()); //将消息加到readable里
int32_t len = static_cast<int32_t>(message.size()); //求得message字符串的长度
int32_t be32 = muduo::net::sockets::hostToNetwork32(len); //将长度转换为网络字节序
buf.prepend(&be32, sizeof be32); //在readable前面加入长度(*****)
conn->send(&buf);//发送消息
这里重点讲解
buf.prepend(&be32, sizeof be32); //在readable前面加入长度(*****)
如图,为什么能后来把长度放到前面呢?
重点:因为buffer里预留了8个字节,这个我们只要把readindex指针向前移动4个字节就好了。
这样消息发送就解决了()。
这个时候讲建立连接,客户端发起请求后,服务器会有什么动作,我们见服务器的类代码。
class ChatServer : boost::noncopyable
{
public:
ChatServer(EventLoop* loop,
const InetAddress& listenAddr)
: server_(loop, listenAddr, "ChatServer"),
codec_(boost::bind(&ChatServer::onStringMessage, this, _1, _2, _3))
{
server_.setConnectionCallback(
boost::bind(&ChatServer::onConnection, this, _1));
server_.setMessageCallback(
boost::bind(&LengthHeaderCodec::onMessage, &codec_, _1, _2, _3));
}
void start()
{
server_.start();
}
private:
void onConnection(const TcpConnectionPtr& conn)
{
LOG_INFO << conn->localAddress().toIpPort() << " -> "
<< conn->peerAddress().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;
TcpServer server_;
LengthHeaderCodec codec_;
ConnectionList connections_;
};
这个时候就会调用onConnection这个回调函数。
void onConnection(const TcpConnectionPtr& conn)
{
LOG_INFO << conn->localAddress().toIpPort() << " -> "
<< conn->peerAddress().toIpPort() << " is "
<< (conn->connected() ? "UP" : "DOWN");
if (conn->connected())
{
connections_.insert(conn);
}
else
{
connections_.erase(conn);
}
}
将这个连接插入到服务器的连接set里。
然后这时候客户端的消息发送过来,立马触发
server_.setMessageCallback(
boost::bind(&LengthHeaderCodec::onMessage, &codec_, _1, _2, _3));
所以立马调用onMessage这个函数。所以我们来看其代码,
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(); //得到readIndex指针
int32_t be32 = *static_cast<const int32_t*>(data); // SIGBUS //取得4个字节,即消息的长度
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) //这里重要,就是readAble部分的字节数大于(消息长度 + 头部长度)
//说明,至少收到了一条完整的消息。
{
buf->retrieve(kHeaderLen); //readIndex指针移动4个字节的头部长度
muduo::string message(buf->peek(), len); //得到len个字节的message长度
messageCallback_(conn, message, receiveTime); //立马调用回调函数
buf->retrieve(len); // readIndex移动len个字节,移动到下一个消息的头部
}
else
{
break;
}
}
}
代码详细讲解看注释.
在onMessage里调用了messageCallback_(conn, message, receiveTime)
回调函数,我们来看看回调函数是怎么写的。
void onStringMessage(const TcpConnectionPtr&,const string& message,Timestamp)
{
for (ConnectionList::iterator it = connections_.begin(); it != connections_.end();++it)
{
codec_.send(get_pointer(*it), message);
}
}
所以得到一条完整的信息以后,立马就把这条完整的信息发送给客户端。