muduo库使用示例之聊天服务器(上)

版权声明:guojawee https://blog.csdn.net/weixin_36750623/article/details/84726240

本文代码存放在muduo\examples\asio\chat目录下

聊天服务器示意图

实现的功能:任何一个Client给Server发送消息后,Server都会将该消息回射给连接上来的所有Client
在这里插入图片描述

muduo实现一个聊天室服务器,客户发送的消息将广播到连入的所有客户(包括自己)。

程序的执行流程以及时序图:
在这里插入图片描述

  1. 当Server接收到Client发送的消息后,将回调注册的LengthHeaderCodec::onMessage函数,onMessage函数将对接收到的数据报进行解析[包头+包体],解析完成后再回调注册的StringMessageCallback messageCallback_函数(注册的是ChatServer::onStringMessage)。
  2. 在ChatServer::onStringMessage调用了LengthHeaderCodec::send函数,send封装成[包头+包体],发送。

1. 消息编码类:LengthHeaderCodec

消息的字节流定义成这种形式 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 <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) //注册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

2. 服务器的实现

主线程有一个main Reactor负责accept连接,然后把已连接套接字挂在某个sub Reactor中(I/O Thread),至于怎么选择,以达到每个工作线程的“负载均衡”,muduo采用round-robin的方式。
在这里插入图片描述

结论:由于mutex的存在,多线程并不能并发执行,而是串行的,分析原因:

  1. C1向服务器发送一条消息hello,服务器通过一个IO线程转发给所有的客户端
  2. 与此同时(假设此时服务器还没有将hello全部转发给所有的客户端),C2又向服务器发送一条消息world,那么服务器将通过另一个IO线程转发给所有的客户端,但是由于锁的存在,(必须等到第一个IO线程将hello全部发送给客户端之后,另一个IO线程才能进入临界区将world再发送给客户端),这两个线程并不能并发执行,而是串行的。
  3. 假设客户端发送的数据包很大,第一个IO线程将数据发送给客户端的事件很长,将导致第二个IO线程一直阻塞 ==> 这样设计的效率非常低下。
#include "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
{
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]);
  }
}

3.客户端代码

两个线程,一个线程用来从标准输入读入发送的消息,另外一个线程用Reactor处理网络I/O,这里用两个线程的原因是因为作者没有把标准输入输出加入到Reactor的想法,在UNP的单线程Reactor中有管理0,1,2(标准输入、标准输出、标准错误)监听读入键盘数据的示例。

#include "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:
  //该函数在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]);
  }
}

猜你喜欢

转载自blog.csdn.net/weixin_36750623/article/details/84726240