代码提交

This commit is contained in:
liujing33 2025-05-08 17:47:04 +08:00
parent 2b47aa76d7
commit eb8d03fca2
10 changed files with 3502 additions and 3018 deletions

1
.gitignore vendored
View File

@ -3,3 +3,4 @@ node_modules/
src/.vuepress/.cache/
src/.vuepress/.temp/
src/.vuepress/dist/
.idea/*

12
.idea/blog.iml Normal file
View 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
View 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
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

View File

@ -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>

View File

@ -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",

View File

@ -9,4 +9,4 @@ date: 2025-05-06
## 自我介绍
> 你好哇哈哈哈哈的武器恶趣味大苏打撒旦eqweqwesadasdwewcasdqweqwfc1231233dsczxcxzc
> 一个瞎鼓捣得程序员

View 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的优势与应用场景
相比传统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>
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB