群聊服务器简单描述
客户端发送消息 到服务器,服务器转发消息给每个客户端、
boost库安装可以去官网,也可以去看我的另一篇博文
c++回声服务器,上面有我给出的boost库 百度云盘
boost库的案例实现
我加入部分注释,方便理解。
首先是 客户端和服务器都共同要用的头文件
chat_message.hpp
//
// chat_message.hpp
// ~~~~~~~~~~~~~~~~
//
// Copyright (c) 2003-2019 Christopher M. Kohlhoff (chris at kohlhoff dot com)
//
// Distributed under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
//
#ifndef CHAT_MESSAGE_HPP
#define CHAT_MESSAGE_HPP
#include <cstdio>
#include <cstdlib>
#include <cstring>
/*
聊天消息类
定义了两个私有成员变量
1.data_ 缓冲区(大小为 消息头长度 + 最大消息主体长度 = 4 + 512)
2.body_length 消息主体的长度
以及两个共有枚举
1.header_length 消息头长度(固定4个字节,头部这个4个字节用来记录消息内容的长度)
2.max_body_length 最大消息主体长度(消息长度最长设置为512字节)
公有方法
1.返回缓存区的指针
2.返回缓存区的大小
3.返回消息的缓冲区指针
4.返回消息的长度(不包括头部)
5.设置消息的长度(如果超过最大长度,则设置为最大长度)
6.解码头部同时设置消息的长度
7.解码头部同时设置缓冲区的头部(即前4个字节)
*/
class chat_message
{
public:
enum { header_length = 4 };
enum { max_body_length = 512 };
chat_message()
: body_length_(0)
{
}
const char* data() const
{
return data_;
}
char* data()
{
return data_;
}
std::size_t length() const
{
return header_length + body_length_;
}
const char* body() const
{
return data_ + header_length;
}
char* body()
{
return data_ + header_length;
}
std::size_t body_length() const
{
return body_length_;
}
void body_length(std::size_t new_length)
{
body_length_ = new_length;
if (body_length_ > max_body_length)
body_length_ = max_body_length;
}
bool decode_header()
{
char header[header_length + 1] = "";
std::strncat(header, data_, header_length);
body_length_ = std::atoi(header);
if (body_length_ > max_body_length)
{
body_length_ = 0;
return false;
}
return true;
}
void encode_header()
{
char header[header_length + 1] = "";
std::sprintf(header, "%4d", static_cast<int>(body_length_));
std::memcpy(data_, header, header_length);
}
private:
char data_[header_length + max_body_length];
std::size_t body_length_;
};
#endif // CHAT_MESSAGE_HPP
服务器的实现
//
// chat_server.cpp
// ~~~~~~~~~~~~~~~
//
// Copyright (c) 2003-2019 Christopher M. Kohlhoff (chris at kohlhoff dot com)
//
// Distributed under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
//
#define _CRT_SECURE_NO_WARNINGS
#include <cstdlib>
#include <deque>
#include <iostream>
#include <list>
#include <memory>
#include <set>
#include <utility>
#include <boost/asio.hpp>
#include "chat_message.hpp"
using boost::asio::ip::tcp;
/*
文件内容大纲
1.别名2个
chat_message_queue; 聊天消息的队列容器
chat_participant_ptr 聊天参与者的共享指针
2.定义了4个类
chat_participant(抽象类) 聊天参与者的抽象类,定义了1个接口 deliver()消息发送 ,以及基本礼貌 虚析构函数
chat_room(聊天房间) 管理聊天参与者的加入和离开,还有聊天消息的记录
chat_session(聊天会话) 继承了聊天参与者类,以及共享启用类
chat_server(聊天服务器) 运行每一个聊天会话
chat_room(聊天房间)
定义了2个私有成员变量
1.participants_ 聊天参与者的容器(set容器 聊天参与者的共享指针)
2.recent_msgs_ 最近消息队列
定义了1个私有枚举
1.max_recent_msgs 限制最近消息为100条
定义了3个共有方法
1.连接给每个新加入的参与者推送 队列中的消息
2.参与者离开时从聊天参与者容器中移除
3.新消息记录到聊天消息队列中,然后给每个参与者发送此消息
chat_session(聊天会话)
定义了4个私有成员
1.socket_ tcp的套接字
2.room_ 聊天房间的引用
3.read_msg_ 读取聊天消息
4.write_msgs_ 自身的消息队列
定义了2个共有方法
1.开始会话将自身的共享指针传递给聊天房间加入聊天,然后调用自身私有方法do_read_header()
2.发送消息,先检查自身消息是否为空,接着将消息压入消息队列,再根据之前的检查,如果消息为空 则调用 私有方法do_write()
ps: 吐槽一下,这两次 非运算有点意义不明,可能是作者的习惯所致
定义了3个私有方法
1.读取消息头部,没有错误和成功解码到消息长度则调用私有方法do_read_body(),调用聊天房间离开方法,将自身的共享指针传进去
2.读取消息,没有错误则调用聊天房间的发送消息把消息传进去,接着调用读取头部,否则调用聊天房间离开方法,将自身的共享指针传进去
3.写入消息,没有错误则调用弹出第一个消息,在判断你消息队列是否已经为空,不为空则继续调用写消息。
否则产生错误,就调用聊天房间的离开方法,将自身共享指针传进去
chat_server(聊天服务器)
定义2个私有成员变量
1.acceptor_ 客户端套接字接收
2.room_ 聊天房间
定义了1个私有方法
1.接收客户端的套接字,没有错误创建一个会话,传递进去套接字和聊天房间,并且开启会话,然后调用自身继续等待客户端接入
代码分析: ps: 博主的分析,不保证准确无误吧。看官们,看看就好。
1.聊天房间要做的事:
1.记录参与者,谁加入了,谁离开了。
2.记录消息记录,对每个参与者进来就把所记录的消息发送给它。
3.转发每个客户端会话的消息分发给其他客户端会话。
2.客户端会话(聊天会话)要做的事:
1.加入聊天房间
2.接收聊天房间发过来的消息
3.发送消息到聊天房间
这样就会遇到一个问题,客户端会话需要持有聊天房间的接口,聊天房间需要持有客户端会话的接口。
这样就会遇到一个语法上的限制:
定义类型是有先后顺序之分的,后定义的类型可以持有先定义的类型作为接口。
先定义的类型是没办法,获取后面定义的类型进行操作的。
这样就需要一个定义在聊天房间之前的类型,做接口使用、这个接口,作者定义为 聊天参与者。
这个类本身没有必要实体化,其作用就是为了 让聊天房间能够通过这个可操作对象 与 聊天会话进行通信。
这样关系它们大概是这样的
聊天消息 -> 聊天参与者(抽象类) -> 聊天房间 -> 聊天会话 -> 聊天服务器
首先我要明确一点,我说的可操作对象是什么
举例:
由于聊天消息 和 聊天参与者定义在聊天房间之前。
聊天房间可以将,聊天消息 和 聊天参与者,定义为成员变量也可以定义成接口参数 也就可操作的对象。
反过来却不行,因为那个时候聊天房间没有诞生呢。
对于聊天会话来说,聊天房间是可操作的,但是聊天房间只能操作 聊天参与者。
那么让聊天会话,继承聊天参与者。聊天房间就能操作到 聊天参与者了。
解释部分,少见的疑难杂点。
enable_shared_from_this 类型
当一个类,用到共享指针,并且还要通过这个类的返回或者获取到,这个类的共享指针,就需要继承这个类。
不能这样
struct Bad
{
// 错误写法:用不安全的表达式试图获得 this 的 shared_ptr 对象
std::shared_ptr<Bad> getptr() {
return std::shared_ptr<Bad>(this);
}
~Bad() { std::cout << "Bad::~Bad() called\n"; }
};
应该这样
struct Good: std::enable_shared_from_this<Good> // 注意:继承
{
std::shared_ptr<Good> getptr() {
return shared_from_this();
}
};
shared_from_this() 返回共享 *this 所有权的 shared_ptr。
!ec 这个是 操作符重载函数 !
*/
//----------------------------------------------------------------------
typedef std::deque<chat_message> chat_message_queue;
//----------------------------------------------------------------------
class chat_participant
{
public:
virtual ~chat_participant() {}
virtual void deliver(const chat_message& msg) = 0;
};
typedef std::shared_ptr<chat_participant> chat_participant_ptr;
//----------------------------------------------------------------------
class chat_room
{
public:
void join(chat_participant_ptr participant)
{
participants_.insert(participant);
for (auto msg: recent_msgs_)
participant->deliver(msg);
}
void leave(chat_participant_ptr participant)
{
participants_.erase(participant);
}
void deliver(const chat_message& msg)
{
recent_msgs_.push_back(msg);
while (recent_msgs_.size() > max_recent_msgs)
recent_msgs_.pop_front();
for (auto participant: participants_)
participant->deliver(msg);
}
private:
std::set<chat_participant_ptr> participants_;
enum { max_recent_msgs = 100 };
chat_message_queue recent_msgs_;
};
//----------------------------------------------------------------------
class chat_session
: public chat_participant,
public std::enable_shared_from_this<chat_session>
{
public:
chat_session(tcp::socket socket, chat_room& room)
: socket_(std::move(socket)),
room_(room)
{
}
void start()
{
room_.join(shared_from_this());
do_read_header();
}
void deliver(const chat_message& msg)
{
bool write_in_progress = !write_msgs_.empty();
write_msgs_.push_back(msg);
if (!write_in_progress)
{
do_write();
}
}
private:
void do_read_header()
{
auto self(shared_from_this());
boost::asio::async_read(socket_,
boost::asio::buffer(read_msg_.data(), chat_message::header_length),
[this, self](boost::system::error_code ec, std::size_t /*length*/)
{
if (!ec && read_msg_.decode_header())
{
do_read_body();
}
else
{
room_.leave(shared_from_this());
}
});
}
void do_read_body()
{
auto self(shared_from_this());
boost::asio::async_read(socket_,
boost::asio::buffer(read_msg_.body(), read_msg_.body_length()),
[this, self](boost::system::error_code ec, std::size_t /*length*/)
{
if (!ec)
{
room_.deliver(read_msg_);
do_read_header();
}
else
{
room_.leave(shared_from_this());
}
});
}
void do_write()
{
auto self(shared_from_this());
boost::asio::async_write(socket_,
boost::asio::buffer(write_msgs_.front().data(),
write_msgs_.front().length()),
[this, self](boost::system::error_code ec, std::size_t /*length*/)
{
if (!ec)
{
write_msgs_.pop_front();
if (!write_msgs_.empty())
{
do_write();
}
}
else
{
room_.leave(shared_from_this());
}
});
}
tcp::socket socket_;
chat_room& room_;
chat_message read_msg_;
chat_message_queue write_msgs_;
};
//----------------------------------------------------------------------
class chat_server
{
public:
chat_server(boost::asio::io_context& io_context,
const tcp::endpoint& endpoint)
: acceptor_(io_context, endpoint)
{
do_accept();
}
private:
void do_accept()
{
acceptor_.async_accept(
[this](boost::system::error_code ec, tcp::socket socket)
{
if (!ec)
{
std::make_shared<chat_session>(std::move(socket), room_)->start();
}
do_accept();
});
}
tcp::acceptor acceptor_;
chat_room room_;
};
//----------------------------------------------------------------------
int main(int argc, char* argv[])
{
try
{
if (argc < 2)
{
std::cerr << "Usage: chat_server <port> [<port> ...]\n";
return 1;
}
boost::asio::io_context io_context;
std::list<chat_server> servers;
for (int i = 1; i < argc; ++i)
{
tcp::endpoint endpoint(tcp::v4(), std::atoi(argv[i]));
servers.emplace_back(io_context, endpoint);
}
io_context.run();
}
catch (std::exception& e)
{
std::cerr << "Exception: " << e.what() << "\n";
}
return 0;
}
客户端代码
//
// chat_client.cpp
// ~~~~~~~~~~~~~~~
//
// Copyright (c) 2003-2019 Christopher M. Kohlhoff (chris at kohlhoff dot com)
//
// Distributed under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
//
#define _CRT_SECURE_NO_WARNINGS
#include <cstdlib>
#include <deque>
#include <iostream>
#include <thread>
#include <boost/asio.hpp>
#include "chat_message.hpp"
using boost::asio::ip::tcp;
/*
文件内大纲
1.别名1个
chat_message_queue 聊天消息队列
1.定义了一个类
chat_client 聊天客户端
chat_client(聊天客户端)
定义了4个私有成员变量
1.io_context_ IO上下文引用
2.read_msg_ 聊天消息
3.socket_ 套接字
4.write_msgs_ 消息队列
定义了2个共有方法
1.先判断消息队列是否为空,然后将消息放入消息队列中,接着判断如果消息队列为空则调用私有方法do_write()
2.关闭套接字
定义了4个私有方法
1.连接到服务器
2.读取消息头部,没有错误则调用私有方法do_read_body(),否则关闭套接字
3.读取消息,没有错误则将信息打印到控制台,然后调用私有方法do_read_header(),否则关闭套接字
4.将消息发送到服务器,没有错误 ,则弹出第一个已经写完的消息,判断如果还有消息继续调用自身,否则关闭套接字
*/
typedef std::deque<chat_message> chat_message_queue;
class chat_client
{
public:
chat_client(boost::asio::io_context& io_context,
const tcp::resolver::results_type& endpoints)
: io_context_(io_context),
socket_(io_context)
{
do_connect(endpoints);
}
void write(const chat_message& msg)
{
boost::asio::post(io_context_,
[this, msg]()
{
bool write_in_progress = !write_msgs_.empty();
write_msgs_.push_back(msg);
if (!write_in_progress)
{
do_write();
}
});
}
void close()
{
boost::asio::post(io_context_, [this]() { socket_.close(); });
}
private:
void do_connect(const tcp::resolver::results_type& endpoints)
{
boost::asio::async_connect(socket_, endpoints,
[this](boost::system::error_code ec, tcp::endpoint)
{
if (!ec)
{
do_read_header();
}
});
}
void do_read_header()
{
boost::asio::async_read(socket_,
boost::asio::buffer(read_msg_.data(), chat_message::header_length),
[this](boost::system::error_code ec, std::size_t /*length*/)
{
if (!ec && read_msg_.decode_header())
{
do_read_body();
}
else
{
socket_.close();
}
});
}
void do_read_body()
{
boost::asio::async_read(socket_,
boost::asio::buffer(read_msg_.body(), read_msg_.body_length()),
[this](boost::system::error_code ec, std::size_t /*length*/)
{
if (!ec)
{
std::cout.write(read_msg_.body(), read_msg_.body_length());
std::cout << "\n";
do_read_header();
}
else
{
socket_.close();
}
});
}
void do_write()
{
boost::asio::async_write(socket_,
boost::asio::buffer(write_msgs_.front().data(),
write_msgs_.front().length()),
[this](boost::system::error_code ec, std::size_t /*length*/)
{
if (!ec)
{
write_msgs_.pop_front();
if (!write_msgs_.empty())
{
do_write();
}
}
else
{
socket_.close();
}
});
}
private:
boost::asio::io_context& io_context_;
tcp::socket socket_;
chat_message read_msg_;
chat_message_queue write_msgs_;
};
int main(int argc, char* argv[])
{
try
{
if (argc != 3)
{
std::cerr << "Usage: chat_client <host> <port>\n";
return 1;
}
boost::asio::io_context io_context;
tcp::resolver resolver(io_context);
auto endpoints = resolver.resolve(argv[1], argv[2]);
chat_client c(io_context, endpoints);
std::thread t([&io_context](){ io_context.run(); });
char line[chat_message::max_body_length + 1];
while (std::cin.getline(line, chat_message::max_body_length + 1))
{
chat_message msg;
msg.body_length(std::strlen(line));
std::memcpy(msg.body(), line, msg.body_length());
msg.encode_header();
c.write(msg);
}
c.close();
t.join();
}
catch (std::exception& e)
{
std::cerr << "Exception: " << e.what() << "\n";
}
return 0;
}
运行效果
有缘在更…