故事叙述
我们的主人公 小凯。 小凯是一家公司的屌丝程序员,日常与代码为伴。这一天 公司来了个实习生 小雪,小雪年轻漂亮,让小凯心生倾慕,夜深人静时,小凯辗转难眠,终于鼓起勇气,想给小雪发一条消息:“在干嘛?”
那么我们来帮小凯实现这个愿望
一 、先来快速创建一个 vue 前端项目,给小凯提供一个发消息的页面
- 创建vue项目小连招
vue create my-project //创建项目my-project
npm install vue-router //安装路由
npm run serve //启动项目
- 我们来用AI快速生成一个Chat.vue页面并设置主题 “可爱粉”,小凯已经着急了
<script setup>
import { ref, onMounted } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
const userId = ref('')
const messageInput = ref('')
const messages = ref([])
onMounted(() => {
userId.value = route.query.id || ''
if (!userId.value) {
alert('请在URL中添加?id=1来标识用户身份')
}
})
const sendMessage = () => {
if (messageInput.value.trim()) {
messages.value.push({
id: Date.now(),
text: messageInput.value,
sender: userId.value,
timestamp: new Date().toLocaleTimeString()
})
messageInput.value = ''
}
}
const handleEnter = (e) => {
if (e.key === 'Enter') {
sendMessage()
}
}
</script>
<template>
<div class="chat-container">
<div class="chat-header">
<div class="header-info">
<span class="chat-title">{{ userId }}</span>
</div>
</div>
<div class="messages-area">
<div v-for="msg in messages" :key="msg.id"
:class="['message-item', msg.sender === userId ? 'message-self' : 'message-other']">
<div class="message-bubble">
<p class="message-text">{{ msg.text }}</p>
<span class="message-time">{{ msg.timestamp }}</span>
</div>
</div>
</div>
<div class="input-area">
<textarea
v-model="messageInput"
@keydown="handleEnter"
placeholder="输入消息..."
class="message-input"
rows="3"
></textarea>
<div class="send-section">
<button class="close-btn">关闭</button>
<div class="send-dropdown">
<button class="send-btn" @click="sendMessage">发送</button>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.chat-container {
display: flex;
flex-direction: column;
width: 50vw;
height: 50vh;
background: linear-gradient(135deg, #ffe6f0 0%, #fff0f5 100%);
font-family: 'Microsoft YaHei', sans-serif;
border-radius: 16px;
box-shadow: 0 8px 32px rgba(255, 143, 171, 0.25);
overflow: hidden;
}
.chat-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 20px;
background: linear-gradient(to right, #ffc8dd, #ffb3c6);
box-shadow: 0 2px 8px rgba(255, 182, 193, 0.3);
}
.header-info {
display: flex;
align-items: center;
gap: 12px;
}
.chat-title {
font-size: 18px;
font-weight: bold;
color: #8b4513;
}
.messages-area {
flex: 1;
overflow-y: auto;
padding: 20px;
display: flex;
flex-direction: column;
gap: 12px;
}
.message-item {
display: flex;
margin-bottom: 8px;
}
.message-self {
justify-content: flex-end;
}
.message-other {
justify-content: flex-start;
}
.message-bubble {
max-width: 60%;
padding: 12px 16px;
border-radius: 18px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
position: relative;
}
.message-self .message-bubble {
background: linear-gradient(135deg, #ffb3c6, #ff8fab);
color: white;
border-bottom-right-radius: 4px;
}
.message-other .message-bubble {
background: white;
color: #333;
border-bottom-left-radius: 4px;
}
.message-text {
margin: 0 0 4px 0;
font-size: 14px;
line-height: 1.5;
word-wrap: break-word;
}
.message-time {
font-size: 11px;
opacity: 0.7;
}
.input-area {
padding: 16px 20px;
background: rgba(255, 255, 255, 0.9);
border-top: 2px solid rgba(255, 182, 193, 0.3);
}
.message-input {
width: 100%;
border: 2px solid #ffd1dc;
border-radius: 12px;
resize: none;
font-size: 14px;
font-family: inherit;
outline: none;
transition: all 0.3s;
background: white;
}
.message-input:focus {
border-color: #ffb3c6;
box-shadow: 0 0 8px rgba(255, 179, 198, 0.4);
}
.send-section {
display: flex;
justify-content: flex-end;
align-items: center;
gap: 12px;
margin-top: 12px;
}
.close-btn {
padding: 10px 24px;
background: white;
border: 2px solid #ddd;
border-radius: 8px;
color: #666;
font-size: 14px;
cursor: pointer;
transition: all 0.3s;
}
.close-btn:hover {
background: #f5f5f5;
border-color: #ccc;
}
.send-dropdown {
display: flex;
align-items: center;
}
.send-btn {
padding: 10px 28px;
background: linear-gradient(to right, #ffb3c6, #ff8fab);
border: none;
border-radius: 8px 0 0 8px;
color: white;
font-size: 14px;
font-weight: bold;
cursor: pointer;
transition: all 0.3s;
}
.send-btn:hover {
background: linear-gradient(to right, #ff8fab, #ff5d8f);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(255, 143, 171, 0.4);
}
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: rgba(255, 230, 240, 0.5);
}
::-webkit-scrollbar-thumb {
background: #ffb3c6;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #ff8fab;
}
</style>
3.修改mian.js 添加路由
import { createApp } from 'vue'
import { createWebHistory } from 'vue-router'
import { createRouter } from 'vue-router'
import App from './App.vue'
import Chat from './components/Chat.vue'
const routes = [
{ path: '/Chat', component: Chat }
]
const router = createRouter({
history: createWebHistory(),
routes
})
const app = createApp(App)
app.use(router)
app.mount('#app')
- 修改 App.vue,引入路由试图,并调整一下样式
<template>
<div id="app">
<router-view />
</div>
</template>
<script>
export default {
name: 'App'
}
</script>
<style>
html, body {
margin: 0;
padding: 0;
height: 100%;
}
#app {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
}
</style>
小凯发消息的页面已经搭好,我们来看看 ,http://localhost:8080/Chat?id=111

二、快速创建后端项目,让小凯能把消息发送给小雪
- 来个最新配置 小凯已经急的不行
idea-> 新建-> 项目 ->SpringBoot-> maven->JDK21-> Springboot4.1.0 - 将来小凯可能要发更多消息给更多人 ,我们来引入通信神器 Netty
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.1.0</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.15.2</version>
</dependency>
</dependencies>
- Netty三大组件 Channel(通道)、EventLoop(事件循环)和 ChannelFuture(异步结果),来快速构建一个 NettyConfig.java
@Configuration
public class NettyConfig {
@Value("${netty.port:8090}")
private int port;
//Boss 线程组
private EventLoopGroup bossGroup;
//Worker 线程组
private EventLoopGroup workerGroup;
//通道 Future
private ChannelFuture channelFuture;
//@PostConstruct:Spring 容器初始化完成后自动调用此方法
@PostConstruct
public void start() {
new Thread(() -> {
try {
// 1个线程,专门接收连接
bossGroup = new NioEventLoopGroup(1);
// 默认线程数,处理读写业务 默认 CPU核心数×2
workerGroup = new NioEventLoopGroup();
//启动引导类
ServerBootstrap bootstrap = new ServerBootstrap();
//设置引导参数
bootstrap.group(bossGroup, workerGroup) //绑定两个线程组
.channel(NioServerSocketChannel.class) // 使用 NIO 的 ServerSocket 通道
.option(ChannelOption.SO_BACKLOG, 128) // 连接等待队列最大长度 128
.childOption(ChannelOption.SO_KEEPALIVE, true) // 保持长连接,定期探测
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
// pipeline 是 Netty 的责任链,数据进来会依次经过 pipeline 中的每个 Handler。
// 每个新连接都加上自定义处理器
ch.pipeline().addLast(new NettyServerHandler());
}
});
// 绑定端口,sync() 等待绑定完成
channelFuture = bootstrap.bind(port).sync();
System.out.println("Netty 服务器启动,端口: " + port);
// 阻塞,直到通道关闭
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "netty-server").start();
}
//@PreDestroy:Spring 容器销毁前自动调用。
@PreDestroy
public void stop() {
if (channelFuture != null) {
// 关闭通道
channelFuture.channel().close();
}
if (bossGroup != null) {
//优雅关闭 boss 线程组(处理完当前任务再停止)
bossGroup.shutdownGracefully();
}
if (workerGroup != null) {
//优雅关闭 worker 线程组(处理完当前任务再停止)
workerGroup.shutdownGracefully();
}
}
}
4.创建一个 NettyServerHandler 处理器
public class NettyServerHandler extends SimpleChannelInboundHandler<String> {
//客户端连接时触发
@Override
public void channelActive(ChannelHandlerContext ctx) {
//打印客户端信息
System.out.println("客户端连接: " + ctx.channel().remoteAddress());
}
//收到消息时触发
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) {
System.out.println("收到消息: " + msg);
//Unpooled 是 Netty 中一个专门用来创建和管理 ByteBuf 的工具类。
//copiedBuffer 方法会创建一个包含给定数组内容的新的 ByteBuf。
ByteBuf response = Unpooled.copiedBuffer("服务器已收到: " + msg, StandardCharsets.UTF_8);
//写入缓冲区 刷出到网络
ctx.writeAndFlush(response);
}
//发生异常时触发
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
}
- 增加配置application.yml
netty:
port: 8129
- 启动项目

三、快速实现前后端通信功能
- 先来优化一下Ai生成的聊天页面Chat.vue,让它显示名字,而并非id
import { ref, computed,onMounted }
// ... existing code ...
const userMap = {
'1': '小凯',
'2': '小雪'
}
const userName = computed(() => userMap[userId.value] || '未知用户')
// ... existing code ...
<span class="chat-title">{{ userName }}</span>
- http://localhost:8080/Chat?id=1,http://localhost:8080/Chat?id=2 小凯和小雪名字已经出来了

- 连接 ws,以及各个ws各个状态操作
//目标用户id
const targetId = computed(() => userId.value === '1' ? '2' : '1')
//目标用户名字
const targetName = computed(() => userMap[targetId.value] || '对方')
const ws = ref(null)
const messagesArea = ref(null)
const connectWebSocket = () => {
ws.value = new WebSocket('ws://localhost:8129/ws')
ws.value.onopen = () => {
console.log('WebSocket 连接成功')
ws.value.send(JSON.stringify({
type: 'register',
userId: userId.value
}))
}
ws.value.onmessage = (event) => {
const data = JSON.parse(event.data)
if (data.type === 'chat') {
messages.value.push({
id: Date.now(),
text: data.content,
sender: data.fromId,
senderName: data.fromName,
timestamp: new Date(data.timestamp).toLocaleTimeString()
})
scrollToBottom()
} else if (data.type === 'system') {
messages.value.push({
id: Date.now(),
text: data.content,
sender: 'system',
timestamp: new Date().toLocaleTimeString()
})
scrollToBottom()
}
}
ws.value.onclose = () => {
console.log('WebSocket 连接关闭')
}
ws.value.onerror = (err) => {
console.error('WebSocket 错误:', err)
}
- 增加一个滚动到消息列表底部的常用工具函数scrollToBottom,修改发送消息函数sendMessage
import { ref, computed,onMounted,nextTick } from 'vue'
// ... existing code ...
const scrollToBottom = async () => {
await nextTick()
if (messagesArea.value) {
messagesArea.value.scrollTop = messagesArea.value.scrollHeight
}
}
//发送消息
const sendMessage = () => {
if (messageInput.value.trim() && ws.value && ws.value.readyState === WebSocket.OPEN) {
const chatMessage = {
type: 'chat',
fromId: userId.value,
fromName: userName.value,
toId: targetId.value,
content: messageInput.value
}
ws.value.send(JSON.stringify(chatMessage))
messages.value.push({
id: Date.now(),
text: messageInput.value,
sender: userId.value,
timestamp: new Date().toLocaleTimeString()
})
messageInput.value = ''
scrollToBottom()
}
}
// ... existing code ...
<div class="messages-area" ref="messagesArea">
- 设置ws连接时机,以及优化 窗口
onMounted(() => {
userId.value = route.query.id || ''
if (!userId.value) {
alert('请在URL中添加?id=1来标识用户身份')
}else{
connectWebSocket()
}
})
// ... existing code ...
<span class="chat-title">{{ userName }}</span>
<span class="chat-subtitle">正在和 {{ targetName }} 聊天</span>
- 后端NettyConfig中加入三个处理器,并增加 userChannels 来保存 注册的用户信息
public static final ConcurrentHashMap<String, Channel> userChannels = new ConcurrentHashMap<>();
//... existing code ...
//将HTTP请求的字节流解码成HttpRequest对象,让后端能读到“小凯”发的消息内容。
ch.pipeline().addLast(new HttpServerCodec());
//将 HTTP 请求中的多个零散片段(如请求头和请求体可能被拆分多次到达)聚合成一个完整的 FullHttpRequest 对象,方便后续逻辑一次性处理所有信息。
ch.pipeline().addLast(new HttpObjectAggregator(65536));
// WebSocket 协议升级处理器:处理握手和帧编解码
ch.pipeline().addLast(new WebSocketServerProtocolHandler("/ws"));
- 修改 自定义处理器 的收到消息逻辑
protected void channelRead0(ChannelHandlerContext ctx, WebSocketFrame frame) {
//先用 instanceof 判断是否为文本帧(TextWebSocketFrame)这里只处理文本消息
if (frame instanceof TextWebSocketFrame textFrame) {
try {
//获取消息的纯文本内容(JSON 格式)
String text = textFrame.text();
// 使用 Jackson 的 ObjectMapper 将 JSON 字符串解析为 JsonNode 树节点。
JsonNode jsonNode = objectMapper.readTree(text);
//从 JSON 中提取 type 字段,用于后续路由分发
String type = jsonNode.path("type").asText();
if ("register".equals(type)) {
currentUserId = jsonNode.path("userId").asText();
NettyConfig.userChannels.put(currentUserId, ctx.channel());
System.out.println("用户注册: " + currentUserId);
return;
}
if ("chat".equals(type)) {
String fromId = jsonNode.path("fromId").asText();
String toId = jsonNode.path("toId").asText();
String content = jsonNode.path("content").asText();
String fromName = jsonNode.path("fromName").asText();
String message = objectMapper.writeValueAsString(Map.of(
"type", "chat",
"fromId", fromId,
"fromName", fromName,
"content", content,
"timestamp", System.currentTimeMillis()
));
//查找目标用户连接
Channel targetChannel = NettyConfig.userChannels.get(toId);
//如果目标用户在线,则发送消息
if (targetChannel != null && targetChannel.isActive()) {
//发送消息给目标用户
targetChannel.writeAndFlush(new TextWebSocketFrame(message));
System.out.println("消息转发: " + fromId + " -> " + toId + " : " + content);
} else {
//如果目标用户不在线,则返回系统消息给发送者
ctx.channel().writeAndFlush(new TextWebSocketFrame(
objectMapper.writeValueAsString(Map.of(
"type", "system",
"content", "对方不在线"
))
));
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
整体逻辑梳理
- 小凯打开 http://localhost:8080/Chat?id=1,前端通过 WebSocket 连接 ws://localhost:8129/ws,发送 register 消息注册 userId=1
- 小雪打开 http://localhost:8080/Chat?id=2,同样连接并注册 userId=2
- 小凯发消息时,前端发送 JSON {type:“chat”, fromId:“1”, fromName:“小凯”, toId:“2”, content:“在干嘛”}
- Netty 服务端收到后,查找 userId=2 对应的 Channel,将消息转发给小雪
- 小雪的前端收到消息后显示在聊天窗口,小雪也可以同样方式回复给小凯
好,迫不及待的小凯 终于可以 给小雪 发消息了!!


小凯发送消息之后,终于美滋滋的进入了梦乡!!!
142

被折叠的 条评论
为什么被折叠?



