我们的聊天程序服务器使用springboot和使用netty进行通讯。客户端使用javafx和netty。服务器项目和客户端的项目分别搭建两个项目: cc_chat_server和cc_chat.
一,服务器项目搭建
使用idea搭建,并引入maven依赖:
<properties>
<java.version>1.8</java.version>
<mybatis-plus.version>3.5.1</mybatis-plus.version>
<netty.version>4.1.53.Final</netty.version>
<hutool.version>5.8.15</hutool.version>
<crypto.version>5.3.6.RELEASE</crypto.version>
<fastjson.version>1.2.79</fastjson.version>
<msgpack.version>0.9.1</msgpack.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.msgpack</groupId>
<artifactId>msgpack-core</artifactId>
<version>${msgpack.version}</version>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
<dependency>
<groupId>com.chen</groupId>
<artifactId>cc_chat_protocol</artifactId>
<version>1.0-SNAPSHOT</version>
<exclusions>
<exclusion>
<artifactId>lombok</artifactId>
<groupId>org.projectlombok</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-crypto</artifactId>
<version>${crypto.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>${fastjson.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>${hutool.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<!--排除logback-->
<exclusion>
<artifactId>spring-boot-starter-logging</artifactId>
<groupId>org.springframework.boot</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>${netty.version}</version>
</dependency>
<!-- 添加log4j2 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
1,创建netty服务
netty是一个高性能的网络框架,使用netty我们可以很容易搭建一个网络服务器:
package com.chen.chatServer.netty;
/**
* @author @Chenxc
* @date 2023/8/19 10:02
**/
@Component
public class ChatNettyServer{
private Logger logger = LogManager.getLogger(ChatNettyServer.class);
@Value("${netty.port}")
private int port;
private NioEventLoopGroup boss = null;
private NioEventLoopGroup workers = null;
private ServerBootstrap sb = null;
@Lookup
public ChatNettyServerHandler getHandler(){
return null;
}
private void init(){
logger.info("正在启动ChatNettyServer。。。。");
try{
boss = new NioEventLoopGroup(2);
workers = new NioEventLoopGroup();
sb = new ServerBootstrap();
sb.group(boss, workers)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 1024)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(10*1024*1024,0,4,0,4));
ch.pipeline().addLast(new MsgPackDecoder());
ch.pipeline().addLast(new LengthFieldPrepender(4));
ch.pipeline().addLast(new MsgPackEncoder());
ch.pipeline().addLast("readTimeoutHandler",new ReadTimeoutHandler(60));
ch.pipeline().addLast(getHandler());
}
});
sb.bind(port).sync().channel().closeFuture().sync();
}catch (Exception e){
logger.error("netty服务器ChatNettyServer启动异常:{}"+e.toString());
}finally {
if(null != boss){
boss.shutdownGracefully();
}
if(null != workers){
workers.shutdownGracefully();
}
}
}
private void stopServer(){
if(null != boss){
boss.shutdownGracefully();
}
if(null != workers){
workers.shutdownGracefully();
}
}
public void startNettyServer(){
init();
}
public void stopNettyServer(){
stopServer();
}
}
2,netty服务器处理器
用于处理netty接受到请求:
package com.chen.chatServer.netty;
/**
* @author @Chenxc
* @date 2023/8/19 10:39
**/
@Component()
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class ChatNettyServerHandler extends ChannelInboundHandlerAdapter {
private Logger logger = LogManager.getLogger(ChatNettyServerHandler.class);
@Autowired
private RequestHandler requestHandler;
@Autowired
private UserService userService;
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
logger.info("用户{}上线",ctx.channel().remoteAddress());
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
logger.info("用户{}下线",ctx.channel().remoteAddress());
ClientCache.remove(ctx);
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}
3,启动netty服务
我们使用springboot搭建项目,所以需要在启动springboot时启动netty服务。实现
org.springframework.boot.CommandLineRunner
即可
/**
* @author @Chenxc
* @date 2023/8/19 10:26
**/
@Component
public class StartNettyServer implements CommandLineRunner {
@Autowired
private ChatNettyServer chatNettyServer;
@Override
public void run(String... args) throws Exception {
chatNettyServer.startNettyServer();
}
}
这样在启动springboot时启动netty服务
二,客户端项目搭建
客户端使用javafx和netty。项目使用的jdk版本时1.8,所以Javafx在jdk1.8中是自带的。
maven依赖:
<dependencies>
<dependency>
<groupId>com.chen</groupId>
<artifactId>cc_chat_protocol</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.15</version>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.53.Final</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.0-alpha2</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.11.1</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.79</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j-impl</artifactId>
<version>2.10.0</version>
</dependency>
<!--异步日志依赖-->
<dependency>
<groupId>com.lmax</groupId>
<artifactId>disruptor</artifactId>
<version>3.3.4</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.msgpack</groupId>
<artifactId>msgpack-core</artifactId>
<version>0.9.1</version>
</dependency>
</dependencies>
1,创建netty客户端
package com.chen.netty;
/** netty聊天客户端
* @author @Chenxc
* @date 2023/8/19 0:14
**/
public class ChatNettyClient {
public static final Logger LOGGER = LogManager.getLogger(ChatNettyClient.class);
private static NioEventLoopGroup group;
public volatile static boolean ONLINE = false;
public volatile static ChannelHandlerContext context;
private static void init(){
try {
group = new NioEventLoopGroup();
Bootstrap bootstrap = new Bootstrap();
Channel channel = bootstrap.group(group)
.channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(10*1024*1024,0,4,0,4));
ch.pipeline().addLast(new MsgPackDecoder());
ch.pipeline().addLast(new LengthFieldPrepender(4));
ch.pipeline().addLast(new MsgPackEncoder());
ch.pipeline().addLast("readTimeoutHandler",new ReadTimeoutHandler(30));
ch.pipeline().addLast(new ChatNettyHandler());
}
}).connect(Config.HOST, Config.NETTY_PORT).sync().channel();
channel.closeFuture().sync();
}catch (Exception e){
LOGGER.error("连接服务器错误:{}",e.toString());
}finally {
if(null != group){
group.shutdownGracefully();
}
}
}
public static void connectServer(){
ThreadPool.getPool().execute(()->{init();});
}
public static void disconnect(){
if(null != group){
group.shutdownGracefully();
}
}
}
2,netty客户端处理器
用于处理netty客户端接受到返回:
package com.chen.netty;
/**
* @author @Chenxc
* @date 2023/8/19 10:28
**/
public class ChatNettyHandler extends ChannelInboundHandlerAdapter {
public static final Logger LOGGER = LogManager.getLogger(ChatNettyHandler.class);
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
LOGGER.info("成功连接");
SendUtil.reLogin(UserUtil.currentUser);
ChatNettyClient.context = ctx;
ChatNettyClient.ONLINE = true;
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
LOGGER.error("与服务器断开连接,正在重新连接...");
ChatNettyClient.context = null;
ChatNettyClient.ONLINE = false;
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
LOGGER.info("连接异常");
ChatNettyClient.context = null;
ChatNettyClient.ONLINE = false;
System.err.println(cause);
ctx.close();
}
}
3,启动netty客户端
因为使用javafx,我们只需要创建一个启动类并继承
javafx.application.Application
并重写start(Stage primaryStage)方法即可。
package com.chen;
/**
* Hello world!
*
*/
public class App extends Application {
public static final Logger LOGGER =LogManager.getLogger(App.class);
public static Stage stage;
private static final int W = 380;
private static final int H = 480;
public static void main( String[] args ) {
launch(args);
}
@Override
public void start(Stage primaryStage) throws Exception {
stage = primaryStage;
String fxml = "fxml/login.fxml";
URL location = App.class.getClassLoader().getResource(fxml);
InputStream inputStream = App.class.getClassLoader().getResourceAsStream(fxml);
Pane pane = loadFXML(stage, location, inputStream);
if (pane == null) {
return;
}
stage.setWidth(W);
stage.setHeight(H);
primaryStage.getIcons().add(new Image("img/logo.png"));
popupWindow(pane, stage, "cc聊天");
connectNetty();
stage.setOnCloseRequest(event -> {
ChatNettyClient.disconnect();
System.exit(0);
}
private void connectNetty() {
Timer timer = new Timer();
TimerTask timerTask = new TimerTask() {
@Override
public void run() {
if (!ChatNettyClient.ONLINE) {
ChatNettyClient.connectServer();
} else {
SendUtil.sendHeart();
}
}
};
timer.scheduleAtFixedRate(timerTask,0,6000);
}
private static void showStage(Pane root, Stage stage, Stage parent, String stageName) {
Scene scene = new Scene(root);
//if (null != App.stage && null != stage && !App.stage.equals(stage)) {
stage.setTitle(stageName);
//}
stage.setScene(scene);
stage.centerOnScreen();
stage.setResizable(false);
if (parent != null && !stage.equals(parent)) {
stage.initModality(Modality.WINDOW_MODAL);
//stage.initOwner(parent);
}
stage.show();
}
public static void popupWindow(Pane root, Stage stage, String stageName) {
showStage(root, stage, App.stage, stageName);
}
/**
* 加载fxml
* @param stage
* @param location
* @param in
* @return
*/
public static Pane loadFXML(Stage stage, URL location, InputStream in) {
FXMLLoader loader = new FXMLLoader();
//loader.setControllerFactory(context::getBean);
loader.setBuilderFactory(new JavaFXBuilderFactory());
loader.setLocation(location);
try {
Pane pane = (Pane) loader.load(in);
FxPageInterface page = (FxPageInterface) loader.getController();
page.setMain(stage);
File tempFile = new File(location.getPath().trim());
String fileName = tempFile.getName();
pages.put(fileName, page);
if (loader.getController() instanceof UploadCallBackInterface) {
pagesUpload.put(fileName, loader.getController());
}
if (loader.getController() instanceof UserAvatarInLogin) {
getAvatar.put(fileName, loader.getController());
}
return pane;
} catch (Exception ex) {
ex.printStackTrace();
return null;
}
}
}
这样我们的项目客户端与服务器通信就可以了。