【stomp实战】websocket原理解析与简单使用

一、WebSocket 原理

WebSocket是HTML5提供的一种浏览器与服务器进行全双工通讯的网络技术,属于应用层协议。它基于TCP传输协议,并复用HTTP的握手通道。浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接, 并进行双向数据传输。

WebSocket 的出现就解决了半双工通信的弊端。它最大的特点是:服务器可以向客户端主动推动消息,客户端也可以主动向服务器推送消息。

在这里插入图片描述
WebSocket 特点的如下:
● 支持双向通信,实时性更强
● 可以发送文本,也可以发送二进制数据
● 建立在TCP协议之上,服务端的实现比较容易
● 数据格式比较轻量,性能开销小,通信高效
● 没有同源限制,客户端可以与任意服务器通信
● 协议标识符是ws(如果加密,则为wss),服务器网址就是 URL
● 与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。

二、WebSocket 握手

WebSocket 服务端使用标准 TCP 套接字监听进入的连接。下文假定服务端监听 http://example.com 的 8000 端口,响应 http://example.com/chat 上的 GET 请求。

握手是 WebSocket 中 “Web”。它是从 HTTP 到 WebSocket 的桥梁。在握手过程中,协商连接的细节,并且如果行为不合法,那么任何一方都可以在完成前退出。服务端必须仔细理解客户端的所有要求,否则可能出现安全问题。

2.1 客户端握手请求

客户端通过联系服务端,请求 WebSocket 连接的方式,发起 WebSocket 握手流程。客户端发送带有如下请求头的标准 HTTP 请求(HTTP 版本必须是 1.1 或更高,并且请求方法必须是 GET):

GET /chat HTTP/1.1
Host: example.com:8000
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13

在这里,客户端可以请求扩展和/或子协议。此外,也可以使用常见的请求头,比如 User-Agent、Referer、Cookie 或者身份验证请求头。这些请求头与 WebSocket 没有直接关联。

如果存在不合法的请求头,那么服务端应该发送 400 响应(“Bad Request”),并且立即关闭套接字。通常情况下,服务端可以在 HTTP 响应体中提供握手失败的原因 。如果服务端不支持该版本的 WebSocket,那么它应该发送包含它支持的版本的 Sec-WebSocket-Version 头。在上面的示例中,它指示 WebSocket 协议的版本为 13。

在请求头中,最值得关注的是 Sec-WebSocket-Key。接下来,将讲述它。

2.2 服务端握手响应

当服务端收到握手请求时,将发送一个特殊响应,该响应表明协议将从 HTTP 变更为 WebSocket。
在这里插入图片描述

该响应头大致如下(记住,每个响应头行以 \r\n 结尾,在最后一行的后面添加额外的 \r\n,以说明响应头结束):

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

此外,服务端可以在这里对扩展/子协议请求做出选择。Sec-WebSocket-Accept响应头很重要,服务端必须通过客户端发送的Sec-WebSocket-Key请求头生成它。具体的方式是,将客户端的Sec-WebSocket-Key与字符串"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"(“魔法字符串”)连接在一起,然后对结果进行 SHA-1 哈希运算,最后返回哈希值的 Base64 编码。

因此,如果 Key 为"dGhlIHNhbXBsZSBub25jZQ==“,那么Sec-WebSocket-Accept响应头的值是"s3pPLMBiTxaQ9kYGzzhZRbK+xOo=”。服务端发送这些响应头后,握手完成,可以开始交换数据。

下面的 Python 代码根据Sec-WebSocket-Key请求头生成Sec-WebSocket-Accept响应头的值:

import typing
from hashlib import sha1
import base64

SEC_WS_MAGIC_STRING: bytes = b"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"

def get_sec_ws_accept(sec_ws_key: typing.Union[bytes, str]) -> bytes:
    if isinstance(sec_ws_key, str):
        sec_ws_key = sec_ws_key.encode()
    return base64.b64encode(sha1(sec_ws_key + SEC_WS_MAGIC_STRING).digest())

if __name__ == "__main__":
    assert get_sec_ws_accept(b"dGhlIHNhbXBsZSBub25jZQ==") == b"s3pPLMBiTxaQ9kYGzzhZRbK+

三、数据帧(Data Framing)

3.1 概览

在 WebSocket 协议中,使用一系列帧传输数据。为避免混淆网络中间人(比如拦截代理),以及出于安全考虑,客户端必须对发送给服务端的所有帧进行掩码(Mask)处理。(注意,无论 WebSocket 协议是否运行在 TLS 上,都需要进行掩码处理。)服务端在收到未进行掩码处理的帧时,必须关闭连接。在这种情况下,服务端可以发送状态码为 1002(协议错误)的关闭帧。服务端不得对发送给客户端的任何帧进行掩码处理。如果客户端检测到掩码帧,那么必须关闭连接。在这种情况下,可以使用状态码 1002(协议错误)。

基础帧协议定义了一种帧类型,包括操作码(Opcode)、有效载荷长度,以及“扩展数据”和“应用数据”的指定位置,它们一起定义“有效载荷数据”。一些位和操作码被保留,以供未来扩展协议。

在握手完成后,端点被发送关闭帧前,客户端和服务端可以随时传输数据帧。

3.2 基础帧协议

帧的格式如下图所示:
在这里插入图片描述

FIN:1 比特

表示该帧是消息中的最后一个分片。第一个分片也可能是最后一个分片。

RSV1、RSV2、RSV3:每个 1 比特

除非协商了定义非零值含义的扩展,否则必须为 0。如果收到非零值,并且没有协商的扩展定义该非零值的含义,那么接收端点必须使该 WebSocket 连接失败。

操作码:4 比特

定义对“有效载荷数据”的解释。如果收到未知操作码,那么接收端点必须使该 WebSocket 连接失败。定义的值如下:

%x0 表示延续帧
%x1 表示文本帧
%x2 表示二进制帧
%x3-7 为将来的非控制帧预留
%x8 表示连接关闭
%x9 表示 PING
%xA 表示 PONG
%xB-F 为将来的控制帧保留
掩码:1 比特

定义“有效载荷数据”是否被掩码处理。如果设置为 1,那么掩码键出现在 Masking-key 中,它用于解除“有效载荷数据”的掩码。从客户端发送到服务器的所有帧都将此位设置为 1。

有效载荷长度:7 比特,7+16 比特,或 7+64 比特

“有效载荷数据”的长度,单位是字节:如果设置为 0-125,那么它是有效载荷长度。如果设置为 126,那么接下来的 2 个字节(被解释为 16 位无符号整数)是有效载荷长度。如果设置为 127,那么接下来的 8 个字节(被解释为 64 位无符号整数,最高有效位必须为 0)是有效载荷长度。多字节长度量使用网络字节序表示。注意,在所有情况下,必须使用最小字节数编码长度,比如,124 字节长的字符串的长度不能编码为序列 126, 0, 124。有效载荷的长度是“扩展数据”的长度 + “应用数据”的长度。“扩展数据”的长度可能为 0,在这种情况下,有效载荷长度是“应用数据”的长度。

掩码键:0 或 4 字节

从客户端发送到服务端的所有帧必须通过包含在帧里的 32 位数值进行掩码处理。如果掩码位为 1,那么该字段存在,如果掩码位为 0,那么该字段不存在。

有效载荷数据:(x+y) 字节

“有效载荷数据”被定义为将 “扩展数据” 与 “应用数据” 连接在一起。

扩展数据:x 字节

除非已经协商了扩展,否则“扩展数据”为 0 字节。所有扩展必须指定"扩展数据"的长度,或者如何计算该长度,并且在开始握手期间,必须协商扩展的使用方式。如果存在,那么“扩展数据”包含在总有效载荷长度中。

应用数据:y 字节

任意“应用数据”,占用帧中“扩展数据”后面的剩余部分。“应用数据”的长度等于有效载荷长度减去“扩展数据”的长度。

3.3 消息分片(Message Fragmentation)
FIN 和 Opcode 字段共同协作,发送被拆分成单独帧的消息。这被称为消息分片。分片仅适用于 Opcode 0x0 到 0x2 的情况。

Opcode 说明帧的用途。如果为 0x1,那么有效载荷是文本。如果为 0x2,那么有效载荷是二进制数据。如果为 0x0,那么该帧是延续帧;这意味着服务端应该将该帧的有效载荷连接到其从该客户端收到的最后一个帧。在下面的草图中,服务端对发送文本消息的客户端做出响应。第一条消息以单个帧发送,而第二条消息用三个帧发送。下图仅显示客户端的 FIN 和 Opcode 细节:

Client: FIN=1, opcode=0x1, msg="hello"
Server: (process complete message immediately) Hi.
Client: FIN=0, opcode=0x1, msg="and a"
Server: (listening, new message containing text started)
Client: FIN=0, opcode=0x0, msg="happy new"
Server: (listening, payload concatenated to previous message)
Client: FIN=1, opcode=0x0, msg="year!"
Server: (process complete message) Happy new year to you too!

注意,第一个帧包含整个消息(FIN=1,并且opcode!=0x0),因此服务端可以按需处理或响应。客户端发送的第二个帧的有效载荷是文本(opcode=0x1),但整个消息尚未到达(FIN=0)。该消息的所有剩余部分使用延续帧(opcode=0x0)发送,并且消息的最后一帧用FIN=1标记。

四、Websocket的使用

4.1 客户端开发示例

const ws = new WebSocket('ws://localhost:8080/ws/zhangsan');
ws.onmessage = messageEvent => {
  console.log('onmessage: ', messageEvent.data)
}
ws.onopen = messageEvent => {
  console.log('onopen: ', messageEvent)
}
ws.onclose = messageEvent => {
  console.log('onclose: ', messageEvent)
}
ws.onerror = messageEvent => {
  console.log('onerror: ', messageEvent)
}

// 发送消息
ws.send('hello world~')

在这里插入图片描述

4.2 服务端开发示例

4.2.1 J2EE的websocket

下面的示例是用javaee的接口来进行开发,而不是用springboot提供的实现
依赖引入

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

配置类

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

@Configuration
public class WebSocketConfig {
    @Bean
    public ServerEndpointExporter serverEndpointExporter(){
        return new ServerEndpointExporter();
    }
}

服务端代码

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;

@Slf4j
@Component
@ServerEndpoint("/ws/{userName}")
public class MyServerEndpoint {

    public void sendMessage(Session session, String message) throws IOException {
        log.info("准备向客户端程序{}发送消息:{}", session.getId(), message);
        session.getBasicRemote().sendText(message);
    }

    @OnMessage
    public void onMessage(@PathParam("userName") String userName,String message, Session session) {
        log.info("收到来自客户端{}的消息! sessionId: {}, 消息:{}", userName, session.getId(), message);
        try {
            sendMessage(session, message);
        } catch (IOException e) {
            log.error("消息发送失败!", e);
        }
    }

    @OnOpen
    public void onOpen(@PathParam("userName") String userName, Session session) {
        log.info("客户端程序{}建立连接成功! sessionId:{}", userName, session.getId());
    }

    @OnClose
    public void onClose(@PathParam("userName") String userName, Session session, CloseReason closeReason) {
        log.info("客户端{}断开连接,原因:{}", userName, closeReason);
    }

    @OnError
    public void onError(Session session, Throwable throwable) {
        log.info("连接{}发生错误!", session.getId());
        throwable.printStackTrace();
    }
}

4.2.2 SpringFramework提供的websocket

依赖引入

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

配置类

@Slf4j
@Configuration
@EnableWebSocket
public class SockConfig implements WebSocketConfigurer {

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(echoWebSocketHandler(), "/ws/{userName}")
            .setAllowedOriginPatterns("*")
            .setHandshakeHandler(new DefaultHandshakeHandler())
            .addInterceptors(new HttpSessionHandshakeInterceptor() {
                @Override
                public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
                    // 认证等操作。。。
                    return true;
                }
            });
    }

    public WebSocketHandler echoWebSocketHandler() {
        return new TextWebSocketHandler() {
            @Override
            protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
                // 收到的信息
                String requestMsg = message.getPayload();
                System.out.println("服务器收到:" + requestMsg);
                // 组织响应信息
                String responseMsg = "服务器返回: " + requestMsg;
                System.out.println(responseMsg);
                TextMessage respMsg = new TextMessage(responseMsg.getBytes(StandardCharsets.UTF_8));
                // 返回给客户端
                session.sendMessage(respMsg);
            }
        };
    }
}

推荐使用springBoot提供的包来进行开发,封装了很多功能,简化了开发

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/380720.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Python入门知识点分享——(二十)继承和方法重写

今天是大年三十&#xff0c;祝大家龙年大吉&#xff0c;当然无论何时何地&#xff0c;我们都不要忘记继续学习。今天介绍的是继承和方法重写这两种面向对象编程特点。继承机制指的是&#xff0c;一个类&#xff08;我们称其为子类或派生类&#xff09;可以使用另一个类&#xf…

备战蓝桥杯---动态规划(基础3)

本专题主要介绍在求序列的经典问题上dp的应用。 我们上次用前缀和来解决&#xff0c;这次让我们用dp解决把 我们参考不下降子序列的思路&#xff0c;可以令f[i]为以i结尾的最大字段和&#xff0c;易得&#xff1a; f[i]max(a[i],a[i]f[i-1]); 下面是AC代码&#xff1a; #in…

vue3 之 商城项目—登陆

整体认识 登陆页面的主要功能就是表单校验和登陆登出业务 路由配置 模版 <script setup></script><template><div><header class"login-header"><div class"container m-top-20"><h1 class"logo"&g…

【数学建模】【2024年】【第40届】【MCM/ICM】【A题 七鳃鳗性别比与资源可用性】【解题思路】

我们通过将近半天的搜索数据&#xff0c;查到了美国五大湖中优势物种的食物网数据&#xff0c;以Eric伊利湖为例&#xff0c;共包含34各优势物种&#xff0c;相互之间的关系如下图所示&#xff1a; 一、题目 &#xff08;一&#xff09; 赛题原文 2024 MCM Problem A: Reso…

OpenCV 笔记(21):图像色彩空间

1. 图像色彩空间 图像色彩空间是用于定义颜色范围的数学模型。 它规定了图像中可以使用的颜色以及它们之间的关系。它决定了图像中可以显示的颜色范围。不同的色彩空间可以包含不同的颜色范围&#xff0c;因此选择合适的色彩空间对于确保图像在不同设备上看起来一致非常重要。…

【开源】基于JAVA+Vue+SpringBoot的假日旅社管理系统

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 系统介绍2.2 QA 问答 三、系统展示四、核心代码4.1 查询民宿4.2 新增民宿评论4.3 查询民宿新闻4.4 新建民宿预订单4.5 查询我的民宿预订单 五、免责说明 一、摘要 1.1 项目介绍 基于JAVAVueSpringBootMySQL的假日旅社…

专业145+总分400+合肥工业大学833信号分析与处理综合考研经验电子信息通信,真题,大纲,参考书

今年专业课145总分400&#xff0c;我总结一下自己的专业课合肥工业大学833信号分析与处理和其他几门的复习经验。希望对大家复习有帮助。 我所用的教材是郑君里的《信号与系统》&#xff08;第三版&#xff09;和高西全、丁玉美的《数字信号处理》&#xff08;第四版&#xff…

HiveSQL——共同使用ip的用户检测问题【自关联问题】

注&#xff1a;参考文章&#xff1a; SQL 之共同使用ip用户检测问题【自关联问题】-HQL面试题48【拼多多面试题】_hive sql 自关联-CSDN博客文章浏览阅读810次。0 问题描述create table log( uid char(10), ip char(15), time timestamp);insert into log valuesinsert into l…

微软AD域替代方案,助力企业摆脱hw期间被攻击的窘境

在红蓝攻防演练&#xff08;hw行动&#xff09;中&#xff0c;AD域若被攻击成功&#xff0c;是其中一个扣分最多的一项内容。每年&#xff0c;宁盾都会接到大量AD在hw期间被攻击&#xff0c;甚至是被打穿的企业客户。过去&#xff0c;企业还会借助2FA双因子认证加强OA、Exchang…

蓝桥杯每日一题------背包问题(一)

背包问题 阅读小提示&#xff1a;这篇文章稍微有点长&#xff0c;希望可以对背包问题进行系统详细的讲解&#xff0c;在看的过程中如果有任何疑问请在评论区里指出。因为篇幅过长也可以进行选择性阅读&#xff0c;读取自己想要的那一部分即可。 前言 背包问题可以看作动态规…

UE4运用C++和框架开发坦克大战教程笔记(十八)(第55~57集)

UE4运用C和框架开发坦克大战教程笔记&#xff08;十八&#xff09;&#xff08;第55~57集&#xff09; 55. UI 进入退出动画HideOther 面板出现时隐藏其他面板添加面板出现和收起的动画效果编写遮罩管理器前的准备 56. 弹窗进入界面57. UI 显示隐藏与遮罩转移完善遮罩管理器 55…

ARM汇编[0] hello world

文章目录 简述寄存器语法系统调用例程 简述 如果不了解x86汇编的话建议先了解下&#xff0c;x86资料多、环境好搞、容易入门 阿尔可是急于求成的人&#xff0c;希望赶快看到成果&#xff1b; 所以本篇文章不会东讲西讲展开讲&#xff0c;只讲让hello world汇编能跑起来的关键…

2 月 9 日算法练习- 数据结构 - 除夕快乐♪٩(´ω`)و♪

翻转括号序列 暴力过20%数据 思路&#xff1a;括号合法序列问题可以利用前缀和&#xff0c;将"(“看成 1&#xff0c;”)"看成 0&#xff0c;规律是到某个位置为止的前缀和>0并且到最后前缀和0。 #include<bits/stdc.h> using namespace std; const int N…

鸿蒙原生应用再添新丁!央视新闻 入局鸿蒙

鸿蒙原生应用再添新丁&#xff01;央视新闻 入局鸿蒙 来自 HarmonyOS 微博2月9日消息&#xff0c;#央视新闻启动鸿蒙原生应用开发#中央广播电视总台旗舰央视新闻客户端正式宣布&#xff0c;将基于HarmonyOS NEXT鸿蒙星河版&#xff0c;启动央视新闻 鸿蒙原生应用开发&#xf…

扑克牌大小(模拟)

题目 import java.util.Scanner; public class Main {public static void main(String[] args) {Scanner sc new Scanner(System.in);String s sc.nextLine();String[] ss s.split("-");StringBuffer s1 new StringBuffer();StringBuffer s2 new StringBuffer(…

Java基础知识总结(持续更新中)

Java基础知识&#xff08;持续更新&#xff09; 类型转化&#xff1a;数字、字符串、字符之间相互转化 数字 <-> 字符串 // 数字转字符串 // method1int number 5;String str String.valueOf(number);// method2int number 5;Integer itr number; //int装箱为对…

vue3 之 通用组件统一注册全局

components/index.js // 把components中的所组件都进行全局化注册 // 通过插件的方式 import ImageView from ./ImageView/index.vue import Sku from ./XtxSku/index.vue export const componentPlugin {install (app) {// app.component(组件名字&#xff0c;组件配置对象)…

moduleID的使用

整个平台上有很多相同的功能&#xff0c;但是需要不同的内容。例如各个模块自己的首页上有滚动新闻、有友好链接等等。为了公用这些功能&#xff0c;平台引入了moduleID的解决方案。 在前端的配置文件中&#xff0c;配置了模块号&#xff1a; 前端页面请求滚动新闻时&#xff0…

一款VMP内存DUMP及IAT修复工具

前言 加壳是恶意软件常用的技巧之一&#xff0c;随着黑客组织技术的不断成熟&#xff0c;越来越多的恶意软件家族都开始使用更高级的加壳方式&#xff0c;以逃避各种安全软件的检测&#xff0c;还有些恶意软件在代码中会使用各种多态变形、加密混淆、反调试、反反分析等技巧&a…

基于JAVA的教学资源共享平台 开源项目

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 数据中心模块2.2 课程档案模块2.3 课程资源模块2.4 课程作业模块2.5 课程评价模块 三、系统设计3.1 用例设计3.2 类图设计3.3 数据库设计3.3.1 课程档案表3.3.2 课程资源表3.3.3 课程作业表3.3.4 课程评价表 四、系统展…