Java实现WebSocket案例

wen java案例 1

本文目录导读:

Java实现WebSocket案例

  1. 项目结构
  2. Maven依赖 (pom.xml)
  3. WebSocket服务端实现
  4. 消息模型和编码解码器
  5. WebSocket客户端实现
  6. 服务器启动类
  7. 配置文件(可选)
  8. 前端HTML测试页面
  9. 测试用例
  10. 运行说明
  11. 功能特性
  12. 注意事项

我来为您提供一个完整的Java WebSocket实现案例,使用标准的Java WebSocket API(JSR 356)和嵌入式服务器。

项目结构

websocket-demo/
├── pom.xml
└── src/
    └── main/java/com/example/websocket/
        ├── WebSocketServer.java
        ├── WebSocketClient.java
        ├── ChatMessage.java
        └── WebSocketServerApp.java

Maven依赖 (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.example</groupId>
    <artifactId>websocket-demo</artifactId>
    <version>1.0-SNAPSHOT</version>
    <properties>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
    <dependencies>
        <!-- Java EE WebSocket API -->
        <dependency>
            <groupId>javax.websocket</groupId>
            <artifactId>javax.websocket-api</artifactId>
            <version>1.1</version>
            <scope>provided</scope>
        </dependency>
        <!-- JSON 处理库 - 使用 Jackson -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.13.3</version>
        </dependency>
        <!-- Tyrus(参考实现)作为WebSocket客户端实现 -->
        <dependency>
            <groupId>org.glassfish.tyrus</groupId>
            <artifactId>tyrus-client</artifactId>
            <version>1.15</version>
            <scope>test</scope>
        </dependency>
        <!-- Log4j2 -->
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-core</artifactId>
            <version>2.17.1</version>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-slf4j-impl</artifactId>
            <version>2.17.1</version>
        </dependency>
        <!-- 测试依赖 -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13.2</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>11</source>
                    <target>11</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

WebSocket服务端实现

package com.example.websocket;
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Logger;
@ServerEndpoint(value = "/chat/{username}", 
                encoders = {MessageEncoder.class}, 
                decoders = {MessageDecoder.class},
                configurator = CustomConfigurator.class)
public class WebSocketServer {
    private static final Logger logger = Logger.getLogger(WebSocketServer.class.getName());
    // 存储所有在线客户端会话
    private static final Map<String, Session> onlineUsers = new ConcurrentHashMap<>();
    private String username;
    /**
     * 连接建立时调用
     */
    @OnOpen
    public void onOpen(Session session, @PathParam("username") String username) {
        this.username = username;
        onlineUsers.put(username, session);
        logger.info("用户 " + username + " 已连接");
        // 广播上线消息
        ChatMessage message = new ChatMessage("SYSTEM", "User " + username + " joined the chat!");
        broadcastToAll(message);
        // 发送在线用户列表给新连接的用户
        sendOnlineUsers(session);
    }
    /**
     * 接收客户端消息时调用
     */
    @OnMessage
    public void onMessage(String message, Session session) {
        logger.info("从 " + username + " 收到消息: " + message);
        try {
            // 如果是JSON字符串,转换为ChatMessage对象
            ChatMessage chatMessage = MessageUtil.fromJson(message);
            chatMessage.setSender(username);
            chatMessage.setTimestamp(System.currentTimeMillis());
            // 广播消息
            broadcastToAll(chatMessage);
        } catch (Exception e) {
            logger.warning("消息处理失败: " + e.getMessage());
            sendErrorMessage(session, "消息格式不正确");
        }
    }
    /**
     * 连接关闭时调用
     */
    @OnClose
    public void onClose(Session session, CloseReason reason) {
        onlineUsers.remove(username);
        logger.info("用户 " + username + " 断开连接,原因: " + reason.getReasonPhrase());
        // 广播离线消息
        ChatMessage message = new ChatMessage("SYSTEM", "User " + username + " has left the chat.");
        broadcastToAll(message);
    }
    /**
     * 发生错误时调用
     */
    @OnError
    public void onError(Session session, Throwable error) {
        logger.severe("WebSocket错误: " + error.getMessage());
        error.printStackTrace();
        try {
            if (session.isOpen()) {
                session.close(new CloseReason(CloseReason.CloseCodes.UNEXPECTED_CONDITION, error.getMessage()));
            }
        } catch (IOException e) {
            logger.severe("关闭连接失败: " + e.getMessage());
        }
    }
    /**
     * 广播消息给所有在线用户
     */
    private void broadcastToAll(ChatMessage message) {
        String jsonMessage = MessageUtil.toJson(message);
        onlineUsers.values().forEach(session -> {
            if (session.isOpen()) {
                try {
                    synchronized (session) {
                        session.getBasicRemote().sendText(jsonMessage);
                    }
                } catch (IOException e) {
                    logger.warning("发送消息失败给: " + session.getId() + ", 错误: " + e.getMessage());
                }
            }
        });
    }
    /**
     * 发送在线用户列表
     */
    private void sendOnlineUsers(Session session) {
        ChatMessage message = new ChatMessage("SYSTEM", "ONLINE_USERS:" + String.join(",", onlineUsers.keySet()));
        try {
            session.getBasicRemote().sendText(MessageUtil.toJson(message));
        } catch (IOException e) {
            logger.warning("发送在线列表失败: " + e.getMessage());
        }
    }
    /**
     * 发送错误消息给特定会话
     */
    private void sendErrorMessage(Session session, String error) {
        try {
            ChatMessage errorMessage = new ChatMessage("SYSTEM", "ERROR:" + error);
            session.getBasicRemote().sendText(MessageUtil.toJson(errorMessage));
        } catch (IOException e) {
            logger.severe("发送错误消息失败: " + e.getMessage());
        }
    }
    /**
     * 自定义配置器
     */
    public static class CustomConfigurator extends ServerEndpointConfig.Configurator {
        @Override
        public void modifyHandshake(ServerEndpointConfig config, 
                                   HandshakeRequest request, 
                                   HandshakeResponse response) {
            // 可以在这里添加额外的握手验证或配置
            logger.info("开始握手: " + request.getRequestURI());
        }
    }
}

消息模型和编码解码器

package com.example.websocket;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
public class ChatMessage {
    private String sender;
    private String content;
    private long timestamp;
    private String type;
    public ChatMessage() {}
    public ChatMessage(String sender, String content) {
        this.sender = sender;
        this.content = content;
        this.timestamp = System.currentTimeMillis();
        this.type = "MESSAGE";
    }
    // Getter和Setter方法
    public String getSender() { return sender; }
    public void setSender(String sender) { this.sender = sender; }
    public String getContent() { return content; }
    public void setContent(String content) { this.content = content; }
    public long getTimestamp() { return timestamp; }
    public void setTimestamp(long timestamp) { this.timestamp = timestamp; }
    public String getType() { return type; }
    public void setType(String type) { this.type = type; }
    @Override
    public String toString() {
        return "ChatMessage{" +
                "sender='" + sender + '\'' +
                ", content='" + content + '\'' +
                ", timestamp=" + timestamp +
                ", type='" + type + '\'' +
                '}';
    }
}
/**
 * 消息编码器
 */
import javax.websocket.EncodeException;
import javax.websocket.Encoder;
import javax.websocket.EndpointConfig;
public class MessageEncoder implements Encoder.Text<ChatMessage> {
    @Override
    public String encode(ChatMessage message) throws EncodeException {
        return MessageUtil.toJson(message);
    }
    @Override
    public void init(EndpointConfig endpointConfig) {}
    @Override
    public void destroy() {}
}
/**
 * 消息解码器
 */
import javax.websocket.DecodeException;
import javax.websocket.Decoder;
import javax.websocket.EndpointConfig;
public class MessageDecoder implements Decoder.Text<ChatMessage> {
    @Override
    public ChatMessage decode(String s) throws DecodeException {
        return MessageUtil.fromJson(s);
    }
    @Override
    public boolean willDecode(String s) {
        return s != null;
    }
    @Override
    public void init(EndpointConfig endpointConfig) {}
    @Override
    public void destroy() {}
}
/**
 * JSON消息工具类
 */
public class MessageUtil {
    private static final ObjectMapper objectMapper = new ObjectMapper();
    public static String toJson(ChatMessage message) {
        try {
            return objectMapper.writeValueAsString(message);
        } catch (IOException e) {
            e.printStackTrace();
            return "{}";
        }
    }
    public static ChatMessage fromJson(String json) {
        try {
            return objectMapper.readValue(json, ChatMessage.class);
        } catch (IOException e) {
            e.printStackTrace();
            return new ChatMessage("UNKNOWN", json);
        }
    }
}

WebSocket客户端实现

package com.example.websocket;
import javax.websocket.*;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Scanner;
import java.util.logging.Logger;
@ClientEndpoint
public class WebSocketClient {
    private static final Logger logger = Logger.getLogger(WebSocketClient.class.getName());
    private Session session;
    private String username;
    @OnOpen
    public void onOpen(Session session) {
        this.session = session;
        logger.info("连接到服务器");
        System.out.println("已连接到服务器");
    }
    @OnMessage
    public void onMessage(String message) {
        System.out.println("收到消息: " + message);
    }
    @OnClose
    public void onClose(Session session, CloseReason reason) {
        logger.info("连接关闭: " + reason.getReasonPhrase());
        System.out.println("连接已关闭");
    }
    @OnError
    public void onError(Session session, Throwable error) {
        logger.severe("客户端错误: " + error.getMessage());
    }
    /**
     * 连接到WebSocket服务器
     */
    public void connect(String url) throws URISyntaxException, IOException, DeploymentException {
        WebSocketContainer container = ContainerProvider.getWebSocketContainer();
        session = container.connectToServer(this, new URI(url));
    }
    /**
     * 发送消息
     */
    public void sendMessage(String message) throws IOException {
        if (session != null && session.isOpen()) {
            session.getBasicRemote().sendText(message);
        } else {
            throw new IOException("WebSocket会话未打开");
        }
    }
    /**
     * 断开连接
     */
    public void close() throws IOException {
        if (session != null) {
            session.close(new CloseReason(CloseReason.CloseCodes.NORMAL_CLOSURE, "客户端关闭"));
        }
    }
    /**
     * 客户端启动器 - 命令行交互模式
     */
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        System.out.print("请输入用户名: ");
        String username = scanner.nextLine();
        WebSocketClient client = new WebSocketClient();
        try {
            // 连接到服务器(默认端口8080)
            String serverUrl = "ws://localhost:8080/websocket/chat/" + username;
            client.connect(serverUrl);
            System.out.println("输入消息进行聊天,输入 'quit' 退出");
            while (true) {
                System.out.print("> ");
                String input = scanner.nextLine();
                if (input.equalsIgnoreCase("quit")) {
                    client.close();
                    break;
                }
                client.sendMessage(input);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            scanner.close();
        }
    }
}

服务器启动类

package com.example.websocket;
import org.apache.catalina.Context;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.startup.Tomcat;
import javax.servlet.ServletException;
import javax.websocket.server.ServerContainer;
import javax.websocket.server.ServerEndpointConfig;
import java.io.File;
import java.util.logging.Logger;
/**
 * 嵌入式Tomcat服务器启动类
 */
public class WebSocketServerApp {
    private static final Logger logger = Logger.getLogger(WebSocketServerApp.class.getName());
    public static void main(String[] args) throws Exception {
        int port = 8080;
        String contextPath = "/websocket";
        // 创建Tomcat实例
        Tomcat tomcat = new Tomcat();
        tomcat.setPort(port);
        tomcat.setBaseDir(System.getProperty("java.io.tmpdir"));
        tomcat.getConnector();
        // 设置Host配置
        tomcat.getHost().setAppBase(".");
        tomcat.getHost().setAutoDeploy(true);
        // 创建Context
        Context context = tomcat.addContext(contextPath, new File(".").getAbsolutePath());
        // 添加自定义Servlet(可选,用于HTTP请求处理)
        Tomcat.addServlet(context, "helloServlet", new HelloServlet());
        context.addServletMappingDecoded("/hello", "helloServlet");
        // 添加WebSocket支持
        ServerContainer serverContainer = (ServerContainer) tomcat.getServletContext()
                .getAttribute(ServerContainer.class.getName());
        if (serverContainer != null) {
            // 配置WebSocket连接超时和缓冲区大小
            serverContainer.setDefaultMaxSessionIdleTimeout(3000); // 3秒
            serverContainer.setDefaultMaxSessionTimeout(300000); // 5分钟超时
            // 注册WebSocket端点
            serverContainer.addEndpoint(WebSocketServer.class);
            logger.info("WebSocket端点已注册");
        }
        // 启动Tomcat
        tomcat.start();
        logger.info("服务器启动成功,监听端口: " + port + ", 路径: " + contextPath);
        logger.info("WebSocket连接地址: ws://localhost:" + port + contextPath + "/chat/{username}");
        // 等待关闭
        tomcat.getServer().await();
    }
    /**
     * 简单的HTTP Servlet
     */
    public static class HelloServlet extends javax.servlet.http.HttpServlet {
        @Override
        protected void doGet(javax.servlet.http.HttpServletRequest req, 
                           javax.servlet.http.HttpServletResponse resp) 
                throws javax.servlet.ServletException, java.io.IOException {
            resp.setContentType("text/html;charset=UTF-8");
            resp.getWriter().println("<h1>你好,WebSocket测试页面</h1>");
            resp.getWriter().println("<h2>WebSocket端点: /websocket/chat/{username}</h2>");
        }
    }
}

配置文件(可选)

创建 src/main/resources/log4j2.xml:

<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
    <Properties>
        <Property name="logPath">logs</Property>
        <Property name="consolePatternLayout">%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n</Property>
    </Properties>
    <Appenders>
        <Console name="Console" target="SYSTEM_OUT">
            <PatternLayout pattern="${consolePatternLayout}"/>
        </Console>
        <RollingFile name="RollingFile" fileName="${logPath}/websocket.log"
                     filePattern="${logPath}/websocket-%d{yyyy-MM-dd}-%i.log">
            <PatternLayout>
                <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %logger{36} - %msg%n</pattern>
            </PatternLayout>
            <Policies>
                <SizeBasedTriggeringPolicy size="10MB"/>
            </Policies>
        </RollingFile>
    </Appenders>
    <Loggers>
        <Root level="info">
            <AppenderRef ref="Console"/>
            <AppenderRef ref="RollingFile"/>
        </Root>
    </Loggers>
</Configuration>

前端HTML测试页面

创建 web/index.html:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">WebSocket聊天室</title>
    <style>
        body { font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; }
        #messages { border: 1px solid #ccc; height: 400px; overflow-y: scroll; padding: 10px; margin-bottom: 10px; }
        .message { margin: 5px 0; padding: 5px; border-radius: 4px; background: #f1f1f1; }
        .system { color: #999; font-style: italic; }
        .private { background: #ffffee; border: 1px solid #cccc99; }
        input, button { padding: 8px; margin: 5px; }
        #messageInput { width: 70%; }
    </style>
</head>
<body>
    <h1>WebSocket聊天室</h1>
    <div id="loginSection">
        <input type="text" id="username" placeholder="手机号" />
        <button onclick="connect()">连接</button>
    </div>
    <div id="chatSection" style="display: none;">
        <div id="messages"></div>
        <div>
            <input type="text" id="messageInput" placeholder="输入消息...">
            <input type="text" id="targetUser" placeholder="目标用户(留空为广播)">
            <button onclick="sendMessage()">发送</button>
            <button onclick="disconnect()">断开连接</button>
        </div>
    </div>
    <script>
        let ws = null;
        function connect() {
            const username = document.getElementById('username').value.trim();
            if (!username) {
                alert('请输入用户名');
                return;
            }
            const wsUrl = `ws://localhost:8080/websocket/chat/${username}`;
            // 创建WebSocket连接
            ws = new WebSocket(wsUrl);
            // 连接建立事件
            ws.onopen = function(event) {
                console.log('连接成功');
                document.getElementById('loginSection').style.display = 'none';
                document.getElementById('chatSection').style.display = 'block';
                addMessage('已连接到服务器');
            };
            // 接收消息事件
            ws.onmessage = function(event) {
                console.log('收到消息:', event.data);
                try {
                    const data = JSON.parse(event.data);
                    // 处理系统消息
                    if (data.sender === 'SYSTEM' && data.content.startsWith('ONLINE_USERS:')) {
                        const users = data.content.substring('ONLINE_USERS:'.length);
                        addMessage(`在线用户: ${users}`, 'system');
                    } else if (data.sender === 'SYSTEM' && data.content.startsWith('ERROR:')) {
                        addMessage(`错误: ${data.content.substring(6)}`, 'system');
                    } else {
                        // 普通聊天消息
                        addMessage(`${data.sender}: ${data.content}`);
                    }
                } catch (e) {
                    addMessage(event.data);
                }
            };
            // 连接关闭事件
            ws.onclose = function(event) {
                console.log('连接关闭');
                addMessage('连接已关闭', 'system');
                document.getElementById('loginSection').style.display = 'block';
                document.getElementById('chatSection').style.display = 'none';
            };
            // 错误事件
            ws.onerror = function(event) {
                console.error('WebSocket错误:', event);
                addMessage('连接错误: ' + event.message, 'system');
            };
        }
        function sendMessage() {
            if (!ws || ws.readyState === WebSocket.CLOSED) {
                alert('请先连接');
                return;
            }
            const content = document.getElementById('messageInput').value.trim();
            const targetUser = document.getElementById('targetUser').value.trim();
            if (!content) {
                alert('请输入消息');
                return;
            }
            const message = {
                sender: document.getElementById('username').value.trim(),
                content: targetUser ? `@${targetUser} ${content}` : content,
                timestamp: Date.now(),
                type: 'MESSAGE'
            };
            ws.send(JSON.stringify(message));
            document.getElementById('messageInput').value = '';
        }
        function disconnect() {
            if (ws) {
                ws.close();
            }
        }
        function addMessage(text, type = '') {
            const messagesDiv = document.getElementById('messages');
            const messageEl = document.createElement('div');
            messageEl.className = `message ${type}`;
            messageEl.textContent = text;
            messagesDiv.appendChild(messageEl);
            messagesDiv.scrollTop = messagesDiv.scrollHeight;
        }
        // 回车发送消息
        document.getElementById('messageInput').addEventListener('keypress', function(e) {
            if (e.key === 'Enter') {
                sendMessage();
            }
        });
    </script>
</body>
</html>

测试用例

package com.example.websocket.test;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import javax.websocket.*;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.logging.Logger;
import static org.junit.Assert.*;
public class WebSocketTest {
    private static final Logger logger = Logger.getLogger(WebSocketTest.class.getName());
    private static final String WS_URL = "ws://localhost:8080/websocket/chat/testUser";
    private CountDownLatch latch;
    private String receivedMessage;
    private Session session;
    @Before
    public void setup() throws Exception {
        // 这里假设服务器已经在运行
        // 在实际测试中,可以启动嵌入式服务器
    }
    @Test
    public void testWebSocketConnection() throws Exception {
        latch = new CountDownLatch(1);
        WebSocketContainer container = ContainerProvider.getWebSocketContainer();
        // 使用匿名客户端连接
        Session testSession = container.connectToServer(new Endpoint() {
            @Override
            public void onOpen(Session session, EndpointConfig config) {
                logger.info("测试连接已打开");
                // 发送测试消息
                try {
                    session.getBasicRemote().sendText("Hello WebSocket!");
                } catch (IOException e) {
                    fail("发送消息失败: " + e.getMessage());
                }
            }
            @Override
            public void onMessage(Session session, String message) {
                logger.info("收到消息: " + message);
                receivedMessage = message;
                latch.countDown();
            }
            @Override
            public void onError(Session session, Throwable error) {
                logger.severe("错误: " + error.getMessage());
                fail("WebSocket错误: " + error.getMessage());
            }
            @Override
            public void onClose(Session session, CloseReason closeReason) {
                logger.info("连接关闭: " + closeReason.getReasonPhrase());
            }
        }, new URI(WS_URL));
        // 等待消息接收
        assertTrue(latch.await(5, TimeUnit.SECONDS));
        assertNotNull(receivedMessage);
        assertTrue(receivedMessage.contains("Hello WebSocket!"));
    }
    @Test
    public void testMessageEncoding() {
        ChatMessage message = new ChatMessage("sender", "Hello");
        String json = MessageUtil.toJson(message);
        assertNotNull(json);
        assertTrue(json.contains("sender"));
        assertTrue(json.contains("Hello"));
    }
    @After
    public void cleanup() {
        if (session != null) {
            try {
                session.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

运行说明

1 启动服务器

mvn clean package
java -jar target/websocket-demo-1.0-SNAPSHOT.jar

2 测试连接

  1. 使用浏览器打开 web/index.html 测试页面
  2. 使用命令行客户端:
    java -cp target/websocket-demo-1.0-SNAPSHOT.jar com.example.websocket.WebSocketClient

3 使用工具测试

可以使用以下工具进行测试:

  • Postman: 支持WebSocket测试
  • wscat: Node.js的WebSocket客户端工具
# 安装wscat
npm install -g wscat
# 连接测试
wscat -c ws://localhost:8080/websocket/chat/testUser

功能特性

  • ✅ 完整的WebSocket实现
  • ✅ 消息编解码
  • ✅ 广播消息
  • ✅ 在线用户管理
  • ✅ 错误处理
  • ✅ 前端测试页面
  • ✅ 嵌入式服务器
  • ✅ 单元测试

注意事项

  1. 安全性: 实际生产环境需要添加认证和授权
  2. 性能: 对于大规模用户,需要考虑集群支持和负载均衡
  3. 心跳机制: 建议添加心跳检测来处理断开的连接
  4. 消息大小: 注意配置WebSocket消息大小限制

这个案例提供了完整的WebSocket解决方案,包含服务器、客户端、前端页面和测试用例,可以直接运行和学习。

抱歉,评论功能暂时关闭!