最近在项目中需要用到 WebSocket 进行通信。于是简单学习了一下,并且搭建了一个简易的聊天室,在此记录下并分享。
后端使用了 SpringBoot 集成的 WebSocket,前端使用了 Next.js。
PS:每一步都贴了详细的代码和注释,复制到项目里就能跑起来~
什么是 WebSocket
WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议,它使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
Websocket 的优点在于:
- 可以与任何 Web 浏览器一起使用
- 传递二进制数据支持 JSON,XML 等格式
- 具有较低的延迟,从而可以实现更快的通信
- 在客户端和服务器之间保持长时间的连接,从而可以减少 HTTP 请求的数量。
SpringBoot 整合 WebSocket
话不多说,直接上代码。
首先创建 SpringBoot 项目,引入 WebSocket 依赖。
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
创建 WebSocketConfig:
java
@Configuration
@EnableWebSocketMessageBroker
@CrossOrigin(origins = "http://localhost:3000")
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
// 配置WebSocket的端点,即客户端将连接到的端点
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws").setAllowedOrigins("http://localhost:3000");
}
// 配置消息代理,负责将消息从一个端点路由到另一个端点
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
// 指定应用程序消息的前缀
registry.setApplicationDestinationPrefixes("/app");
// 启用一个简单的消息代理,用于向客户端广播消息到特定的主题(topic)
registry.enableSimpleBroker("/topic");
}
}
定义 Message 类和 MessageType,用来表示消息类型。
java
public class ChatMessage {
private String content;
private String sender;
private MessageType type;
}
public enum MessageType {
CHAT,
JOIN,
LEAVE
}
创建 Controller:
java
@Controller
public class ChatController {
// 当客户端发送消息到 "/chat.sendMessage" 时,将消息广播到 "/topic/public"
@MessageMapping("/chat.sendMessage")
@SendTo("/topic/public")
public ChatMessage sendMessage(@Payload ChatMessage chatMessage) {
// 将收到的消息原样返回,用于广播给所有订阅 "/topic/public" 的客户端
return chatMessage;
}
// 当客户端发送消息到 "/chat.addUser" 时,将用户加入聊天,并广播到 "/topic/public"
@MessageMapping("/chat.addUser")
@SendTo("/topic/public")
public ChatMessage addUser(@Payload ChatMessage chatMessage, SimpMessageHeaderAccessor headerAccessor) {
// 将用户添加到会话属性中,以便识别用户
headerAccessor.getSessionAttributes().put("username", chatMessage.getSender());
// 将加入聊天的消息广播给所有订阅 "/topic/public" 的客户端
return chatMessage;
}
}
创建一个事件监听器,用来处理会话断开连接的事件。
java
@Component
@RequiredArgsConstructor
@Slf4j
public class WebSocketEventListener {
// 用于向客户端发送消息的操作类
private final SimpMessageSendingOperations messageSendingOperations;
// 监听 WebSocket 会话断开连接的事件
@EventListener
public void handleWebSocketDisconnectListener(SessionDisconnectEvent event) {
// 从事件中获取 StompHeaderAccessor,用于处理 Stomp 消息头
StompHeaderAccessor headerAccessor = StompHeaderAccessor.wrap(event.getMessage());
// 从会话属性中获取用户名
String username = (String) Objects.requireNonNull(headerAccessor.getSessionAttributes()).get("username");
// 如果用户名不为空,表示用户已经加入聊天
if (username != null) {
// 记录用户断开连接的日志
log.info("User disconnected: {}", username);
// 创建一个消息,表示用户离开聊天
var chatMessage = ChatMessage.builder()
.type(MessageType.LEAVE)
.sender(username)
.build();
// 将离开聊天的消息广播到 "/topic/public",通知其他在线用户
messageSendingOperations.convertAndSend("/topic/public", chatMessage);
}
}
}
服务端就创建完成了,接下来创建前端。
前端连接 WebSocket
创建一个 Next.js 项目,然后安装 @stomp/stompjs
包。
bash
npm i @stomp/stompjs
前端代码比较简单,为了演示,我把代码放在了同一个文件中:
tsx
'use client';
import { Client, StompConfig } from '@stomp/stompjs';
import { useSearchParams } from 'next/navigation';
import { useCallback, useEffect, useState } from 'react';
type Message = {
content: string;
sender: string;
type: 'JOIN' | 'CHAT' | 'LEAVE';
};
const stompConfig: StompConfig = {
brokerURL: 'ws://localhost:8080/ws',
debug: function (str: string) {
console.log('STOMP: ' + str);
},
reconnectDelay: 200,
};
const ChatPage = () => {
const searchParams = useSearchParams();
const username = searchParams.get('username');
const [stompClient, setStompClient] = useState(() => new Client(stompConfig));
const [messageList, setMessageList] = useState<Message[]>([]);
const [message, setMessage] = useState('');
const handleMessage = useCallback((msg: any) => {
const newMessage = JSON.parse(msg.body);
setMessageList((prevMessages) => [...prevMessages, newMessage]);
}, []);
const handleSendMessage = () => {
const chatMessage = {
sender: username,
content: message,
type: 'CHAT',
};
stompClient.publish({
destination: '/app/chat.sendMessage',
body: JSON.stringify(chatMessage),
});
setMessage('');
};
const handleInputChange = (e: any) => {
setMessage(e.target.value);
};
useEffect(() => {
stompClient.activate();
stompClient.onConnect = () => {
stompClient.publish({
destination: '/app/chat.addUser',
body: JSON.stringify({ sender: username, type: 'JOIN' }),
});
stompClient?.subscribe('/topic/public', handleMessage);
};
return () => {
stompClient?.deactivate();
};
}, [stompClient, handleMessage, username]);
return (
<div className="flex flex-col max-w-xl h-screen m-auto">
<h1 className="text-2xl font-medium text-center my-4">Spring Boot Websocket Chat Demo</h1>
<div className="flex-grow px-4">
<div className="border-t">
{messageList.map((message, index) => (
<div key={index} className="py-3">
{message.type === 'JOIN' && (
<p className="text-center text-gray-400 text-sm">{`${
message.sender === username ? '你' : message.sender
} 加入了聊天室`}</p>
)}
{message.type === 'CHAT' && (
<div className={`${message.sender === username ? 'text-right' : 'text-left'} mb-2`}>
<p className="text-sm font-bold">{message.sender === username ? '你' : message.sender}</p>
<p className="bg-blue-200 rounded p-2 inline-block">{message.content}</p>
</div>
)}
{message.type === 'LEAVE' && (
<p className="text-center text-gray-400 text-sm">{`${message.sender} 离开了聊天室`}</p>
)}
</div>
))}
</div>
</div>
<div className="m-4 flex items-center">
<input
type="text"
value={message}
onChange={handleInputChange}
className="border rounded px-3 py-2 mr-2 focus:outline-none focus:ring focus:border-blue-300 flex-grow"
placeholder="Type your message..."
/>
<button
onClick={handleSendMessage}
className="bg-blue-500 text-white px-4 py-2 rounded transition duration-300 hover:bg-blue-600 focus:outline-none focus:ring focus:border-blue-300"
>
Send
</button>
</div>
</div>
);
};
export default ChatPage;
测试运行效果
启动项目,访问 http://localhost:3000/chat?username=xxx
。
这里依次打开三个浏览器窗口:
bash
http://localhost:3000/chat?username=喜羊羊
http://localhost:3000/chat?username=美羊羊
http://localhost:3000/chat?username=沸羊羊
通过查看浏览器的控制台,可以看到输出信息,主要是 STOMP 打印的 debug 日志,说明连接成功了。
下面是运行效果:
通过最左侧的窗口可以看到,每当有新用户加入,每个窗口都会显示出一条信息。之后在任意窗口中发送信息,也会广播给所有用户。
最后,因为我们实现了 handleWebSocketDisconnectListener
,在用户断开连接时,也会向所有用户发送消息,效果如下:
总结
Spring Boot 整合 WebSocket 还是比较简单的,只需要引入 Spring Boot 的 WebSocket 依赖,并创建 WebSocketConfig 和 ChatController 类,一个最基本的功能就实现了。
客户端可以使用 @stomp/stompjs
包来连接 WebSocket 服务,通过订阅和发布消息实现前后端的实时通信。
当然本文中的实例只是一个很简单的版本,并不支持消息存储、私聊等功能,如果大家有兴趣,可以点赞或留言,我会弄个 v2 版本出来。感谢 🙏