Java课程设计(单人)——聊天应用

1.项目简介,涉及技术

用户打开应用,进行注册,然后登录后进入主界面,主要有聊天、联系人(群聊)和添加联系人(群聊)三个分页,
可以通过添加联系人(群聊)发起聊天会话,还有删除联系人(群聊)等一些其他功能。

涉及技术:

netty用于实现通信,protobuf配合netty对信息进行结构化,spring boot主要使用到ioc,至于mybatis、mysql就是数据库相关。

2.项目git地址

服务端
客户端

3.项目git提交记录截图


4.项目功能架构图、主要包关系图



5.项目运行截图

6.项目关键代码

通信部分

服务端通信的接收流程是:NettyServerHandler拦截到客户端发送的信息,调用userOrderDispatch根据信息类型分发到各个Handler(已经实现的对某种特定信息的处理方法),然后Handler根据需求调用server。
服务端通信的发送流程是:将需要发送的信息包装到OrderMessage中,从指定channel发送出去。
(客户端类似)

protobuf的message

1.代码中的消息结构化使用protobuf,使用OrderMessage管理多个message,使用枚举来确定信息类型,oneof orderBody是信息体,且每个OrderMessage中最多出现其中的一个,节省空间

message OrderMessage {

    //定义一个枚举类型,message可以是枚举中的一个
    enum OrderType{
        UserLoginType=0;
        UserRegisterType=1;
        LoginSucceedType=2;
        AddConversationType=3;
        AddConversationSucceedType=4;
        SendPersonalChatMessageType=5;
        SendGroupChatMessageType=6;
        RemoveConversationType=7;
        LoginFailureType=8;
        RegisterSucceedType=9;
        RegisterFailureType=10;
        SearchLinkmanType=11;
        SearchLinkmanSucceedType=12;
        SearchGroupType=13;
        SearchGroupSucceedType=14;
        JoinGroupType=15;
        AddLinkmanType=16;
        JoinGroupSucceedType=17;
        AddLinkmanSucceedType=18;
        CreateGroupType=19;
        RemoveGroupType=20;
        RemoveLinkmanType=21;
        CreateGroupSucceedType=22;
        CreateGroupFailureType=23;
        LogOutType=24;
    }

    //用来标识是哪一个指令
    OrderType orderType=1;

    //表示每次枚举类型最多出现其中的一个,节省空间
    oneof orderBody{
        UserLogin userLogin=2;
        UserRegister userRegister=3;
        LoginSucceed loginSucceed=4;
        AddConversation addConversation=5;
        AddConversationSucceed addConversationSucceed=6;
        SendPersonalChatMessage sendPersonalChatMessage=7;
        SendGroupChatMessage sendGroupChatMessage=8;
        RemoveConversation removeConversation=9;
        LoginFailure loginFailure=10;
        RegisterSucceed registerSucceed=11;
        RegisterFailure registerFailure=12;
        SearchLinkman searchLinkman=13;
        SearchLinkmanSucceed searchLinkmanSucceed=14;
        SearchGroup searchGroup=15;
        SearchGroupSucceed searchGroupSucceed=16;
        JoinGroup joinGroup=17;
        AddLinkman addLinkman=18;
        JoinGroupSucceed joinGroupSucceed=19;
        AddLinkmanSucceed addLinkmanSucceed=20;
        CreateGroup createGroup=21;
        RemoveGroup removeGroup=22;
        RemoveLinkman removeLinkman=23;
        CreateGroupSucceed createGroupSucceed=24;
        CreateGroupFailure createGroupFailure=25;
        LogOut logOut=26;
    }

}

2.客户端发送注册请求的message

message UserRegister{
    string account=1;
    string password=2;
    string nickname=3;
}

服务端的一个接收信息的流程

1.服务端使用NettyServerHandler对与客户端建立的连接channel进行UserOrder.OrderMessage类型的信息拦截,然后调用userOrderDispatch进行派发

public class NettyServerHandler extends SimpleChannelInboundHandler<UserOrder.OrderMessage> {



    private UserOrderController userOrderController= (UserOrderController) SpringContextUtil.getBean("userOrderController");

    private NettyOnlineServer nettyOnlineServer= (NettyOnlineServer) SpringContextUtil.getBean("nettyOnlineServer");

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, UserOrder.OrderMessage msg) throws Exception {

        System.out.println("消息到达");
        userOrderController.userOrderDispatch(ctx,msg);

    }

    /**
     *  数据读取完毕
     */
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {

    }

    /**
     *  处理异常, 一般是需要关闭通道
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        ctx.close();
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        nettyOnlineServer.removeOnlineUser(ctx.channel());
        super.channelInactive(ctx);
    }
}

2.服务端将消息派发到各个Handler

 public void userOrderDispatch(ChannelHandlerContext ctx, UserOrder.OrderMessage msg){
        UserOrder.OrderMessage.OrderType orderType=msg.getOrderType();
        switch (orderType){
            case UserLoginType:
                userLoginTypeHandler(ctx,msg);
                break;
            case UserRegisterType:
                userRegisterTypeHandler(ctx,msg);
                break;
            case AddConversationType:
                addConversationHandler(ctx,msg);
                break;
            case SendPersonalChatMessageType:
                sendPersonalChatMessageHandler(ctx,msg);
                break;
            case SendGroupChatMessageType:
                sendSendGroupChatMessageHandler(ctx,msg);
                break;
            case RemoveConversationType:
                removeConversationHandler(ctx,msg);
                break;
            case SearchLinkmanType:
                searchLinkmanHandler(ctx,msg);
                break;
            case SearchGroupType:
                searchGroupHandler(ctx,msg);
                break;
            case JoinGroupType:
                joinGroupHandler(ctx,msg);
                break;
            case AddLinkmanType:
                addLinkmanHandler(ctx,msg);
                break;
            case CreateGroupType:
                createGroupHandler(ctx,msg);
                break;
            case RemoveGroupType:
                removeGroupHandler(ctx,msg);
                break;
            case RemoveLinkmanType:
                removeLinkmanHandler(ctx,msg);
                break;
            case LogOutType:
                logOutHandler(ctx,msg);
            default:
        }

    }

3.处理注册的Handler,调用了server

 public void userRegisterTypeHandler(ChannelHandlerContext ctx, UserOrder.OrderMessage msg){
        userOrderServer.userRegister(ctx,msg);
    }

4.进行注册的server,先对要进行注册的账号进行验证,是否已经注册,验证方式为,通过账号查询数据库得到user,如果user为空,说明未注册,那么就进行注册,并返回注册成功给客户端。
如果不为空,说明改账号已经注册,那么返回账号已注册给客户端。

 public void userRegister(ChannelHandlerContext ctx, UserOrder.OrderMessage msg){
        UserOrder.UserRegister userRegister = msg.getUserRegister();
        String account = userRegister.getAccount();
        String password = userRegister.getPassword();
        String nickname = userRegister.getNickname();

        User user = userServer.getUserByAccount(account);
        if(user==null){
            User user1 = new User();
            user1.setAccount(account);
            user1.setPassword(password);
            user1.setNickname(nickname);
            userServer.insertUser(user1);

            //注册成功
            UserOrder.RegisterSucceed.Builder registerSucceed=UserOrder.RegisterSucceed.newBuilder();
            UserOrder.OrderMessage.Builder orderMessageBuilder = UserOrder.OrderMessage.newBuilder();
            orderMessageBuilder.setOrderType(UserOrder.OrderMessage.OrderType.RegisterSucceedType)
                    .setRegisterSucceed(registerSucceed);
            //使用channel发送,是从第一个handler开始
            ctx.channel().writeAndFlush(orderMessageBuilder.build());

        }else{
            //1账号已存在
            int type=1;

            //注册失败
            UserOrder.RegisterFailure.Builder registerFailure=UserOrder.RegisterFailure.newBuilder()
                    .setType(type);
            UserOrder.OrderMessage.Builder orderMessageBuilder = UserOrder.OrderMessage.newBuilder();
            orderMessageBuilder.setOrderType(UserOrder.OrderMessage.OrderType.RegisterFailureType)
                    .setRegisterFailure(registerFailure);
            //使用channel发送,是从第一个handler开始
            ctx.channel().writeAndFlush(orderMessageBuilder.build());

        }
    }

客户端登录功能

1.点击登录按钮后触发loginButtonOnAction,获取用户输入的账号、密码,调用loginOrder

    void loginButtonOnAction(ActionEvent event) {
        String account = accountTextField.getText();
        String password = passwordTextField.getText();
        if("".equals(account)==true){
            warningLabel.setText("请输入账号");
            return;
        }
        if("".equals(password)==true){
            warningLabel.setText("请输入密码");
            return;
        }
       userOrderServer.loginOrder(account,password);
    }

2.loginOrder将账号、密码发送到服务端

 public void loginOrder(String account, String password) {
        UserOrder.OrderMessage orderMessage = UserOrder.OrderMessage.newBuilder()
                .setOrderType(UserOrder.OrderMessage.OrderType.UserLoginType)
                .setUserLogin(UserOrder.UserLogin.newBuilder()
                        .setAccount(account)
                        .setPassword(password)
                        .build())
                .build();
        nettyContextUtil.getCurrentChannel().writeAndFlush(orderMessage);
    }

3.服务端验证成功后返回loginSucceedType,客户端分发到loginSucceedTypeHandler,loginSucceedTypeHandler对结构化的信息转化为javabean
然后向界面注入数据进行初始化。

 public void loginSucceedTypeHandler(ChannelHandlerContext ctx, UserOrder.OrderMessage msg){
        User user = transformController.parseTransformToUser(msg);
        Platform.runLater(new Runnable() {
            @Override
            public void run() {
                //更新JavaFX的主线程的代码放在此处
                stageController.setStage("HomePageView","LoginView");

                LoginController loginView = (LoginController) stageController.getController("LoginView");
                loginView.getPasswordTextField().setText("");
                loginView.getAccountTextField().setText("");
                loginView.getWarningLabel().setText("");

                ObservableList linkmanObservableList = (ObservableList) SpringContextUtil.getBean("linkmanObservableList");
                ObservableList chatGroupObservableList = (ObservableList) SpringContextUtil.getBean("chatGroupObservableList");
                ObservableList conversationObservableList = (ObservableList) SpringContextUtil.getBean("conversationObservableList");


                linkmanObservableList.setAll(user.getLinkmanList());
                chatGroupObservableList.setAll(user.getChatGroupList());
                conversationObservableList.setAll(user.getConversationList());

            }
        });
    }

javaFX

界面布局使用fxml,不需要多介绍,所以主要介绍controller部分

conversationJFXListView是显示会话的一个listview组件,给conversationJFXListView设置一个可观察数组conversationObservableList(通过改变可观察数组中的数据,listview会动态更新),然后是对conversationJFXListView中的cell进行自定义;

  ObservableList conversationObservableList = (ObservableList) SpringContextUtil.getBean("conversationObservableList");
        conversationJFXListView.setItems(conversationObservableList);
        conversationJFXListView.setCellFactory(new Callback<ListView<Conversation>, ListCell<Conversation>>() {
            @Override
            public ListCell<Conversation> call(ListView<Conversation> param) {

                return getConversationCell(param);
            }
        });

conversationJFXListView中cell的自定义,通过添加一些ImageView、label就可实现,利用VBOx、HBox进行布局

 private ListCell getConversationCell(ListView<Conversation> param) {
        ListCell<Conversation> conversationListCell = new ListCell<Conversation>() {
            @Override
            protected void updateItem(Conversation item, boolean empty) {
                super.updateItem(item, empty);
                if (empty == false) {
                    if (item.getType() == 1) {
                        GroupConversation groupConversation = item.getGroupConversation();
                        HBox hBox = new HBox(10);
                        ImageView headImage = new ImageView("image/group.png");
                        headImage.setPreserveRatio(true);
                        headImage.setFitHeight(48);
                        VBox vBox = new VBox(5);

                        Label nicknameLabel = new Label(groupConversation.getCurrentChatGroup().getGroupName());
                        
                        vBox.getChildren().addAll(nicknameLabel);
                        hBox.getChildren().addAll(headImage, vBox);
                        this.setGraphic(hBox);
                    } else {
                        PersonalConversation personalConversation = item.getPersonalConversation();
                        HBox hBox = new HBox(10);
                        ImageView headImage = new ImageView("image/personal.png");
                        headImage.setPreserveRatio(true);
                        headImage.setFitHeight(48);

                        Label nicknameLabel = new Label(personalConversation.getCurrentLinkman().getNickname());
                        Label remarkLabel = new Label(personalConversation.getCurrentLinkman().getRemark());

                        if (personalConversation.getCurrentLinkman().getRemark().equals("") == false) {
                            hBox.getChildren().addAll(headImage, remarkLabel);
                        } else {
                            hBox.getChildren().addAll(headImage, nicknameLabel);
                        }

                        this.setGraphic(hBox);
                    }
                } else {
                    this.setGraphic(null);
                }
            }

        };

7.项目代码扫描结果及改正

大部分警告是命名不规范和if没有大括号,注解没有使用正确


8.尚待改进

未读消息,表情等功能未实现
原文地址:https://www.cnblogs.com/codedawn/p/12174116.html