Netty 是互联网各种框架中以及物联网里都不可缺少的一部分技术栈技能,同时它也是非常重要的一个 Socket 框架。Netty 的应用非常广泛,无论在互联网、大数据以及通信和游戏行业中,都有 Netty 的身影。比如一线大厂阿里的 RPC 框架,Dubbo 协议默认使用 Netty 作为基础通信组件,用于各节点间的内部通信。
淘宝的消息中间件 RocketMQ 的消息生产者与消费者,也是采用 Netty 作为高性能、异步通信组件。除了阿里系、淘宝系,其他很多一线大厂也都在使用 Netty 构建高性能、分布式的网络服务。
前段时间写完 Netty 系列教程,看似系统,但是回顾起来还是零散,看完后一些读者反馈无法真实的上手业务,让我能否写一写关于 Netty 实际业务中的应用。那么为了更好的让自己和大家都能拿一个熟知又有趣的场景,最好的就是模仿 PC 端的微信聊天。之后这样不仅学习到了 Netty,还让 Java 程序员使用自己的语言技术栈开发了一款桌面聊天程序。
以上专栏内容请查看本专栏第一章节:专栏学习简述以及全套源码获取
说回来往往一个新知识点的学习到上手分为三个阶段;运行 HelloWord、熟练使用 API、和最终的落地应用。而这最后一步也是最重要的一步,只要将各个知识点与实际要实现的业务功能相结合,才能不断的提升自己的技能。
对一个技能最好的掌握方式就是使用自己熟悉的工具进行实践落地,通过一点点功能的实现和阅读的一堆堆的逻辑中,建设自己对代码的认知提升,对整个框架的深化理解。
为此我开始使用 Netty+JavaFx 以及 SpringBoot 等技术栈,开始搭建仿桌面版微信聊天程序,在这个过程当中显示梳理分析功能结构。这个过程可以使用 xmind 作为你的工具,整理自己的思路。接下来开始对我需要使用的技术栈做案例测试,验证核心功能是否可以满足我的需求。验证完成后开始做架构设计以及业务流程,直到最终的编码实现功能。
本专栏是作者小傅哥使用 JavaFx
、Netty4.x
、SpringBoot
、Mysql
等技术栈和偏向于 DDD 领域驱动设计方式,搭建的仿桌面版微信聊天工程实现通信核心功能。
本专栏会以三个大章节内容,逐步进行讲解:
第一部分 - UI 开发:使用 JavaFx
与 Maven
搭建 UI 桌面工程,逐步讲解登录框体、聊天框体、对话框、好友栏等各项 UI 展示及操作事件。从而在这一章节中让 Java 程序员学会开发桌面版应用;
第二部分 - 架构设计:在这一章节中我们会使用 DDD 领域驱动设计的四层模型结构与 Netty 结合使用,架构出合理的分层框架。同时还有相应库表功能的设计。相信这些内容学习后,你一定也可以假设出更好的框架;
第三部分 - 功能实现:这部分我们主要将通信中的各项功能逐步实现,包括;登录、添加好友、对话通知、消息发送、断线重连等各项功能。最终完成整个项目的开发,同时也可以让你从实践中学会技能。
功能演示图
登陆页面
聊天页面
添加好友
消息提醒
多图解析
小傅哥,一线互联网后端工程师,CSDN 博客专家,精通 Java、Netty、Spring、SpringBoot 等技术栈,以及擅长中间件开发。
订购本专栏可获得专属海报(在 GitChat 服务号领取),分享专属海报每成功邀请一位好友购买,即可获得 25% 的返现奖励,多邀多得,上不封顶,立即提现。
提现流程:在 GitChat 服务号中点击「我 - 我的邀请 - 提现」。
购买本专栏后,服务号会自动弹出入群二维码和暗号。如果你没有收到那就先关注微信服务号「GitChat」,或者加我们的小助手「GitChatty6」咨询。(入群方式可查看 第 1 篇 文末说明)。
从本章节开始我们会陆续实现各个框体的 UI 开发,内容会包括;框体拆解、工程结构、代码开发,以及最后编写事件和接口。
在 JavaFx 中,一个框体包含;窗口 (Stage)、场景(Scene)、布局(Pane)、控件(Button 等) 这四方面内容。而开发过程中可以使用 xml 和编码两种方式进行处理,一般一些预定好的会使用 xml 结构,如果是随着我们业务行为触达而产生的会开发到代码中来生成。
那么接下来我们的目标是开发一个登陆框体,样式如下;
按照我们的 UI 开发诉求,将整个页面进行拆解,以方便清楚知道我们的各种类型元素放置位置;
序号 | 模块 | 宽 * 高 | 描述 |
---|---|---|---|
1 | 整体框体 | 540 * 415 | 一个整体的 4px 的圆角面板, 去掉默认的标题和工具栏 |
2 | 背景图片 | 540 * 158 | 设置的一个背景图 |
3 | 最小化、退出 | 43 * 32 | 两个同样大小的 Button |
4 | 用户 ID 输入框 | 250 * 45 | 明文输入框 |
5 | 用户密码输入框 | 250 * 45 | 密文输入框 |
6 | 登录按钮 | 250 * 45 | 登陆按钮 Button,鼠标进入时变换背景色,点击触发登陆 |
7 | 版本展示 | 400 * 25 | 透明的无背景可以调整,一般展示版本编号如;v1.0 |
8 | 头像 | 100 * 100 | 圆角头像图片,整个可以使用 Image 等元素开发 |
9 | 标头 | 200 * 15 | 展示名称,例如;憨憨·语约 |
itstack-naive-chat-ui-02└── src ├── main │ ├── java │ │ └── org.itstack.navice.chat.ui │ │ ├── view │ │ │ └── Login.java │ │ └── Application.java │ └── resources │ └── fxml.login │ ├── css │ │ └── login.css │ ├── img │ │ ├── close_0.png │ │ ├── close_1.png │ │ ├── head_default_100.png │ │ ├── logo.png │ │ ├── min_0.png │ │ ├── min_1.png │ │ └── show.png │ └── login.fxml └── test └── java └── org.itstack.test └── ApiTest.java
在 maven 管理下我们将配置文件放到资源文件夹下;fxml/login/login.fxml
整体外框 xml
操作栏;最小化、关闭(Pane)
,来装载元素头像(Pane)
用户 ID 输入框(TextField)
promptText="账号"
密码输入框(PasswordField)
登陆按钮(Button)
text="登 陆"
版本(Label)
#login{ -fx-background-radius: 4px; -fx-border-width: 1px; -fx-border-radius: 4px; -fx-border-color: rgb(180,180,180); -fx-background-color: white;}#operation{-fx-border-color: rgb(180,180,180); -fx-border-width: 1px 1px 0 1px; -fx-border-radius: 4px 4px 0 0; -fx-background-image: url("/fxml/login/img/system/show.png");}.close,.close:pressed{ -fx-background-radius: 2px; -fx-background-position: center center; -fx-background-repeat: no-repeat; -fx-background-size: 43px 34px; -fx-background-color: transparent; -fx-background-image: url("/fxml/login/img/system/close_0.png"); -fx-cursor: hand; -fx-border-width: 0;}.close:hover{ -fx-background-color: #f45454; -fx-background-image: url("/fxml/login/img/system/close_1.png"); -fx-border-width: 1px 1px 0 0; -fx-border-color: rgb(180,180,180); -fx-border-radius: 2px;}...
Login.java & 登陆页面初始化
public class Login extends Stage { private static final String RESOURCE_NAME = "/fxml/login/login.fxml"; private Parent root; public Login() { try {root = FXMLLoader.load(getClass().getResource(RESOURCE_NAME)); } catch (IOException e) {e.printStackTrace(); } Scene scene = new Scene(root); scene.setFill(Color.TRANSPARENT); setScene(scene); initStyle(StageStyle.TRANSPARENT); setResizable(false); this.getIcons().add(new Image("/fxml/login/img/system/logo.png")); }}
单个窗体的需要继承 Stage,也就是继承了窗口类,并需要在里面创建场景,才可以运行展示
在这里我们加载配置元素 login.fxml
,初始化窗体的基本信息
在布局中我们设置了填充为透明色,以及初始化样式 StageStyle.TRANSPARENT
最后我们设置了状态栏的图标样式,这里我们设置了模仿微信的样式,颜色略有差异
this.getIcons().add(new Image("/fxml/login/img/system/logo.png"));
Application.java & 启动类
public class Application extends javafx.application.Application{ @Override public void start(Stage primaryStage) throws Exception {Login login = new Login(); login.show();} public static void main(String[] args) {launch(args); }}
这里的 Application 继承了 JavaFx 的 Application,并实现 start 启动
在这里我们初始化登陆窗体,并通过 login.show()
调用窗体的展现
上面这个结构是一个固定的模板代码,也是配置到 maven 中的启动类路径;
com.zenjava javafx-maven-plugin 8.8.3 org.itstack.navice.chat.ui.Application
在上一章节中我们把登陆窗体开发完成了,并进行了效果演示。那么接下来我们就需要在这个窗体里面添加行为事件和接口,待完成内容如下;
序号 | 类型 | 描述 |
---|---|---|
1 | 事件 | 鼠标拖拽窗体移动 |
2 | 事件 | 最小化到快捷栏 |
3 | 事件 | 退出当前窗体 |
4 | 事件 | 使用用户 ID 和密码登陆 |
5 | 接口 | 登陆成功,执行跳转操作 |
6 | 接口 | 登陆失败,执行提示操作 |
在桌面版程序开发中不同于 web。桌面版开发需要有界面的事件的发起,例如 Button 按钮点击,当接收外部条件变化后要有接口承载,例如登陆成功后的页面跳转。但是在 web 中大部分时候只需要一个 http 请求同步响应即可。
另外也可能有一部分桌面开发程序中是类似同步请求和反馈的,那么在一个事件的发起后,就直接影响事件内容的变化,来改变窗体或者填充数据行为。
以下的章节我们会先去非常直接简单的添加事件和接口,以更清晰的直观的了解这部分内容的开发。之后我们会进行一次小的 重构
,以此来适应更好的拓展。
itstack-naive-chat-ui-03└── src ├── main │ ├── java │ │ └── org.itstack.navice.chat.ui │ │ ├── view │ │ │ └── Login.java │ │ └── Application.java │ └── resources │ └── fxml.login │ ├── css │ ├── img │ └── login.fxml └── test └── java └── org.itstack.test └── ApiTest.java
接下来我们会在现有代码中,org.itstack.navice.chat.ui.view.Login.java
,进行编写事件和接口。
private double xOffset; private double yOffset;private void move() { root.setOnMousePressed(event -> {xOffset = getX() - event.getScreenX(); yOffset = getY()- event.getScreenY(); root.setCursor(Cursor.CLOSED_HAND); }); root.setOnMouseDragged(event -> {setX(event.getScreenX() + xOffset); setY(event.getScreenY() + yOffset);}); root.setOnMouseReleased(event -> {root.setCursor(Cursor.DEFAULT); });}
root.lookup
private void min() {Button login_min = $("login_min", Button.class); login_min.setOnAction(event -> {System.out.println("最小化窗体"); setIconified(true); });}
setIconified(true)
即可,如果有些快捷键操作,弹出窗体可以动态设置为 falseprivate void quit() {Button login_close = $("login_close", Button.class); login_close.setOnAction(event -> {System.out.println("退出窗体"); close(); System.exit(0); });}
close()
操作以退出窗体,并执行程序退出 System.exit(0)
socket
通信的时候,退出还需要断开服务端连接,记录个人状态等操作。例如;某某地方登陆、某某地方退出、时间、设备等信息,后续在通信开发中我们会陆续完善部分功能private void login() {TextField userId = $("userId", TextField.class); PasswordField userPassword = $("userPassword", PasswordField.class); $("login_button", Button.class).setOnAction(event -> {System.out.println("登陆操作"); System.out.println("用户 ID:" + userId.getText()); System.out.println("用户密码:" + userPassword.getText()); });}
itstack-naive-chat-ui-04└── src ├── main │ ├── java │ │ └── org.itstack.navice.chat.ui │ │ ├── view │ │ │ └── login │ │ │ │ ├── ILoginEvent.java │ │ │ │ ├── ILoginMethod.java │ │ │ │ ├── LoginController.java │ │ │ │ ├── LoginEventDefine.java │ │ │ │ ├── LoginInit.java │ │ │ │ └── LoginView.java │ │ │ └── UIObject.java │ │ └── Application.java │ └── resources │ └── fxml.login │ ├── css │ ├── img │ └── login.fxml └── test └── java └── org.itstack.test └── ApiTest.java
抽象后的类功能结构,如下;
序号 | 类 | 描述 |
---|---|---|
1 | ILoginEvent | 事件接口类,具体实现交给调用方。例如我们在点击登陆后将属于窗体的功能处理完毕后,实际的验证交给外部 |
2 | ILoginMethod | 方法接口类,在上面我们说过桌面程序的开发基本都是事件触达和等待回调,那么我们给外部提供接口主要用于类似登陆处理完毕后,来执行相应方法进行窗体切换或者数据填充 |
3 | LoginController | 窗体的控制管理类,也是一个窗体的管家;因为它会继承窗体的装载、实现接口方法、初始化界面、初始化事件定义 |
4 | LoginEventDefine | 窗体事件定义,例如将登陆、最小化、退出等在这里完成定义 |
5 | LoginInit | 窗体的初始化操作,可以创建一些待填充的元素 |
6 | LoginView | 窗体的展示,主要用于扩展一些随着用户操作新展示的元素,例如后续在聊天窗体新增的消息提醒等 |
7 | UIObject | UI 父类定义,这是一个抽象类,提供了基础的初始化内容和接口,以及定义抽象方法 |
ILoginEvent.java & 窗体事件接口
public interface ILoginEvent { /** * 登陆验证 * @param userId 用户 ID * @param userPassword 用户密码 */ void doLoginCheck(String userId, String userPassword);}
ILoginMethod.java & 窗体方法接口
public interface ILoginMethod { /** * 打开登陆窗口 */ void doShow(); /** * 登陆失败 */ void doLoginError(); /** * 登陆成功;跳转聊天窗口 [关闭登陆窗口,打开新窗口] */ void doLoginSuccess();}
show()
来展示页面LoginController.java & 窗体的控制管理类
public class LoginController extends LoginInit implements ILoginMethod { private LoginView loginView; private LoginEventDefine loginEventDefine; public LoginController(ILoginEvent loginEvent) {super(loginEvent); } @Override public void initView() {loginView = new LoginView(this, loginEvent); } @Override public void initEventDefine() {loginEventDefine = new LoginEventDefine(this, loginEvent, this); } @Override public void doShow() {super.show(); } @Override public void doLoginError() {System.out.println("登陆失败,执行提示操作"); } @Override public void doLoginSuccess() {System.out.println("登陆成功,执行跳转操作"); // 关闭原窗口 close();}}
initView()
,初始化了窗体页面,如果随着后续的窗体内容的增加,这部分初始化的内容也会有所增加。initEventDefine()
,等窗体初始化完成后,我们就可以初始化我们的事件定义。LoginEventDefine.java & 窗体事件定义
public class LoginEventDefine { private LoginInit loginInit; private ILoginEvent loginEvent; private ILoginMethod loginMethod; public LoginEventDefine(LoginInit loginInit, ILoginEvent loginEvent, ILoginMethod loginMethod) { this.loginInit = loginInit; this.loginEvent = loginEvent; this.loginMethod = loginMethod; loginInit.move(); min(); quit(); doEventLogin();} // 事件;最小化 private void min() { loginInit.login_min.setOnAction(event -> {loginInit.setIconified(true); });} // 事件;退出 private void quit() { loginInit.login_close.setOnAction(event -> {loginInit.close(); System.exit(0); });} // 事件;登陆 private void doEventLogin() { loginInit.login_button.setOnAction(event -> {loginEvent.doLoginCheck(loginInit.userId.getText(), loginInit.userPassword.getText());}); }}
LoginInit.java & 窗体的初始化操作
public abstract class LoginInit extends UIObject { private static final String RESOURCE_NAME = "/fxml/login/login.fxml"; protected ILoginEvent loginEvent; public Button login_min; // 登陆窗口最小化 public Button login_close; // 登陆窗口退出 public Button login_button; // 登陆按钮 public TextField userId; // 用户账户窗口 public PasswordField userPassword;// 用户密码窗口 LoginInit(ILoginEvent loginEvent) { this.loginEvent = loginEvent; try {root = FXMLLoader.load(getClass().getResource(RESOURCE_NAME)); } catch (IOException e) {e.printStackTrace(); } Scene scene = new Scene(root); scene.setFill(Color.TRANSPARENT); setScene(scene); initStyle(StageStyle.TRANSPARENT); setResizable(false); this.getIcons().add(new Image("/fxml/login/img/system/logo.png")); obtain(); initView(); initEventDefine();} private void obtain() {login_min = $("login_min", Button.class); login_close = $("login_close", Button.class); login_button = $("login_button", Button.class); userId = $("userId", TextField.class); userPassword = $("userPassword", PasswordField.class); }}
initView()
、initEventDefine()
。obtain()
方法中可以看到,我们这里就已经初始化获取了基本需要的元素,这样也方面我们后续的使用,不需要重复获取。LoginView.java & 窗体的展示
public class LoginView { private LoginInit loginInit; private ILoginEvent loginEvent; public LoginView(LoginInit loginInit, ILoginEvent loginEvent) { this.loginInit = loginInit; this.loginEvent = loginEvent; }}
UIObject.java & UI 父类定义
public abstract class UIObject extends Stage { protected Parent root; private double xOffset; private double yOffset; public T $(String id, Class clazz) {return (T) root.lookup("#" + id); } public void clearViewListSelectedAll(ListView... listViews) {for (ListView listView : listViews) {listView.getSelectionModel().clearSelection();} } public void move() { root.setOnMousePressed(event -> {xOffset = getX() - event.getScreenX(); yOffset = getY()- event.getScreenY(); root.setCursor(Cursor.CLOSED_HAND); }); root.setOnMouseDragged(event -> {setX(event.getScreenX() + xOffset); setY(event.getScreenY() + yOffset);}); root.setOnMouseReleased(event -> {root.setCursor(Cursor.DEFAULT); });} // 初始化页面 public abstract void initView(); // 初始化事件定义 public abstract void initEventDefine();}
initView()
、initEventDefine()
,这样主要为了方便统一名称下的初始化操作。尤其在团队编码中,更加重要首先我们在 org.itstack.naive.chat.ui.Application
中,添加我们的窗体启动代码,同时我们还实现了事件并传给构造函数;
public class Application extends javafx.application.Application { @Override public void start(Stage primaryStage) throws Exception {ILoginMethod login = new LoginController((userId, userPassword) -> {System.out.println("登陆 userId:" + userId + "userPassword:" + userPassword); });
login.doShow();}
public static void main(String[] args) {launch(args); }}
点击运行,效果如下;
阅读全文: http://gitbook.cn/gitchat/column/5e5d29ac3fbd2d3f5d05e05f