代码提交
This commit is contained in:
parent
2b47aa76d7
commit
eb8d03fca2
1
.gitignore
vendored
1
.gitignore
vendored
@ -3,3 +3,4 @@ node_modules/
|
||||
src/.vuepress/.cache/
|
||||
src/.vuepress/.temp/
|
||||
src/.vuepress/dist/
|
||||
.idea/*
|
||||
|
12
.idea/blog.iml
Normal file
12
.idea/blog.iml
Normal file
@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/temp" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/tmp" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
8
.idea/modules.xml
Normal file
8
.idea/modules.xml
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/blog.iml" filepath="$PROJECT_DIR$/.idea/blog.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
6
.idea/vcs.xml
Normal file
6
.idea/vcs.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
@ -5,7 +5,7 @@ import HitokotoBlogHero from "vuepress-theme-hope/presets/HitokotoBlogHero.js";
|
||||
|
||||
<template>
|
||||
<BlogHero>
|
||||
<template #info="info" #bg>
|
||||
<template #info="info">
|
||||
<HitokotoBlogHero v-bind="info" />
|
||||
</template>
|
||||
</BlogHero>
|
||||
|
@ -26,6 +26,7 @@ export default sidebar({
|
||||
{
|
||||
text: "Java",
|
||||
collapsible: true,
|
||||
expanded: true,
|
||||
icon: "mdi:language-java",
|
||||
prefix: "java/",
|
||||
children: [
|
||||
@ -38,7 +39,7 @@ export default sidebar({
|
||||
},
|
||||
{
|
||||
text: "工具箱",
|
||||
icon: "simple-icons:framework",
|
||||
icon: "mdi:tools",
|
||||
collapsible: true,
|
||||
prefix: "工具箱/",
|
||||
children: "structure",
|
||||
|
@ -9,4 +9,4 @@ date: 2025-05-06
|
||||
## 自我介绍
|
||||
|
||||
|
||||
> 你好哇哈哈哈哈的武器恶趣味大苏打撒旦eqweqwesadasdwewcasdqweqwfc1231233dsczxcxzc
|
||||
> 一个瞎鼓捣得程序员
|
456
src/programming/java/工具箱/WebSocket和HTTP关系.md
Normal file
456
src/programming/java/工具箱/WebSocket和HTTP关系.md
Normal file
@ -0,0 +1,456 @@
|
||||
---
|
||||
icon: bi:arrows-expand
|
||||
date: 2025-05-08
|
||||
category:
|
||||
- JAVA
|
||||
- 通信协议
|
||||
tag:
|
||||
- websocket
|
||||
- http
|
||||
---
|
||||
|
||||
# WebSocket和HTTP关系
|
||||
<!-- more -->
|
||||
## 1. WebSocket简介
|
||||
|
||||
WebSocket 是 HTML5 提供的一种在单个 TCP 连接上进行全双工通讯的协议。它使客户端和服务器之间的数据交换变得更加简单高效,并允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需完成一次握手,便可创建持久性的连接,实现双向数据传输。
|
||||
<!-- more -->
|
||||
## 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 项目结构
|
||||

|
||||
|
||||
### 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>
|
||||
|
||||
|
||||
```
|
BIN
src/programming/java/工具箱/assert/1746684922168.png
Normal file
BIN
src/programming/java/工具箱/assert/1746684922168.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
Loading…
Reference in New Issue
Block a user