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

流程设计

米虫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 2023-08-02 23:04  $$X$$  阅读(56)  评论(0)    收藏  举报