blog/src/programming/java/工具箱/WebSocket和HTTP关系.md

457 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
icon: bi:arrows-expand
date: 2025-05-08
category:
- JAVA
- 通信协议
tag:
- websocket
- http
title: WebSocket和HTTP关系
---
# WebSocket和HTTP关系
<!-- more -->
## 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的优势与应用场景
相比传统HTTPWebSocket具有以下优势
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
<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>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.3</version>
<relativePath/>
</parent>
<artifactId>websocket</artifactId>
<packaging>jar</packaging>
<name>websocket</name>
<url>http://maven.apache.org</url>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>
```
### 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<ChatMessage, 'type'>) {
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
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount } from 'vue';
import wsService from '../utils/websocket.service';
const messages = ref<{ content: string; sender: string; type: 'CHAT' | 'JOIN' | 'LEAVE' }[]>([]);
const messageInput = ref('');
const username = ref(''); // 可替换为动态用户名
// 处理发送消息
function handleSendMessage() {
const content = messageInput.value.trim();
if (!content) return;
wsService.sendMessage({
content,
sender: username.value,
});
messageInput.value = '';
}
// 接收消息回调
function onMessageReceived(msg: { content: string; sender: string; type: 'CHAT' | 'JOIN' | 'LEAVE' }) {
messages.value.push(msg);
}
function createConnect(){
// 建立连接并注册消息回调
wsService.connect(username.value, onMessageReceived);
}
onMounted(() => {
});
onBeforeUnmount(() => {
// 组件卸载前断开连接
wsService.disconnect();
});
</script>
<template>
<div class="chat-container">
<h2>WebSocket 聊天室</h2>
<input v-model="username" placeholder="输入姓名" @keyup.enter="createConnect">
<div class="messages">
<div v-for="(msg, index) in messages" :key="index" class="message">
<strong>{{ msg.sender }}:</strong> {{ msg.content }}
</div>
</div>
<input
v-model="messageInput"
@keyup.enter="handleSendMessage"
placeholder="输入消息..."
/>
</div>
</template>
<style scoped>
.chat-container {
max-width: 600px;
margin: auto;
padding: 20px;
}
.messages {
border: 1px solid #ccc;
padding: 10px;
height: 300px;
overflow-y: auto;
margin-bottom: 10px;
}
.message {
margin-bottom: 5px;
}
input {
width: 100%;
padding: 8px;
}
</style>
```