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 {
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, ListCell>() {
@Override
public ListCell call(ListView param) {
return getConversationCell(param);
}
});
conversationJFXListView中cell的自定义,通过添加一些ImageView、label就可实现,利用VBOx、HBox进行布局
private ListCell getConversationCell(ListView param) {
ListCell conversationListCell = new ListCell() {
@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.尚待改进
未读消息,表情等功能未实现