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

14 KiB
Raw Blame History

icon date category tag title
bi:arrows-expand 2025-05-08
JAVA
通信协议
websocket
http
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
  1. 服务器响应升级如果服务器支持WebSocket协议会返回101状态码表示协议正在切换
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
  1. 协议切换完成此时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

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>