代码地址
仓库地址
聊天室
创建springboot项目
因为我的NettyServer 是8080 所以我把服务端口改为了8081
引入netty 依赖
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.92.Final</version>
</dependency>
IMServerHandler 处理用户连接以及发送消息等功能的逻辑
package site.zhourui.netty.handler;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import java.util.HashMap;
import java.util.Map;
/**
* @author zr 2024/8/13
*/
public class IMServerHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
private static final Map<Channel, String> userNicknames = new HashMap<>();
private static final ObjectMapper objectMapper = new ObjectMapper();
@Override
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame frame) throws Exception {
String text = frame.text();
Map<String, String> data = objectMapper.readValue(text, HashMap.class);
String type = data.get("type");
if ("set_nickname".equals(type)) {
String nickname = data.get("nickname");
userNicknames.put(ctx.channel(), nickname);
System.out.println("User set nickname: " + nickname);
// 广播用户加入消息
broadcastMessage("系统", "用户 " + nickname + " 加入聊天");
} else if ("chat_message".equals(type)) {
String message = data.get("message");
String nickname = userNicknames.get(ctx.channel());
// 避免 nickname 未设置的情况
if (nickname == null) {
nickname = "Anonymous";
userNicknames.put(ctx.channel(), nickname);
}
// 广播聊天消息
broadcastMessage(nickname, message);
}
}
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
System.out.println("新用户连接: " + ctx.channel().id().asShortText());
}
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
String nickname = userNicknames.remove(ctx.channel());
if (nickname != null) {
System.out.println("User disconnected: " + nickname);
// 广播用户离开消息
broadcastMessage("系统", "用户 " + nickname + " 离开聊天");
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
private void broadcastMessage(String senderNickname, String message) {
for (Channel channel : userNicknames.keySet()) {
try {
Map<String, String> response = new HashMap<>();
response.put("nickname", senderNickname);
response.put("message", message);
channel.writeAndFlush(new TextWebSocketFrame(objectMapper.writeValueAsString(response)));
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
NettyServer 聊天室服务端
package site.zhourui.netty.server;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
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.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.handler.stream.ChunkedWriteHandler;
import site.zhourui.netty.handler.IMServerHandler;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author zr 2024/8/13
*/
public class NettyServer {
private final int port;
// 存储所有连接的用户
public static final ConcurrentHashMap<String, Channel> userChannels = new ConcurrentHashMap<>();
public NettyServer(int port) {
this.port = port;
}
public void run() throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new HttpServerCodec());
ch.pipeline().addLast(new ChunkedWriteHandler());
ch.pipeline().addLast(new HttpObjectAggregator(8192));
ch.pipeline().addLast(new WebSocketServerProtocolHandler("/ws"));
ch.pipeline().addLast(new StringDecoder());
ch.pipeline().addLast(new StringEncoder());
ch.pipeline().addLast(new IMServerHandler());
}
})
.option(ChannelOption.SO_BACKLOG, 128)
.childOption(ChannelOption.SO_KEEPALIVE, true);
ChannelFuture f = b.bind(port).sync();
System.out.println("Netty IM server started on port " + port);
f.channel().closeFuture().sync();
} finally {
workerGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
}
}
// 记录用户连接
public static void addUser(String userId, Channel channel) {
userChannels.put(userId, channel);
}
// 获取用户连接
public static Channel getUserChannel(String userId) {
return userChannels.get(userId);
}
// 移除用户连接
public static void removeUser(String userId) {
userChannels.remove(userId);
}
}
将NettyServer 标记为组件,随springboot启动
package site.zhourui.netty.component;
import org.springframework.stereotype.Component;
import site.zhourui.netty.server.NettyServer;
import javax.annotation.PostConstruct;
/**
* @author zr 2024/8/13
*/
@Component
public class NettyServerComponent {
@PostConstruct
public void startNettyServer() {
Thread nettyThread = new Thread(() -> {
try {
new NettyServer(8080).run();
} catch (Exception e) {
e.printStackTrace();
}
});
nettyThread.setDaemon(true); // 设置为守护线程
nettyThread.start();
}
}
chatClient.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebSocket IM Client</title>
<style>
#chat-container {
width: 50%;
margin: auto;
margin-top: 50px;
border: 1px solid #ccc;
padding: 20px;
border-radius: 5px;
}
#messages {
height: 300px;
overflow-y: scroll;
border: 1px solid #ccc;
padding: 10px;
margin-bottom: 20px;
}
#message-input {
width: 80%;
padding: 10px;
border-radius: 5px;
border: 1px solid #ccc;
}
#send-button {
padding: 10px 20px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
}
#send-button:hover {
background-color: #45a049;
}
/* 样式定义弹窗 */
#config-modal {
display: none;
position: fixed;
z-index: 1;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
}
#config-form {
background-color: #fff;
margin: 15% auto;
padding: 20px;
border: 1px solid #ccc;
width: 300px;
border-radius: 5px;
}
#config-form input {
width: 100%;
padding: 10px;
margin-bottom: 10px;
border: 1px solid #ccc;
border-radius: 5px;
}
#config-form button {
width: 100%;
padding: 10px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
}
#config-form button:hover {
background-color: #45a049;
}
</style>
</head>
<body>
<div id="config-modal">
<div id="config-form">
<input type="text" id="server-address" placeholder="Server Address" value="ws://localhost">
<input type="text" id="server-port" placeholder="Port" value="8080">
<input type="text" id="nickname" placeholder="Nickname">
<button id="connect-button">Connect</button>
</div>
</div>
<div id="chat-container">
<h1>WebSocket IM Client</h1>
<div id="messages"></div>
<input type="text" id="message-input" placeholder="Type your message here..." />
<button id="send-button">Send</button>
</div>
<script>
let socket;
let nickname = null;
// 显示配置对话框
const modal = document.getElementById('config-modal');
modal.style.display = 'block';
document.getElementById('connect-button').addEventListener('click', function() {
const serverAddress = document.getElementById('server-address').value;
const serverPort = document.getElementById('server-port').value;
nickname = document.getElementById('nickname').value;
if (!serverAddress || !serverPort || !nickname) {
alert("All fields are required!");
return;
}
// 创建 WebSocket 连接
socket = new WebSocket(`${serverAddress}:${serverPort}/ws`);
// 连接打开事件
socket.onopen = function() {
console.log('Connected to IM Server');
socket.send(JSON.stringify({ type: 'set_nickname', nickname: nickname }));
modal.style.display = 'none'; // 连接成功后隐藏配置对话框
};
// 处理收到的消息
socket.onmessage = function(event) {
const data = JSON.parse(event.data);
const senderNickname = data.nickname;
const message = data.message;
const messageDiv = document.createElement('div');
if (senderNickname === nickname) {
messageDiv.textContent = `me: ${message}`;
messageDiv.style.color = 'blue'; // 给自己的消息加一个样式
} else {
messageDiv.textContent = `${senderNickname}: ${message}`;
}
document.getElementById('messages').appendChild(messageDiv);
document.getElementById('messages').scrollTop = document.getElementById('messages').scrollHeight; // 自动滚动到底部
};
// 连接关闭事件
socket.onclose = function() {
console.log('Disconnected from IM Server');
};
});
// 发送消息
document.getElementById('send-button').addEventListener('click', function() {
const message = document.getElementById('message-input').value.trim();
if (message) {
socket.send(JSON.stringify({ type: 'chat_message', message: message }));
document.getElementById('message-input').value = ''; // 清空输入框
}
});
// 支持按 Enter 键发送消息
document.getElementById('message-input').addEventListener('keypress', function(event) {
if (event.key === 'Enter') {
document.getElementById('send-button').click();
}
});
</script>
</body>
</html>
项目结构
- 浏览器打开chatClient.html 第一个用户设置昵称为111
- 加入聊天室后即可接收到消息
- 浏览器再次打开chatClient.html 第二个用户设置昵称为222
- 222用户显示
- 111用户显示:222用户也加入聊天
- 111发送消息
- 222收到消息
弹幕
弹幕和聊天室服务器目前是一样的,只是客户端有部分不同
barrageClient.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebSocket IM Client with Stylish Danmaku</title>
<style>
body {
margin: 0;
padding: 0;
background-image: url('https://example.com/your-background.jpg'); /* 替换为你想要的背景图 */
background-size: cover;
background-position: center;
height: 100vh;
font-family: Arial, sans-serif;
color: white;
}
#chat-container {
width: 70%;
height: 80%;
margin: auto;
margin-top: 5%;
padding: 20px;
border-radius: 10px;
position: relative;
overflow: hidden; /* 确保弹幕不会超出容器 */
background: rgba(0, 0, 0, 0.6); /* 半透明黑色背景 */
}
.danmaku-message {
position: absolute;
white-space: nowrap;
font-size: 18px;
padding: 5px 10px;
border-radius: 5px;
color: #fff;
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.8);
animation: danmaku-move 7s linear infinite;
background: rgba(0, 0, 0, 0.5); /* 半透明背景 */
}
@keyframes danmaku-move {
from {
left: 100%; /* 从屏幕右侧外开始 */
}
to {
left: -100%; /* 移动到屏幕左侧外 */
}
}
#message-input {
width: 70%;
padding: 15px;
border-radius: 25px;
border: 1px solid #ccc;
background-color: rgba(255, 255, 255, 0.8);
color: #333;
}
#send-button {
padding: 15px 25px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 25px;
cursor: pointer;
margin-left: 10px;
font-size: 16px;
}
#send-button:hover {
background-color: #45a049;
}
#config-modal {
display: none;
position: fixed;
z-index: 1;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
}
#config-form {
background-color: #fff;
margin: 15% auto;
padding: 20px;
border: 1px solid #ccc;
width: 300px;
border-radius: 10px;
}
#config-form input {
width: 100%;
padding: 10px;
margin-bottom: 10px;
border: 1px solid #ccc;
border-radius: 25px;
}
#config-form button {
width: 100%;
padding: 10px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 25px;
cursor: pointer;
}
#config-form button:hover {
background-color: #45a049;
}
</style>
</head>
<body>
<div id="config-modal">
<div id="config-form">
<input type="text" id="server-address" placeholder="Server Address" value="ws://localhost">
<input type="text" id="server-port" placeholder="Port" value="8080">
<input type="text" id="nickname" placeholder="Nickname">
<button id="connect-button">Connect</button>
</div>
</div>
<div id="chat-container">
<h1>WebSocket IM Client</h1>
<input type="text" id="message-input" placeholder="Type your message here..." />
<button id="send-button">Send</button>
</div>
<script>
let socket;
let nickname = null;
// 显示配置对话框
const modal = document.getElementById('config-modal');
modal.style.display = 'block';
document.getElementById('connect-button').addEventListener('click', function() {
const serverAddress = document.getElementById('server-address').value;
const serverPort = document.getElementById('server-port').value;
nickname = document.getElementById('nickname').value;
if (!serverAddress || !serverPort || !nickname) {
alert("All fields are required!");
return;
}
// 创建 WebSocket 连接
socket = new WebSocket(`${serverAddress}:${serverPort}/ws`);
// 连接打开事件
socket.onopen = function() {
console.log('Connected to IM Server');
socket.send(JSON.stringify({ type: 'set_nickname', nickname: nickname }));
modal.style.display = 'none'; // 连接成功后隐藏配置对话框
};
// 处理收到的消息
socket.onmessage = function(event) {
const data = JSON.parse(event.data);
const senderNickname = data.nickname;
const message = data.message;
const messageDiv = document.createElement('div');
messageDiv.classList.add('danmaku-message');
if (senderNickname === nickname) {
messageDiv.textContent = `me: ${message}`;
messageDiv.style.color = 'lightblue'; // 给自己的消息加一个样式
} else {
messageDiv.textContent = `${senderNickname}: ${message}`;
}
const chatContainer = document.getElementById('chat-container');
messageDiv.style.top = `${Math.random() * (chatContainer.clientHeight - 20)}px`; // 随机设置弹幕的高度
chatContainer.appendChild(messageDiv);
// 删除弹幕元素以避免占用内存
setTimeout(() => {
chatContainer.removeChild(messageDiv);
}, 7000); // 弹幕持续7秒
};
// 连接关闭事件
socket.onclose = function() {
console.log('Disconnected from IM Server');
};
});
// 发送消息
document.getElementById('send-button').addEventListener('click', function() {
const message = document.getElementById('message-input').value.trim();
if (message) {
socket.send(JSON.stringify({ type: 'chat_message', message: message }));
document.getElementById('message-input').value = ''; // 清空输入框
}
});
// 支持按 Enter 键发送消息
document.getElementById('message-input').addEventListener('keypress', function(event) {
if (event.key === 'Enter') {
document.getElementById('send-button').click();
}
});
</script>
</body>
</html>
- 浏览器打开chatClient.html 第一个用户设置昵称为1,第二个用户设置昵称为2
- 加入时用户提醒
- 用户1发送消息
- 用户2发送消息