Java Netty实现ws服务器入门
🚀 Netty WebSocket 服务器完整开发流程文档
📋 项目概述
本文档详细介绍如何使用 Maven + Netty 从零开始构建一个完整的 WebSocket 聊天服务器,支持浏览器和命令行客户端。
🛠️ 第一步:环境准备
必需工具
- JDK 17+
- Apache Maven 3.6+
- IDE (IntelliJ IDEA 推荐)
📄 第二步:Maven 配置 (pom.xml)
完整 pom.xml 配置
<?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>
<groupId>com.enterprise</groupId>
<artifactId>netty-websocket-demo</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<name>Netty WebSocket Demo</name>
<description>基于Netty的WebSocket聊天服务器</description>
<properties>
<!-- Java 版本 -->
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<!-- 核心依赖版本 -->
<netty.version>4.1.100.Final</netty.version>
<slf4j.version>2.0.9</slf4j.version>
<logback.version>1.4.14</logback.version>
<!-- Maven 插件版本 -->
<maven.compiler.plugin.version>3.11.0</maven.compiler.plugin.version>
</properties>
<dependencies>
<!-- Netty 网络框架 -->
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>${netty.version}</version>
</dependency>
<!-- 日志系统 -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>${logback.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<!-- Java 编译插件 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven.compiler.plugin.version}</version>
<configuration>
<source>${maven.compiler.source}</source>
<target>${maven.compiler.target}</target>
<encoding>${project.build.sourceEncoding}</encoding>
</configuration>
</plugin>
<!-- Exec 插件用于运行Java程序 -->
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<systemProperties>
<systemProperty>
<key>file.encoding</key>
<value>UTF-8</value>
</systemProperty>
</systemProperties>
</configuration>
</plugin>
</plugins>
</build>
</project>
关键配置说明
- Netty版本: 4.1.100.Final (最新稳定版)
- Java版本: 17 (长期支持版本)
- 编码: UTF-8 (支持中文)
- Exec插件: 支持命令行运行Java程序
🏗️ 第三步:目录结构创建
├── server/
│ ├── NettyServer.java # WebSocket服务器主类
│ ├── WebSocketServerHandler.java # WebSocket消息处理器
│ └── HttpRequestHandler.java # HTTP请求处理器
└── client/
├── NettyClient.java # WebSocket客户端主类
└── WebSocketClientHandler.java # WebSocket客户端处理器
💻 第四步:核心代码实现
4.1 WebSocket 服务器主类
文件: NettyServer.java
package com.enterprise.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.http.websocketx.extensions.compression.WebSocketServerCompressionHandler;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.stream.ChunkedWriteHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Netty WebSocket服务端示例
* 实现一个WebSocket聊天服务器,支持浏览器直接连接
*/
public class NettyServer {
private static final Logger logger = LoggerFactory.getLogger(NettyServer.class);
private final int port;
public NettyServer(int port) {
this.port = port;
}
public void start() {
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 128)
.childOption(ChannelOption.SO_KEEPALIVE, true)
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ChannelPipeline pipeline = ch.pipeline();
// HTTP编解码器
pipeline.addLast("http-codec", new HttpServerCodec());
// HTTP消息聚合器
pipeline.addLast("http-aggregator", new HttpObjectAggregator(65536));
// 支持大文件传输
pipeline.addLast("http-chunked", new ChunkedWriteHandler());
// HTTP请求处理器(用于提供测试页面)
pipeline.addLast("http-handler", new HttpRequestHandler());
// WebSocket压缩
pipeline.addLast("websocket-compression", new WebSocketServerCompressionHandler());
// WebSocket协议处理器
pipeline.addLast("websocket-protocol", new WebSocketServerProtocolHandler("/ws", null, true));
// 自定义WebSocket业务处理器
pipeline.addLast("websocket-handler", new WebSocketServerHandler());
}
});
ChannelFuture future = bootstrap.bind(port).sync();
logger.info("WebSocket服务器启动成功,监听端口: {}", port);
logger.info("请在浏览器中访问: ws://localhost:{}/ws", port);
logger.info("或访问测试页面: http://localhost:{}/", port);
future.channel().closeFuture().sync();
} catch (InterruptedException e) {
logger.error("服务器启动异常", e);
Thread.currentThread().interrupt();
} finally {
workerGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
logger.info("Netty服务器已关闭");
}
}
public static void main(String[] args) {
int port = 8080;
if (args.length > 0) {
try {
port = Integer.parseInt(args[0]);
} catch (NumberFormatException e) {
logger.warn("无效的端口号,使用默认端口: {}", port);
}
}
new NettyServer(port).start();
}
}
文件: WebSocketServerHandler.java
package com.enterprise.automation.netty.server;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketFrame;
import io.netty.util.concurrent.GlobalEventExecutor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
/**
* WebSocket服务器消息处理器
* 处理WebSocket客户端的连接、断开和消息
*
* @author Enterprise Test Team
* @version 2.0.0
*/
public class WebSocketServerHandler extends SimpleChannelInboundHandler<WebSocketFrame> {
private static final Logger logger = LoggerFactory.getLogger(WebSocketServerHandler.class);
// 存储所有连接的客户端
private static final ChannelGroup channels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
// 存储用户信息
private static final ConcurrentHashMap<String, String> userMap = new ConcurrentHashMap<>();
// 用户计数器
private static final AtomicInteger userCounter = new AtomicInteger(0);
// 时间格式化器
private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm:ss");
@Override
public void channelActive(ChannelHandlerContext ctx) {
// 只记录连接,不进行用户注册(等待WebSocket握手完成)
logger.debug("新连接建立: {}", ctx.channel().remoteAddress());
}
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
// 检查是否是WebSocket握手完成事件
if (evt instanceof io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler.HandshakeComplete) {
// WebSocket握手完成,注册用户
String channelId = ctx.channel().id().asShortText();
String username = "用户" + userCounter.incrementAndGet();
// 添加到频道组
channels.add(ctx.channel());
userMap.put(channelId, username);
logger.info("WebSocket用户加入: {} ({})", username, ctx.channel().remoteAddress());
// 发送欢迎消息给新用户
String welcomeMsg = String.format("欢迎 %s 加入聊天室!当前在线用户: %d", username, channels.size());
ctx.writeAndFlush(new TextWebSocketFrame(formatMessage("系统", welcomeMsg)));
// 通知其他用户有新用户加入
String joinMsg = String.format("%s 加入了聊天室,当前在线用户: %d", username, channels.size());
broadcastToOthers(ctx, formatMessage("系统", joinMsg));
}
super.userEventTriggered(ctx, evt);
}
@Override
public void channelInactive(ChannelHandlerContext ctx) {
String channelId = ctx.channel().id().asShortText();
String username = userMap.remove(channelId);
// 从频道组移除
channels.remove(ctx.channel());
logger.info("WebSocket客户端断开: {} ({})", username, ctx.channel().remoteAddress());
// 通知其他用户有用户离开
if (username != null) {
String leaveMsg = String.format("%s 离开了聊天室,当前在线用户: %d", username, channels.size());
broadcastToAll(formatMessage("系统", leaveMsg));
}
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, WebSocketFrame frame) {
String channelId = ctx.channel().id().asShortText();
String username = userMap.get(channelId);
if (frame instanceof TextWebSocketFrame) {
String message = ((TextWebSocketFrame) frame).text().trim();
logger.info("收到来自 {} 的消息: {}", username, message);
// 处理特殊命令
if (message.startsWith("/")) {
handleCommand(ctx, username, message);
} else {
// 广播普通消息给所有用户
String formattedMsg = formatMessage(username, message);
broadcastToAll(formattedMsg);
}
} else {
// 不支持的WebSocket帧类型
logger.warn("不支持的WebSocket帧类型: {}", frame.getClass().getSimpleName());
}
}
/**
* 处理特殊命令
*/
private void handleCommand(ChannelHandlerContext ctx, String username, String command) {
String response;
switch (command.toLowerCase()) {
case "/help":
response = "可用命令:\n" +
"/help - 显示帮助信息\n" +
"/users - 显示在线用户数\n" +
"/time - 显示当前时间\n" +
"/ping - 测试连接";
break;
case "/users":
response = String.format("当前在线用户数: %d", channels.size());
break;
case "/time":
response = "当前时间: " + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
break;
case "/ping":
response = "pong! 连接正常";
break;
default:
response = "未知命令: " + command + ",输入 /help 查看可用命令";
break;
}
// 只发送给命令发送者
ctx.writeAndFlush(new TextWebSocketFrame(formatMessage("系统", response)));
}
/**
* 格式化消息
*/
private String formatMessage(String sender, String message) {
String time = LocalDateTime.now().format(TIME_FORMATTER);
return String.format("[%s] %s: %s", time, sender, message);
}
/**
* 向所有用户广播消息
*/
private void broadcastToAll(String message) {
channels.writeAndFlush(new TextWebSocketFrame(message));
}
/**
* 向除了指定用户外的所有用户广播消息
*/
private void broadcastToOthers(ChannelHandlerContext excludeCtx, String message) {
channels.stream()
.filter(channel -> !channel.equals(excludeCtx.channel()))
.forEach(channel -> channel.writeAndFlush(new TextWebSocketFrame(message)));
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
String channelId = ctx.channel().id().asShortText();
String username = userMap.get(channelId);
logger.error("WebSocket连接异常 [{}]: {}", username, cause.getMessage());
ctx.close();
}
}
文件: HttpRequestHandler.java
package com.enterprise.automation.netty.server;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.*;
import io.netty.util.CharsetUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* HTTP请求处理器
* 为WebSocket客户端提供测试页面
*
* @author Enterprise Test Team
* @version 2.0.0
*/
public class HttpRequestHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
private static final Logger logger = LoggerFactory.getLogger(HttpRequestHandler.class);
@Override
protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) {
// 只处理非WebSocket的HTTP请求
String uri = request.uri();
// 如果是WebSocket升级请求,传递给下一个处理器
if (isWebSocketUpgrade(request)) {
ctx.fireChannelRead(request.retain());
return;
}
// 处理普通HTTP请求
if (uri.equals("/")) {
// 返回WebSocket测试页面
sendWebSocketTestPage(ctx, request);
} else {
// 返回404
send404(ctx, request);
}
}
/**
* 检查是否为WebSocket升级请求
*/
private boolean isWebSocketUpgrade(FullHttpRequest request) {
return request.headers().get("Upgrade") != null &&
"websocket".equalsIgnoreCase(request.headers().get("Upgrade"));
}
/**
* 发送WebSocket测试页面
*/
private void sendWebSocketTestPage(ChannelHandlerContext ctx, FullHttpRequest request) {
String html = getWebSocketTestPageHtml();
FullHttpResponse response = new DefaultFullHttpResponse(
HttpVersion.HTTP_1_1,
HttpResponseStatus.OK,
Unpooled.copiedBuffer(html, CharsetUtil.UTF_8)
);
response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html; charset=UTF-8");
response.headers().set(HttpHeaderNames.CONTENT_LENGTH, response.content().readableBytes());
ctx.writeAndFlush(response);
logger.info("已发送WebSocket测试页面给: {}", ctx.channel().remoteAddress());
}
/**
* 发送404页面
*/
private void send404(ChannelHandlerContext ctx, FullHttpRequest request) {
String html = "<html><body><h1>404 Not Found</h1><p>请访问 <a href=\"/\">主页</a> 进行WebSocket测试</p></body></html>";
FullHttpResponse response = new DefaultFullHttpResponse(
HttpVersion.HTTP_1_1,
HttpResponseStatus.NOT_FOUND,
Unpooled.copiedBuffer(html, CharsetUtil.UTF_8)
);
response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html; charset=UTF-8");
response.headers().set(HttpHeaderNames.CONTENT_LENGTH, response.content().readableBytes());
ctx.writeAndFlush(response);
}
/**
* 获取WebSocket测试页面HTML
*/
private String getWebSocketTestPageHtml() {
return "<!DOCTYPE html>\n" +
"<html>\n" +
"<head>\n" +
" <meta charset=\"UTF-8\">\n" +
" <title>WebSocket 聊天测试</title>\n" +
" <style>\n" +
" body { font-family: Arial, sans-serif; margin: 20px; }\n" +
" .container { max-width: 800px; margin: 0 auto; }\n" +
" .chat-box { border: 1px solid #ccc; height: 400px; overflow-y: scroll; padding: 10px; margin: 10px 0; background: #f9f9f9; }\n" +
" .input-area { display: flex; margin: 10px 0; }\n" +
" #messageInput { flex: 1; padding: 8px; border: 1px solid #ccc; }\n" +
" button { padding: 8px 15px; margin-left: 5px; }\n" +
" .status { padding: 10px; margin: 10px 0; border-radius: 4px; }\n" +
" .connected { background: #d4edda; color: #155724; border: 1px solid #c3e6cb; }\n" +
" .disconnected { background: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }\n" +
" .message { margin: 5px 0; padding: 5px; }\n" +
" .system-message { color: #007bff; font-style: italic; }\n" +
" .user-message { color: #333; }\n" +
" .commands { background: #e9ecef; padding: 10px; margin: 10px 0; border-radius: 4px; }\n" +
" </style>\n" +
"</head>\n" +
"<body>\n" +
" <div class=\"container\">\n" +
" <h1>🚀 Netty WebSocket 聊天室</h1>\n" +
" \n" +
" <div id=\"status\" class=\"status disconnected\">未连接</div>\n" +
" \n" +
" <div class=\"commands\">\n" +
" <strong>可用命令:</strong> /help, /users, /time, /ping\n" +
" </div>\n" +
" \n" +
" <div id=\"chatBox\" class=\"chat-box\"></div>\n" +
" \n" +
" <div class=\"input-area\">\n" +
" <input type=\"text\" id=\"messageInput\" placeholder=\"输入消息...\" disabled>\n" +
" <button id=\"sendBtn\" disabled>发送</button>\n" +
" <button id=\"connectBtn\">连接</button>\n" +
" <button id=\"disconnectBtn\" disabled>断开</button>\n" +
" </div>\n" +
" </div>\n" +
"\n" +
" <script>\n" +
" let websocket = null;\n" +
" const chatBox = document.getElementById('chatBox');\n" +
" const messageInput = document.getElementById('messageInput');\n" +
" const sendBtn = document.getElementById('sendBtn');\n" +
" const connectBtn = document.getElementById('connectBtn');\n" +
" const disconnectBtn = document.getElementById('disconnectBtn');\n" +
" const status = document.getElementById('status');\n" +
"\n" +
" function connect() {\n" +
" const wsUrl = 'ws://' + window.location.host + '/ws';\n" +
" websocket = new WebSocket(wsUrl);\n" +
"\n" +
" websocket.onopen = function() {\n" +
" updateStatus('已连接到服务器', 'connected');\n" +
" enableControls(true);\n" +
" addMessage('系统: 连接成功!', 'system-message');\n" +
" };\n" +
"\n" +
" websocket.onmessage = function(event) {\n" +
" addMessage(event.data, 'user-message');\n" +
" };\n" +
"\n" +
" websocket.onclose = function() {\n" +
" updateStatus('连接已断开', 'disconnected');\n" +
" enableControls(false);\n" +
" addMessage('系统: 连接已断开', 'system-message');\n" +
" };\n" +
"\n" +
" websocket.onerror = function(error) {\n" +
" addMessage('系统: 连接错误 - ' + error, 'system-message');\n" +
" };\n" +
" }\n" +
"\n" +
" function disconnect() {\n" +
" if (websocket) {\n" +
" websocket.close();\n" +
" }\n" +
" }\n" +
"\n" +
" function sendMessage() {\n" +
" const message = messageInput.value.trim();\n" +
" if (message && websocket && websocket.readyState === WebSocket.OPEN) {\n" +
" websocket.send(message);\n" +
" messageInput.value = '';\n" +
" }\n" +
" }\n" +
"\n" +
" function addMessage(message, className) {\n" +
" const div = document.createElement('div');\n" +
" div.className = 'message ' + className;\n" +
" div.textContent = message;\n" +
" chatBox.appendChild(div);\n" +
" chatBox.scrollTop = chatBox.scrollHeight;\n" +
" }\n" +
"\n" +
" function updateStatus(text, className) {\n" +
" status.textContent = text;\n" +
" status.className = 'status ' + className;\n" +
" }\n" +
"\n" +
" function enableControls(connected) {\n" +
" messageInput.disabled = !connected;\n" +
" sendBtn.disabled = !connected;\n" +
" connectBtn.disabled = connected;\n" +
" disconnectBtn.disabled = !connected;\n" +
" \n" +
" if (connected) {\n" +
" messageInput.focus();\n" +
" }\n" +
" }\n" +
"\n" +
" // 事件监听\n" +
" connectBtn.onclick = connect;\n" +
" disconnectBtn.onclick = disconnect;\n" +
" sendBtn.onclick = sendMessage;\n" +
" \n" +
" messageInput.onkeypress = function(e) {\n" +
" if (e.key === 'Enter') {\n" +
" sendMessage();\n" +
" }\n" +
" };\n" +
"\n" +
" // 页面加载完成后自动连接\n" +
" window.onload = function() {\n" +
" addMessage('系统: 页面加载完成,点击\"连接\"按钮开始聊天', 'system-message');\n" +
" };\n" +
" </script>\n" +
"</body>\n" +
"</html>";
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
logger.error("HTTP请求处理异常: {}", cause.getMessage());
ctx.close();
}
}
客户端文件--可选
文件: NettyClient.java
package com.enterprise.automation.netty.client;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.http.DefaultHttpHeaders;
import io.netty.handler.codec.http.HttpClientCodec;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.websocketx.*;
import io.netty.handler.codec.http.websocketx.extensions.compression.WebSocketClientCompressionHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URI;
/**
* Netty WebSocket客户端示例
* 连接到WebSocket聊天服务器并进行交互
*
* @author Enterprise Test Team
* @version 2.0.0
*/
public class NettyClient {
private static final Logger logger = LoggerFactory.getLogger(NettyClient.class);
private final String host;
private final int port;
private final String path;
private Channel channel;
private WebSocketClientHandshaker handshaker;
public NettyClient(String host, int port) {
this.host = host;
this.port = port;
this.path = "/ws";
}
/**
* 启动客户端并连接服务器
*/
public void start() {
EventLoopGroup group = new NioEventLoopGroup();
try {
// 创建WebSocket握手器
URI uri = new URI("ws://" + host + ":" + port + path);
handshaker = WebSocketClientHandshakerFactory.newHandshaker(
uri, WebSocketVersion.V13, null, true, new DefaultHttpHeaders());
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group)
.channel(NioSocketChannel.class)
.option(ChannelOption.SO_KEEPALIVE, true)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ChannelPipeline pipeline = ch.pipeline();
// HTTP编解码器
pipeline.addLast("http-codec", new HttpClientCodec());
// HTTP消息聚合器
pipeline.addLast("http-aggregator", new HttpObjectAggregator(8192));
// 自定义WebSocket客户端处理器
pipeline.addLast("websocket-handler", new WebSocketClientHandler(handshaker));
}
});
// 连接服务器
ChannelFuture future = bootstrap.connect(host, port).sync();
if (future.isSuccess()) {
logger.info("正在连接到WebSocket服务器 ws://{}:{}{}", host, port, path);
channel = future.channel();
// 等待WebSocket握手完成
WebSocketClientHandler handler = (WebSocketClientHandler) channel.pipeline().get("websocket-handler");
handler.handshakeFuture().sync();
logger.info("成功连接到WebSocket服务器!");
// 启动用户输入线程
startUserInputThread();
// 等待连接关闭
channel.closeFuture().sync();
} else {
logger.error("连接WebSocket服务器失败");
}
} catch (Exception e) {
logger.error("客户端运行异常", e);
} finally {
group.shutdownGracefully();
logger.info("客户端已断开连接");
}
}
/**
* 启动用户输入线程
*/
private void startUserInputThread() {
Thread inputThread = new Thread(() -> {
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
try {
System.out.println("=== 欢迎使用WebSocket聊天客户端 ===");
System.out.println("输入消息并按回车发送,输入 /help 查看可用命令,输入 /quit 退出");
System.out.println("或者在浏览器中访问: http://" + host + ":" + port + "/");
System.out.print("请输入消息: ");
String input;
while ((input = reader.readLine()) != null) {
if (channel != null && channel.isActive()) {
// 发送WebSocket文本消息到服务器
channel.writeAndFlush(new TextWebSocketFrame(input));
// 如果是退出命令,跳出循环
if ("/quit".equalsIgnoreCase(input.trim())) {
break;
}
System.out.print("请输入消息: ");
} else {
logger.warn("连接已断开,无法发送消息");
break;
}
}
} catch (IOException e) {
logger.error("读取用户输入异常", e);
} finally {
// 关闭连接
if (channel != null && channel.isActive()) {
channel.close();
}
}
});
inputThread.setName("UserInputThread");
inputThread.setDaemon(true);
inputThread.start();
}
/**
* 发送消息
*/
public void sendMessage(String message) {
if (channel != null && channel.isActive()) {
channel.writeAndFlush(new TextWebSocketFrame(message));
} else {
logger.warn("连接未建立或已断开,无法发送消息: {}", message);
}
}
/**
* 关闭客户端
*/
public void close() {
if (channel != null) {
channel.close();
}
}
public static void main(String[] args) {
String host = "localhost";
int port = 8080;
// 解析命令行参数
if (args.length >= 1) {
host = args[0];
}
if (args.length >= 2) {
try {
port = Integer.parseInt(args[1]);
} catch (NumberFormatException e) {
logger.warn("无效的端口号,使用默认端口: {}", port);
}
}
logger.info("准备连接WebSocket服务器 ws://{}:{}/ws", host, port);
new NettyClient(host, port).start();
}
}
文件: WebSocketClientHandler.java
package com.enterprise.automation.netty.client;
import io.netty.channel.*;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.websocketx.*;
import io.netty.util.CharsetUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* WebSocket客户端消息处理器
* 处理WebSocket连接、消息接收和异常
*
* @author Enterprise Test Team
* @version 2.0.0
*/
public class WebSocketClientHandler extends SimpleChannelInboundHandler<Object> {
private static final Logger logger = LoggerFactory.getLogger(WebSocketClientHandler.class);
private final WebSocketClientHandshaker handshaker;
private ChannelPromise handshakeFuture;
public WebSocketClientHandler(WebSocketClientHandshaker handshaker) {
this.handshaker = handshaker;
}
public ChannelFuture handshakeFuture() {
return handshakeFuture;
}
@Override
public void handlerAdded(ChannelHandlerContext ctx) {
handshakeFuture = ctx.newPromise();
}
@Override
public void channelActive(ChannelHandlerContext ctx) {
logger.info("开始进行WebSocket握手...");
handshaker.handshake(ctx.channel());
}
@Override
public void channelInactive(ChannelHandlerContext ctx) {
logger.info("WebSocket连接已断开");
System.out.println(">>> 与WebSocket服务器连接已断开");
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, Object msg) {
Channel ch = ctx.channel();
if (!handshaker.isHandshakeComplete()) {
try {
handshaker.finishHandshake(ch, (FullHttpResponse) msg);
logger.info("WebSocket握手成功!");
System.out.println(">>> 已连接到WebSocket聊天服务器");
handshakeFuture.setSuccess();
} catch (WebSocketHandshakeException e) {
logger.error("WebSocket握手失败: {}", e.getMessage());
handshakeFuture.setFailure(e);
}
return;
}
if (msg instanceof FullHttpResponse) {
FullHttpResponse response = (FullHttpResponse) msg;
throw new IllegalStateException(
"意外的FullHttpResponse(getStatus=" + response.status() +
", content=" + response.content().toString(CharsetUtil.UTF_8) + ')');
}
WebSocketFrame frame = (WebSocketFrame) msg;
if (frame instanceof TextWebSocketFrame) {
TextWebSocketFrame textFrame = (TextWebSocketFrame) frame;
// 直接输出收到的消息,不再加前缀
System.out.println("\r" + textFrame.text());
System.out.print("请输入消息: ");
} else if (frame instanceof PongWebSocketFrame) {
logger.debug("收到Pong帧");
} else if (frame instanceof CloseWebSocketFrame) {
logger.info("收到服务器关闭连接请求");
System.out.println(">>> 服务器主动关闭连接");
ch.close();
} else {
logger.warn("不支持的WebSocket帧类型: {}", frame.getClass().getSimpleName());
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
logger.error("WebSocket客户端异常: {}", cause.getMessage());
System.out.println(">>> WebSocket连接发生异常: " + cause.getMessage());
if (!handshakeFuture.isDone()) {
handshakeFuture.setFailure(cause);
}
ctx.close();
}
}
关键技术点说明
-
EventLoopGroup:
bossGroup: 处理连接接收workerGroup: 处理I/O操作
-
ChannelPipeline配置顺序:
HTTP编解码 → HTTP聚合 → 文件传输 → HTTP处理 → WebSocket协议 → 业务处理 -
多协议支持: 同时支持HTTP和WebSocket协议
🎯 第五步:启动脚本
创建启动脚本 scripts/netty-demo.bat
@echo off
chcp 65001 > nul
echo ========================================
echo Netty WebSocket 聊天服务器/客户端启动脚本
echo ========================================
echo.
:menu
echo 请选择要启动的程序:
echo 1. 启动服务器
echo 2. 启动客户端
echo 3. 编译项目
echo 4. 退出
echo.
set /p choice=请输入选项 (1-4):
if "%choice%"=="1" goto server
if "%choice%"=="2" goto client
if "%choice%"=="3" goto compile
if "%choice%"=="4" goto exit
echo 无效选项,请重新选择
goto menu
:compile
echo.
echo 正在编译项目...
mvn clean compile
if errorlevel 1 (
echo 编译失败!请检查代码。
pause
goto menu
)
echo 编译成功!
pause
goto menu
:server
echo.
echo 启动Netty WebSocket服务器...
echo 默认端口: 8080
echo 注意:如果端口被占用,请选择其他端口!
echo 启动后可在浏览器访问: http://localhost:端口号/
set /p port=请输入端口号 (直接回车使用默认):
if "%port%"=="" set port=8080
echo 启动WebSocket服务器,端口: %port%
echo 正在启动服务器...
mvn exec:java -Dexec.mainClass="com.enterprise.netty.server.NettyServer" -Dexec.args="%port%"
pause
goto menu
:client
echo.
echo 启动Netty WebSocket客户端...
echo 默认服务器: localhost:8080
echo 注意:请确保WebSocket服务器已经启动!
echo 或者直接在浏览器中访问: http://localhost:端口号/
set /p host=请输入服务器地址 (直接回车使用localhost):
if "%host%"=="" set host=localhost
set /p port=请输入服务器端口 (直接回车使用8080):
if "%port%"=="" set port=8080
echo 连接到服务器: %host%:%port%
echo 正在启动客户端...
mvn exec:java -Dexec.mainClass="com.enterprise.netty.client.NettyClient" -Dexec.args="%host% %port%"
pause
goto menu
:exit
echo 退出程序
exit /b 0
🚀 第六步:运行流程
6.1 编译项目
mvn clean compile
6.2 启动服务器
# 方法一:使用脚本
scripts\netty-demo.bat
# 方法二:直接命令行
mvn exec:java -Dexec.mainClass="com.enterprise.netty.server.NettyServer" -Dexec.args="8080"
6.3 连接测试
浏览器客户端(推荐)
- 浏览器访问:
http://localhost:8080/ - 点击"连接"按钮
- 开始聊天
命令行客户端
mvn exec:java -Dexec.mainClass="com.enterprise.netty.client.NettyClient" -Dexec.args="localhost 8080"
📚 第七步:核心概念总结
Netty 核心组件
- EventLoopGroup - 事件循环组
- Channel - 网络连接通道
- ChannelPipeline - 处理器链
- ChannelHandler - 消息处理器
- Bootstrap - 启动器配置
WebSocket 协议特点
- 全双工通信 - 客户端和服务器可同时发送数据
- 实时性 - 低延迟,适合聊天、游戏等场景
- 协议升级 - 基于HTTP握手升级到WebSocket
- 帧格式 - 支持文本帧、二进制帧、控制帧
🎉 完成!
现在你拥有一个完整的企业级WebSocket聊天服务器,支持:
- 🌐 浏览器直接访问
- 💬 实时多人聊天
- 📱 命令行客户端
- 🔧 完整异常处理
- 📚 丰富学习价值
下一步扩展
- 添加用户认证
- 实现私聊功能
- 消息持久化
- 文件传输
- SSL/TLS加密
开始你的WebSocket学习之旅吧!🚀

浙公网安备 33010602011771号