Websocket原理和实践

一、概述

1.websocket是什么?

WebSocket是一种在单个TCP连接上进行全双工通信的协议。WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。

WebSocket与HTTP协议不同,它使用了独立的端口,并且在建立连接后,不需要在每次数据传输时重新建立连接。这使得它特别适合于实时应用程序,例如聊天,在线游戏和股票交易等,这些应用程序需要高速,双向的数据传输。

WebSocket协议是HTML5标准的一部分,因此它可以在现代浏览器中使用。WebSocket API可以在JavaScript中使用,这样就可以在网页上直接使用WebSocket进行通信。

2.websocket和HTTP对比

WebSocket是一种与HTTP不同的协议。两者都位于OSI模型的应用层,并且都依赖于传输层的TCP协议。 虽然它们不同,但是RFC 6455中规定:it is designed to work over HTTP ports 80 and 443 as well as to support HTTP proxies and intermediaries(WebSocket通过HTTP端口80和443进行工作,并支持HTTP代理和中介),从而使其与HTTP协议兼容。 为了实现兼容性,WebSocket握手使用HTTP Upgrade头[1]从HTTP协议更改为WebSocket协议。

其具体的区别如下:

  1. 通信方式不同: HTTP协议是一种请求-响应协议,客户端发送请求给服务器,服务器返回响应。而WebSocket协议是一种全双工协议,客户端和服务器都可以主动发送消息。
  2. 链接状态不同: HTTP协议是无状态的,每次请求都是独立的。而WebSocket协议是有状态的,建立了连接之后,客户端和服务器之间可以维持长连接。
  3. 数据传输不同: HTTP协议是基于文本的,而WebSocket协议是基于二进制的。
  4. 延迟不同: HTTP协议每次请求都需要建立连接,等待响应,传输数据,释放连接,这整个过程都需要一些时间。而WebSocket协议只需要建立一次连接,之后就可以高效地进行数据传输,所以延迟更小。

3.websocket的优势和劣势

  1. **较少的控制开销。**在连接建立后,服务器和客户端之间交换数据时,用于协议控制的数据包头部相对较小。在不包含扩展的情况下,对于服务器到客户端的内容,此头部大小只有2至10字节(和数据包长度有关);对于客户端到服务器的内容,此头部还需要加上额外的4字节的掩码。相对于HTTP请求每次都要携带完整的头部,此项开销显著减少了。
  2. **更强的实时性。**由于协议是全双工的,所以服务器可以随时主动给客户端下发数据。相对于HTTP请求需要等待客户端发起请求服务端才能响应,延迟明显更少;即使是和Comet等类似的长轮询比较,其也能在短时间内更多次地传递数据。
  3. **保持连接状态。**与HTTP不同的是,Websocket需要先建立连接,这就使得其成为一种有状态的协议,之后通信时可以省略部分状态信息。而HTTP请求可能需要在每个请求都携带状态信息(如身份认证等)。
  4. **更好的二进制支持。**Websocket定义了二进制帧,相对HTTP,可以更轻松地处理二进制内容。
  5. **可以支持扩展。**Websocket定义了扩展,用户可以扩展协议、实现部分自定义的子协议。如部分浏览器支持压缩等。
  6. **更好的压缩效果。**相对于HTTP压缩,Websocket在适当的扩展支持下,可以沿用之前内容的上下文,在传递类似的数据时,可以显著地提高压缩率。

4.websocket应用场景

  1. 在线聊天:实现实时的文本聊天功能。
  2. 直播和视频会议:实现实时的音频和视频传输。
  3. 游戏:实现实时的游戏状态同步和控制。
  4. 实时监控和控制:实现实时的硬件监控和控制。
  5. 数据可视化:实现实时的数据可视化和分析。
  6. 在线协作:实现实时的文档协作和编辑。
  7. 智能家居:实现实时的智能家居控制。
  8. 推送消息:实现实时的推送消息功能

这些场景中,WebSocket的实时通信特性使得WebSocket成为了一种理想的选择。

利用websocket处理在线聊天业务的常见流程:

二、原理

1.WebSocket通信原理和机制

HTTP通信方式是请求-响应的单工通信,他的通信模式是一问一答式的,如果需要服务端主动发送消息给客户端,他是做不到的。

Websocket的通信方式是全双工模式,无论客户端还是服务端,都能够自主发起通信。但是WebSocket 是独立的、建立在TCP上的协议。Websocket 通过 HTTP/1.1 协议的101状态码进行握手。为了建立Websocket连接,需要通过浏览器发出请求,之后服务器进行回应,这个过程通常称为"握手"(Handshaking)。握手是通过HTTP协议完成的,但是一旦建立连接后,数据传输将使用WebSocket协议。

WebSocket通信的流程如下:

  1. 客户端发送一个HTTP请求,请求的目的是为了要建立一个WebSocket连接。
  2. 服务器收到请求后,给出一个HTTP响应,并升级连接到WebSocket协议。
  3. 客户端和服务器之间建立一个WebSocket连接。
  4. 客户端和服务器之间可以进行双向通信,发送文本和二进制数据。
  5. 当客户端或服务器关闭连接时,WebSocket连接也会关闭。

与 HTTP 通信不同的是,WebSocket 通信是基于TCP的,所以它是一个持久连接。它允许服务器主动发送信息给客户端,而不是等待客户端的请求。这使得 WebSocket 通信成为了实现实时应用的理想选择。

建立了websocket连接,请求的URL会以ws://xx开始,在请求头中会有upgrade:websocket标志,说明该请求是websocket请求。

后续客户端和服务器的消息通信会在同一个回话中展示。

2.Java Websocket API介绍

与HTTP不同,websocket的通信具有生命周期,此生命周期由websocket协议本身支撑,具体实现websocket的语言都需要支持。

  1. 建立连接事件:此事件发生在端点上建立新连接时并且在任何其他事件发生之前

  2. 发送消息事件:此事件接收WebSocket对话中另一端发送的消息。它可以发生在WebSocket端点接收了打开事件之后且在接收关闭事件关闭连接之前的任意时刻

  3. 出现错误事件:此事件在WebSocket连接或者端点发生错误时产生

  4. 连接关闭事件:此事件表示WebSocket端点的连接或者端点目前正在部分的关闭,它可以由参与连接的任意一个端点发出

针对Java来说,websocket的事件都会有对应的方法处理,其主要通过编程式端点服务来处理:

  • @OnOpen:处理建立连接的事件;
java 复制代码
	@OnOpen
	public void init(Session session, EndpointConfig config) {
	
	}
  • @OnMessage:处理建立连接后收发消息的事件;
java 复制代码
	@OnMessage
	public void handleTextMessage(String textMessage) {
		
	}
-----------
	@OnMessage
	public void handleBinaryMessage(byte[] messageData, Session session) {
		
	}
  • @OnError:处理消息时候发生错误的处理;
  • @OnClose:处理连接关闭的事件;

三、实践

1.Springboot集成Websocket

通过Springboot集成Websocket有两种实现,分别是基于注解的实现和实现接口的方式,下面分别展开:

1.1.基于注解

添加pom依赖

在pom配置文件中引入spring-boot-starter-websocket

java 复制代码
<!--websocket-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

在配置类中增加websocket配置Bean

java 复制代码
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

/**
 * @author yangnk
 * @desc
 * @date 2023/08/15 00:30
 **/
@Configuration
public class WebSocketConfig {
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}

实现websocketServer

实现一个websocketserver,需要添加@ServerEndpoint("/websocket/{name}")注解,该注解需要加在类上,表示请求websocket的url地址,再实现websocket各个声明周期的注解的方法。

声明ConcurrentHashMap<String, WebSocketServer> webSocketMap用来保存websocket连接的session。

java 复制代码
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.util.concurrent.ConcurrentHashMap;

/**
 * @author yangnk
 * @desc
 * @date 2023/08/15 00:31
 **/

/**
 * @ServerEndpoint 注解的作用
 *
 * @ServerEndpoint 注解是一个类层次的注解,它的功能主要是将目前的类定义成一个websocket服务器端,
 * 注解的值将被用于监听用户连接的终端访问URL地址,客户端可以通过这个URL来连接到WebSocket服务器端
 */
@Slf4j
@Component
@ServerEndpoint("/websocket/{name}")
public class WebSocketServer {

    /**
     * 与某个客户端的连接对话,需要通过它来给客户端发送消息
     */
    private Session session;

    /**
     * 标识当前连接客户端的用户名
     */
    private String name;

    /**
     * 用于存所有的连接服务的客户端,这个对象存储是安全的
     * 注意这里的kv,设计的很巧妙,v刚好是本类 WebSocket (用来存放每个客户端对应的MyWebSocket对象)
     */
    private static ConcurrentHashMap<String, WebSocketServer> webSocketMap = new ConcurrentHashMap<>();


    /**
     * 连接建立成功调用的方法
     * session为与某个客户端的连接会话,需要通过它来给客户端发送数据
     */
    @OnOpen
    public void OnOpen(Session session, @PathParam(value = "name") String name){

        log.info("----------------------------------");
        this.session = session;
        this.name = name;
        // name是用来表示唯一客户端,如果需要指定发送,需要指定发送通过name来区分
        webSocketMap.put(name,this);
        log.info("[WebSocket] 连接成功,当前连接人数为:={}", webSocketMap.size());
        GroupSending(name+" 来了");
    }

    /**
     * 连接关闭调用的方法
     */
    @OnClose
    public void OnClose(){
        webSocketMap.remove(this.name);
        log.info("[WebSocket] 退出成功,当前连接人数为:={}", webSocketMap.size());
        GroupSending(name+" 走了");
    }

    /**
     * 收到客户端消息后调用的方法
     */
    @OnMessage
    public void OnMessage(String message_str){
        log.info("[WebSocket] 收到消息:{}",message_str);
        //判断是否需要指定发送,具体规则自定义
        //message_str的格式 TOUSER:user2;message:aaaaaaaaaaaaaaaaaa;
        if(message_str.indexOf("TOUSER") == 0){
            //取出 name和message的值
            String[] split = message_str.split(";");
            String[] split1 = split[0].split(":");
            String[] split2 = split[1].split(":");
            String name = split1[1];
            String message = split2[1];
            //指定发送
            AppointSending(name,message);
        }else{
            //群发
            GroupSending(message_str);
        }
    }

    /**
     * 发生错误时调用
     * @param session
     * @param error
     */
    @OnError
    public void onError(Session session, Throwable error){
        log.info("发生错误");
        error.printStackTrace();
    }

    /**
     * 群发
     * @param message
     */
    public void GroupSending(String message){
        for (String name : webSocketMap.keySet()){
            try {
                webSocketMap.get(name).session.getBasicRemote().sendText(name + ":" + message);
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }

    /**
     * 指定发送
     * @param name
     * @param message
     */
    public void AppointSending(String name,String message){
        try {
            webSocketMap.get(name).session.getBasicRemote().sendText(name + ":" + message);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

控制器Controller调用方法

java 复制代码
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
/**
 * @author yangnk
 * @desc
 * @date 2023/08/15 00:32
 **/

@RestController("web_Scoket_system")
@RequestMapping("/api/socket")
public class SystemController {

    @Autowired
    WebSocketServer socketServer;

    //推送数据接口
    @ResponseBody
    @RequestMapping("/socket/push/{name}")
    public String pushToWeb(@PathVariable String name, @RequestHeader String message) {
        Map<String,Object> result = new HashMap<>();
        socketServer.AppointSending(name, message);
        result.put("name", name);
        result.put("msg", message);
        return result.toString();
    }
}

验证结果

可以在在线websocket测试网站:https://www.bejson.com/httputil/websocket/ 进行验证。

1.2.实现接口

TODO

2.完整代码

https://github.com/yangnk/SpringBoot_Learning/tree/master/SpringBootExample/src/main/java/com/yangnk/webSocketExample

3.常见问题

  1. websocket的默认超时实践为30min,如果需要长时间保持websocket连接,就需要设置保活机制。

TODO

  1. 补充通过实现接口的方式来使用websocket;

参考资料

  1. WebSocket:https://zh.wikipedia.org/zh-cn/WebSocket (官网)
  2. springboot整合websocket最基础入门使用教程详解:https://developer.aliyun.com/article/983651 (websocket demo写的比较详细,具有实操性)
  3. SpringBoot 集成 WebSocket,实现后台向前端推送信息:https://cloud.tencent.com/developer/article/1830998 (websocket demo写的不错)
  4. 一文了解WebSocket及Springboot集成WebSocket:https://developer.aliyun.com/article/1152716 (讲了比较多原理方面的东西)
  5. springboot整合webSocket(看完即入门):https://juejin.cn/post/7139334773356363783
  6. WebSocket使用介绍,看这篇就够了:https://juejin.cn/post/7244407068098560037#heading-6 (websocket的进阶使用)
  7. Java WebSocket 基础 生命周期:https://blog.csdn.net/sakuragio/article/details/98673473 (讲了非常详细的websocket事件和API使用情况)

本文由博客一文多发平台 OpenWrite 发布!

相关推荐
代码魔法师Sunny2 天前
4.WebSocket 配置与Nginx 的完美结合
websocket·网络协议
kunkun1013 天前
关于Websocket
网络·websocket·网络协议
flying robot3 天前
websocket的使用
websocket
azheng2225 天前
WebSocket消息帧的组成结构
websocket
._Ha!n.5 天前
WebSocket实现消息实时推送
网络·websocket·网络协议
蜀中孤鹰6 天前
由浅入深逐步理解spring boot中如何实现websocket
spring boot·后端·websocket
测试界的酸菜鱼6 天前
C# 如何处理 WebSocket 连接异常
开发语言·websocket·c#
布兰妮甜6 天前
WebSocket详解:从前端到后端的全栈理解
前端·websocket·网络协议
代码魔法师Sunny6 天前
2.WebSocket进阶: 深入探究实时通信的最佳实践与优化技巧
网络·websocket·网络协议
ઈ一笑ഒ6 天前
Django+websocket实现一个简单聊天
网络·websocket·网络协议