聊天室界面
1、引入pom文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.0</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.demo</groupId>
<artifactId>netty-chat</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--我这里使用的是jfinal-enjoy模板引擎-->
<dependency>
<groupId>com.jfinal</groupId>
<artifactId>enjoy</artifactId>
<version>5.1.2</version>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.65.Final</version> <!-- 使用最新版本 -->
</dependency>
</dependencies>
</project>
2、enjoy模板引擎配置
package com.demo.config;
import com.jfinal.template.Engine;
import com.jfinal.template.ext.spring.JFinalViewResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class EnjoyConfig {
@Bean(name = "jfinalViewResolver")
public JFinalViewResolver getJFinalViewResolver() {
// 创建用于整合 spring boot 的 ViewResolver 扩展对象
JFinalViewResolver jfr = new JFinalViewResolver();
// 对 spring boot 进行配置
jfr.setSuffix(".html");
jfr.setContentType("text/html;charset=UTF-8");
jfr.setOrder(0);
// 设置在模板中可通过 #(session.value) 访问 session 中的数据
jfr.setSessionInView(true);
// 获取 engine 对象,对 enjoy 模板引擎进行配置,配置方式与前面章节完全一样
Engine engine = JFinalViewResolver.engine;
// 热加载配置能对后续配置产生影响,需要放在最前面
engine.setDevMode(true);
// 使用 ClassPathSourceFactory 从 class path 与 jar 包中加载模板文件
engine.setToClassPathSourceFactory();
// 在使用 ClassPathSourceFactory 时要使用 setBaseTemplatePath
// 设置静态资源路径在 /static 下
engine.setBaseTemplatePath("/static/");
return jfr;
}
}
3、netty服务端
package com.demo.netty;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import io.netty.util.concurrent.DefaultEventExecutorGroup;
import io.netty.util.concurrent.EventExecutorGroup;
import io.netty.util.concurrent.GlobalEventExecutor;
public class NettyChatServer {
private final int port;
private final EventExecutorGroup eventExecutorGroup;
private final ChannelGroup channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
public NettyChatServer(int port) {
this.port = port;
this.eventExecutorGroup = new DefaultEventExecutorGroup(4); // 用于在handler中处理耗时任务
}
public void start() throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
// 字符串编解码器,用于将消息编码成字符串和解码成字符串
pipeline.addLast(new HttpServerCodec());
pipeline.addLast(new HttpObjectAggregator(65536));
pipeline.addLast(new WebSocketServerProtocolHandler("/websocket"));
pipeline.addLast(eventExecutorGroup, new ChatServerHandler(channelGroup));
// 添加自定义的聊天处理器
// pipeline.addLast(eventExecutorGroup, new ChatServerHandler(channelGroup));
}
});
ChannelFuture channelFuture = serverBootstrap.bind(port).sync();
System.out.println("Chat Server started on port " + port);
channelFuture.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
public void stop() {
// 停止服务器
}
public static void main(String[] args) {
int port = 8888;
NettyChatServer chatServer = new NettyChatServer(port);
try {
chatServer.start();
} catch (Exception e) {
e.printStackTrace();
}
}
}
4、netty消息处理器
package com.demo.netty;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.group.ChannelGroup;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketFrame;
public class ChatServerHandler extends SimpleChannelInboundHandler<WebSocketFrame> {
private final ChannelGroup channelGroup;
public ChatServerHandler(ChannelGroup channelGroup) {
this.channelGroup = channelGroup;
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, WebSocketFrame frame) throws Exception {
// 处理WebSocket消息
if (frame instanceof TextWebSocketFrame) {
TextWebSocketFrame textFrame = (TextWebSocketFrame) frame;
String message = textFrame.text();
// 在服务器控制台上输出消息
System.out.println("Received message: " + message);
// 将消息广播给所有连接的客户端
channelGroup.writeAndFlush(new TextWebSocketFrame(message));
}
}
@Override
public void handlerAdded(ChannelHandlerContext ctx) {
// 新客户端连接时添加到ChannelGroup
channelGroup.add(ctx.channel());
}
@Override
public void handlerRemoved(ChannelHandlerContext ctx) {
// 客户端断开连接时从ChannelGroup中移除
channelGroup.remove(ctx.channel());
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
// 异常处理
cause.printStackTrace();
ctx.close();
}
}
5、controller
package com.demo.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class ChatController {
@RequestMapping("/chat")
public String main() {
return "main";
}
}
6、main.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>简单聊天室</title>
<!-- 引入Bootstrap CSS文件 -->
<link href="./bootstarp/css/bootstrap.min.css" rel="stylesheet">
<link href="./css/main.css" rel="stylesheet">
</head>
<body>
<div class="container">
<div class="row">
<div class="col-md-8 offset-md-2">
<div id="chat-container">
<div id="chat-header">
<h2>简单聊天室</h2>
</div>
<div id="chat-box">
<!-- 示例聊天消息 -->
<!-- <div class="message">
<div class="avatar">A</div>
<div class="message-content">
<div class="sender-name">User 1</div>
<div class="message-text">Hello, how are you?</div>
</div>
</div>-->
<!-- 示例聊天消息结束 -->
</div>
<div id="message-buttons">
<input type="text" id="message-input" placeholder="输入消息...">
<input type="file" id="file-input">
<button class="btn btn-primary" onclick="sendMessage()">发送文本</button>
<button class="btn btn-primary" onclick="sendFile()">发送文件</button>
<button class="btn btn-danger" id="clear-button" onclick="clearChat()">清空聊天</button>
</div>
</div>
</div>
</div>
</div>
<!-- 引入jQuery -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<!-- 引入Bootstrap JS文件 -->
<script src="./bootstarp/js/bootstrap.min.js"></script>
<script src="./js/main.js"></script>
</body>
</html>
7、main.css
body {
background-color: #f2f2f2;
font-family: Arial, Helvetica, sans-serif;
margin: 0;
padding: 0;
}
#chat-container {
max-width: 600px;
margin: 20px auto;
background-color: #fff;
border-radius: 5px;
box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
#chat-header {
background-color: #007BFF;
color: #fff;
padding: 10px;
text-align: center;
border-top-left-radius: 5px;
border-top-right-radius: 5px;
}
#chat-box {
max-height: 100vh;
width: 100%;
overflow-y: scroll;
height: 600px;
}
.message {
display: flex;
margin: 10px;
padding: 10px;
border-bottom: 1px solid #ccc;
}
.avatar {
width: 50px;
height: 50px;
background-color: #007BFF;
color: #fff;
border-radius: 50%;
text-align: center;
line-height: 50px;
margin-right: 10px;
}
.message-content {
flex-grow: 3;
}
.sender-name {
font-weight: bold;
margin-bottom: 5px;
}
.message-text {
word-wrap: break-word;
}
#message-input, #file-input {
width: 100%;
padding: 10px;
border: 1px solid #ccc;
}
#message-buttons {
padding: 10px;
text-align: center;
}
button {
padding: 10px 20px;
background-color: #007BFF;
color: #fff;
border: none;
cursor: pointer;
margin-right: 10px;
}
button:hover {
background-color: #0056b3;
}
#clear-button {
background-color: #dc3545;
}
@media (max-width: 768px) {
#chat-container {
margin-top: 10px;
}
#chat-box {
max-height: 200px;
}
}
8、main.js
// WebSocket连接
const socket = new WebSocket('ws://localhost:8888/websocket'); // 请将 your_netty_server_address 替换为实际的Netty WebSocket服务器地址
socket.addEventListener('open', (event) => {
console.log('WebSocket连接已建立');
});
socket.addEventListener('message', (event) => {
// 解析消息
console.log(event)
const data = JSON.parse(event.data);
const chatBox = document.getElementById('chat-box');
if (data.type === 'text') {
// 接收到文本消息
const messageDiv = document.createElement('div');
messageDiv.classList.add('message');
const avatarDiv = document.createElement('div');
avatarDiv.classList.add('avatar');
avatarDiv.textContent = data.sender.charAt(0); // 使用发送者的首字母作为头像内容
const messageContentDiv = document.createElement('div');
messageContentDiv.classList.add('message-content');
const senderNameDiv = document.createElement('div');
senderNameDiv.classList.add('sender-name');
senderNameDiv.textContent = data.sender;
const messageTextDiv = document.createElement('div');
messageTextDiv.classList.add('message-text');
messageTextDiv.textContent = data.message;
messageContentDiv.appendChild(senderNameDiv);
messageContentDiv.appendChild(messageTextDiv);
messageDiv.appendChild(avatarDiv);
messageDiv.appendChild(messageContentDiv);
chatBox.appendChild(messageDiv);
} else if (data.type === 'file') {
// 接收到文件消息
const fileURL = URL.createObjectURL(data.file);
const messageDiv = document.createElement('div');
messageDiv.classList.add('message');
const avatarDiv = document.createElement('div');
avatarDiv.classList.add('avatar');
avatarDiv.textContent = data.sender.charAt(0); // 使用发送者的首字母作为头像内容
const messageContentDiv = document.createElement('div');
messageContentDiv.classList.add('message-content');
const senderNameDiv = document.createElement('div');
senderNameDiv.classList.add('sender-name');
senderNameDiv.textContent = data.sender;
const fileLink = document.createElement('a');
fileLink.href = fileURL;
fileLink.textContent = '下载文件';
fileLink.download = data.fileName;
messageContentDiv.appendChild(senderNameDiv);
messageContentDiv.appendChild(fileLink);
messageDiv.appendChild(avatarDiv);
messageDiv.appendChild(messageContentDiv);
chatBox.appendChild(messageDiv);
}
// 滚动到最新消息
chatBox.scrollTop = chatBox.scrollHeight;
});
function sendMessage() {
const messageInput = document.getElementById('message-input');
const message = messageInput.value.trim();
if (message !== '') {
// 发送文本消息到服务器
const data = {
sender: 'YSK',
type: 'text',
message: message
};
socket.send(JSON.stringify(data));
// 清空输入框
messageInput.value = '';
}
}
function sendFile() {
const fileInput = document.getElementById('file-input');
const file = fileInput.files[0];
if (file) {
// 发送文件到服务器
const reader = new FileReader();
reader.onload = function (event) {
const data = {
type: 'file',
fileName: file.name,
file: event.target.result
};
socket.send(JSON.stringify(data));
};
reader.readAsArrayBuffer(file);
// 清空文件选择框
fileInput.value = '';
}
}
function clearChat() {
const chatBox = document.getElementById('chat-box');
chatBox.innerHTML = '';
}
// 监听Enter键,发送文本消息
const messageInput = document.getElementById('message-input');
messageInput.addEventListener('keyup', function (event) {
if (event.key === 'Enter') {
sendMessage();
}
});
9、springbootApplication
package com.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.freemarker.FreeMarkerAutoConfiguration;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.web.servlet.ServletComponentScan;
@SpringBootApplication(exclude = {FreeMarkerAutoConfiguration.class, DataSourceAutoConfiguration.class})
public class SpringbootApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootApplication.class, args);
}
}
10、启动springbootApplication、NettyChatServer
浏览器访问 http://localhost/chat
![在这里插入图片描述](https://img-blog.csdnimg.cn/d7a4e7521ae54006892e7adfda852149.png