WebSocket_http服务端可以主动给客户端发消息吗-程序员宅基地

技术标签: 网络  websocket  网络协议  

WebSocket

1.http协议

概念:HTTP协议是一种无状态无连接,单向的应用层协议,它采用的是请求/响应模型。通信请求只能由客户端发起,服务端对请求做出应答处理,一个http请求对应着一个响应。

缺点:因为它的特性是一个响应对应着一个请求,所以,服务器不能主动发送消息给客户端(浏览器)这种单向请求的特点,注定了如果服务器有连续的状态变化,客户端要获知就非常麻烦。大多数web应用程序通过频繁的异步AJAX请求实现长轮询。轮询的效率低,非常浪费资源(因为必须不停连接,或者HTTP连接始终打开)。

无状态:

无状态是指协议对事物处理没有记忆能力,服务器不知道客户端是什么状态。即我们给服务器发送HTTP请求之后,服务器根据请求 会给我们发送数据过来,但是发送完,不会记录任何信息。

优点:解放了服务器,每一次请求“点到为止”不会造成不必要连接的占用

缺点:每次请求会传输大量重复的内容信息。

无连接:

它的含义是限制每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,即断开连接。采用这种方式可以节省传输时间。

长连接和短连接

长连接

优点:可以省去较多的TCP建立和关闭的操作,减少浪费,节约时间。对于频繁请求资源的客户端适合使用长连接。

缺点:client端一般不会主动关闭连接,当client与server之间的连接一直不关闭,随着客户端连接越来越多,server会保持过多连接。server端需要采取一些策略来控制

使用场景:长连接多用于操作频繁,点对点的通讯,而且连接数不能太多情况。

短连接

优点:对于服务器来说管理较为简单,存在的连接都是有用的连接,不需要额外的控制手段。

缺点:客户端和服务器每进行一次HTTP操作,就建立一次连接,任务结束就中断连接。需要频繁的建立连接

使用场景:WEB网站的http服务一般都用短链接,因为长连接对于服务端来说会耗费一定的资源,而像WEB网站这么频繁的成千上万甚至上亿客户端的连接用短连接

2.WebSocket介绍

WebSocket 协议在2008年诞生,2011年成为国际标准。所有浏览器都已经支持了。WebSocket 是一种网络通信协议, 由HTML5提出的一种在单个 TCP 连接上进行全双工通讯的协议。它的最大特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,具有持久化,一般应用在:聊天室,股票基金报价,协同办公等等...其他特点包括:

  • 建立在 TCP 协议之上,服务器端的实现比较容易。

  • 与 HTTP 协议有着良好的兼容性。默认端口也是 80 和 443 ,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。

  • 数据格式比较轻量,性能开销小,通信高效。

  • 可以发送文本,也可以发送二进制数据。

  • 没有同源限制,客户端可以与任意服务器通信。

  • 协议标识符是ws(如果加密,则为wss),服务器网址就是 URL。

Http和WebSocket的区别

相同点
  • 都是一样基于TCP的,都是可靠性传输协议。

  • 都是应用层协议。

不同点
  • 模拟WebSocket协议,可以双向发送或接受信息。HTTP是单向的。

  • WebSocket是需要握手进行建立连接的。

  • WebSocket是有状态,Http是无状态的

联系

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

3.通信方式

全双工:就是可以服务器和客户端进行互发消息,既可以发送数据也可以接收数据.它允许数据同时在两个方向上传输,因此,全双工通信是两个单工通信方式的结合,它要求发送设备和接收设备都有独立的接收和发送能力;在同一时间可以同时接受和发送信息,实现双向通信,举例:电话通信。一个很宽的

半双工:半双工数据传输允许数据在两个方向上传输,但是,在同一时间,只允许数据在一个方向上传输,它实际上是一种切换方向的单工通信;在同一时间只可以有一方接受或发送信息,可以实现双向通信。举例:对讲机,独木桥。

单工:单工是指数据传输只支持数据在一个方向上传输;在同一时间只有一方能接受或发送信息,不能实现双向通信,举例:电视,广播。

4.WebSocket协议

本协议有两部分:握手和数据传输(握手是基于http协议的)

客户端发送的握手请求

GET  /chat HTTP/1.1
 Host: XXX.com
 Connection: Upgrade//一个申请协议升级的 HTTP 请求
 Upgrade: websocket//告诉服务器给升级到websocket协议
 Sec-WebSocket-Version: 13// 告诉服务器所使用的协议版本
 Sec-WebSocket-key: XXXX//是base64加密的字符串 浏览器自动生成 
  

服务端响应客户端握手请求

HTTP/1.1 101 Switching Protocols   //返回101状态码
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=   //对Sec-WebSocket-key的加密 同意握手建立链接 客户端收到 Sec-WebSocket-Accept后 将本地的Sec-WebSocket-key 编码做一个对比来验证

字段说明:

头名称 说明
Connection:Upgrade 标识该HTTP请求是一个协议升级请求
Upgrade:WebSocket 协议升级为WebSocket协议
Sec-WebSocket-Version:13 客户端支持WebSocket的版本
Sec-WebSocket-Key: 客户端采用base64编码的24位随机字符序列,服务器接受客户端HTTP协议升级的证明。要求服务端响应一个对应加密的Sec-WebSocket-Accept头信息作为应答
Sec-WebSocket-Extensions 协议扩展类型

4.1 客户端(浏览器)实现 前端

1.websocket对象

实现WebSocket的Web浏览器将通过WebSocket对象公开所有必需的客户端功能(主要指支持Html5的浏览器)。

以下API用于创建WebSocket对象:

var ws = new WebSocket(url);//参数url格式说明:ws://ip地址:端口号/资源名称
2.websocket事件

WebSocket对象的相关事件

事件 事件处理程序 描述
open Socket.onopen 连接建立时触发
message Socket.onmessage 客户端接收服务端数据时触发
error Socket.onerror 通信发生错误时触发
close Socket.onclose 连接关闭时触发
3.websocket方法

WebSocket对象的相关方法:

方法 描述
send() 使用连接发送数据

4.2服务端实现

Tomcat的7.0.5版本开始支持WebSocket,并且实现了Java WebSocket规范(JSR356)。

Java WebSocket应用由一系列的websocketEndpoint组成。Endpoint是一个Java对象,代表 WebSocket链接的一端,对于服务端,我们可以视为处理具体 WebSocket消息的接口,就像Servlet之与http请求一样。可以通过注解的方式定义Endpoint.

Endpoint实例在WebSocket握手时创建,并在客户端与服务端链接过程中有效,最后在链接关闭时结束。在Endpoint接口中明确定义了与其生命周期相关的方法,规范实现者确保生命周期的各个阶段调用实例的相关方法。生命周期如下:

注解 含义描述
@OnClose 当会话关闭时调用。
@OnOpen 当开启一个新的会话时调用,该方法是客户端与服务端握手成功后调用的方法。
@OnError 当连接过程中异常时调用。

服务端如何接受客户端发送的数据呢?

通过为Session添加MessageHandler消息处理器来接受消息,当采用注解方式定义Endpoint时,我们还可以通过@OnMessage注解指定接收消息的方法。

服务端如何推送数据给客户端呢?

发送消息则由RemoteEndpoint完成,其实例由Session维护,根据使用情况,我们可以通过Session.getBasicRemote获取同步消息发送的实例,然后调用其sendXxx()方法就可以发送消息,可以通过Session.getAsyncRemote获取异步消息发送实例。

4.3代码实现

1.在pom.xml文件导入springboot需要的依赖
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
​
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-websocket</artifactId>
    </dependency>
​
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-lang3</artifactId>
        <version>3.10</version>
    </dependency>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>1.2.66</version>
    </dependency>
​
    <dependency>
        <groupId>org.thymeleaf</groupId>
        <artifactId>thymeleaf-spring5</artifactId>
    </dependency>
    <dependency>
        <groupId>org.thymeleaf.extras</groupId>
        <artifactId>thymeleaf-extras-java8time</artifactId>
    </dependency>
​
</dependencies>
​
<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>
2.新建config包创建websocketconfig

  • ServerEndpointExporter

    官方的解释是:当注入这个对象的时候,可以将webSocket对象注入Spring容器中。

  • session

  • webSocket会话对象,发送消息时需要

开启websocket支持

package com.cto.config;
​
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
import javax.websocket.server.ServerEndpoint;
//开启websocket支持
@Configuration
public class WebSocketConfig {
    @Bean
    public static ServerEndpointExporter serverEndpointExporter(){
        return new ServerEndpointExporter();
    }
}
3.websocketserver

编写服务端的代码

这就是重点了,核心都在这里。

因为WebSocket是类似客户端服务端的形式(采用ws协议),那么这里的WebSocketService其实就相当于一个ws协议的Controller

直接@ServerEndpoint("/imserver/{userId}") 、@Component启用即可,然后在里面实现@OnOpen开启连接,@onClose关闭连接,@onMessage接收消息等方法。

新建一个ConcurrentHashMap webSocketMap 用于接收当前userId的WebSocket,方便IM之间对userId进行推送消息。单机版实现到这里就可以。

package com.xinzhi.config;
​
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
​
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.ConcurrentModificationException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CopyOnWriteArraySet;
​
@ServerEndpoint("/websocket/{userId}")
@Component
public class WebSocketServer {
   private static int onlineCount=0;//连接人数
   private Session session;//建立连接
   private static String userId="";//用户的昵称
   //所有用户的连接信息,CopyOnWriteArraySet:线程安全的无序的集合
  private static  CopyOnWriteArraySet<WebSocketService> sendAll = new CopyOnWriteArraySet<>();
   //设置get方法来获取连接人数
   public static int getOnlineCount(){
       return onlineCount;
   }
​
   //用户连接的时候自增
    //synchronized:同步锁
    public synchronized void addOnlineCount(){
        WebSocketServer.onlineCount++;
    }
    //用户连接的时候自减
    public synchronized void subOnlineCount(){
       WebSocketServer.onlineCount--;
    }
​
    /**
     * 建立连接的方法
     * @param session 用户连接的对象
     * @param userId 用户的ID
     */
    @OnOpen
    public void onOpen(Session session, @PathParam("userId")String userId){
       this.session = session;
       this.userId = userId;
       sendAll.add(this);  //把用户的昵称连接会话和信息存到sendAll里面
       addOnlineCount();   //调用上方自增的方法
        System.out.println("有用户连接,当前连接人数为:"+getOnlineCount());
    }
​
    /**
     * 用户关闭连接
     */
    @OnClose
    public void onClose(){
       sendAll.remove(this);
       subOnlineCount(); //调用上方自减的方法
        System.out.println("有用户断开连接,当前人数为:"+getOnlineCount());
    }
​
    /**用户连接异常
     *
     * @param throwable 抛出异常
     */
    @OnError
    public void onError(Throwable throwable){
       throwable.printStackTrace();
        System.out.println("连接错误");
    }
​
    /**
     *实现服务器主动推送消息
     * @param message 消息
     * @throws IOException
     */
    public void sendMessage(String message) throws IOException {
       this.session.getBasicRemote().sendText(message);
    }
​
    /**
     * 收到客户端消息调用的方法
     * @param message 客户端发送过来的消息
     */
    @OnMessage
    public void onMessage(String message){
        System.out.println("用户"+userId+message);
        for (WebSocketServer webSocketServer : sendAll){
            try {
                webSocketServer.sendMessage(message); //调用sendMessage方法
            } catch (IOException e) {
                e.printStackTrace();
                continue;
            }
        }
    }
}
​
4.编写前端代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <!--引入jquery文件-->
    <script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.js"></script>  
    <script>
        var socket;  //定义了一个socket的变量
        function openSocket() {
           if (typeof(WebSocket) == "undefined"){  //表示一个异常
               console.log("您的浏览不支持websocket"); //弹出一个日志
           }else {
               console.log("您的浏览器支持websocket")
               //实例化websocket对象,指定要连接的服务器地址与端口,建立连接
               //等同于socket = new WebSocket("ws://localhost:8080/xxxx/im/25");
               //var socketUrl="${request.contextPath}/im/"+$("userId").val();
               var host=window.location.host; //定义了一个host变量
               socket=new WebSocket("ws://"+"localhost:8080/"+"/websocket/"+$("#userId").val())
               socket.onopen=function () {
                   console.log("已开启")
               }
               socket.onclose=function () {8
                   console.log("已关闭")
               }
               socket.onmessage=function (evt) {//参数数据
                   console.log(evt.data)
               }
               socket.onerror=function () {
                   console.log("连接异常")
               }
           }
        }
        function sendMessage() {
           socket.send($("#B").val())
        }
    </script>
​
</head>
<body>
  <div>请输入您的消息:<input type="text" name="B" id="B"></div>
  <div>请输入您的昵称:<input type="text" name="userId" id="userId"></div>
    <!-- onclick 绑定点击事件-->
  <div><input type="button" value="开启websocket" onclick="openSocket()"></div>
  <div><input type="button" value="发送消息" onclick="sendMessage()"></div>
</body>
</html>
5.启动项目测试

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_65700815/article/details/135701273

智能推荐

Windows 10搭建FTP 服务器保姆教学_win10ftr服务器-程序员宅基地

文章浏览阅读1k次。windows10搭建FTP服务器保姆教程:Ⅰ。我们需要打开windows的FTP服务器功能。Ⅱ。我们需要切断防火墙对FTP服务器的拦截。Ⅲ。新建一个本地用户。Ⅳ。新建一个存放传输文件的文件夹Ⅴ。搭建FTP服务器。_win10ftr服务器

Unity插件——Odin 学习笔记(一)_unity odin-程序员宅基地

文章浏览阅读2.6w次,点赞27次,收藏108次。本文章是为了记录学习Unity插件Odin,使用该插件可以让我们更快速便捷的开发Unity工具前言Unity原生编辑编辑器的方法有两种——IMGUI和新的UIElement1.UIElement我之前做过介绍,开发模式类似于HTML+CSS,如果有类似经验的人开发及相对容易,但是就目前而言不太适合编辑器开发,我觉得比较适合取代UGUI进行UI开发2.IMGUI入门比较简单,使用Edit..._unity odin

python 数据类型介绍_python中数据类型的作用-程序员宅基地

文章浏览阅读1.8k次。在 Python 中,数据类型是区分数据的种类和存储方式的标识符。它定义了数据的取值范围、占用空间大小、可操作特性等。Python 中常见的数据类型包括数字、字符串、列表、元组、集合和字典等。数据类型在编程中的作用主要有以下几个方面:不同的数据类型需要占用不同的内存空间,因此在内存空间的管理上,数据类型具有重要的作用。例如,在处理大量数据时,选择合适的数据类型可以有效地减少内存占用,提高程序的执行效率。数据类型定义了数据的表达方式和可操作性,使得程序能够对不同类型的数据进行有效的处理。_python中数据类型的作用

[kubernetes]-kubernetes使用nodeport暴露jacoco地址_kubernetes暴露jacoco端口-程序员宅基地

文章浏览阅读740次。测试环境 需要启动jacoco 并暴露端口 replicas为1 但是node为2个 没有指定pod在哪个节点上启动。因此打算用nodeport的方式暴露出来,这样认准一个node+nodeport就可以跑jacoco了haozhuo-health-deployment.yamlapiVersion: apps/v1kind: Deploymentmetadata:# deployment名字 和svc 和ingress绑定没关系 name: haozhuo-health-dp na_kubernetes暴露jacoco端口

SpringCloud整合项目Maven导入失败无法解析 org.springframework.cloud:spring-cloud-starter-XXXX:unknown_无法解析 org.springframework.cloud:spring-cloud-depend-程序员宅基地

文章浏览阅读1.6k次。springcloud maven导入失败_无法解析 org.springframework.cloud:spring-cloud-dependencies:pom:finchley.m8

PictureSelector 相册全白不显示问题,真服了_pictureselector.create 相册显示不出-程序员宅基地

文章浏览阅读566次。.load(url) .into(new ImageViewTarget<Bitmap>(imageView) { @Override public void onLoadStarted(@Nullable Drawable placeholder) { super.onLoadStarted(placeholder); ..._pictureselector.create 相册显示不出

随便推点

机器学习-决策树:python “AttributeError: '_csv.reader' object has no attribute'next'”_attributeerror: '_csv.writer' object has no attrib-程序员宅基地

文章浏览阅读532次。(1)reader.next()改为next(reader)(2)open(‘AllElectronics.csv’, ‘rb’)中的rb改成rt_attributeerror: '_csv.writer' object has no attribute 'writeheader

银行卡验证工具类分享_private static card card = card.getinstance();-程序员宅基地

文章浏览阅读4.9k次。用于验证的请求接口:https://ccdcapi.alipay.com/validateAndCacheCardInfo.json?_input_charset=utf-8&cardNo=6217002430035835629&cardBinCheck=true返回:{“bank”:”CCB”,”validated”:true,”cardType”:”DC”,”key”:”621700243003_private static card card = card.getinstance();

fatal error C1083: 无法打开包括文件: “Eigen\Dense”: No such file or directory_c1083无法打开包括文件: “eigen/dense”: no such file or dire-程序员宅基地

文章浏览阅读7.9k次,点赞18次,收藏30次。参考资料:CSDN、博客园等网上众多资料Eigen库,哎,感觉自己好蠢~我的包含路径是到你的路径…\Eigen我include<Eigen\Dense>标红,其实只要include就行,因为你的路径已经包含到Eigenl了这里要记一下的是你的包含路径到你要找的库的前一级用图来解释一下吧!我的Eigen库路径如下先来一波错误示范:在这里我们要使用Eigen下的Dense..._c1083无法打开包括文件: “eigen/dense”: no such file or directoryh:\work\6

科研绘图学习笔记1_科研笔记1:科研绘图-程序员宅基地

文章浏览阅读182次。在选定对应的色轮配色方案后,我们可根据它提供的 HEX 颜色码或 R、G、B 值进行配色的拾取,从而完成插图颜色的选择。ColorBrewer 2.0 是一个专业的在线配色方案网站,它提供了大量的颜色搭配主题,这些主题是众多绘图工具(如 Matplotlib、ggplot2 等)内置的绘图颜色主题。要避免出现文字较少、图表较多的情况,即无须将原始数据和中间处理过程涉及的插图全部展示在论文中,而应在有复杂和多维数据的情况下,提高精选插图的能力,而非简单地堆砌插图。在科研论文配图绘制的过程中应用较少。_科研笔记1:科研绘图

2020年南大计算机、软件工程考研经验分享_南京大学考研压分吗-程序员宅基地

文章浏览阅读2.1w次,点赞40次,收藏150次。南大20计算机学院考研打算考南京大学通信的学弟学妹你们好,首先欢迎大家选择我们计算机学院,计算机学院是我们学校的王牌学院。为了帮大家更好的了解我们学院的情况以及如何准备考研,我将从以下两个方面来介绍,觉得我写的还尚可的话,可以给我手动点赞哈。(2020年南大计算机考研资料分享及经验交流群:645638416)1:学院报考情况由于2019年的南京大学考研的数据在2020年的9月份才会公布..._南京大学考研压分吗

LInux高级系统编程-4 信号-程序员宅基地

文章浏览阅读1.3k次,点赞34次,收藏21次。所需头文件函数//将set集合置空//将所有信号加入set集合//将signo信号加入到set 集合//从set集合中移除signo 信号//判断信号是否存在于 集合中。

推荐文章

热门文章

相关标签