disgare 的博客
首页
博客
分类
标签
首页
博客
分类
标签
  • 网络

    • 计算机网络学习笔记
    • 网络安全相关
    • 域名和子网掩码
    • CORS 跨域资源共享
    • DNS、HTTP 与 HTTPS
    • Server-Sent Events (SSE)
    • WebSocket 长连接
      • 介绍
        • 创建过程
        • 优点
      • Java 中实现 webSocket
        • STOMP
        • 依赖以及配置类
        • 实现
      • 使用 Socket.IO 实现
        • 消息事件处理器
        • Socket.IO 原理
      • nginx 配置的各种问题
        • 请求 400
        • 在 https下使用 ws,提示不安全
        • 在不支持 ssl 的情况下,直接用 wss 链接
        • 如果我们设置 location 不正确的时候
  • 计算机基础

    • 操作系统 IO 相关知识
    • 操作系统学习笔记
    • 程序的机器级表示
    • 音频文件基础
    • 正则表达式相关概念
    • ffmpeg 的安装以及实现音频切分功能
    • Hex 和 Base64 编码
    • XML 的使用
  • 数据结构与算法

    • 动态规划算法学习笔记
    • 基于比较的排序算法的最坏情况下的最优下界为什么是O(nlogn)
    • 集合与数据结构学习笔记
    • 面试常见算法总结
    • 算法导论第二部分排序学习笔记
    • 算法导论第一部分学习笔记
  • Java

    • 对象之间的映射与转换
    • 反射学习笔记
    • 泛型相关概念
    • 关于 boolean 类型的坑
    • 如何使用 lambda 表达式实现排序
    • CompletableFuture 相关用法
    • CompletableFuture 源码浅要阅读
    • FutureTask 源码阅读
    • Guava 常用 API
    • Guava 源码阅读:Multimap 相关
    • Jackson 的各种使用
    • Java 的 Excel 相关操作
    • java 的常见性能问题分析以及出现场景
    • java 基础知识
    • JAVA 枚举的基础和原理
    • Java 图片文件上传下载处理
    • Java 序列化
    • Java 异常
    • Java 语法糖
    • Java 中关于字符串处理的常用方法
    • Java 中强、软、弱、虚引用
    • JAVA 注解小结
    • Java Http 访问框架
    • Java Stream 的使用
    • Java8 新特性
    • netty 学习笔记
    • Scanner 的各种用法
    • Servlet 学习笔记
    • String、StringBuffer、StringBuilder 学习笔记
  • JVM

    • 虚拟机执行子系统
    • JVM 自动内存管理
    • Linux 中 JVM 常用工具以及常见问题解决思路
  • Linux

    • crontab 表达式
    • Linux 常见命令
    • Linux 文件系统
  • 中间件

    • 关于定时任务原理
    • 详解 kafka
    • ES 搜索引擎
    • flink 提交流程
    • Grape-RAG
    • Hadoop 基础原理
  • 多线程

    • 多线程基础学习笔记
    • 简单了解并发集合
    • 如何手写单例
    • 深入理解 java 多线程安全
    • 生产者消费者问题
    • 线程池作用、用法以及原理
    • AQS 组件
    • ThreadLocal 原理以及使用
  • 非关系型数据库

    • Redis 集群
    • Redis 数据结构、对象与数据库
    • Redis 学习笔记
  • 关系型数据库

    • B+ 树的插入、删除和数据页分裂机制
    • MySQL 的 binglog、redolog、undolog
    • MySQL 的记录存储结构、存储引擎与 Buffer Pool
    • MySQL 基本的特性
    • MySQL 开发规范
    • MySQL 事务与锁与 MVCC
    • MySQL 数据类型、字符集相关内容
    • MySQL 索引与索引优化
    • PostgreSQL 更新数据时 HOT优化
    • PostgreSQL 相关用法
  • Python

    • Python 基础语法
    • Python 学习
  • Spring 项目

    • Lombok 的常用注解
    • maven 小结
    • MyBatis 框架的使用
    • MyBatis 重要知识点总结
    • MybatisPlus 的使用
    • Spring 框架基础使用
    • Spring 事务相关
    • Spring IOC 的原理及源码
    • Spring AOP 的使用和原理
    • SpringBoot 的原理
    • SpringBoot 基础使用
    • SpringWeb 重要知识点
  • 分布式

    • 初步了解 docker
    • 从 ACID 到 BASE 事务处理的实现
    • 访问远程服务
    • 分布式 id
    • 分布式缓存相关问题
    • 分布式集群理论和分布式事务协议
    • 分布式架构的观测
    • 分布式一致性算法
    • 负载均衡 Load Balancing
    • 关于分布式系统 RPC 中高可用功能的实现
    • 集群间数据同步的目的
    • 三高问题下的系统优化
    • 数据库分库分表
    • 详解 Spring Cloud
    • Dubbo 基础概念
    • Gossip 协议
    • nginx 学习笔记
    • Protobuf 通信协议
    • Zookeeper 基础学习
  • 架构设计

    • 参数校验与异常处理
    • 抽象方法与设计模式
    • 代码整洁之道
    • 权限系统设计
    • 用低内存处理大量数据
    • 设计模式——策略模式
    • 设计模式——过滤器模式在 Spring 中的实践
    • 状态模式
    • 统一结果返回
    • 为什么要打日志?怎么打日志?打什么日志?
    • 运维监控常见指标含义
    • 资深研发进阶
    • DDD 架构学习笔记
    • Java 常用的规则引擎
    • MVC 架构学习笔记
  • AI

    • 如何编写 Prompt
    • Agent 工程架构
    • LLM 相关内容
    • NLP 相关知识
    • vibe coding 最佳实践
    • windows 下 ollama 迁移到 D 盘
  • 开发工具

    • 如何画时序图、流程图、状态流转图
    • excel 关于 =vlookup 的用法
    • git 的学习以及使用
    • IDEA 插件推荐
    • IDEA 常用快捷键以及调试
    • Shell 脚本
    • swagger 的使用
  • 前端

    • 简单了解前端页面开发
    • 伪静态是什么
    • GitHub Pages 部署教程
    • Vercel 部署教程
    • vue-admin-template 简单使用
    • VuePress 博客搭建指南
  • 项目

    • 面试刷题网——技术方案
    • 影视资源聚合站——技术方案
  • 问题记录

    • 定时任务单线程消费 redis 中数据导致消费能力不足
    • 提供可传递的易受攻击的依赖项
    • Liteflow 在 SpringBoot 启动时无法注入组件问题 couldn‘t find chain with the id[THEN(NodeComponent)]
  • 金融

    • 股票分析——关于电力
    • 股票技术面——量价关系
    • 股票技术面——盘口
    • 股票技术面——基础
    • 基础的金融知识
    • 基金与股票
    • 韭菜的自我总结
    • 聊聊价值投资
  • 其他

    • 程序员职场工作需要注意什么
    • 创业全链路SOP:从灵光一现到系统化增长的实战指南
    • 观罗翔讲刑法随笔
    • 价格和价值
    • 立直麻将牌效益理论
    • 梅花易数学习笔记
    • 压力管理
2023-09-01
计算机网络
目录

WebSocket 长连接

# 介绍

WebSocket 是一种网络传输协议,可在单个 TCP 连接上进行全双工通信(允许数据同时在两个方向上传输),位于 OSI 模型的应用层

早期,很多网站为了实现推送技术,所用的技术都是轮询(也叫短轮询)。轮询是指由浏览器每隔一段时间向服务器发出 HTTP 请求,然后服务器返回最新的数据给客户端。常见的轮询方式分为轮询与长轮询

在这种情况下,HTML5 定义了 WebSocket 协议,能更好的节省服务器资源和带宽(因为 HTTP 请求可能会包含较长的头部,但真正有效的可能只有小部分),并且能够更实时地进行通讯,该协议在连接期间会一直占用一个端口。Websocket 使用 ws 或 wss 的统一资源标志符(URI),其中 wss 表示使用了 TLS 的 Websocket,ws 与 HTTP 协议有良好的兼容性

WS 协议和 WSS 协议两个均是 WebSocket 协议,两者一个是非安全的,一个是安全的。就好比 HTTP 协议和 HTTPS 协议的差别,非安全的没有证书,安全的如同 HTTPS 一样需要 SSL 证书,证书当然是配置在 ng 上的

# 创建过程

WebSocket 是先通过 http 创建的,随后才使用 WebSocket 的包来传输数据,创建过程如下:

  • 首先建立 TCP 链接,三次握手,构建传输层的链接
  • 使用 http(如果是 wss 使用 https)来传递基础信息,比如所使用的 webSocket 版本、Sec-WebSocket-Key 等信息
  • 服务器接受信息后会返回带有特殊信息的响应,表示已经接收到了 webSocket 建立请求
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: 
Sec-WebSocket-Protocol: chat
1
2
3
4
5
  • 客户端收到连接成功的消息后,开始借助于 TCP 传输信道进行全双工通信。通信阶段使用独立的 WebSocket 协议

我们在进行信息传输的过程中可能会走 ng 做网关,如果 ng 不支持我们的信息的话会导致长连接连不上,常见的 ng 问题见下文

# 优点

普遍认为,WebSocket 的优点有如下几点:

  • 较少的控制开销:在连接创建后,服务器和客户端之间交换数据时,用于协议控制的数据包头部相对较小
  • 更强的实时性:由于协议是全双工的,所以服务器可以随时主动给客户端下发数据。相对于 HTTP 请求需要等待客户端发起请求服务端才能响应,延迟明显更少
  • 更好的二进制支持:WebSocket 定义了二进制帧,相对 HTTP,可以更轻松地处理二进制内容

# Java 中实现 webSocket

java 中实现 webSocket 一般有两种方法,一种是使用 WebSocket 的一个子协议 stomp,另外一个是使用 Socket.IO 协议实现。我们先介绍 stomp 实现方法

WebSocket 协议是一种相当低级的协议。它定义了如何将字节流转换为帧。帧可以包含文本或二进制消息。由于消息本身不提供有关如何路由或处理它的任何其他信息,因此很难在不编写其他代码的情况下实现更复杂的应用程序。幸运的是,WebSocket 规范允许在更高的应用程序级别上使用子协议。STOMP 是其中之一

# STOMP

STOMP:Simple (or Streaming) Text Orientated Messaging Protocol,即简单文本定向消息协议。它被用于定义常用消息传递的格式,STOMP 可以用于任何可靠的双向流网络协议,如 TCP 和 WebSocket,虽然 STOMP 是一个面向文本的协议,但消息可以是文本或二进制

它常配合 WebSocket 一起用,让前端和后端像发短信一样互发消息,而不是裸着用 WebSocket 字节流(WebSocket 没有定义在其中传输的消息格式,用户可以乱发消息,为了不让用户乱发消息,我们定义了一个消息格式规范),STOMP 的格式是:

  • 命令:CONNECT / SEND / SUBSCRIBE
  • 头:destination、content-type
  • 正文:消息文本

服务器和客户端都按这套「约定」来解析消息

# 依赖以及配置类

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-websocket</artifactId>
        </dependency>
        <dependency>
            <groupId>javax.websocket</groupId>
            <artifactId>javax.websocket-api</artifactId>
        </dependency>
1
2
3
4
5
6
7
8

配置类里将我们写的规则类注册进去

WebSocketHandlerRegistry 的 addHandler 方法是将 WebSocketHandler 和对应的 URL 路径注册到 WebSocketHandlerRegistry 中,以供后续的 WebSocket 连接请求进行匹配和处理。当有 WebSocket 连接请求到达时,WebSocketHandlerRegistry 会根据请求的 URL 路径找到对应的 WebSocketHandler,并将请求交给该 Handler 进行处理

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

    @Autowired
    private MyWsHandler myWsHandler;

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(myWsHandler, "/ws/voice/remind.json")
                //允许跨域
                .setAllowedOrigins("*");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 实现

继承 AbstractWebSocketHandler,实现方法,即可自定义在连接、传入、中断等时候分别可以执行的操作。这里我们选择继承其子类 TextWebSocketHandler 来处理文本消息

@Component
@Slf4j
public class MyWsHandler extends TextWebSocketHandler {

    /**
     * 这个是管理 session 的类
     */
    @Resource
    WsSessionManager wsSessionManager;

    /**
     * 定义了客户端链接服务器的时候会执行什么操作
     */
    @Override
    public void afterConnectionEstablished(WebSocketSession session) {
        log.info("建立ws连接");
        wsSessionManager.add(session.getId(), session);
    }

    /**
     * handleTextMessage 方法是用来处理收到的文本消息的。当客户端发送文本消息到服务器端时,服务器端会调用 handleTextMessage 方法来处理该消息。在该方法中,开发者可以编写自定义的业务逻辑来处理文本消息,例如解析消息内容、调用其他服务进行处理等。同时,开发者还可以在该方法中向客户端发送文本消息,以实现双向通信
     */
    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        log.info("发送文本消息");
        // 获得客户端传来的消息
        String payload = message.getPayload();
        log.info("server 接收到消息 " + payload);
        session.sendMessage(new TextMessage("server 发送给的消息 " + payload + ",发送时间:" + LocalDateTime.now().toString()));
        // 推荐写法
        // 浏览器关闭后再写数据会触发异常,这里的死循环就会中断
        while (true) {
            Monitor.count("websocket.voice-remind-polling");
            // 回写真实数据前写个空数据判断连接是否正常
            // 1是为了在浏览器页面关闭时能及时中断循环,2是长时间不回写数据连接会中断(NG设置的2分钟超时)
            session.sendMessage(heartBeatResMsg);
            onTextMessage(session, message, userName);
        }
    }

    /**
     * 出现错误与异常的时候执行的操作
     */
    @Override
    public void handleTransportError(WebSocketSession session, Throwable exception) {
        log.error("异常处理");
        wsSessionManager.removeAndClose(session.getId());
    }

    /**
     * 连接中断后执行的操作
     */
    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
        log.info("关闭ws连接");
        wsSessionManager.removeAndClose(session.getId());
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58

我们能在建立连接的时候将一些信息放在 session 里,WebSocketSession::getAttributes 是一个获取 WebSocketSession 对象的属性的方法,这个方法返回一个 Map<String, Object> 对象,其中包含了 WebSocketSession 对象中所有的属性。这些属性是在 WebSocketSession 对象创建时通过 WebSocketHandler 的 afterConnectionEstablished 方法存储的

在 afterConnectionEstablished 方法中,WebSocketSession 对象被创建并存储在内存中。此时,WebSocketSession 对象的属性可以通过调用 WebSocketSession 的 setAttribute 方法进行设置。setAttribute 方法接受两个参数,第一个参数是属性的名称,第二个参数是属性的值

我们在处理的时候需要解决心跳检测机制,用户关闭浏览器后我们应该关闭链接,如果是后端实现后端可以不停的写空数据(比如空的中括号大括号什么的)。浏览器关闭后再写数据会触发异常,死循环就会中断,websocket 会关闭

# 使用 Socket.IO 实现

Socket.IO 在 WebSocket 上封装了一些东西,让 WebSocket 的使用更加便捷,因此 Socket.IO 是 WebSocket 的升级版,包含:

  • 每个数据包添加了额外的元数据。这就是为什么 WebSocket 客户端将无法成功连接到 Socket.IO 服务器,而 Socket.IO 客户端也将无法连接到普通 WebSocket 服务器
  • 使用 netty 提供 IO 多路复用,使得一个选择器(线程)可以管理多个套接字链接,减少了线程数,并且尽可能的减少了等待数据的阻塞时间
  • 如果不能建立 WebSocket 连接,连接将退回到 HTTP 长轮询
  • 包含一个内置的心跳机制,它会定期检查连接的状态。如果校验失败会告知前后端,让业务去做后续处理,比如重新连接等等

在大量用户连接到服务器时,使用 Socket.IO 是个不错的选择,比如手机端的操作以及公共聊天室的实现

# 消息事件处理器

@Component
public class MessageEventHandler {

    private final SocketIOServer server;

    private static final Logger logger = LoggerFactory.getLogger(MessageEventHandler.class);

    @Autowired
    public MessageEventHandler(SocketIOServer server) {
        this.server = server;
    }

    //添加connect事件,当客户端发起连接时调用
    @OnConnect
    public void onConnect(SocketIOClient client) {
        if (client != null) {
            String username = client.getHandshakeData().getSingleUrlParam("username");
            String password = client.getHandshakeData().getSingleUrlParam("password");
            String sessionId = client.getSessionId().toString();
            logger.info("连接成功, username=" + username + ", password=" + password + ", sessionId=" + sessionId);
        } else {
            logger.error("客户端为空");
        }
    }

    //添加@OnDisconnect事件,客户端断开连接时调用,刷新客户端信息
    @OnDisconnect
    public void onDisconnect(SocketIOClient client) {
        logger.info("客户端断开连接, sessionId=" + client.getSessionId().toString());
        client.disconnect();
    }

    // 消息接收入口
    @OnEvent(value = "chatevent")
    public void onEvent(SocketIOClient client, AckRequest ackRequest, ChatMessage chat) {
        logger.info("接收到客户端消息");
        if (ackRequest.isAckRequested()) {
            // send ack response with data to client
            ackRequest.sendAckData("服务器回答chatevent, userName=" + chat.getUserName() + ",message=" + chat.getMessage());
        }
    }

    // 登录接口
    @OnEvent(value = "login")
    public void onLogin(SocketIOClient client, AckRequest ackRequest, LoginRequest message) {
        logger.info("接收到客户端登录消息");
        if (ackRequest.isAckRequested()) {
            // send ack response with data to client
            ackRequest.sendAckData("服务器回答login", message.getCode(), message.getBody());
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52

启动器

@Component
@Order(1)
public class ServerRunner implements CommandLineRunner {
    private final SocketIOServer server;
    private static final Logger logger = LoggerFactory.getLogger(ServerRunner.class);

    @Autowired
    public ServerRunner(SocketIOServer server) {
        this.server = server;
    }

    @Override
    public void run(String... args) {
        logger.info("ServerRunner 开始启动啦...");
        server.start();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# Socket.IO 原理

简单聊一下 Socket.IO 的底层原理,它是使用了 netty 框架来实现 NIO 的,因此核心原理就是选择器。和 WebSocket 不一样,不是一个连接对应一个线程,我们在这直接加一层选择器来让多个连接映射少量线程,大大减少了线程资源,NIO Socket 工作流程:

  • 将 Channel 注册到 Selector 上;Channel 是操作系统内核数据在虚拟机中映射的对象,指的是已经从缓冲区读到操作系统内核区的数据(这个过程是由操作系统 poll、epoll 等方法完成的)。Selector 就是选择器
  • 调用 Selector.select 方法,选择发生的操作 Ready 事件;如果没有触发操作 Ready 事件,则一直阻塞。如果 Ready 事件发生,则 select 方法底层会把各个 Channel 背后的 Ready 情况写入到 PollArrayWrapper 对应的 revents 中

PollArrayWrapper 是 Selector 内部维护的连续内存数组,用来动态维护 socket 的注册关系以及 socket 的 IO 操作 ready 情况,里面有 Channel 的 id,以及 events 和对应的 revents

events 指的是 socketChannel 中注册的操作类型,比如数据读、数据写等等操作

revents 是指 events 的就绪情况。在调用 selector.select 时,会触发本地方法调用获取注册的 socket 的操作就绪情况,并且将结果会更新到 revents 中。然后选择器会调用 selectedKeys,根据 events(注册的操作)和 revents(就绪操作)通过一定的算法判断是否匹配被选中的。如果被选中说明数据已经准备好了,指定线程来处理数据

# nginx 配置的各种问题

# 请求 400

如果使用 webSocket 并且使用 nginx 做转发的话,会报以下错误:

failed: Error during WebSocket handshake: Unexpected response code: 400
1

这个问题其实是由于客户端错误或不存在的域名导致的,如果代码没有错误的话,可能是 ng 的配置不对

# 在 https下使用 ws,提示不安全

Mixed Content: The page at 'https://www.joshua317.com/1.html' was loaded over HTTPS, but attempted to connect to the insecure WebSocket endpoint 'ws://im.joshua317.com/'. This request has been blocked; this endpoint must be available over WSS.

Uncaught DOMException: Failed to construct 'WebSocket': An insecure WebSocket connection may not be initiated from a page loaded over HTTPS.
1
2
3

问题出现在 nginx 的配置文件,需要修改 nginx.conf 文件。在 linux 终端中敲入 vim /etc/nginx/nginx.conf,找到 location 这个位置,配置文件如下所示:

server {
        listen       80;
        server_name  school.godotdotdot.com;
        charset utf-8;

        location / {
            proxy_pass http://127.0.0.1:3000;
            proxy_set_header Host $host;
            proxy_http_version 1.1; 
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection "upgrade";
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_connect_timeout 60;
            proxy_read_timeout 600;
            proxy_send_timeout 600;
        }

        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }

    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

其中最重要的是下面这三行

proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
1
2
3

其中第一行是告诉 nginx 使用 HTTP/1.1 通信协议,这是 websoket 必须要使用的协议

第二行和第三行告诉 nginx,当它想要使用 WebSocket 时,响应 http 升级请求

# 在不支持 ssl 的情况下,直接用 wss 链接

index.ts:8 WebSocket connection to 'ws://im.joshua317.com/' failed: Error in connection establishment: net::ERR_SSL_VERSION_OR_CIPHER_MISMATCH
或者
failed: Error in connection establishment: net::ERR_CERT_COMMON_NAME_INVALID
1
2
3

因为 HTTPS 是基于 SSL 依靠证书来验证服务器的身份,并为浏览器和服务器之间的通信加密,所以在 HTTPS 站点调用某些非 SSL 验证的资源时浏览器可能会阻止,简单来说就是验证不过

所以这个问题其实是 ng 不支持 https 导致的,我们加个证书来让它支持

server {
    listen 80;
    server_name im.joshua317.com;
    #调整成自己的证书即可,重点重点重点
    ssl_certificate /usr/local/nginx/conf/ssl/xxxx.crt;
    ssl_certificate_key /usr/local/nginx/conf/ssl/xxxx.key;
    ssl_session_timeout 5m;
     #调整成自己的即可,重点重点重点
    ssl_ciphers xxxxxxxxxxxxx;
1
2
3
4
5
6
7
8
9

# 如果我们设置 location 不正确的时候

failed: Error during WebSocket handshake: Unexpected response code: 404
1

综上,websocket 的状态码和 http 的其实差不多

#WebSocket
最后更新: 2/28/2026, 11:31:29 AM
Server-Sent Events (SSE)
操作系统 IO 相关知识

← Server-Sent Events (SSE) 操作系统 IO 相关知识→

最近更新
01
vibe coding 最佳实践
02-24
02
立直麻将牌效益理论
02-23
03
伪静态是什么
02-08
更多文章>
Theme by Vdoing
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式