-
单聊
public class SessionUtil {
// userId -> channel 的映射
private static final Map<String, Channel> userIdChannelMap = new ConcurrentHashMap<>();
public static void bindSession(Session session, Channel channel) {
userIdChannelMap.put(session.getUserId(), channel);
channel.attr(Attributes.SESSION).set(session);
}
public static void unBindSession(Channel channel) {
if (hasLogin(channel)) {
userIdChannelMap.remove(getSession(channel).getUserId());
channel.attr(Attributes.SESSION).set(null);
}
}
public static boolean hasLogin(Channel channel) {
return channel.hasAttr(Attributes.SESSION);
}
public static Session getSession(Channel channel) {
return channel.attr(Attributes.SESSION).get();
}
public static Channel getChannel(String userId) {
return userIdChannelMap.get(userId);
}
}
主要的代码逻辑就是,当客户端发来消息的时候,拿到消息发送方的会话信息,通过消息发送方的会话信息构造要发送的消息,拿到消息接收方的 channel,将消息发送给消息接收方,就是这么简单
SessionUtil
里面维持了一个 useId -> channel 的映射 map,调用bindSession()
方法的时候,在 map 里面保存这个映射关系,SessionUtil
还提供了getChannel()
方法,这样就可以通过 userId 拿到对应的 channel。- 除了在 map 里面维持映射关系之外,在
bindSession()
方法中,我们还给 channel 附上了一个属性,这个属性就是当前用户的Session
,我们也提供了getSession()
方法,非常方便地拿到对应 channel 的会话信息。- 这里的
SessionUtil
其实就是前面小节的LoginUtil
,这里重构了一下,其中hasLogin()
方法,只需要判断当前是否有用户的会话信息即可。- 在
LoginRequestHandler
中,我们还重写了channelInactive()
方法,用户下线之后,我们需要在内存里面自动删除 userId 到 channel 的映射关系,这是通过调用SessionUtil.unBindSession()
来实现的。
-
群聊
如上图,要实现群聊,其实和单聊类似
- A,B,C 依然会经历登录流程,服务端保存用户标识对应的 TCP 连接
- A 发起群聊的时候,将 A,B,C 的标识发送至服务端,服务端拿到之后建立一个群聊 ID,然后把这个 ID 与 A,B,C 的标识绑定
- 群聊里面任意一方在群里聊天的时候,将群聊 ID 发送至服务端,服务端拿到群聊 ID 之后,取出对应的用户标识,遍历用户标识对应的 TCP 连接,就可以将消息发送至每一个群聊成员
关键代码
public class CreateGroupRequestHandler extends SimpleChannelInboundHandler<CreateGroupRequestPacket> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, CreateGroupRequestPacket createGroupRequestPacket) {
List<String> userIdList = createGroupRequestPacket.getUserIdList();
List<String> userNameList = new ArrayList<>();
// 1. 创建一个 channel 分组
ChannelGroup channelGroup = new DefaultChannelGroup(ctx.executor());
// 2. 筛选出待加入群聊的用户的 channel 和 userName
for (String userId : userIdList) {
Channel channel = SessionUtil.getChannel(userId);
if (channel != null) {
channelGroup.add(channel);
userNameList.add(SessionUtil.getSession(channel).getUserName());
}
}
// 3. 创建群聊创建结果的响应
CreateGroupResponsePacket createGroupResponsePacket = new CreateGroupResponsePacket();
createGroupResponsePacket.setSuccess(true);
createGroupResponsePacket.setGroupId(IDUtil.randomId());
createGroupResponsePacket.setUserNameList(userNameList);
// 4. 给每个客户端发送拉群通知
channelGroup.writeAndFlush(createGroupResponsePacket);
System.out.print("群创建成功,id 为[" + createGroupResponsePacket.getGroupId() + "], ");
System.out.println("群里面有:" + createGroupResponsePacket.getUserNameList());
}
}
- 首先,我们这里创建一个
ChannelGroup
。这里简单介绍一下ChannelGroup
:它可以把多个 chanel 的操作聚合在一起,可以往它里面添加删除 channel,可以进行 channel 的批量读写,关闭等操作,详细的功能读者可以自行翻看这个接口的方法。这里我们一个群组其实就是一个 channel 的分组集合,使用ChannelGroup
非常方便。- 接下来,我们遍历待加入群聊的 userId,如果存在该用户,就把对应的 channel 添加到
ChannelGroup
中,用户昵称也添加到昵称列表中。- 然后,我们创建一个创建群聊响应的对象,其中
groupId
是随机生成的,群聊创建结果一共三个字段,这里就不展开对这个类进行说明了。- 最后,我们调用
ChannelGroup
的聚合发送功能,将拉群的通知批量地发送到客户端,接着在服务端控制台打印创建群聊成功的信息,至此,服务端处理创建群聊请求的逻辑结束。