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();
    }
}

关键技术点说明

  1. EventLoopGroup:

    • bossGroup: 处理连接接收
    • workerGroup: 处理I/O操作
  2. ChannelPipeline配置顺序:

    HTTP编解码 → HTTP聚合 → 文件传输 → HTTP处理 → WebSocket协议 → 业务处理
    
  3. 多协议支持: 同时支持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 连接测试

浏览器客户端(推荐)

  1. 浏览器访问:http://localhost:8080/
  2. 点击"连接"按钮
  3. 开始聊天

命令行客户端

mvn exec:java -Dexec.mainClass="com.enterprise.netty.client.NettyClient" -Dexec.args="localhost 8080"

📚 第七步:核心概念总结

Netty 核心组件

  1. EventLoopGroup - 事件循环组
  2. Channel - 网络连接通道
  3. ChannelPipeline - 处理器链
  4. ChannelHandler - 消息处理器
  5. Bootstrap - 启动器配置

WebSocket 协议特点

  • 全双工通信 - 客户端和服务器可同时发送数据
  • 实时性 - 低延迟,适合聊天、游戏等场景
  • 协议升级 - 基于HTTP握手升级到WebSocket
  • 帧格式 - 支持文本帧、二进制帧、控制帧

🎉 完成!

现在你拥有一个完整的企业级WebSocket聊天服务器,支持:

  • 🌐 浏览器直接访问
  • 💬 实时多人聊天
  • 📱 命令行客户端
  • 🔧 完整异常处理
  • 📚 丰富学习价值

下一步扩展

  • 添加用户认证
  • 实现私聊功能
  • 消息持久化
  • 文件传输
  • SSL/TLS加密

开始你的WebSocket学习之旅吧!🚀

posted @ 2025-08-29 17:36  朝阳1  阅读(18)  评论(0)    收藏  举报