一、关于WebSocket

http是前端主动发送请求给后端,后端被动返回响应,而websocket是双向的双工通讯,后端也能主动给前端发送消息

消息推送的常见方法:

  1. 轮询
    每间隔一定时间发送http请求,让后端发送数据
  2. 长轮询
    发送ajax请求,后端数据未更新时就阻塞(若时间长到超时,返回请求),能减少轮询的时间
  3. Sse(server-sent event)
    在服务端和客户端之间开一个通道,有数据变更时以数据流(text/event-stream)的方法传输到客户端

4.websocket
全双工:允许数据在两个方向上同时传输

半双工:允许数据在两个方向上传输,但是一次只能在一个方向上传输

单工:信息只能沿着一个方向传输(http请求 )



前端API:

  1. 创建对象
    let ws=new WebSocket(URL);
    URL: ws://localhost/访问路径   
    注:默认端口为8080
  2. 相关事件
    ws.onopen:连接建立时触发;
    ws.onmessage:客户端收到服务器端发送的数据时触发
    ws.onclose:连接断开时触发
  3. 方法
    send():发送数据给服务端

后端API:
需要tomcat7.0.5版本以上
1.Java WebSocket由一系列Endpoint对象组成,代表WebSocket链接的一端

2.Endpoint在后端接收到http连接时实例化,并只在连接时有效,断开连接后结束;
两种方法定义EndPoint
1.编程式:类继承javax.websocket.Endpoint并实现方法
2.注解式:在pojo类上加@ServerEndpoint

对应的方法及注解:
onOpen():当开启一个会话时调用(握手成功后调用)
onClose():当会话关闭时调用

onError():当链接过程异常时调用



 

二、使用WebSocket

(一)引入依赖

需引入WebSocket依赖,有几种方式:

1. 原生 Java WebSocket API(JSR-356)

如果直接使用 Java 标准的 WebSocket API(JSR-356),无需额外依赖,因为 Java EE 环境(如 Tomcat 8+、GlassFish 等)已内置支持。但如果是纯 Java SE 项目或需要显式引入 API 接口

<dependency>
    <groupId>javax.websocket</groupId>
    <artifactId>javax.websocket-api</artifactId>
    <version>1.1</version>
    <scope>provided</scope> <!-- 服务器已内置,无需打包 -->
</dependency>

2. Spring Boot 项目(推荐)

Spring Boot 对 WebSocket 有封装,使用 spring-boot-starter-websocket 可快速集成,自动适配底层服务器(如 Tomcat):

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
    <version>2.7.0</version> <!-- 版本可根据项目实际使用的 Spring Boot 版本调整 -->
</dependency>

3.Tomcat 服务器单独依赖

如果项目直接部署在 Tomcat 上,且需要显式引入 Tomcat 的 WebSocket 实现(通常无需,Tomcat 已内置):

<dependency>
    <groupId>org.apache.tomcat.embed</groupId>
    <artifactId>tomcat-embed-websocket</artifactId>
    <version>9.0.65</version> <!-- 与 Tomcat 版本对应 -->
    <scope>provided</scope>
</dependency>

(二)类

项目整体目录:

config包:

GetHttpSessionConfigurator类:
import javax.servlet.http.HttpSession;
import javax.websocket.HandshakeResponse;
import javax.websocket.server.HandshakeRequest;
import javax.websocket.server.ServerEndpointConfig;

public class GetHttpSessionConfigurator extends ServerEndpointConfig.Configurator {
    @Override
    public void modifyHandshake(ServerEndpointConfig config, HandshakeRequest request, HandshakeResponse response) {
        HttpSession httpSession = (HttpSession) request.getHttpSession();
        config.getUserProperties().put(HttpSession.class.getName(),httpSession);
    }
}

WebSocketConfig类:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

@Configuration
public class WebSocketConfig {

    @Bean
    public ServerEndpointExporter serverEndpointExporter(){
        return new ServerEndpointExporter();
    }

}

controller类:

import com.ljp.websocket.pojo.Result;
import com.ljp.websocket.pojo.User;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpSession;
@RestController
public class UserController {
    @PostMapping("/login")
    public Result login(@RequestBody User user, HttpSession session) {
        Result result = new Result();
        if (user != null && "123".equals(user.getPassword())) {
            result.setFlag(true);
            // 将数据存储到 session 对象中
            session.setAttribute("user", user.getUsername());
        } else {
            result.setFlag(false);
            result.setMessage("登陆失败");
        }
        return result;
    }
}

pojo包:

Message类:

/**
浏览器发送给服务器的websocket数据
 */
public class Message {
    private String toName;
    private String message;

    public String getToName() {
        return toName;
    }

    public void setToName(String toName) {
        this.toName = toName;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }
}
Result类:
import lombok.Data;

@Data
public class Result {
  public   boolean flag;
  public   String message;
}
ResultMessage类:

**
 * 服务器发送给浏览器的websocket数据
 */
public class ResultMessage {

    private boolean isSystem;
    private String fromName;
    private Object message;//如果是系统消息是数组

    public boolean getIsSystem() {
        return isSystem;
    }

    public void setIsSystem(boolean isSystem) {
        this.isSystem = isSystem;
    }

    public String getFromName() {
        return fromName;
    }

    public void setFromName(String fromName) {
        this.fromName = fromName;
    }

    public Object getMessage() {
        return message;
    }

    public void setMessage(Object message) {
        this.message = message;
    }
}

user类:
 

import lombok.Data;

@Data
public class User {
    private int id;
    private String username;
    private String password;

}
Utils包:
MessageUtils类:


import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ljp.websocket.pojo.Result;
import com.ljp.websocket.pojo.ResultMessage;

/**
 用来封装消息的工具类
 */
public class MessageUtils {

    public static String getMessage(boolean isSystemMessage,String fromName, Object message) {
        try {
            ResultMessage result = new ResultMessage();
            result.setIsSystem(isSystemMessage);
            result.setMessage(message);
            if(fromName != null) {
                result.setFromName(fromName);
            }
            ObjectMapper mapper = new ObjectMapper();

            return mapper.writeValueAsString(result);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
        return null;
    }
}

ws包:
 

ChatEndpoint类:


import com.alibaba.fastjson.JSON;
import com.ljp.websocket.config.GetHttpSessionConfigurator;
import com.ljp.websocket.pojo.Message;
import com.ljp.websocket.utils.MessageUtils;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpSession;
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

@ServerEndpoint(value = "/chat",configurator = GetHttpSessionConfigurator.class)
@Component
public class ChatEndpoint {
    private static final Map<String, Session> onlineUsers = new ConcurrentHashMap<>();
    public HttpSession httpsession;

    @OnOpen
    public void onOpen(Session session, EndpointConfig config) {
        //1.通过HttpSession获取到用户名
        httpsession = (HttpSession) config.getUserProperties().get(HttpSession.class.getName());
        String user =(String) httpsession.getAttribute("user");
       onlineUsers.put(user,session);
        //2.广播消息,将登录的所以用户推送给所有在线用户
        String message = MessageUtils.getMessage(true, null, onlineUsers.keySet());
        broadcastAllUsers(message);
    }
     //广播方法
    private void broadcastAllUsers(String message) {
        try {
            //遍历map集合
            Set<Map.Entry<String, Session>> entries = onlineUsers.entrySet();
            for (Map.Entry<String, Session> entry : entries) {
                //获取到所有用户对应的session对象
                Session session = entry.getValue();
                //发送消息(这里是同步)
                session.getBasicRemote().sendText(message);
            }
        } catch (Exception e) {
            //记录日志
        }
    }

    @OnMessage
    public void onMessage(String message) {
        try {
            //将消息推送给指定用户
            Message msg= JSON.parseObject(message,Message.class);
            //获取到接收者的名称
            String toName = msg.getToName();
            String mess=msg.getMessage();
            //获取消息接收方的session
            Session session = onlineUsers.get(toName);
            String msg1 = MessageUtils.getMessage(false, (String) httpsession.getAttribute("user"), mess);
            session.getBasicRemote().sendText(msg1);
        }catch (Exception e){
            e.printStackTrace();
        }

    }
    @OnClose
    public void onClose(Session session) {
         //1.从onlineUsers剔除下线用户
        String user =(String) httpsession.getAttribute("user");
           onlineUsers.remove(user);
        //2.广播消息,将下线的用户推送给所有在线用户
        String message = MessageUtils.getMessage(true, null, onlineUsers.keySet());
        broadcastAllUsers(message);

    }

}

(三)前端页面

index.html:
 

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>聊天登录</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            background-color: #f4f4f4;
            display: flex;
            justify-content: center;
            align-items: center;
            height: 100vh;
            margin: 0;
        }
        .login-container {
            background-color: white;
            padding: 20px;
            border-radius: 8px;
            box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
            width: 300px;
        }
        .login-container h2 {
            text-align: center;
            margin-bottom: 20px;
        }
        .form-group {
            margin-bottom: 15px;
        }
        .form-group label {
            display: block;
            margin-bottom: 5px;
        }
        .form-group input {
            width: 100%;
            padding: 8px;
            border: 1px solid #ddd;
            border-radius: 4px;
            box-sizing: border-box;
        }
        button {
            width: 100%;
            padding: 10px;
            background-color: #4CAF50;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-size: 16px;
        }
        button:hover {
            background-color: #45a049;
        }
        .error-message {
            color: #f44336;
            margin-bottom: 10px;
            text-align: center;
        }
    </style>
</head>
<body>
<div class="login-container">
    <h2>聊天登录</h2>
    <div id="errorMessage" class="error-message" style="display: none;"></div>
    <form id="loginForm">
        <div class="form-group">
            <label for="username">用户名:</label>
            <input type="text" id="username" required>
        </div>
        <div class="form-group">
            <label for="password">密码:</label>
            <input type="password" id="password" required>
        </div>
        <button type="submit">登录</button>
    </form>
</div>

<script>
    document.getElementById('loginForm').addEventListener('submit', function(e) {
        e.preventDefault();
        const username = document.getElementById('username').value;
        const password = document.getElementById('password').value;

        fetch('http://localhost:8080/login', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json'
            },
            body: JSON.stringify({ username, password })
        })
            .then(response => response.json())
            .then(data => {
                console.log(data);
                if (data.flag) {
                    // 登录成功,跳转到聊天页面
                    window.location.href = 'chat.html'; // 请确保chat.html存在或修改为实际路径
                } else {
                    // 登录失败,显示错误信息
                    const errorMessage = document.getElementById('errorMessage');
                    errorMessage.textContent = data.message || '登录失败,请重试';
                    errorMessage.style.display = 'block';
                }
            })
            .catch(error => {
                console.error('登录请求出错:', error);
                const errorMessage = document.getElementById('errorMessage');
                errorMessage.textContent = '网络错误,请重试';
                errorMessage.style.display = 'block';
            });
    });
</script>
</body>
</html>

chat.html:
 

<!DOCTYPE html>
<html lang="zh-CN">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>WebSocket 聊天示例</title>
    <style>
        #chatContainer {
            width: 400px;
            margin: 0 auto;
            border: 1px solid #ccc;
            border-radius: 5px;
            padding: 10px;
        }

        #messageList {
            height: 300px;
            overflow-y: auto;
            margin-bottom: 10px;
            border: 1px solid #eee;
            padding: 5px;
        }

        input[type="text"] {
            width: 70%;
            padding: 5px;
        }

        button {
            padding: 5px 10px;
        }

        #onlineUsers {
            margin-bottom: 10px;
            font-weight: bold;
        }
    </style>
</head>

<body>
<div id="chatContainer">
    <div id="onlineUsers">在线用户:</div>
    <div id="messageList"></div>
    <input type="text" id="messageInput" placeholder="输入消息">
    <button onclick="sendMessage('lisi')">给lisi发送</button>
    <button onclick="sendMessage('zhangsan')">给zhangsan发送</button>
</div>

<script>
    let socket;
    let username;

                connectWebSocket();


    function connectWebSocket() {
        socket = new WebSocket('ws://' + window.location.host + '/chat');
        socket.onopen = function (event) {
            console.log('WebSocket 连接已建立');
        };
        socket.onmessage = function (event) {
            try {
                const data = JSON.parse(event.data);
                console.log(data);
                const messageList = document.getElementById('messageList');
                const onlineUsersDiv = document.getElementById('onlineUsers');

                if (data.isSystem) {
                    // 系统消息(在线用户更新等)
                    if (data.message && Array.isArray(data.message)) {
                        onlineUsersDiv.innerHTML = '在线用户:' + data.message.join(', ');
                    } else {
                        console.warn('在线用户列表格式不正确:', data.message);
                        onlineUsersDiv.innerHTML = '在线用户:无法获取';
                    }
                } else {
                    // 聊天消息
                    const messageItem = document.createElement('div');
                    // 修正:使用 data.fromName 而不是 data.from
                    messageItem.innerText = (data.fromName || '系统') + ':' + data.message;
                    messageList.appendChild(messageItem);
                }
            } catch (error) {
                console.error('消息解析失败:', error, '原始数据:', event.data);
            }
        };
        socket.onclose = function (event) {
            console.log('WebSocket 连接已关闭');
        };
        socket.onerror = function (event) {
            console.error('WebSocket 出错:', event);
        };
    }

    function sendMessage(name) {
        const input = document.getElementById('messageInput');
        const message = input.value.trim();
        const messageList = document.getElementById('messageList');
        if (message!== '') {
            const sendData = {
                toName: name,
                message: message
            };
            socket.send(JSON.stringify(sendData));
            const messageItem = document.createElement('div');
            messageItem.innerText = "我" + ':' + input.value;
            messageList.appendChild(messageItem);
            input.value = '';
        }
    }
</script>
</body>

</html>

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐