本文是仿微信聊天程序专栏的第十篇文章,主要简要说明一下,注册、登录的业务流程实现,通过事件通知实现消息收发。
流程设计
米虫IM采用Netty进行数据通讯,客户端在触发一些事件时将消息通过IM客户端发送到服务端,服务反馈消息通过事件通知的形式触发JavaFX的UI控制。
大致的流程如下:
在上图中,JavaFX的Controller通过EventDispatcher注册事件监听,IM客户端接收到服务端的消息后触发指定的事件。
这样,当JavaFX Controller监听的事件触发时,可以通过回调函数更新界面UI。
为了,开发调试方便,客户端程序中通过DebugCaller这个类来模拟Client消息发送和消息接收、事件触发。
事件管理
米虫IM客户端的事件都由EventDispatcher来管理,Controller注册事件监听,IM.Client收到服务端消息触发事件:
public class EventDispatcher { private static final Map<Event, EventCaller<?>> callers = new ConcurrentHashMap<>(); public static <T> void register(Event event, EventCaller<T> caller) { callers.put(event, caller); } public static <T> void register(Event event, Consumer<T> dataConsumer) { register(event, dataConsumer, FXContext.getPrimaryStage()); } public static <T> void register(Event event, Consumer<T> dataConsumer, Stage owner) { callers.put(event, (code, data, message) -> { if (code != IMCode.OK) { Platform.runLater(() -> FX.error(code + ":" + message, owner)); } else { dataConsumer.accept((T) data); } }); } public static <T> void dispatch(Event event, T data) { dispatch(event, IMCode.OK, data, null); } public static <T> void dispatch(Event event, int code, String message) { dispatch(event, code, null, message); } public static <T> void dispatch(Event event, int code, T data, String message) { EventCaller<?> caller = callers.get(event); Optional.ofNullable(caller).ifPresent(c -> ((EventCaller<T>) c).accept(code, data, message)); } }
发送消息
米虫IM将发送消息一些业务抽象为Caller接口,在注册、登录流程中需要用到的业务如下:
- 注册
- 登录
- 获取用户信息
那么Caller接口声明如下:
public interface Caller { void register(String nickname, String username, String password); void login(String username, String password); void userinfo(String token); }
收发模拟
为了客户端测试方便,目前并没有真正的接入IM系统,所以消息的收发由DebugCaller来模拟,DebugCaller实现如下:
/** * @author michong */ public class DebugCaller implements Caller { private final Map<String, UserData> userDB = new ConcurrentHashMap<>(); private final Map<Long, String> tokenCache = new ConcurrentHashMap<>(); @Override public void register(String nickname, String username, String password) { if (userDB.containsKey(username)) { EventDispatcher.dispatch(Event.REGISTER, IMCode.REGISTER__USERNAME_ALREADY_EXISTS, "账号已被注册"); } else { UserData user = new UserData(); user.setId((long) userDB.size()); user.setNickname(nickname); user.setUsername(username); user.setPassword(password); userDB.put(username, user); RegisterDTO data = new RegisterDTO(); data.setUsername(username); EventDispatcher.dispatch(Event.REGISTER, data); } } @Override public void login(String username, String password) { UserData user = userDB.get(username); if (Objects.isNull(user) || !Objects.equals(user.getPassword(), password)) { EventDispatcher.dispatch(Event.LOGIN, IMCode.LOGIN__USERNAME_OR_PASSWORD_ERROR, "账号密码不匹配"); } else { StringJoiner sj = new StringJoiner(":"); sj.add(String.valueOf(user.getId())); sj.add(String.valueOf(System.currentTimeMillis())); sj.add(username); LoginDTO data = new LoginDTO(); data.setId(user.getId()); data.setToken(sj.toString()); tokenCache.put(data.getId(), data.getToken()); EventDispatcher.dispatch(Event.LOGIN, data); } } @Override public void userinfo(String token) { UserData user = fromToken(token); boolean offline = Objects.isNull(user); if (!offline && !tokenCache.containsKey(user.getId())) { offline = true; } if (offline) { EventDispatcher.dispatch(Event.USERINFO, IMCode.SESSION_TOKEN_INVALID, "会话已失效"); } else { user = userDB.get(user.getUsername()); UserinfoDTO data = new UserinfoDTO(); data.setNickname(user.getNickname()); data.setUsername(user.getUsername()); EventDispatcher.dispatch(Event.USERINFO, data); } } private UserData fromToken(String token) { try { int idx = token.indexOf(":"); long id = Long.parseLong(token.substring(0, idx)); // timestamp valid ??? String username = token.substring(token.indexOf(":", idx + 1) + 1); UserData data = new UserData(); data.setId(id); data.setUsername(username); return data; } catch (Exception e) { return null; } } }
完善流程
上面几个步骤主要实现了最开始提到设计的右边部分,关于Controller的事件监听和消息发送需要完善一下之前的注册和登录的Controller。
- 注册监听
完善RegisterController的initializeEvent方法:
class RegisterController { void initializeEvent() { EventDispatcher.register(Event.REGISTER, data -> { RegisterDTO r = (RegisterDTO) data; FX.info(r.getUsername() + " 注册成功", FXContext.getLoginStage()); onLoginClick(null); }, FXContext.getLoginStage()); } }
在事件初始化方法中,注册监听REGISTER事件,注册成功时,显示账号注册成功,然后跳转到登录页面。
- 注册触发
调整RegisterController的onRegisterClick方法:
class RegisterController { public void onRegisterClick(ActionEvent actionEvent) { form.getForm().persist(); if (form.getForm().isValid()) { RegisterVO r = form.getVO(); FXContext.getCaller().register(r.getNickname(), r.getUsername(), r.getPassword()); } } }
调整之前的代码,当用户点击注册时,调用Caller的register的接口方法,将信息发送到服务端,服务端处理完成会触发REGISTER事件,从而跳转到登录页面。
- 登录监听
跟注册类似的,完善LoginController的initializeEvent方法:
class LoginController { void initializeEvent() { EventDispatcher.register(Event.LOGIN, data -> { LoginDTO r = (LoginDTO) data; FXContext.getCaller().userinfo(r.getToken()); FXContext.getLoginStage().close(); FXContext.getPrimaryStage().show(); }, FXContext.getLoginStage()); } }
注册LOGIN事件,即当登录成功时,拉取用户信息,并关闭登录窗口,显示主界面窗口。
- 触发登录
调整LoginController的onLoginClick方法:
class LoginController { public void onLoginClick(ActionEvent actionEvent) { form.getForm().persist(); if (form.getForm().isValid()) { LoginVO r = form.getVO(); FXContext.getCaller().login(r.getUsername(), r.getPassword()); } } }
即,当用户登录的时候,将登录信息发送到服务端进行校验,登录成功后触发LOGIN事件,实现主界面跳转。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)