muduo中boost.Asio的聊天服务器解析

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);
    	}
}

所以得到一条完整的信息以后,立马就把这条完整的信息发送给客户端。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值