--- icon: bi:arrows-expand date: 2025-05-08 category: - JAVA - 通信协议 tag: - websocket - http title: WebSocket和HTTP关系 --- # WebSocket和HTTP关系 ## 1. WebSocket简介 WebSocket 是 HTML5 提供的一种在单个 TCP 连接上进行全双工通讯的协议。它使客户端和服务器之间的数据交换变得更加简单高效,并允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需完成一次握手,便可创建持久性的连接,实现双向数据传输。 ## 2. WebSocket与HTTP的关系 ### 2.1 协议转换过程 WebSocket依赖于HTTP连接初始化,但随后会进行协议升级。具体转换过程如下: 1. **初始HTTP请求**:每个WebSocket连接都始于一个HTTP请求。客户端发送标准的HTTP请求,但包含特殊的头信息,表明希望升级为WebSocket协议: ``` GET /chat HTTP/1.1 Host: server.example.com Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== Origin: http://example.com Sec-WebSocket-Version: 13 ``` 2. **服务器响应升级**:如果服务器支持WebSocket协议,会返回101状态码,表示协议正在切换: ``` HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= ``` 3. **协议切换完成**:此时HTTP请求已完成其使命,如果协议升级成功,客户端会触发`onopen`事件;否则触发`onerror`事件。此后的所有通信都使用WebSocket协议,不再依赖HTTP。 ### 2.2 为什么WebSocket要依赖HTTP协议连接? WebSocket选择依赖HTTP协议有几个重要原因: 1. **设计理念**:WebSocket设计之初就是为HTTP增强通信能力(尤其是全双工通信),因此在HTTP协议基础上扩展是自然的选择,能够复用HTTP的基础设施。 2. **最大兼容性**:基于HTTP连接可获得最广泛的兼容支持。即使服务器不支持WebSocket,也能建立HTTP通信并返回相应错误,这比完全无响应要好得多。 3. **防火墙友好**:大多数网络环境允许HTTP流量通过(80和443端口),基于HTTP的WebSocket更容易穿越防火墙和代理服务器。 4. **减少实现复杂度**:复用现有的HTTP基础设施,无需从零开始构建新的协议栈。 ### 2.3 HTTP与WebSocket的主要区别 |特性|HTTP|WebSocket| |---|---|---| |连接类型|无状态、短连接|有状态、长连接| |通信方式|单向(请求-响应)|双向(全双工)| |数据交互模式|客户端主动请求|双方均可主动发送| |数据传输量|每次请求都有HTTP头|握手后数据传输更轻量| |实时性|弱(通常需轮询)|强(可即时推送)| |使用场景|传统网页请求、RESTful API|聊天应用、实时数据更新、在线游戏等| ## 3. WebSocket的优势与应用场景 相比传统HTTP,WebSocket具有以下优势: 1. **降低延迟**:一旦建立连接,通信双方可随时发送数据,无需重复建立连接。 2. **减少网络流量**:WebSocket在握手后的通信中没有HTTP头的开销,数据传输更高效。 3. **实时双向通信**:服务器可以主动推送信息给客户端,无需客户端轮询。 4. **保持连接状态**:WebSocket是有状态协议,可维护连接上下文信息。 典型应用场景: - 实时通讯应用(聊天室、即时通讯工具) - 在线协作工具(协同编辑文档) - 实时数据展示(股票行情、体育赛事直播) - 游戏应用(在线多人游戏) - 物联网设备通信 ## 4. 总结 WebSocket与HTTP是相辅相成的关系,而非替代关系。WebSocket通过HTTP协议完成初始握手,随后升级为独立的协议,实现更高效的双向通信。两种协议各有优势,应根据应用场景选择合适的通信方式。在需要实时性、双向通信的场景中,WebSocket展现出明显优势;而对于简单的数据获取和传统网页浏览,HTTP仍然是最佳选择。 ## 5. 案例(服务端) ### 5.1 项目结构 ![1746684922168.png](assert/1746684922168.png) ### 5.2 依赖配置 ```xml 4.0.0 org.springframework.boot spring-boot-starter-parent 2.4.3 websocket jar websocket http://maven.apache.org org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-websocket org.projectlombok lombok true ``` ### 5.3 主应用类 ```java package com.mangmang; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class WebSocketDemoApplication { public static void main(String[] args) { SpringApplication.run(WebSocketDemoApplication.class, args); } } ``` ### 5.4 WebSocket配置类 ```java package com.mangmang.config; import org.springframework.context.annotation.Configuration; import org.springframework.messaging.simp.config.MessageBrokerRegistry; import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; import org.springframework.web.socket.config.annotation.StompEndpointRegistry; import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; @Configuration @EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { @Override public void registerStompEndpoints(StompEndpointRegistry registry) { // 注册STOMP协议的节点(endpoint) registry.addEndpoint("/ws").setAllowedOrigins("http://10.6.212.39:5173/"); } @Override public void configureMessageBroker(MessageBrokerRegistry registry) { // 配置消息代理,前缀为/topic的消息将会被路由到消息代理 registry.enableSimpleBroker("/topic"); // 以/app开头的消息将会被路由到@MessageMapping注解的方法中 registry.setApplicationDestinationPrefixes("/app"); } } ``` ### 5.5 消息实体类 ```java package com.mangmang.model; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; @Data @Builder @NoArgsConstructor @AllArgsConstructor public class Message { private String content; private String sender; private MessageType type; public enum MessageType { CHAT, JOIN, LEAVE } } ``` ### 5.6 消息控制类 ``` package com.mangmang.controller; import com.mangmang.model.Message; import lombok.extern.slf4j.Slf4j; import org.springframework.messaging.handler.annotation.MessageMapping; import org.springframework.messaging.handler.annotation.Payload; import org.springframework.messaging.handler.annotation.SendTo; import org.springframework.messaging.simp.SimpMessageHeaderAccessor; import org.springframework.stereotype.Controller; import java.util.Objects; @Slf4j @Controller public class MessageController { @MessageMapping("/chat.sendMessage") @SendTo("/topic/public") public Message sendMessage(@Payload Message message) { log.info(message.toString()); return message; } @MessageMapping("/chat.addUser") @SendTo("/topic/public") public Message addUser(@Payload Message message, SimpMessageHeaderAccessor headerAccessor) { log.info(message.toString()); // 将用户名添加到WebSocket会话 Objects.requireNonNull(headerAccessor.getSessionAttributes()).put("username", message.getSender()); return message; } } ``` ### 5.7 websocket断连通知类 ```java package com.mangmang.listener; import com.mangmang.model.Message; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.context.event.EventListener; import org.springframework.messaging.simp.SimpMessageHeaderAccessor; import org.springframework.messaging.simp.SimpMessageSendingOperations; import org.springframework.stereotype.Component; import org.springframework.web.socket.messaging.SessionDisconnectEvent; import java.util.Objects; @Slf4j @Component @RequiredArgsConstructor public class WebSocketEventListener { private final SimpMessageSendingOperations messagingTemplate; @EventListener public void handleWebSocketDisconnectListener(SessionDisconnectEvent event) { // 获取会话属性 SimpMessageHeaderAccessor headerAccessor = SimpMessageHeaderAccessor.wrap(event.getMessage()); // 从会话中获取用户名 String username = (String) Objects.requireNonNull(headerAccessor.getSessionAttributes()).get("username"); if (username != null) { log.info("用户断开连接: {}", username); // 创建一个离开消息 Message message = Message.builder() .type(Message.MessageType.LEAVE) .sender(username) .content("下线了") .build(); // 发送消息到公共主题 messagingTemplate.convertAndSend("/topic/public", message); } } } ``` ## 6. 案例(客户端) ### 6.1 封装websocket ```ts import {Client} from '@stomp/stompjs'; interface ChatMessage { content: string; sender: string; type: 'CHAT' | 'JOIN' | 'LEAVE'; } class WebSocketService { private stompClient: Client | null = null; connect(username: string, onMessageReceived: (msg: ChatMessage) => void) { // 创建原生 WebSocket 连接 const socket = new WebSocket('ws://10.6.212.39:8099/ws'); // 确保这里是 WebSocket 协议 this.stompClient = new Client({ webSocketFactory: () => socket, onConnect: () => { console.log('STOMP connected'); // 订阅公共主题 this.stompClient?.subscribe('/topic/public', (message) => { const chatMsg: ChatMessage = JSON.parse(message.body); onMessageReceived(chatMsg); }); // 发送用户加入消息 this.sendAddUserMessage(username); }, onStompError: (frame) => { console.error('Broker reported error: ', frame.headers['message']); console.error('Additional details: ', frame.body); }, // 关闭时清理资源 onDisconnect: () => { console.log('Disconnected from STOMP'); this.stompClient = null; } }); // 激活客户端 this.stompClient.activate(); } sendMessage(chatMessage: Omit) { if (this.stompClient?.connected) { this.stompClient.publish({ destination: '/app/chat.sendMessage', body: JSON.stringify(chatMessage), }); } else { console.warn('WebSocket not connected'); } } sendAddUserMessage(username: string) { if (this.stompClient?.connected) { this.stompClient.publish({ destination: '/app/chat.addUser', body: JSON.stringify({content: "加入聊天", sender: username, type: 'JOIN'}), }); } } disconnect() { if (this.stompClient) { this.stompClient.deactivate(); this.stompClient = null; } } } export default new WebSocketService(); ``` ### 6.2 页面 ```vue ```