1. 为什么要有踢人逻辑
在服务器中经常会设计的同账户异地登陆时,将旧有账号的连接断开,必要时先发送下线消息通知旧账号的客户端,然后关闭这个连接。
服务器设计中尽量不要采用服务器主动关闭连接,那样会造成大量TIME_WAIT问题,这个之后再说。
先用一个图示说明踢人逻辑
旧客户端登录

当有新客户端连接时

上述图形演示的是单服务器踢人逻辑,多服务器踢人逻辑应配合分布式锁,锁住分布式的操作,保证在一个时刻只能一个客户端登录,客户端登陆完再解锁。
分布式登录我们放在下一节,这一节我们模拟两个客户端同账号登录同一个服务器,实现踢人逻辑。
2. 分布式锁和redis封装
为了更方便操作,我们将分布式锁加锁和解锁的操作封装到redis接口中
因为分布式锁也会占用连接,为了防止连接被占用耗尽连接池,所以我们提前扩大连接池的数量为10
1 | RedisMgr::RedisMgr() { |
封装加锁操作, 内部调用了之前封装的分布式锁DistLock
1 | std::string RedisMgr::acquireLock(const std::string& lockName, |
解锁操作
1 | bool RedisMgr::releaseLock(const std::string& lockName, |
3. 加锁和解锁调用
对于踢人逻辑,最难的就是思考如何加锁和解锁,进行踢人,以保证将来分布式登录也会安全。
这里我们先考虑几个情形
- B新登录,此时A已登录,这种最简单,根据uid找到A的session发送踢人通知。
- B新登录,此时A将下线,这种要保证B和A互斥,要么B先登陆完,A再下线,要么A先下线,B再登录。
这么做的好处就是保证互斥
如果B先登录,会将uid对应的session更新为最新的。A下线时会优先查找uid对应的session,发现不是自己,则直接退出即可,同时不需要修改uid对应的session为空。
如果A先退出,A下线时会优先查找uid对应的session, 发现uid对应的session和自己的连接吻合,则会将uid对应的session设置为空,然后B登录,将uid对应的session设置为新连接,这样是安全的。
- B登录,A退出,此时C查找uid发送消息,三个操作都会添加分布式锁。谁先竞争到锁谁操作,能保证操作的互斥。
基本就是这三种情况。接下来我们回顾下uid和Session的对应关系
4. 用户和会话关系
添加用户和会话关联
1 | class UserMgr: public Singleton<UserMgr> |
UserMgr中可以根据uid查找到对应的CSession。具体实现
1 |
|
大家有没有注意到,对Session的操作没有加分布式锁,只加了线程锁,因为我的思路是在最外层加分布式锁,而接口内部只加线程锁,保证同一个服务器操作的原子性。
CSession类和之前一样, 里面有user_id和session_id
1 | class CSession: public std::enable_shared_from_this<CSession> |
通过上述结构,我们可以通过UserMgr查找到CSession, 也可以通过CSession查找到userid, 实现了双向关联
5.登录添加分布式锁
我们需要对登录流程添加分布式锁,收到登录请求会做如下事情
- 判断
token和uid是否合理 - 根据
uid构造分布式锁key,然后实现分布式锁加锁操作。比如uid为1001,则分布式锁的key为”lock_1001” - 加锁后通过defer自动析构解锁
- 通过
uid获取用户之前登录的服务器,如果存在则说明uid对应的用户还在线,此时要做踢人,判断serverip和现在的服务器ip是否相等,如果相等则说明是
本服务器踢人,只需要通过线程锁控制好并发逻辑即可,将uid对应的旧session发送信息通知客户端下线,并且将旧session从server中移除。
如果不是本服务器,则要做跨服踢人,调用grpc踢人即可,留作之后做。
- 登录成功后,要将
uid和对应的ip信息写入redis,方便以后跨服查找。另外uid对应的session信息也要写入redis, 同时将uid和session关联,这样可以通过uid快速找到session
1 | void LogicSystem::LoginHandler(shared_ptr<CSession> session, const short &msg_id, const string &msg_data) { |
6. 检测离线处理
服务器也会检测到离线也会清理连接,但是要注意,连接可以不按照分布式锁加锁清理,但是连接的信息要加分布式锁后再更新。
比如是否将uid对应的session更新到redis中,因为很可能用户在别的新服务器登录,新服务器给旧的客户端通知离线,旧的客户端不按理连接,导致旧的服务器检测连接断开,此时不能将uid对应的session清空,因为uid对应的session已经被新服务器更新了。

在发送和接收的时候都可能检测到对方离线而报错,所以在AsyncReadBody和AsyncReadHead以及AsyncWrite等错误处理的时候记得加上连接清理操作
我们以读取body为例
1 | void CSession::AsyncReadBody(int total_len) |
7. 测试效果
本节先测试单服务器同账号不同客户端登录情况,为了将同账号客户端派发到同一个服务器,暂时修改StatusServer的派发逻辑为同一个服务器
1 | ChatServer StatusServiceImpl::getChatServer() { |


8. 待做事项
- 跨服踢人留作下一节处理
- 心跳检测未作,留作以后处理
- 心跳检测发现僵尸连接,需要踢人,留作以后处理。