基于moduo库的网络聊天服务器

1:服务器类,基于muduo库开发

//moduo网络库
//Tcpserver服务器
//TcpClient客户端
// epoll + 线程池
//网络的I/O代码和业务代码区分开
//用户的连接和断开,用户的可读写事件
#include <muduo/net/TcpServer.h>
#include <muduo/net/EventLoop.h>  //
#include <iostream>
#include <functional>
#include <string>
using namespace std;
using namespace muduo::net;
using namespace muduo;
using namespace placeholders;
/*基于muduo网络库开发服务器程序
1:组合tcpserver对象
2:创建EventLoop事件循环对象的指针 
3:明确tcpserver构造函数参数,输出chatserver
4:服务器类的构造函数中,注册处理连接的回调函数和处理读写事件的回调函数
5:设置合适的服务端线程数量,muduo自己分配io和work线程
*/

class CharServer {
public:
	CharServer(EventLoop* loop,				 //事件循环
		const InetAddress& listenAddr,		 //IP+Port
		const string& nameArg)               //服务器的名称 ,线程名字
		:server_(loop, listenAddr, nameArg),
		loop_(loop) {
		//给服务器注册用户连接的创建和断开回调
		//_1 是参数const TcpConnectionPtr&
		//关注连接的状态
		server_.setConnectionCallback(std::bind(&CharServer::onConnection,this,_1));
		//给服务器注册用户读写事件回调 
		//关注事件读写的状态
		server_.setMessageCallback(std::bind(&CharServer::onMessage, this, _1, _2,_3));
		//设置服务器段的线程数量; 1个i/o 3个work
		server_.setThreadNum(4);
	}
	//开启事件循环
	void start() {
		server_.start();
	}
private:
	//专门处理用户的连接创建和断开 epoll listenfd accpet 
	//成员方法有this指针 这里onConnection会有2个参数,this,conn
	void onConnection(const TcpConnectionPtr& conn) {
		if (conn->connected()) {
			cout << conn->peerAddress().toIpPort() << "-->" << conn->localAddress().toIpPort() << "online" << endl;
		}
		else {
			cout << conn->peerAddress().toIpPort() << "-->" << conn->localAddress().toIpPort() << "offline" << endl;
			conn->shutdown(); //close(fd);   服务端的fd回收
			loop_->quit();    //退出epoll
		}
	}
	//默认有this指针
	//处理用户的读写事件
	void onMessage(const TcpConnectionPtr& conn, //连接
		Buffer* buffer,  //缓冲区
		Timestamp time) { //接收数据的时间信息 
		string buf = buffer->retrieveAllAsString();
		cout << "recv data:" << buf << "time:" << time.toString() << endl;
		conn->send(buf);
	}
	TcpServer server_;
	EventLoop* loop_;     //epoll 
};

int main() {
	EventLoop loop; //epoll
	InetAddress listenAddr("127.0.0.1",6000);		 //IP+Port
	CharServer server(&loop, listenAddr,"ChatServer");
	server.start();  //listenfd epoll_ctl->epoll   添加套接字到epoll
	loop.loop(); //epoll.wait  阻塞的方式等待新用户连接  已连接用户的读写事件	 回调onConnection 和onMessage
	return 0;
}

2:Redis,服务器中间件

// redis.hpp

class Redis
{
public:
    Redis();
    ~Redis();

    // 连接redis服务器 
    bool connect();

    // 向redis指定的通道channel发布消息
    bool publish(int channel, string message);

    // 向redis指定的通道subscribe订阅消息
    bool subscribe(int channel);

    // 向redis指定的通道unsubscribe取消订阅消息
    bool unsubscribe(int channel);

    // 在独立线程中接收订阅通道中的消息
    void observer_channel_message();

    // 初始化向业务层上报通道消息的回调对象
    void init_notify_handler(function<void(int, string)> fn);

private:
    // hiredis同步上下文对象,负责publish消息
    redisContext *_publish_context;

    // hiredis同步上下文对象,负责subscribe消息   //阻塞
    redisContext *_subcribe_context;

    // 回调操作,收到订阅的消息,给service层上报    //有消息发生,
    function<void(int, string)> _notify_message_handler;
};
// redis.cpp

#include "redis.hpp"
#include <iostream>
using namespace std;

Redis::Redis()
    : _publish_context(nullptr), _subcribe_context(nullptr)
{
}

Redis::~Redis()
{
    if (_publish_context != nullptr)
    {
        redisFree(_publish_context);
    }

    if (_subcribe_context != nullptr)
    {
        redisFree(_subcribe_context);
    }
}

bool Redis::connect()
{
    // 负责publish发布消息的上下文连接
    // Redis的默认端口是6379
    _publish_context = redisConnect("127.0.0.1", 6379);
    if (nullptr == _publish_context)
    {
        cerr << "connect redis failed!" << endl;
        return false;
    }

    // 负责subscribe订阅消息的上下文连接
    _subcribe_context = redisConnect("127.0.0.1", 6379);
    if (nullptr == _subcribe_context)
    {
        cerr << "connect redis failed!" << endl;
        return false;
    }

    // 在单独的线程中,监听通道上的事件,有消息给业务层进行上报  subscribe 阻塞等待事件发生
    thread t([&]() {
        observer_channel_message();
    });
    t.detach();

    cout << "connect redis-server success!" << endl;

    return true;
}

// 向redis指定的通道channel发布消息  不会阻塞
bool Redis::publish(int channel, string message)
{
    //下面三个函数的调用 把命令写到本地缓存,
    redisReply *reply = (redisReply *)redisCommand(_publish_context, "PUBLISH %d %s", channel, message.c_str());
    if (nullptr == reply)
    {
        cerr << "publish command failed!" << endl;
        return false;
    }
    freeReplyObject(reply);
    return true;
}

// 向redis指定的通道subscribe订阅消息
bool Redis::subscribe(int channel)
{
    // SUBSCRIBE命令本身会造成线程阻塞等待通道里面发生消息,这里只做订阅通道,不接收通道消息
    // 通道消息的接收专门在observer_channel_message函数中的独立线程中进行
    // 只负责发送命令,不阻塞接收redis server响应消息,否则和notifyMsg线程抢占响应资源
    if (REDIS_ERR == redisAppendCommand(this->_subcribe_context, "SUBSCRIBE %d", channel))
    {
        cerr << "subscribe command failed!" << endl;
        return false;
    }
    // redisBufferWrite可以循环发送缓冲区,直到缓冲区数据发送完毕(done被置为1)
    int done = 0;
    while (!done)
    {
        if (REDIS_ERR == redisBufferWrite(this->_subcribe_context, &done))
        {
            cerr << "subscribe command failed!" << endl;
            return false;
        }
    }
    // redisGetReply 阻塞的方式等待远端响应,

    return true;
}

// 向redis指定的通道unsubscribe取消订阅消息
bool Redis::unsubscribe(int channel)
{
    if (REDIS_ERR == redisAppendCommand(this->_subcribe_context, "UNSUBSCRIBE %d", channel))
    {
        cerr << "unsubscribe command failed!" << endl;
        return false;
    }
    // redisBufferWrite可以循环发送缓冲区,直到缓冲区数据发送完毕(done被置为1)
    int done = 0;
    while (!done)
    {
        if (REDIS_ERR == redisBufferWrite(this->_subcribe_context, &done))
        {
            cerr << "unsubscribe command failed!" << endl;
            return false;
        }
    }
    return true;
}

// 在独立线程中接收订阅通道中的消息
void Redis::observer_channel_message()
{
    redisReply *reply = nullptr;
    //redisGetReply 函数会阻塞,  死循环,
    while (REDIS_OK == redisGetReply(this->_subcribe_context, (void **)&reply))
    {
        // 订阅收到的消息是一个带三元素的数组
        if (reply != nullptr && reply->element[2] != nullptr && reply->element[2]->str != nullptr)
        {
            // 给业务层上报通道上发生的消息
            _notify_message_handler(atoi(reply->element[1]->str) , reply->element[2]->str);
        }

        freeReplyObject(reply);
    }

    cerr << ">>>>>>>>>>>>> observer_channel_message quit <<<<<<<<<<<<<" << endl;
}

void Redis::init_notify_handler(function<void(int,string)> fn)
{
    this->_notify_message_handler = fn;
}

总结:

  1. Redis的默认端口是6379。
  2. Redis三个成员变量,一个负责发布消息,一个负责订阅消息。一个负责给service层上报。
  3. Redis独立线程 接受publish信息,当有其他服务器发布消息后,
  4. 服务器先查看自己 unordered_map<int, TcpConnectionPtr> 是否有该用户,没有会调用Redis,发送消息 publish, 订阅该消息的返回值

       redis_.publish(toid, js.dump());    //给其他服务器发消息,其他服务器订阅

       REDIS_OK == redisGetReply(this->_subcribe_context, (void **)&reply) 可以回调,上报给服务器。

   3:Mysql存储层

//User.hpp
class User
{

public:
    User(int id = -1, string name = "", string pwd = "", string state = "offline")
    {
        id_ = id;
        name_ = name;
        password_ = pwd;
        state_ = state;
    }
    ~User(){}

    void SetId(int id)
    {
        id_ = id;
    }
    void SetName(string name)
    {
        name_ = name;
    }

    void SetPassWord(string pwd)
    {
        password_ = pwd;
    }
    void SetState(string state)
    {
        state_ = state;
    }

    int GetId()
    {
        return id_;
    }
    string GetName()
    {
        return name_;
    }

    string GetPassWord()
    {
        return password_;
    }
    string GetState()
    {
        return state_;
    }

private:
    int id_;
    string name_;
    string password_;
    string state_;
};

// UserModel.h
#pragma once
#include "User.hpp"
// User表的数据操作类
// orm层  数据库的表增删改查
class UserModel
{

public:
    UserModel(/* args */){};
    ~UserModel(){};

    bool insert(User &user); // user表增加

    User query(int id);          // 用户号码,查询用户信息
    bool UpdateState(User user); // 更新用户的状态信息
    void ResetState();           // 重置用户的状态信息
private:
    /* data */
};
//UserModel.cpp
#include "UserModel.hpp"
#include "db.h"
#include <iostream>
using namespace std;
bool UserModel::insert(User &user)
{
    // 组装sql语句
    char sql[1024] = {0};
    sprintf(sql, "insert into user(name,password,state) values('%s','%s','%s')", user.GetName().c_str(), user.GetPassWord().c_str(), user.GetState().c_str());

    MySQL mysql;
    if (mysql.connect())
    {
        if (mysql.update(sql))
        {
            // 获取用户成功的主键ID
            user.SetId(mysql_insert_id(mysql.GetConnection()));

            return true;
        }
    }
    return false;
}

User UserModel::query(int id)
{
    // 组装sql语句
    char sql[1024] = {0};
    sprintf(sql, "select * from user where id = %d", id);

    MySQL mysql;
    if (mysql.connect())
    {
        MYSQL_RES *res = mysql.query(sql);
        if (res != nullptr)
        {
            MYSQL_ROW row = mysql_fetch_row(res);
            if (row != nullptr)
            {
                User user;
                user.SetId(atoi(row[0]));
                user.SetName(row[1]);
                user.SetPassWord(row[2]);
                user.SetState(row[3]); // 0 1 2 3 对应字段
                mysql_free_result(res);
                return user;
            }
        }
    }

    return User();
}

bool UserModel::UpdateState(User user)
{
    // 组装sql语句
    char sql[1024] = {0};

    sprintf(sql, "update user set state = '%s' where id = %d", user.GetState().c_str(), user.GetId());
    MySQL mysql;
    if (mysql.connect())
    {
        if (mysql.update(sql))
        {
            return true;
        }
    }
    return false;
}

void UserModel::ResetState()
{
    // 组装sql语句
    char sql[1024] = "update user set state = 'offline' where state = 'online'";

    MySQL mysql;
    if (mysql.connect())
    {
        mysql.update(sql);
    }
}

      总结

  1. 数据库的ORM设计,一个表对应一个ORM类,对象映射关系,
  2. sudo mysql -u root -p,依次输入用户密码,mysql密码
  3. 一些linux下mysql命令

    (1):查看数据库:SHOW DATABASES;

    (2):选择数据库: USE database_name, 查找表:SHOW TABLES;

    (3): 查看表结构:DESCRIBE table_name;

   

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值