百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 编程网 > 正文

WebSocket理论与实战

yuyutoo 2025-03-08 01:59 15 浏览 0 评论

一 WebSocket理论

什么是http请求

http链接分为短链接、长链接,短链接是每次请求都要三次握手才能发送自己的信息。即每一个request对应一个response。长链接是在一定的期限内保持链接(但是是单向的,只能从客户端向服务端发消息,然后服务端才能响应数据给客户端,服务端不可以主动给客户端发消息)。保持TCP连接不断开。客户端与服务器通信,必须要有客户端发起然后服务器返回结果。客户端是主动的,服务器是被动的。

WebSocket

WebSocket他是为了解决客户端发起多个http请求到服务器资源浏览器必须要经过长时间的轮训问题而生的,他实现了多路复用,他是全双工通信。在webSocket协议下客服端和浏览器可以同时发送信息。

建立了WebSocket之后服务器不必在浏览器发送request请求之后才能发送信息到浏览器。这时的服务器已有主动权想什么时候发就可以随时发送信息到浏览器。而且信息当中不必在带有head的部分信息了,与http的长链接通信来对比,这种方式,不仅能降低服务器的压力。而且信息当中也减少了部分多余的信息。

WebSocket与http关系


相同点:

  1. 都是基于tcp的,都是可靠性传输协议
  2. 都是应用层协议

不同点:

  1. WebSocket是双向通信协议,模拟Socket协议,可以双向发送或接受信息
  2. HTTP是单向的
  3. WebSocket是需要浏览器和服务器握手进行建立连接的
  4. 而http是浏览器发起向服务器的连接,服务器预先并不知道这个连接

联系:
WebSocket在建立握手时,数据是通过HTTP传输的。但是建立之后,在真正传输时候是不需要HTTP协议的

http存在的问题:

http是一种无状态协议,每当一次会话完成后,服务端都不知道下一次的客户端是谁,需要每次知道对方是谁,才进行相应的响应,因此本身对于实时通讯就是一种极大的障碍

http协议采用一次请求,一次响应,每次请求和响应就携带有大量的header头,对于实时通讯来说,解析请求头也是需要一定的时间,因此,效率也更低下

最重要的是,需要客户端主动发,服务端被动发,也就是一次请求,一次响应,不能实现主动发送


二 代码实战

SpringBoot集成WebSocket实战

引入相关依赖


    org.springframework.boot
    spring-boot-starter-web



    org.springframework.boot
    spring-boot-starter-websocket



    com.alibaba
    fastjson
    1.2.71

WebSocket配置类

/**
 * @Author:sgw
 * @Date:2023/12/6
 * @Description:WebSocket配置类
 */
@Configuration
public class WebSocketConfig {
    /**
     * 	注入ServerEndpointExporter,
     * 	这个bean会自动注册使用了@ServerEndpoint注解声明的Websocket endpoint
     */
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}

websocket处理消息核心代码

package com.ws.websocket.demos.service;


import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArraySet;

/**
 * @Author:sgw
 * @Date:2023/7/6
 * @Description: 实现websocket处理消息
 */
@Component
//userId是项目每次启动时,都要与服务端的websocket建立长连接的userid值,建立长连接后,服务端就可以随时给指定的用户推送消息了
@ServerEndpoint("/ws/asset/{userId}")
public class MyWebSocket {
    //与某个客户端的连接会话,需要通过它来给客户端发送数据
    private Session session;

    //concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。
    //虽然@Component默认是单例模式的,但springboot还是会为每个websocket连接初始化一个bean,所以可以用一个静态set保存起来。
    //  注:底下WebSocket是当前类名
    private static CopyOnWriteArraySet webSockets = new CopyOnWriteArraySet<>();
    // 用来存:在线连接数
    private static Map sessionPool = new HashMap();
    private static Logger log = LoggerFactory.getLogger(MyWebSocket.class);

    /**
     * 链接成功调用的方法
     */
    @OnOpen
    public void onOpen(Session session, @PathParam(value = "userId") String userId) {
        try {
            this.session = session;
            webSockets.add(this);
            sessionPool.put(userId, session);

            log.info("sessionId值:" + session.getId());
            log.info("【websocket消息】有新的连接,总数为:" + webSockets.size());
            sendMessage(session, "连接成功");
        } catch (Exception e) {
        }
    }

    /**
     * 发送消息,实践表明,每次浏览器刷新,session会发生变化。
     *
     * @param session
     * @param message
     */
    public static void sendMessage(Session session, String message) {
        try {
            session.getBasicRemote().sendText(String.format("%s (From Server,Session ID=%s)", message, session.getId()));
        } catch (IOException e) {
            log.error("发送消息出错:{}", e.getMessage());
            e.printStackTrace();
        }
    }

    /**
     * 链接关闭调用的方法
     */
    @OnClose
    public void onClose() {
        try {
            webSockets.remove(this);

            log.info("【websocket消息】连接断开,总数为:" + webSockets.size());
        } catch (Exception e) {
        }
    }

    /**
     * 收到客户端消息后调用的方法
     *
     * @param message
     */
    @OnMessage
    public void onMessage(String message) {
        log.info("【websocket消息】收到客户端消息:" + message);
    }

    /**
     * 发送错误时的处理
     *
     * @param session
     * @param error
     */
    @OnError
    public void onError(Session session, Throwable error) {
        log.info("用户错误,原因:" + error.getMessage());
        error.printStackTrace();
    }


    // 此为广播消息
    public void sendAllMessage(String message) {
        log.info("【websocket消息】广播消息:" + message);
        for (MyWebSocket webSocket : webSockets) {
            try {
                if (webSocket.session.isOpen()) {
                    webSocket.session.getAsyncRemote().sendText(message);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    // 此为单点消息
    public void sendOneMessage(String userId, String message) {
        Session session = sessionPool.get(userId);
        if (session != null && session.isOpen()) {
            try {
                log.info("【websocket消息】 单点消息:" + message);
                //session.getAsyncRemote().sendText(message);
                session.getAsyncRemote().sendText(String.format("%s (From Server,Session ID=%s)", message, session.getId()));
            } catch (Exception e) {
                e.printStackTrace();
            }
        }else {
            log.info("没找到目前已经建立连接的用户,即要推送给的用户目前没登录");
        }
    }

    // 此为单点消息(多人)
    public void sendMoreMessage(String[] userIds, String message) {
        for (String userId : userIds) {
            Session session = sessionPool.get(userId);
            if (session != null && session.isOpen()) {
                try {
                    log.info("【websocket消息】 单点消息:" + message);
                    session.getAsyncRemote().sendText(message);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }

    }
}

Controller入口类

package com.ws.websocket.demos.web;

import com.alibaba.fastjson.JSONObject;
import com.ws.websocket.demos.service.MyWebSocket;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;


/**
 * @Author:sgw
 * @Date:2023/7/6
 * @Description: websocket入口类
 */

@RequestMapping("/api/ws")
@RestController
public class BasicController {

    @Resource
    private MyWebSocket myWebSocket;

    /**
     * 发送给单个用户websocket消息
     *
     * @param userId  用户的id
     * @param message 消息体
     * @return
     */
    @RequestMapping("/wsTest")
    public String websocket(String userId, String message) {
        JSONObject obj = new JSONObject();
        obj.put("cmd", "topic");//业务类型
        obj.put("msgId", "987654321");//消息id
        obj.put("msgTxt", message);//消息内容

        //单个用户发送 (userId为用户id)
        myWebSocket.sendOneMessage(userId, obj.toJSONString());
        
        return "index.html";
    }

    /**
     * 发送给所有用户websocket消息
     *
     * @param message 消息体
     * @return
     */
    @RequestMapping("/wsTest2")
    public String websocket2(String message) {
        JSONObject obj = new JSONObject();
        obj.put("cmd", "topic");//业务类型
        obj.put("msgId", "987654321");//消息id
        obj.put("msgTxt", message);//消息内容

        //全体发送
        myWebSocket.sendAllMessage(obj.toJSONString());

        return "index.html";
    }

    /**
     * 发送给多个指定用户websocket消息
     *
     * @param message 消息体
     * @return
     */
    @RequestMapping("/wsTest3")
    public String websocket3(String message) {
        JSONObject obj = new JSONObject();
        obj.put("cmd", "topic");//业务类型
        obj.put("msgId", "987654321");//消息id
        obj.put("msgTxt", message);//消息内容
        
        //给多个用户发送 (userIds为多个用户id,逗号‘,’分隔)
        String[] userIds = {"euoiqhfljsak", "jowiqnfdnas"};
        myWebSocket.sendMoreMessage(userIds, obj.toJSONString());

        return "index.html";
    }
}

前端代码

这里的前端代码以最简单的html来进行编写




    
    websocket测试
    



WebSocket测试,在控制台查看测试信息输出!

[url=/api/ws/wsTest?message=单发消息内容&userId=none]单发消息链接[/url]
[url=/api/ws/wsTest1?message=群发给所有用户消息内容]群发消息链接[/url]
[url=/api/ws/wsTest2?message=指定多个用户发消息内容]群发消息链接[/url]

<script type="text/javascript"> var socket; if (typeof (WebSocket) == "undefined") { console.log("遗憾:您的浏览器不支持WebSocket"); } else { console.log("恭喜:您的浏览器支持WebSocket"); //实际业务中,这里需要从系统里获取当前当前登录的用户id var userId = "54321"; /*实现化WebSocket对象 指定要连接的服务器地址与端口建立连接 注意ws、wss使用不同的端口。我使用自签名的证书测试, 无法使用wss,浏览器打开WebSocket时报错 ws对应http、wss对应https。*/ socket = new WebSocket("ws://localhost:8080/ws/asset/" + userId); //连接打开事件 socket.onopen = function () { console.log("Socket 已打开"); socket.send("消息发送测试"); }; //收到消息事件 socket.onmessage = function (msg) { console.log(msg.data); }; //连接关闭事件 socket.onclose = function () { console.log("Socket已关闭"); }; //发生了错误事件 socket.onerror = function () { alert("Socket发生了错误"); } //窗口关闭时,关闭连接 window.unload = function () { socket.close(); }; } </script>

这里模拟登录用户的id是54321(实际业务中,这个id是要从系统里进行获取的),项目启动时,就会加载这段代码,把54321这个用户的前端与后端的websocket服务端进行长连接


三 测试

访问前端首页:http://localhost:8080/,打开f12控制台,可以看到,连接成功


使用postman调接口,给用户id是54321的用户发送消息,看看浏览器这里能不能接收到websocket消息


发现浏览器接收到消息了,如下


如果使用postman,发给其他用户(还没登录的用户),浏览器接收不到消息


浏览器没有新消息:


后端日志:


查看websocket发送与接收的消息


相关推荐

自卑的人容易患抑郁症吗?(自卑会导致抑郁吗)

Filephoto[Photo/IC]Lowself-esteemmakesusfeelbadaboutourselves.Butdidyouknowthatovert...

中考典型同(近)义词组(同义词考题)

中考典型同(近)义词组...

WPF 消息传递简明教程(wpf messagebox.show)

...

BroadcastReceiver的原理和使用(broadcast-suppression)

一、使用中注意的几点1.动态注册、静态注册的优先级在AndroidManifest.xml中静态注册的receiver比在代码中用registerReceiver动态注册的优先级要低。发送方在send...

Arduino通过串口透传ESP 13板与java程序交互

ESP13---是一个无线板子,配置通过热点通信Arduino通过串口透传ESP13板与java程序交互...

zookeeper的Leader选举源码解析(zookeeper角色选举角色包括)

作者:京东物流梁吉超zookeeper是一个分布式服务框架,主要解决分布式应用中常见的多种数据问题,例如集群管理,状态同步等。为解决这些问题zookeeper需要Leader选举进行保障数据的强一致...

接待外国人英文口语(接待外国友人的英语口语对话)

接待外国人英文口语询问访客身份:  MayIhaveyourname,please?  请问您贵姓?  Whatcompanyareyoufrom?  您是哪个公司的?  Could...

一文深入理解AP架构Nacos注册原理

Nacos简介Nacos是一款阿里巴巴开源用于管理分布式微服务的中间件,能够帮助开发人员快速实现动态服务发现、服务配置、服务元数据及流量管理等。这篇文章主要剖析一下Nacos作为注册中心时其服务注册与...

Android面试宝典之终极大招(android面试及答案)

以下内容来自兆隆IT云学院就业部,根据多年成功就业服务经验,以及职业素养课程部分内容,归纳总结:18.请描述一下Intent和IntentFilter。Android中通过Intent...

除了Crontab,Swoole Timer也可以实现定时任务的

一般的定时器是怎么实现的呢?我总结如下:1.使用Crontab工具,写一个shell脚本,在脚本中调用PHP文件,然后定期执行该脚本;2.ignore_user_abort()和set_time_li...

Spark源码阅读:DataFrame.collect 作业提交流程思维导图

本文分为两个部分:作业提交流程思维导图关键函数列表作业提交流程思维导图...

使用Xamarin和Visual Studio开发Android可穿戴设备应用

搭建开发环境我们需要做的第一件事情是安装必要的工具。因此,你需要首先安装VisualStudio。如果您使用的是VisualStudio2010,2012或2013,那么请确保它是一个专业版本或...

Android开发者必知的5个开源库(android 开发相关源码精编解析)

过去的时间里,Android开发逐步走向成熟,一个个与Android相关的开发工具也层出不穷。不过,在面对各种新鲜事物时,不要忘了那些我们每天使用的大量开源库。在这里,向大家介绍的就是,在这个任劳任怨...

Android事件总线还能怎么玩?(android实现事件处理的步骤)

顾名思义,AndroidEventBus是一个Android平台的事件总线框架,它简化了Activity、Fragment、Service等组件之间的交互,很大程度上降低了它们之间的耦合,使我们的代码...

Android 开发中文引导-应用小部件

应用小部件是可以嵌入其它应用(例如主屏幕)并收到定期更新的微型应用视图。这些视图在用户界面中被叫做小部件,并可以用应用小部件提供者发布。可以容纳其他应用部件的应用组件叫做应用部件的宿主(1)。下面的截...

取消回复欢迎 发表评论: