IM系统第四章 – 用户与好友通信
你是如何完成这个功能的?
实现该功能的流程是:
用户与好友通信的前提是:你已经点开了和好友的对话框(上一节已经实现了)。那么你就相当于获取到了好友ID,当用户点击发送消息,就会将好友ID,用户ID,消息内容,时间一并发给服务端;服务端会先将消息利用多线程的方式进行异步落库存储;接下来服务端会判断好友是否在线,判断的标准是Map中是否有好友的通信的管道Channel;如果获取到好友的Channel后,将消息发给好友,然后通过UI接口,将消息显示在界面上。
上述描述的流程如下:(参照 小傅哥 )
实现功能所需的处理:
- 自定义协议;
- UI事件实现;
- 服务端与好友通信功能处理;
- 客户端与好友通信功能处理;
自定义协议:消息请求 协议、消息请求应答 协议
// 消息请求 协议
public class MsgRequest extends Packet {
private String userId; // 用户Id[自己的]
private String friendId; // 好友Id
private String msgText; // 消息内容
private Integer msgType; // 消息类型
private Date msgDate; // 消息时间
构造函数...
get()/set()...
}
// 消息请求应答 协议
public class MsgResponse extends Packet {
private String friendId; // 好友ID[对方]
private Integer msgType; // 消息类型;0文字消息、1固定表情
private String msgText; // 传输消息内容
private Date msgDate; // 消息时间
构造函数...
get()/set()...
}
自定义完协议后,完成客户端需要点击 ”发送消息“才能真正发送消息,那么这是一个事件行为
/**
* 消息发送
*
* @param userId 用户Id
* @param talkId 对话Id(好友ID/群组ID)
* @param talkType 对话框类型;0好友、1群组
* @param msg 发送消息内容
* @param msgType 消息类型;0文字消息、1固定表情
* @param msgDate 发送消息时间
*/
@Override
public void doSendMsg(String userId, String talkId, Integer talkType, String msg, Integer msgType, Date msgDate) {
Channel channel = BeanUtil.getBean("channle", Channel.class);
if(talkType == 0) {
channel.writeAndFlush(new MsgRequest(userId, talkId, msg, msgType, msgDate));
}
if(talkType == 1) {
channel.writeAndFlush(new MsgGroupRequest(userId, talkId, msg, msgType, msgDate));
}
}
接下来来到客户端对消息请求的处理:主要是将传送过来的 消息请求应答 协议的内容传给UI模块的接口作为参数,然后显示在界面上;
// 客户端消息处理器
public class MsgHandler extends MyBizHandler<MsgResponse> {
private UIService uiService;
public MsgHandler(UIService uiService) {
super(uiService);
}
@Override
public void channelRead(Channel channel, MsgResponse msg) {
IChatMethod chat = uiService.getChat();
Platform.runLater(()->{
/**
* 填充对话框消息 --好友[别人的消息]、
* 好友的Id
* 好友发的消息
* 好友发的消息类型
* 好友发消息的时间
* 是否设置为首位
* 是否被选中
* 是否提醒
*/
chat.addTalkMsgUserLeft(msg.getFriendId(), msg.getMsgText(), msg.getMsgType(), msg.getMsgDate(),
true, false, true);
});
}
}
接下来就来到了服务端,服务端主要负责的事情是:将传来的 消息请求协议的内容进行处理,其中包括消息异步落库,然后才是去判断对方用户的channel
是否在线,然后才将信息传入给对方channel
中让对方接收并显示在客户端上,这也是实现了离线消息的存储和转发的问题。
public class MsgHandler extends MyBizHandler<MsgResponse> {
@Override
public void channelRead(Channel channel, MsgRequest msg) {
// 利用多线程的方式 异步落库
userService.asyncAppendChatRecord(new ChatRecordInfo(msg.getUserId(), msg.getFriendId(), msg.getMsgText(), msg.getMsgType(), msg.getMsgDate()));
// 添加对话框[如果对方没有你的对话框则添加]
userService.addTalkBoxInfo(msg.getFriendId(), msg.getUserId(), Constants.TalkType.Friend.getCode());
// 获取好友通道
Channel friendChannel = SocketUtil.getChannel(msg.getFriendId());
if(null == friendChannel) {
return;
}
// 发送消息
friendChannel.writeAndFlush(new MsgResponse(msg.getUserId(), msg.getMsgText(), msg.getMsgType(), msg.getMsgDate()));
}
}
那么问题又来了?**这个异步落库是如何实现的呢?**无非就是开启一个线程的事情,然后该线程执行将消息通过mybatis
框架的 Insert into
操作将消息入库。(虽然看起来很简单,但实现的类层级有点复杂,解耦太多哈哈自己看源码吧)
// 在UserService该接口中定义一个asyncAppendChatRecord()方法
public interface UserService {
void asyncAppendChatRecord(ChatRecordInfo chatRecordInfo);
}
// UserServiceImpl中
public class UserServiceImpl implements UserService {
@Resource
private IUserRepository userRepository; // 类似于数据存储中间站
@Resource
private ThreadPoolExecutor taskExecutor;
// 默认线程池,线程池的线程数为4
private static ExecutorService executorService = Executors.newFixedThreadPool(4);
/**
* 异步添加聊天记录
* @param chatRecordInfo 聊天记录信息
*/
@Override
public void asyncAppendChatRecord(ChatRecordInfo chatRecordInfo) {
taskExecutor.execute(new Runnable(){
@Override
public void run(){
userRepository.appendChatRecord(chatRecordInfo);
}
});
}
}
public interface IUserRepository {
// 添加聊天记录
void appendChatRecord(ChatRecordInfo chatRecordInfo);
}
@Repository("userRepository")
public class UserRepository implements IUserRepository {
@Autowired
private IChatRecordDao chatRecordDao;
@Override
public void appendChatRecord(ChatRecordInfo chatRecordInfo) {
ChatRecord chatRecord = new ChatRecord();
// 给消息记录对象填充属性
chatRecord.setUserId(chatRecordInfo.getUserId());
chatRecord.setFriendId(chatRecordInfo.getFriendId());
chatRecord.setMsgContent(chatRecordInfo.getMsgContent());
chatRecord.setMsgType(chatRecordInfo.getMsgType());
chatRecord.setMsgDate(chatRecordInfo.getMsgDate());
chatRecord.setTalkType(chatRecordInfo.getTalkType());
// 将填充好属性的消息对象传送给dao层对象
chatRecordDao.appendChatRecord(chatRecord);
}
}
真正执行操作数据库的接口类是IChatRecordDao
:
@Mapper
public interface IChatRecordDao {
void appendChatRecord(ChatRecord req); // mybatis中id的名字就是该方法的名字
}
总结
实现用户与好友的通信功能:
- 首先自定义通信协议:
MsgRequest
、MsgResponse
; - 第二个因为你要按 ”发送消息“ ,所以也需要定义事件行为,当点击该按钮的时候,就会发送消息请求协议过去;
- 来到服务端:其步骤是对消息进行处理,先是将消息异步存储,同时去查询好友的
channel
管道是否在线,如果在线就获取到好友的channel
管道直接发送消息过去即可; - 至于这个将消息异步落库的操作:用
ThreadPoolTaskExecutor
开启一个线程,然后将数据传输对象传输过去,因为在服务端中包装不是数据库聊天记录表中的消息对象,而是消息传输对象;所以需要对象转换,转换完成后就可以用Mybatis入库了完成消息存储了。