Skip to main content

聊天服务器项目问题

1.cmake 中 库依赖关系 写反会出现问题

应该是从后往前写->mymuduo 如果包含 pthread 则先写 mymuduo

target_link_libraries(ChatServer mymuduo pthread)

写反会报如下错误:

/usr/lib/gcc/x86_64-linux-gnu/7/../../../../lib/libmymuduo.so: undefined reference to `pthread_create'
/usr/lib/gcc/x86_64-linux-gnu/7/../../../../lib/libmymuduo.so: undefined reference to `sem_wait'
/usr/lib/gcc/x86_64-linux-gnu/7/../../../../lib/libmymuduo.so: undefined reference to `sem_init'
/usr/lib/gcc/x86_64-linux-gnu/7/../../../../lib/libmymuduo.so: undefined reference to `sem_post'
collect2: error: ld returned 1 exit status
CMakeFiles/ChatServer.dir/build.make:94: recipe for target 'bin/ChatServer' failed
make[2]: *** [bin/ChatServer] Error 1
CMakeFiles/Makefile2:67: recipe for target 'CMakeFiles/ChatServer.dir/all' failed
make[1]: *** [CMakeFiles/ChatServer.dir/all] Error 2
Makefile:83: recipe for target 'all' failed
make: *** [all] Error 2

2.error: ‘unordered_map’ does not name a type

unordered_map在std::unordered_map 命名空间下。

3.数据库

MYSQL安装

ubuntu环境安装mysql-server和mysql开发包,包括mysql头文件和动态库文件,命令如下:

sudo apt-get install mysql-server =》 安装最新版MySQL服务器
sudo apt-get install libmysqlclient-dev =》 安装开发包

ubuntu默认安装最新的mysql,但是初始的用户名和密码是自动生成的,按下面步骤修改mysql的root用 户密码为123456

【step 1】tony@tony-virtual-machine:~$ sudo cat /etc/mysql/debian.cnf
[client]
host = localhost
user = debian-sys-maint 《============== 初始的用户名
password = Kk3TbShbFNvjvhpM 《=============== 初始的密码
socket = /var/run/mysqld/mysqld.sock
【step 2】用上面初始的用户名和密码,登录mysql server,修改root用户的密码,命令如下:
tony@tony-virtual-machine:~$ mysql -u debian-sys-maint -pKk3TbShbFNvjvhpM
命令解释: -u后面是上面查看的用户名 -p后面紧跟上面查看的密码
mysql> update mysql.user set authentication_string=password('123456') where
user='root' and host='localhost';
mysql> update mysql.user set plugin="mysql_native_password";
mysql> flush privileges;
Query OK, 0 rows affected (0.01 sec)
mysql> exit
Bye

重新用root和123456登录mysql-server

mysql -u root -p

设置MySQL字符编码utf-8,可以支持中文操作

mysql> show variables like "char%"; # 先查看MySQL默认的字符编码
+--------------------------+----------------------------+
| Variable_name | Value |
+--------------------------+----------------------------+
| character_set_client | utf8 |
| character_set_connection | utf8 |
| character_set_database | latin1 |
| character_set_filesystem | binary |
| character_set_results | utf8 |
| character_set_server | latin1 | 《============不支持中
文!!!
| character_set_system | utf8 |
| character_sets_dir | /usr/share/mysql/charsets/ |
+--------------------------+----------------------------+
8 rows in set (0.06 sec)
mysql> set character_set_server=utf8;
Query OK, 0 rows affected (0.00 sec)

MySQL数据库编程

db.h

#ifndef DB_H
#define DB_H

#include <mysql/mysql.h>
#include <string>
using namespace std;

// 数据库操作类
class MySQL
{
public:
// 初始化数据库连接
MySQL();
// 释放数据库连接资源
~MySQL();
// 连接数据库
bool connect();
// 更新操作
bool update(string sql);
// 查询操作
MYSQL_RES *query(string sql);
// 获取连接
MYSQL* getConnection();
private:
MYSQL *_conn;
};

#endif

db.cpp

#include "db.h"
#include <muduo/base/Logging.h>

// 数据库配置信息
static string server = "127.0.0.1";
static string user = "root";
static string password = "123456";
static string dbname = "chat";

// 初始化数据库连接
MySQL::MySQL()
{
_conn = mysql_init(nullptr);
}

// 释放数据库连接资源
MySQL::~MySQL()
{
if (_conn != nullptr)
mysql_close(_conn);
}

// 连接数据库
bool MySQL::connect()
{
MYSQL *p = mysql_real_connect(_conn, server.c_str(), user.c_str(),
password.c_str(), dbname.c_str(), 3306, nullptr, 0);
if (p != nullptr)
{
// C和C++代码默认的编码字符是ASCII,如果不设置,从MySQL上拉下来的中文显示?
mysql_query(_conn, "set names gbk");
LOG_INFO << "connect mysql success!";
}
else
{
LOG_INFO << "connect mysql fail!";
}

return p;
}

// 更新操作
bool MySQL::update(string sql)
{
if (mysql_query(_conn, sql.c_str()))
{
LOG_INFO << __FILE__ << ":" << __LINE__ << ":"
<< sql << "更新失败!";
return false;
}

return true;
}

// 查询操作
MYSQL_RES *MySQL::query(string sql)
{
if (mysql_query(_conn, sql.c_str()))
{
LOG_INFO << __FILE__ << ":" << __LINE__ << ":"
<< sql << "查询失败!";
return nullptr;
}

return mysql_use_result(_conn);
}

// 获取连接
MYSQL* MySQL::getConnection()
{
return _conn;
}

示例

#include "groupmodel.hpp"
#include "db.h"

// 创建群组
bool GroupModel::createGroup(Group &group)
{
// 1.组装sql语句
char sql[1024] = {0};
sprintf(sql, "insert into allgroup(groupname, groupdesc) values('%s', '%s')",
group.getName().c_str(), group.getDesc().c_str());

MySQL mysql;
if (mysql.connect())
{
if (mysql.update(sql))
{
group.setId(mysql_insert_id(mysql.getConnection()));
return true;
}
}

return false;
}

// 加入群组
void GroupModel::addGroup(int userid, int groupid, string role)
{
// 1.组装sql语句
char sql[1024] = {0};
sprintf(sql, "insert into groupuser values(%d, %d, '%s')",
groupid, userid, role.c_str());

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

// 查询用户所在群组信息
vector<Group> GroupModel::queryGroups(int userid)
{
/*
1. 先根据userid在groupuser表中查询出该用户所属的群组信息
2. 在根据群组信息,查询属于该群组的所有用户的userid,并且和user表进行多表联合查询,查出用户的详细信息
*/
char sql[1024] = {0};
sprintf(sql, "select a.id,a.groupname,a.groupdesc from allgroup a inner join \
groupuser b on a.id = b.groupid where b.userid=%d",userid);

vector<Group> groupVec;

MySQL mysql;
if (mysql.connect())
{
MYSQL_RES *res = mysql.query(sql);
if (res != nullptr)
{
MYSQL_ROW row;
// 查出userid所有的群组信息
while ((row = mysql_fetch_row(res)) != nullptr)
{
Group group;
group.setId(atoi(row[0]));
group.setName(row[1]);
group.setDesc(row[2]);
groupVec.push_back(group);
}
mysql_free_result(res);
}
}

// 查询群组的用户信息
for (Group &group : groupVec)
{
sprintf(sql, "select a.id,a.name,a.state,b.grouprole from user a \
inner join groupuser b on b.userid = a.id where b.groupid=%d",
group.getId());

MYSQL_RES *res = mysql.query(sql);
if (res != nullptr)
{
MYSQL_ROW row;
while ((row = mysql_fetch_row(res)) != nullptr)
{
GroupUser user;
user.setId(atoi(row[0]));
user.setName(row[1]);
user.setState(row[2]);
user.setRole(row[3]);
group.getUsers().push_back(user);
}
mysql_free_result(res);
}
}
return groupVec;
}

// 根据指定的groupid查询群组用户id列表,除userid自己,主要用户群聊业务给群组其它成员群发消息
vector<int> GroupModel::queryGroupUsers(int userid, int groupid)
{
char sql[1024] = {0};
sprintf(sql, "select userid from groupuser where groupid = %d and userid != %d", groupid, userid);

vector<int> idVec;
MySQL mysql;
if (mysql.connect())
{
MYSQL_RES *res = mysql.query(sql);
if (res != nullptr)
{
MYSQL_ROW row;
while ((row = mysql_fetch_row(res)) != nullptr)
{
idVec.push_back(atoi(row[0]));
}
mysql_free_result(res);
}
}
return idVec;
}

INNER JOIN查询

MySQL中的INNER JOIN是一种用于联结两个或多个表的操作。它基于两个表之间的共同字段,将它们连接起来以获取符合特定条件的数据。

INNER JOIN的语法如下:

SELECT 列名
FROM1
INNER JOIN2
ON1.共同字段 =2.共同字段;
查询 userid = 8888的人的好友列表信息
select a.id,a.name,a.state from user a inner join friend b on b.friendid = a.id where b.userid= 8888

以下是INNER JOIN的用法示例:

假设我们有两个表:CustomersOrdersCustomers 表包含客户的详细信息,Orders 表包含客户的订单信息。这两个表中都有一个共同字段 customer_id

我们可以使用INNER JOIN将这两个表连接起来,以获取具有匹配 customer_id 的客户和订单信息。

SELECT Customers.customer_name, Orders.order_number
FROM Customers
INNER JOIN Orders
ON Customers.customer_id = Orders.customer_id;

上述查询将从 Customers 表和 Orders 表中选择 customer_nameorder_number 列,然后连接这两个表。

请注意,ON 子句用于指定连接的条件,即 Customers.customer_id = Orders.customer_id。这表示我们只获取具有相同 customer_id 的客户和订单。

使用INNER JOIN时,只会返回符合连接条件的匹配行。如果某个表中的数据没有匹配项,它将不会被包含在结果中。

这就是MySQL中INNER JOIN的基本用法。它可以帮助我们在多个表中根据关联字段获取相关数据。

4.nginx配置tcp负载均衡

在服务器快速集群环境搭建中,都迫切需要一个能拿来即用的负载均衡器,nginx在1.9版本之前,只支 持http协议web服务器的负载均衡,从1.9版本开始以后,nginx开始支持tcp的长连接负载均衡,但是 nginx默认并没有编译tcp负载均衡模块,编写它时,需要加入--with-stream参数来激活这个模块。

nginx编译加入--with-stream 参数激活tcp负载均衡模块

nginx编译安装需要先安装pcre、openssl、zlib等库,也可以直接编译执行下面的configure命令,根 据错误提示信息,安装相应缺少的库。

下面的make命令会向系统路径拷贝文件,需要在root用户下执行

./configure --with-stream
make && make install

编译完成后,默认安装在了/usr/local/nginx目录。

cd /usr/local/nginx/

nginx -s reload 重新加载配置文件启动
nginx -s stop 停止nginx服务

nginx配置tcp负载均衡

主要在conf目录里面配置nginx.conf文件,配置如下:

img

配置完成后,./nginx -s reload平滑重启。

连接服务器时直接连接 nginx服务器所在的配置端口 8000,nginx服务器可以自动选择配置的服务器。

5.服务器中间件-基于发布-订阅的Redis

集群服务器之间的通信设计

y

redis环境安装和配置

sudo apt-get install redis-server # ubuntu命令安装,ubuntu通过上面命令安装完redis,会自动启动redis服务,通过ps命令确认
ps -ef | grep redis #可以看到redis默认工作在本地主机的6379端口上。

redis发布-订阅相关命令

redis首先是一个强大的缓存服务器,比memcache强大很多,不仅仅支持多种数据结构(不像memcache 只能存储字符串)如字符串、list列表、set集合、map映射表等结构,还可以支持数据的持久化存储 (memcache只支持内存存储),经常被应用到高并发的服务器环境设计之中。

redis的发布-订阅机制:发布-订阅模式包含了两种角色,分别是消息的发布者和消息的订阅者。订阅 者可以订阅一个或者多个频道channel,发布者可以向指定的频道channel发送消息,所有订阅此频道的 订阅者都会收到此消息。 订阅频道的命令是 subscribe,可以同时订阅多个频道,用法是 subscribe channel1 [channel2 ...],如下:

$ redis-cli
127.0.0.1:6379> SUBSCRIBE "zhang san"
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "zhang san"
3) (integer) 1
# <=========== 此处订阅了"zhang san"这个频道,进入订阅阻塞状态,等待该频道上的信息

执行上面命令客户端会进入订阅状态,处于此状态下客户端不能使用除subscribe、unsubscribe、 psubscribe和punsubscribe这四个属于"发布/订阅"之外的命令,否则会报错。打开另一个redis-cli客户端,给"zhang san"频道发布消息,如下:

tony@tony-virtual-machine:~$ redis-cli
127.0.0.1:6379> publish "zhang san" "hello world!"
(integer) 1

第一个redis-cli客户端接收到"zhang san"频道的消息,如下:

127.0.0.1:6379> SUBSCRIBE "zhang san"
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "zhang san"
3) (integer) 1
1) "message"
2) "zhang san"
3) "hello world!"

进入订阅状态后客户端可能收到3种类型的回复。每种类型的回复都包含3个值,第一个值是消息的 类型,根据消类型的不同,第二个和第三个参数的含义可能不同。消息类型的取值可能是以下3个:

  1. subscribe:表示订阅成功的反馈信息。第二个值是订阅成功的频道名称,第三个是当前客户端订阅 的频道数量。
  2. message:表示接收到的消息,第二个值表示产生消息的频道名称,第三个值是消息的内容。
  3. unsubscribe:表示成功取消订阅某个频道。第二个值是对应的频道名称,第三个值是当前客户端订 阅的频道数量,当此值为0时客户端会退出订阅状态,之后就可以执行其他非"发布/订阅"模式的命 令了。

redis发布-订阅的客户端编程

redis支持多种不同的客户端编程语言,例如Java对应jedis、php对应phpredis、C++对应的则是 hiredis。下面是安装hiredis的步骤:

git clone https://github.com/redis/hiredis 
cd hiredis
make
sudo make install

C++ hiredis

redis.hpp

#ifndef REDIS_H
#define REDIS_H

#include <hiredis/hiredis.h>
#include <thread>
#include <functional>
using namespace std;

/*
redis作为集群服务器通信的基于发布-订阅消息队列时,会遇到两个难搞的bug问题,参考我的博客详细描述:
https://blog.csdn.net/QIANGWEIYUAN/article/details/97895611
*/
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;
};

#endif

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发布消息的上下文连接
_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;
}

// 在单独的线程中,监听通道上的事件,有消息给业务层进行上报
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;
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;
}

100.业务待改进

1.注册没有判断是都注册过

2.数据库连接池,不然每次查询数据库都要建立连接池