随笔 - 59,  文章 - 1,  评论 - 0,  阅读 - 24152

本文是仿微信聊天程序专栏的第十篇文章,主要简要说明一下,注册、登录的业务流程实现,通过事件通知实现消息收发。

流程设计

米虫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事件,实现主界面跳转。

posted on   $$X$$  阅读(50)  评论(0编辑  收藏  举报
(评论功能已被禁用)
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)

点击右上角即可分享
微信分享提示