【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 base64SEC_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 {@Beanpublic 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);}@OnMessagepublic 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);}}@OnOpenpublic void onOpen(@PathParam("userName") String userName, Session session) {log.info("客户端程序{}建立连接成功! sessionId:{}", userName, session.getId());}@OnClosepublic void onClose(@PathParam("userName") String userName, Session session, CloseReason closeReason) {log.info("客户端{}断开连接,原因:{}", userName, closeReason);}@OnErrorpublic 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 {@Overridepublic void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {registry.addHandler(echoWebSocketHandler(), "/ws/{userName}").setAllowedOriginPatterns("*").setHandshakeHandler(new DefaultHandshakeHandler()).addInterceptors(new HttpSessionHandshakeInterceptor() {@Overridepublic boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {// 认证等操作。。。return true;}});}public WebSocketHandler echoWebSocketHandler() {return new TextWebSocketHandler() {@Overrideprotected 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.hqwc.cn/news/462198.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

多线程基础详解(看到就是赚到)

&#x1f3a5; 个人主页&#xff1a;Dikz12&#x1f4d5;格言&#xff1a;那些在暗处执拗生长的花&#xff0c;终有一日会馥郁传香欢迎大家&#x1f44d;点赞✍评论⭐收藏 目录 创建线程 1.创建类继承Thread,重写run() 2.实现Runnable,重写run() 3.继承Thread,使用匿名内部类 …

全新抖音快手小红书去水印系统网站源码 | 支持几十种平台

全新抖音快手小红书去水印系统网站源码 | 支持几十种平台

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

先看几道比较简单的题&#xff1a; 直接f[i][j]f[i-1][j]f[i][j-1]即可&#xff08;注意有马的地方赋值为0&#xff09; 下面是递推循环方式实现的AC代码&#xff1a; #include<bits/stdc.h> using namespace std; #define int long long int a[30][30]; int n,m,x,y; …

移动端设置position: fixed;固定定位,底部出现一条缝隙,不知原因,欢迎探讨!!!

1、问题 在父盒子中有一个子盒子&#xff0c;父盒子加了固定定位&#xff0c;需要子盒子上下都有要边距&#xff0c;用margin或者padding挤开时&#xff0c;会出现缝隙是子盒子背景颜色的。 测试过了&#xff0c;有些手机型号有&#xff0c;有些没有&#xff0c;微信小程序同移…

Python数据可视化库之ggplot使用详解

概要 数据可视化是数据分析和数据沟通的关键部分。Python 作为一门强大的数据科学和数据分析工具,提供了多种数据可视化库,其中之一就是 ggplot。ggplot 是一个基于 ggplot2 的 Python 数据可视化库,它可以创建精美且高度可定制的图表,以更好地理解和传达数据。本文将深入…

【Python/网络安全】 Git漏洞之Githack工具基本安装及使用详析

[Python/网络安全] Git漏洞之Githack工具基本安装及使用详析 前言安装步骤工具使用实战总结 前言 Git是一个非常流行的开源分布式版本控制系统&#xff0c;它被广泛用于协同开发和代码管理。许多网站和应用程序都使用Git作为其代码管理系统&#xff0c;并将其部署到生产环境中…

GC调优工具

1、jstat 2、VisualVM GC tool插件 插件下载地址&#xff1a;https://blog.csdn.net/jushisi/article/details/109655175 3、Prometheus和Grafana监控

数据库管理-第148期 最强Oracle监控EMCC深入使用-05(20240208)

数据库管理148期 2024-02-08 数据库管理-第148期 最强Oracle监控EMCC深入使用-05&#xff08;20240208&#xff09;1 性能主页2 ADDM Spotlight3 实时ADDM4 数据库的其他5 主机总结 数据库管理-第148期 最强Oracle监控EMCC深入使用-05&#xff08;20240208&#xff09; 作者&am…

数据结构——单向链表和双向链表的实现(C语言版)

目录 前言 1. 链表 1.1 链表的概念及结构 1.2 链表的分类 2. 单链表接口实现 2.1 数据结构设计与接口函数声明 2.2 创建结点&#xff0c;打印&#xff0c;查找 2.3 尾插&#xff0c;头插&#xff0c;尾删&#xff0c;头删 2.4 插入或删除 2.4.1在指定位置后 2.4.2在…

5周年狂欢,WeTrade众汇积分商城又送车啦!

各位投资者&#xff1a;新年好啊&#xff01; WeTrade众汇承诺积分商城所有礼品&#xff0c;不论价值大小&#xff0c;送出均为真实有效&#xff0c;不做虚假宣传。 WeTrade众汇继2018年9月28日送出特斯拉Model X后&#xff0c;又一次迎来了第二位在积分商城兑换豪车的客户! …

spring boot学习第十二篇:mybatis框架中调用存储过程控制事务性

1、MySQL方面&#xff0c;已经准备好了存储过程&#xff0c;参考&#xff1a;MYSQL存储过程&#xff08;含入参、出参&#xff09;-CSDN博客 2、pom.xml文件内容如下&#xff1a; <?xml version"1.0" encoding"UTF-8"?> <project xmlns"…

Ribbon全方位解析:构建弹性的Java微服务

第1章 引言 大家好,我是小黑,咱们今天聊聊Ribbon,这货是个客户端负载均衡工具,用在Spring Cloud里面能让咱们的服务调用更加灵活和健壮。负载均衡,听起来挺高大上的,其实就是把外界的请求平摊到多个服务器上,避免某个服务器压力太大,其他的却在那儿闲着。 Ribbon的牛…