集群聊天服务器项目(四)——项目总结

集群聊天服务器项目总结

首先是就是项目介绍集群聊天服务器项目(零)——项目介绍中的内容,就不再次copy过来了

项目简单介绍

技术栈

环境和库依赖

按模块介绍整个项目

程序的主要模块是网络模块、业务模块、数据模块、Json、redis发布订阅消息队列模块以及nginx负载均衡模块

网络模块

网络模块底层采用的是陈硕的muduo库,其采用的是 one loop per thread + nonblocking IO 的网络事件模型,其基于epoll的事件处理机制能够高效地处理网络事件,提供了高效的事件处理和内存管理方式。有一个较高muduo通过事件驱动的方式实现了异步I/O,能够支持上万的并发连接。用户只需要关注连接到来socket消息到来时的业务处理。

使用muduo库作为项目的核心网络模块,提供高并发以及高可用网络IO服务,解耦网络和业务模块的代码,提高了系统的可维护性和可扩展

集群聊天服务器项目(四)——项目总结_第1张图片

业务模块

这一模块主要完成相应的业务处理,如

  1. 客户端新用户注册

  2. 客户端用户登录

  3. 添加好友和添加群组

  4. 一对一好友聊天

  5. 群组聊天

  6. 离线消息存储

其工作方式是通过解析收到json数据根据消息类型来调用对应的业务函数

数据模块

数据模块主要就是对MySQL数据库的表和操作一系列封装。

本项目的数据库表为:User表、Friend表、AllGroup表、GroupUser表、OfflineMessage

本项目对MySQL的CRUD基本操作封装为一个类MySQL,然后将表的字段封装为一个类并提供对应的 getset 方法也就是加入 ORM(object Relation Model)类,业务层操作的都是对象,DAO层(数据访问层)即xxxmodel类才访问数据,对表的业务操作的封装(如往表中插入新用户的数据、根据id查询用户信息等)。

比如:user表有id、name、passwd字段,将其封装为User类(ORM层),有对应的数据成员和函数成员getId、getName、setId、setName等,然后封装具体的用户表的操作类UserModel,里面提供插入新用户方法insert(User &user)、查询用户信息方法query(int id)以及更新用户状态的成员函数updateState(User user)

这么做的好处是解耦了业务层和数据层。

其中表的规模大概是5w行以内的数据量。

Json

本项目使用json来序列化和反序列化消息作为私有通信协议

Json是一种轻量级的数据交换格式(也叫数据序列化方式)。Json采用完全独立于编程语言的文本格式来存储和表示数据。简洁和清晰的层次结构使得 Json 成为理想的数据交换语言。

未来计划改进为protobuf

nginx负载均衡模块

该模块的主要作用是实现集群服务器的功能,使得客户端可以连接到不同的服务器上,从而提高整个聊天服务器的并发量(单端口可达6w并发量)。其中配置nginx基于权重的负载/轮询均衡算法。

集群聊天服务器项目(四)——项目总结_第2张图片

nginx的主要用处或优点如下:

  • client请求按负载算法分发到具体业务服务器Chatserver
  • 能和ChatServer保持心跳机制,检测ChatServer保持心跳机制,检测ChatServer故障
  • 能发现新添加的ChatServer设备方便服务器扩展数量

redis发布订阅消息队列

redis发布订阅消息队列的主要作用是在使用了集群服务器之后实现跨服务器聊天。该消息队列在程序中的工作流程大致如下:

用户c1在某个服务器上连接后,要把该连接在redis队列上订阅一个通道号为c1(通道号为用户id)

别的客户端c2在不同的服务器上登录要给c1发消息,会直接发到redis队列上的通道号c1,消息队列会把信息发送到正在订阅通道c1的redis连接上 (阻塞等待消息中),客户端c1在其对应的接收信息线程中可接受到c2发来的信息

每个连接到服务器的用户要开一个单独线程进行监听通道上的事件,有消息给业务层上报。

当服务器发现发送的对象id没有在自己的_userConnMap上,就要往消息队列上publish,消息队列就会把消息发布给订阅者。

消息队列是长连接跨服务器聊天通用方法

消息队列这种中间件是典型的观察者模式的应用实践

遇到的问题

服务器异常退出没有将用户状态置为offline

解决方法:服务器main.cpp 中设置一个信号处理函数来设置用户状态

// chat/src/server/main.cpp
void resetHandler(int)
{
    ChatService::instance()->reset();
    exit(0);
}

客户端异常退出没有将用户状态置为offline

解决方法:连接断开时,调用业务层提供的方法来将对应用户的映射信息从服务器的_userConnMap中删除,并且设置用户状态为offline

// ChatServer.cpp
void ChatServer::onConnection(const TcpConnectionPtr &conn)
{
    if(!conn->connected())
    {
        ChatService::instance()->clientCloseException(conn);
        conn->shutdown();
    }
}

客户端登录后输入logout 退出登录服务端出错

错误信息:

{“id”:4,"

exception caught in Thread ChatServer1

reason: [json.exception.parse_error.101] parse error at line 1, column 10: syntax error while parsing object key - invalid string: missing closing quote; last read: ‘"’; expected string literal

Aborted

排查和解决过程:

gdb调试检查服务端接收到的json序列化的字符串,发现收到的数据不完整,那么可能是发送方发送不完整,定位到发送语句:

send(clientfd, buffer.c_str(), sizeof(buffer.c_str()) + 1, 0);

并且通过测试发现:

sizeof(buffer.c_str()) = 8

strlen(buffer.c_str()) = 31

所以不应该使用 sizeof(buffer.c_str()) 而应该换成 strlen

一个用户没法存储多条离线消息,上线时只能收到一条消息

通过查看MySQL表,发现原因是离线消息表offlinemessage 中的 useridmessage都设置为了 unique,这样导致同一个用户只能有一条离线消息记录到表中,

解决方法:

offlinemessage表 字段都不要设置为unique,因为一个userid可以对应条离线消息

客户端登录时接收一条以上的离线消息导致直接终止

错误信息:

集群聊天服务器项目(四)——项目总结_第3张图片

排查过程

先是检查自己有没有输入中文或者中文字符

然后通过打印登录时从服务器获得的json串,发现输出不完整

这里是个有趣的问题,我之前做百万并发测试的时候把内核的rmemwmem都调到了512,导致了这里read一轮读出的json序列化串不完成造成解析失败。

解决方法:

sudo modprobe ip_conntrack

sudo vim etc/sysctl.conf

按如下修改

集群聊天服务器项目(四)——项目总结_第4张图片

修改之后输入:sudo sysctl -p

nginx编译安装错误

错误1

src/os/unix/ngx_user.c:36:7: error: ‘struct crypt_data’ has no member named ‘current_salt’

36 | cd.current_salt[0] = ~salt[0];

解决方法:

# vim src/os/unix/ngx_user.c

将对应代码注释掉

集群聊天服务器项目(四)——项目总结_第5张图片

错误2

error: cast between incompatible function types from ‘size_t (*)(ngx_http_script_engine_t *)’ {aka ‘long unsigned int (*)(struct  *)’} to ‘void (*)(ngx_http_script_engine_t *)’ {aka ‘void (*)(struct  *)’} [-Werror=cast-function-type]

集群聊天服务器项目(四)——项目总结_第6张图片

解决方法:

输入 vim objs/Makefile 把 -Werror删掉 (-Werror,它要求GCC将所有的警告当成错误进行处理)

错误3:

mv: cannot move ‘/usr/local/nginx/sbin/nginx’ to ‘/usr/local/nginx/sbin/nginx.old’: Permission denied

这表示移动文件没权限

解决方法:

将目标目录的权限更改为当前用户拥有的权限

sudo chown -R $(whoami) /usr/local/nginx

项目面试可能问题

你这里数据都是明文传输,不安全怎么解决?

进行加密

对称加密码算法:加解密效率高,AES加解密算法

非对称加密:公钥和私钥,加密复杂,效率慢,但是安全、RSA算法

实践方法:第一次用非对称加密发送对称密钥,后面双方都用对称密钥加密信息

客户端消息如何按序显示

消息添加序列号seq,接收方维护一个下一次应该接收消息的序列号,如果后发的提前到了,则会将消息缓存起来。加序号不仅可以保证消息按序到达,还可实现其他功能,如消息撤回

如果给消息添加一个时间戳,到达客户端再按时间排序,但是有问题,比如以1s为周期进行消息显示,很可能最先发的消息没有和它相邻的消息一起进行排序,即无法实现全局有序显示。

不能用短链接吗?

http就是B/S 无状态、短链接,无法主动推消息,只能被动响应

服务端要主动推消息,常用websocket

集群聊天服务器需要处理大量的客户端连接请求,而使用短链接会导致频繁的连接和断开操作,增加服务器的负担。当客户端需要发送消息时,短链接需要重新建立连接,而在连接的建立和断开时会产生额外的网络开销和延迟,影响系统的性能。

通常IM即时聊天都在服务器上有长连接模块

如果网络拥塞严重,ChatServer端如何感知客户端在线还是掉线

客户端发FIN包,服务器recv 得到0表示client下线

心跳机制设计:

listen socket 8080 通用业务处理

UDP socket 8080 心跳业务处理

启动一个心跳计时器(server启动),超时1s,把所有账号心跳计数+1

connect成功的client分配一个心跳计数(heartbeatcnt )

如设计的消息格式为: userid:zhangsan1, heartbeatcnt :4

若账号心跳计数 》 5,即判定client掉线,拆除client所有连接及其他资源(业务层的一些数据)

若从TCP 协议分析,传输层,keepalive,用于确定对方没说话还是掉线可以吗?

不可以,因为keepalive 默认关闭,setsockopt开启,默认每隔两小时发送一个空报文段,探测对方是否在线。 若探测无响应,延迟75s继续发送探测包,依旧没有则再探测9次 (75 * 9)。但是若应用层死锁了,传输层检测意义不大了。

所以,基于长连接业务通常都是在业务层自己设计心跳保持机制

怎么保证消息的可靠传输

应用层实现消息确认机制

为什么tcp的消息确认机制不能保证消息可靠传输?

超时重传可能失败

send(fd, buf, buf_size, 0); 返回>0 只是将用户空间数据buf 拷贝到内核空间的TCP发送缓冲区中,不代表发送成功。这是tcp的消息确认机制不能保证消息可靠传输原因之一

最终由内核TCP协议栈 将数据发送出去。

TCP IP MAC最大数据 MTU 1500字节,但TCP和IP头都占20B,故实际携带数据为1460字节

只要是数据传输失败,或是返回ACK失败,C端都认为数据发送失败,启动超时重传定时器,连发几次后若还不行,就发一个RST包。

最终还是要在业务上实现可靠传输:

客户要发送的消息都缓存起来,如下

集群聊天服务器项目(四)——项目总结_第7张图片

历史消息如何存储

主要有两种存储位置:

本地消息存储 和 消息存储

本地

好友qq号作为文件夹,

SQLite 嵌入式数据库(嵌入到当前进程里),方便查询

云消息存储

存储到mysql,若为长时间的聊天数据可以存储到文件服务器上即 dump-> fileserver,因为mysql超过千万行后索引空间占用大,磁盘操作很慢。

除了redis,还知道其他组件能完成相应的功能(消息队列)?

redis功能 : 缓存数据库,k-v,数据持久化;分布式锁、发布订阅channel

服务器中间件:放在后端中间,不能单独跑。如MQ消息队列,kafka, zeromq,rabbitmq,rocketmq

redis运行不稳定,挂了怎么办?

redis消息积累的过快,消费消息过慢,可能导致挂

消息的消费不可靠。

非核心业务、流量不是非常大,可以用redis的发布订阅功能

为什么要用redis作为跨服务器通信的组件,为什么各个server不能相互直接通信呢?

若各个server直连,则服务器还要承担客户端职责,服务器间耦合性高。需要和每个server相互连接,还要不断心跳

【TODO】压力测试

【TODO】按典型程序流程介绍整个项目

你可能感兴趣的:(集群聊天服务器,c++,linux)