14 KiB
icon | date | category | tag | title | ||||
---|---|---|---|---|---|---|---|---|
bi:arrows-expand | 2025-05-08 |
|
|
WebSocket和HTTP关系 |
WebSocket和HTTP关系
1. WebSocket简介
WebSocket 是 HTML5 提供的一种在单个 TCP 连接上进行全双工通讯的协议。它使客户端和服务器之间的数据交换变得更加简单高效,并允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需完成一次握手,便可创建持久性的连接,实现双向数据传输。
2. WebSocket与HTTP的关系
2.1 协议转换过程
WebSocket依赖于HTTP连接初始化,但随后会进行协议升级。具体转换过程如下:
- 初始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
- 服务器响应升级:如果服务器支持WebSocket协议,会返回101状态码,表示协议正在切换:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
- 协议切换完成:此时HTTP请求已完成其使命,如果协议升级成功,客户端会触发
onopen
事件;否则触发onerror
事件。此后的所有通信都使用WebSocket协议,不再依赖HTTP。
2.2 为什么WebSocket要依赖HTTP协议连接?
WebSocket选择依赖HTTP协议有几个重要原因:
-
设计理念:WebSocket设计之初就是为HTTP增强通信能力(尤其是全双工通信),因此在HTTP协议基础上扩展是自然的选择,能够复用HTTP的基础设施。
-
最大兼容性:基于HTTP连接可获得最广泛的兼容支持。即使服务器不支持WebSocket,也能建立HTTP通信并返回相应错误,这比完全无响应要好得多。
-
防火墙友好:大多数网络环境允许HTTP流量通过(80和443端口),基于HTTP的WebSocket更容易穿越防火墙和代理服务器。
-
减少实现复杂度:复用现有的HTTP基础设施,无需从零开始构建新的协议栈。
2.3 HTTP与WebSocket的主要区别
特性 | HTTP | WebSocket |
---|---|---|
连接类型 | 无状态、短连接 | 有状态、长连接 |
通信方式 | 单向(请求-响应) | 双向(全双工) |
数据交互模式 | 客户端主动请求 | 双方均可主动发送 |
数据传输量 | 每次请求都有HTTP头 | 握手后数据传输更轻量 |
实时性 | 弱(通常需轮询) | 强(可即时推送) |
使用场景 | 传统网页请求、RESTful API | 聊天应用、实时数据更新、在线游戏等 |
3. WebSocket的优势与应用场景
相比传统HTTP,WebSocket具有以下优势:
-
降低延迟:一旦建立连接,通信双方可随时发送数据,无需重复建立连接。
-
减少网络流量:WebSocket在握手后的通信中没有HTTP头的开销,数据传输更高效。
-
实时双向通信:服务器可以主动推送信息给客户端,无需客户端轮询。
-
保持连接状态:WebSocket是有状态协议,可维护连接上下文信息。
典型应用场景:
- 实时通讯应用(聊天室、即时通讯工具)
- 在线协作工具(协同编辑文档)
- 实时数据展示(股票行情、体育赛事直播)
- 游戏应用(在线多人游戏)
- 物联网设备通信
4. 总结
WebSocket与HTTP是相辅相成的关系,而非替代关系。WebSocket通过HTTP协议完成初始握手,随后升级为独立的协议,实现更高效的双向通信。两种协议各有优势,应根据应用场景选择合适的通信方式。在需要实时性、双向通信的场景中,WebSocket展现出明显优势;而对于简单的数据获取和传统网页浏览,HTTP仍然是最佳选择。
5. 案例(服务端)
5.1 项目结构
5.2 依赖配置
<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 主应用类
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配置类
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 消息实体类
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断连通知类
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
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 页面
<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>