IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 网络协议 -> WebSocket简介和实例 -> 正文阅读

[网络协议]WebSocket简介和实例

1. WebSocket是什么?

websocket是一种网络协议,其于2011年成为了国际标准(rfc6455)。

websocket解决了HTTP只能单向通信的问题。

比如我们需要服务器能定时向客户端传输一份数据,因为HTTP协议做不到服务器主动向客户端推送消息,所以需要client定时到服务器轮询,服务器收到请求后,将客户端需要的数据返回给客户端。这样轮询效率是低下的,并且浪费了资源。

websocket可以解决这样的问题,服务器可以主动向客户端推送消息,客户端也可以向服务器发送消息,实现了全双工。

2. WebSocket如何工作?

websocket是建立于http之上的,当客户端与服务器端建立了websocket连接后,这个连接将一直存在。

websocket工作可以分为两部分:建立连接和发送数据

2.1 建立连接

2.1.1 建立过程

当建立websocket连接的时候,client会先发送一个upgrade http报文到server,然后server再对此进行回复,从而完成一次握手过程。

client发送的数据包是类似这个样子的:

        GET /chat HTTP/1.1
        Host: server.example.com
        Upgrade: websocket
        Connection: Upgrade
        Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
        Origin: http://example.com
        Sec-WebSocket-Protocol: chat, superchat
        Sec-WebSocket-Version: 13

server返回的数据包大概是这个样子:

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

1)第一行为遵循Request-Line(rfc2616)格式,首先是method,为“GET”,紧跟着Request-URI为“/chat”,“/chat”的是websocket的服务端点(endpoint),最后是http版本号。

2)Host:host是http协议要求必须带的,host指明了client要访问的具体资源,比如一个server(一个IP),部署了多个域名,那么当client访问其中一个域名的时候,就可以通过host进行区分。如果server收到一个client请求,但是没有host,server会返回错误(400)。

3)Upgrade:当client想通过其他协议与server通信的话,就需要带upgrade,服务器统一协议升级后会回复101(switching protocol),并且在response中也会带上upgrade

4)connection: 表名client和server对于长连接如何处理,如果是keep-alive,那么就会保持,如果是close,那么在本次通信完成之后要将连接断开。

5)Sec-WebSocket-Key:这个是为了client和server之间相互验证用的。当server收到Sec-WebSocket-Key之后,它会将这个key链接加上Globally Unique Identifier(GUID)形成一个新的字符串(base64),发送回client(Sec-WebSocket-Accept)。client回进行检查,如果发现收到的不是期望的值,那么websocket的连接就建立不成功。

6)Origin:origin头只包含协议和域名。一般用于CORS跨域请求中

7)Sec-WebSocket-Protocol:指明客户端可以接受那些子协议,可以填写多个,服务器会从中选择一个。

8)Sec-WebSocket-Version:websocket版本

9)Sec-WebSocket-Extensions:websocket扩展头,用于协商本次连接要使用的 WebSocket 扩展:客户端发送支持的扩展,服务器通过返回相同的首部确认自己支持一个或多个扩展,具体扩展内容和含义可以参考(rfc7692

10)Sec-WebSocket-Accept:服务端的回复,意思见Sec-WebSocket-Key

根据协议,在handshake过程中,客户端和服务器都会有很多的检查,比如服务器会检查http协议版本是否大于1.1并且是GET请求等,这些一般对于普通开发者来说,都是不需要关注的。

2.1.2 wireshark抓包

wireshark抓包看了下,第一个包是客户端发送的:

?可以看到,我实际抓的包中还有cookie。

下一个是服务端返回的报文:

?相对简单好多。

2.2?发送数据

handshake成功之后,客户端和服务器端就可以互相发送报文了。

2.2.1 报文结构

报文结构如下:

 0                   1                   2                   3
      0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
     +-+-+-+-+-------+-+-------------+-------------------------------+
     |F|R|R|R| opcode|M| Payload len |    Extended payload length    |
     |I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
     |N|V|V|V|       |S|             |   (if payload len==126/127)   |
     | |1|2|3|       |K|             |                               |
     +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
     |     Extended payload length continued, if payload len == 127  |
     + - - - - - - - - - - - - - - - +-------------------------------+
     |                               |Masking-key, if MASK set to 1  |
     +-------------------------------+-------------------------------+
     | Masking-key (continued)       |          Payload Data         |
     +-------------------------------- - - - - - - - - - - - - - - - +
     :                     Payload Data continued ...                :
     + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
     |                     Payload Data continued ...                |
     +---------------------------------------------------------------+

可以看下各个字段都是什么含义:

  • FIN: 1bit,用来指示这个数据包是不是一条消息的最后一个包
  • RSV1,RSV2,RSV3:3个保留bit
  • opcode:操作码,用于表示“payload data”字段的含义:
    • 0x0:说明这是一个continuous frame,
    • 0x1:说明是文本数据
    • 0x2:说明是二进制数据
    • 0x3-7:保留
    • 0x8:连接断开
    • 0x9:表明是一个ping包
    • 0xA:表明是一个pong包
    • 0xB-F:保留
  • Mask:表明payload data是否是mask的,如果设置为1,那么“Masking-kay”字段要有值
  • payload len:“payload data”的长度,8bit。又分成了3种情况:
    • 如果值为 0-125,那么就表示负载数据的长度;
    • 如果是 126,那么接下来的 2 个字节以16 位的无符号整形作为负载数据的长度(无符号整型);
    • 如果是 127,那么接下来的 8 个字节作为负载数据的长度(无符号整型)。
  • Masking-key:如果“mask”值为0,那么masking-key的长度也是0,否则为4字节。从客户端发送到服务器的数据需要通过masking-key计算掩码后发送。mask算法是公开的,但是我没有细究。
  • payload data:数据包中的实际数据。

2.2.2 发送报文

发送报文的时候,需要有以下条件:

  • websocket连接处于open状态
  • 将要发送的数据封装与websocket数据包中,一个放不下就放在多个包中
  • 如果是客户端发包,payload data必须是mask的
  • 如果协商的时候有扩展,需要应用这些扩展

2.2.2 关闭连接

一般来说,连接的关闭应该是服务器端发起。

服务器先发送一个关闭的控制包,客户端收到后会返回一个关闭的控制包。当两端都发送和收到 关闭连接的控制包之后,websocket连接就被关闭了。

本来我也想wireshark抓包,看下data报文的样子,但是没有抓到,或者是抓到了我不认识?我wireshark用http过滤没有发现websocket报文,后来用websocket直接过滤也不行。不知道哪个地方弄的不对,以后有时间了再研究。

3. WebSocket实例1

环境是idea+jdk8+springboot2.3.12

功能很简单,客户端启动后和服务端建立websocket连接,客户端给服务端发送消息,服务端收到后,通过websocket给客户端回复一个消息。

pom.xml文件如下:

<?xml version="1.0" encoding="UTF-8"?>
<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 https://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.3.12.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example.jpademo</groupId>
    <artifactId>demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>demo</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <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>     
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

3.1 服务端编写

先看服务端。服务端websocket编写有几种方式,我选择是通过config方式,也可以通过注解方式

1. 先编写websocket handler

package com.example.jpademo.demo.handler;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;

import java.io.IOException;
import java.time.LocalDateTime;
import java.util.concurrent.ConcurrentHashMap;

/**
 * ws消息处理类
 */
@Component
@Slf4j
public class MyWsHandler extends TextWebSocketHandler {

    //Logger log = LoggerFactory.getLogger(MyWsHandler.class);
    public  static ConcurrentHashMap<String, WebSocketSession> SESSION_POOL = new ConcurrentHashMap<>();

    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        log.info("建立ws连接");
        System.out.println("after web socker connection is established");
        SESSION_POOL.put(session.getId(),session);
    }

    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        log.info("发送文本消息");
        // 获得客户端传来的消息
        String payload = message.getPayload();
        payload = payload + " : all well";
        log.info("server 接收到消息 " + payload);
        session.sendMessage(new TextMessage("server 发送给的消息 " + payload + ",发送时间:" + LocalDateTime.now().toString()));
    }


    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        log.info("关闭ws连接");

        if (session != null) {
            try {
                // 关闭连接
                session.close();
            } catch (IOException e) {
                // todo: 关闭出现异常处理
                log.info(e.getMessage());
            }
        }

        SESSION_POOL.remove(session.getId());

    }

    @Override
    public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
        log.error("异常处理");
        WsSessionManager.removeAndClose(session.getId());
    }
}

websocket handler要继承TextWebSocketHandler类,当然也可以继承AbstractWebSocketHandler类型,然后override几个方法,通过方法的名字,比较容易看出这些方法都是做什么的:

  • afterConnectionEstablished:建立websocket成功后被触发
  • handleTextMessage:处理对端发送过来的websocket消息
  • afterConnectionClosed:websocket连接关闭后触发
  • handleTransportError:传输出问题触发

代码中的SESSION_POOL用于存放建立的session。当一个新websocket session建立后,就将这个session放到pool中,当一个websocket session被关闭后,就从这个pool删除。在例子中,这个pool其实作用不大,因为服务端在收到客户端消息后,直接就回复了消息。但是对于其他情况,保存session就显得有必要了。

2. 注册websocket

package com.example.jpademo.demo.config;

import com.example.jpademo.demo.handler.MyWsHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

    @Autowired
    private MyWsHandler myWsHandler;

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        System.out.println("hello web socket");
        registry.addHandler(myWsHandler, "/wsok").setAllowedOrigins("*");
    }
}

注册的关键是registry的addHandler方法,这个方法会将"/wsok"(暴露给客户端的请求路径)和第一步创建的MyWsHandler进行绑定。

3.2?客户端编写

这个是前端代码,网上找的:

<!DOCTYPE html>
<html>
? ? <head>
? ? ? ? <meta charset="UTF-8">
? ? ? ? <title></title>
? ? </head>
? ? <body>
? ? ? ? websocket Demo---- user000 <br />
? ? ? ? <input id="text" type="text" />?
? ? ? ? <button οnclick="send()"> Send </button> ??
? ? ? ? <button ? οnclick="closeWebSocket()"> Close </button>
? ? ? ? <div id="message"> ? </div>
? ? ? ??
? ? <script type="text/javascript">
? ? ?//判断当前浏览器是否支持WebSocket
? ? ? if('WebSocket' in window){
? ? ? ? ? //websocket = new WebSocket("ws://localhost:8080/Demo/websocketTest/user000");
?? ??? ? ?websocket = new WebSocket("ws://localhost:8080//wsok");
? ? ? ? ? console.log("link success")
? ? ? }else{
? ? ? ? ? alert('Not support websocket')
? ? ? }
? ? ??
? ? ? //连接发生错误的回调方法
? ? ? websocket.onerror = function(){
? ? ? ? ? setMessageInnerHTML("error");
? ? ? };
? ? ? ?
? ? ? //连接成功建立的回调方法
? ? ? websocket.onopen = function(event){
? ? ? ? ? setMessageInnerHTML("open");
? ? ? }
? ? ? ?console.log("-----")
? ? ? //接收到消息的回调方法
? ? ? websocket.onmessage = function(event){
? ? ? ? ? ? setMessageInnerHTML(event.data);
? ? ? }
? ? ? ?
? ? ? //连接关闭的回调方法
? ? ? websocket.onclose = function(){
? ? ? ? ? setMessageInnerHTML("close");
? ? ? }
? ? ? ?
? ? ? //监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
? ? ? window.onbeforeunload = function(){
? ? ? ? ? websocket.close();
? ? ? }
? ? ? ?
? ? ? //将消息显示在网页上
? ? ? function setMessageInnerHTML(innerHTML){
? ? ? ? ? document.getElementById('message').innerHTML += innerHTML + '<br/>';
? ? ? }
? ? ? ?
? ? ? //关闭连接
? ? ? function closeWebSocket(){
? ? ? ? ? websocket.close();
? ? ? }
? ? ? ?
? ? ? //发送消息
? ? ? function send(){
? ? ? ? ? var message = document.getElementById('text').value;
? ? ? ? ? websocket.send(message);
? ? ? }
? ? </script>
? ? ? ??
? ? </body>
</html>

发送效果如下:

?4. WebSocket实例2

除了上述方法,也可以用原生注解的方法实现websocket

pom.xml文件不变

4.1 服务端编写

1. bean注册

package com.example.jpademo.demo.config;

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

@Configuration
@EnableWebSocket
public class WebSocketOriginConfig {

    @Bean
    public ServerEndpointExporter serverEndpoint() {
        return new ServerEndpointExporter();
    }
}

2. 编写handler

package com.example.jpademo.demo.handler;

import org.springframework.stereotype.Component;
import org.springframework.web.socket.WebSocketSession;

import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;

@Component
@ServerEndpoint("/hellows")
public class WsServerEndpoint {

    public  static ConcurrentHashMap<String, Session> SESSION_POOL = new ConcurrentHashMap<>();

    /**
     * 连接成功
     *
     * @param session
     */
    @OnOpen
    public void onOpen(Session session) {
        System.out.println("连接成功");
        SESSION_POOL.put(session.getId(), session);
    }

    /**
     * 连接关闭
     *
     * @param session
     */
    @OnClose
    public void onClose(Session session) {
        System.out.println("连接关闭");
    }

    /**
     * 接收到消息
     *
     * @param text
     */
    @OnMessage
    public String onMsg(Session session, String text) throws IOException {
        for(Session session1 : SESSION_POOL.values()){
            session1.getBasicRemote().sendText("good morning");
        }
        //SESSION_POOL.get(session.getId()).getBasicRemote().sendText("good morning");
        return "servet 发送:" + text;
    }


}

?与例子1不同的是,此处用注解实现:

  • @ServerEndpoint: 这个注解可以让 spring boot 知道你暴露出去的 ws 应用的路径,与registry.addHandler(myWsHandler, "/wsok").setAllowedOrigins("*");类似
  • @OnOpen:当 websocket 建立连接成功后会触发这个注解方法,它有一个 Session 参数,可以在方法中将这个session保存,以供后续使用
  • @OnClose:当 websocket 连接断开后会触发这个注解修方法,注意它有一个 Session 参数,可以将这个session从pool中删除
  • @OnMessage:当客户端发送消息到服务端时,会触发这个注解方法,?String 参数是客户端传入的值
  • @OnError:当 websocket 建立连接时出现异常会触发这个注解修饰的方法,它有一个 Session 参数,这个我在例子中没有写

4.2 客户端编写

客户端与例子1一样,只是改下websocket的url为:

websocket = new WebSocket("ws://localhost:8080//hellows");

发送效果如下:

?5?参考文档

参考了网上的文章如下,他们都写的比我好,但是自己做一遍能加深印象

【websocket】spring boot 集成 websocket 的四种方式 - 云+社区 - 腾讯云这个配置类很简单,通过这个配置 spring boot 才能去扫描后面的关于 websocket 的注解icon-default.png?t=L9C2https://cloud.tencent.com/developer/article/1530872
万字长文,一篇吃透WebSocket:概念、原理、易错常识、动手实践 - 码农教程本文章向大家介绍万字长文,一篇吃透WebSocket:概念、原理、易错常识、动手实践,主要包括万字长文,一篇吃透WebSocket:概念、原理、易错常识、动手实践使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。http://www.manongjc.com/detail/25-awrtfwjcoejxpjh.html

https://segmentfault.com/a/1190000018214719icon-default.png?t=L9C2https://segmentfault.com/a/1190000018214719WebSocket协议分析 - 功夫Panda - 博客园内容不断更新,目前包括协议中握手和数据帧的分析 1.1 背景 1.2 协议概览 协议包含两部分:握手,数据传输。 客户端的握手如下:GET /chat HTTP/1.1Host: server.exahttps://www.cnblogs.com/caosiyang/archive/2012/08/14/2637721.html还有几篇文章,找不到了,也表示感谢!

?

  网络协议 最新文章
使用Easyswoole 搭建简单的Websoket服务
常见的数据通信方式有哪些?
Openssl 1024bit RSA算法---公私钥获取和处
HTTPS协议的密钥交换流程
《小白WEB安全入门》03. 漏洞篇
HttpRunner4.x 安装与使用
2021-07-04
手写RPC学习笔记
K8S高可用版本部署
mySQL计算IP地址范围
上一篇文章      下一篇文章      查看所有文章
加:2021-10-23 12:49:51  更:2021-10-23 12:50:57 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/26 5:48:59-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码