Bootstrap

简单了解WebSocket协议与Http协议,聊天室实现(WebSocket方式)

WebSocket请求解析

1、http协议

WebSocket请求和Http请求的本质是一样的,所以想要了解WebSocket请求,最好的方法就是与Http请求对应着比较,首先先让我们看一下http请求的过程。因为浏览器默认使用http协议发送请求,所以问题就转为当在浏览器中输入url并敲回车键时发生了什么。

1.1 浏览器根据url解析ip地址

其中运用到了DNS解析协议,DNS解析服务如下所示

浏览器缓存:首先搜索浏览器自身的DNS缓存(缓存的时间比较短,大概只有1分钟,且只能容纳1000条缓存),看自身的缓存中是否是有域名对应的条目,而且没有过期,如果有且没有过期则解析到此结束。

系统缓存:如果浏览器自身的缓存里面没有找到对应的条目,那么浏览器会搜索操作系统自身的DNS缓存,如果找到且没有过期则停止搜索解析到此结束。

路由器缓存:如果系统缓存也没有找到,则会向路由器发送查询请求。

ISP(互联网服务提供商) DNS缓存:如果在路由缓存也没找到,最后要查的就是ISP缓存DNS的服务器。

总而言之,就是在四大缓存中查找有没有与url相对应的ip地址,没有的话就报错

1.2 一个完整的http请求过程

(1)域名解析,将url转化成ip

(2)建立TCP连接,三次握手

(3)Web浏览器向Web服务端发送HTTP请求报文

(4)服务器响应HTTP请求

(5)浏览器解析HTML代码,并请求HTML代码中的资源(JS,CSS,图片)(这是自动向服务器请求下载的)

(6)浏览器对页面进行渲染呈现给客户

(7)断开TCP连接

Http请求或响应其底层都是运用了计算机网络体系结构中运输层的TCP协议,TCP连接是可靠连接,也就是说,任何一个Http请求都会无损到达服务器端,任何一个Http响应都会无损的返回客户端。

1.3 HTTP 1.0 和 HTTP 1.1协议的区别

  • Http1.0协议是短暂协议,每次发送一个Http请求,都要建立一次TCP连接,等到接受到响应后就断开TCP连接;Http1.1协议支持长连接,即在一个TCP连接的过程中支持连续发送多个Http请求**(但连接很有可能因为各种意外情况被关闭)**。并且协议允许客户端不需要等待上一个Http请求有返回响应,可以连续发送,大大节省了时间,其主要体现在请求头中多了一个Connection属性

  • HTTP 1.1还通过增加更多的请求头和响应头来改进和扩充HTTP 1.0的功能。

  • HTTP 1.1的持续连接,也需要增加新的请求头来帮助实现。

    • 例如,Connection请求头的值为Keep-Alive时,客户端通知服务器返回本次请求结果后保持连接;Connection请求头的值为close时,客户端通知服务器返回本次请求结果后关闭连接。
  • HTTP 1.1还提供了与身份认证、状态管理和Cache缓存等机制相关的请求头和响应头。

1.4 Http协议的三大缺陷

无连接:每次请求一次,释放一次连接。所以无连接表示每次连接只能处理一个请求。优点就是节省传输时间,实现简单。我们有时称这种无连接为短连接。对应的就有了长链接,长连接专门解决效率问题。当建立好了一个连接之后,可以多次请求。但是缺点就是容易造成占用资源不释放的问题。当HTTP协议头部中字段Connection:keep-alive表示支持长链接。

无状态:HTTP协议是无状态协议。无状态是指协议对于事务处理没有记忆能力。缺少状态意味着如果后续处理需要前面的信息,则它必须重传,这样可能导致每次连接传送的数据量增大。另一方面,在服务器不需要先前信息时它的应答就较快。为了解决HTTP协议无状态,于是,两种用于保持HTTP连接状态的技术就应运而生了,一个是Cookie,而另一个则是Session。

被动性:HTTP协议只允许一方发送请求,然后另一方被动接受,通常情况下是发送请求的客户端占有主动地位,接受请求的服务器占据被动地位,而且不允许同时发送请求,也就是不支持全双工通信,其本质上是半双工通信

2、WebSocket协议

首先,WebSocket也是一种应用层的协议,其次,他是一种持久化协议,他的底层借用了部分Http协议的内容来实现其功能,但和Http协议并没有什么关系;他的底层仍采用TCP协议进行实现。

他解决了Http协议的三大缺陷

2.1 为什么会出现WebSocket协议

以前客户端想知道服务端的处理进度,要不停地使用 Ajax 进行轮询==(Ajax轮询),让浏览器隔个几秒就向服务器发一次请求,这对服务器压力较大。另外一种轮询就是采用long poll==的方式,这就跟打电话差不多,没收到消息就一直不挂电话,也就是说,客户端发起连接后,如果服务端没消息,就一直不返回 Response 给客户端,当服务端返回一次响应后,客户端就再次建立一次阻塞连接。由此可见,这种轮询方式的连接阶段一直是阻塞的。

2.2 WebSocket请求的方式

(1)域名解析,将url转化成ip

(2)建立TCP连接,一次握手

(3)Web浏览器向Web服务端发送WebSocket请求报文

(4)服务器响应WebSocket请求

(5)Web服务端也可以向Web浏览器发送WebSocket请求报文

(6)客户端响应服务器发来的请求

(7)某一方断开TCP连接

2.3 WebSocket请求的特点

WebSocket 解决了 HTTP 的三个难题。当服务器完成协议升级后( HTTP -> WebSocket ),服务端可以主动推送信息给客户端,解决了轮询造成的同步延迟问题。由于 WebSocket 只需要一次 握手,服务端就能一直与客户端保持通信,直到关闭连接,这样就解决了服务器需要反复解析 HTTP 协议,减少了资源的开销。

2.4 聊天室举例

例如,现在你想开发一个聊天室的网页程序,当一个用户登录进去后,发送一条信息,这条信息需要被所有聊天室内的成员看到。

由于每个成员都是一个独立的客户端,每个人都能发送信息到服务端,不能将每个人发送的聊天记录都存储到服务器的数据库中,所以将所有人发送的新信息发送到服务器短暂存储发送至聊天室后,在由客户端将聊天内容存储至本地。

我画了一个图,方便大家理解

在这里插入图片描述

当一个用户发送了一条新的信息后,服务器接收到将其存储在服务器中,接受每个客户端的询问,是否有新的消息,服务端就会将新发送的消息响应给所有发送请求的客户端,客户端将接受到的聊天信息渲染在自己的聊天室中,然后将信息存储到本地中。

服务端在存储到一定程度时,会将聊天内容删除,节省服务器端的存储资源。每个用户在打开聊天室后,本地会将存储的聊天信息重新渲染至聊天室中。

==这样产生的问题:==新加入的用户有可能因为服务端存储的聊天信息已经被删除而接收不到信息。


技术选择

如果采用ajax轮询或long poll方式,会产生同步延迟问题,还会极大占用资源。一个用户发送的新信息,会收到请求和接收到响应时间差的影响,倘若服务器此时要同时受理许多客户端的请求,那么很有可能会延迟或拒绝用户的请求,用户将收不到新的信息;另外,每次用户发送的Http请求都要建立一次新的TCP连接,这将极大的消耗宝贵的网络资源。

如果采用webSocket方式,在建立一次连接后,由于是全双工通信,一旦有新消息,客户端不需要每隔一段时间就进行一次请求,只需要等待服务端发送过来新的信息即可。当一个用户关闭聊天室,就代表了关闭了连接,由此可见,选择WebSocket协议是最划算的实现方式

2.4.1 前端基于websocket对象

WebSockets 是一种先进的技术。它能够在用户的浏览器和服务器之间关上交互式通信会话。应用此API,您能够向服务器发送音讯并接管事件驱动的响应,而无需通过轮询服务器的形式以取得响应。

下面我们通过这个案例快速了解前端websocket对象

head标签引入vue,element-ui样式库

<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.js"></script>
    <!-- 引入样式 -->
    <link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
    <!-- 引入组件库 -->
    <script src="https://unpkg.com/element-ui/lib/index.js"></script>

body内元素

<div id="app">
        <input type="text" placeholder="请输入姓名" v-model="username">
        <button @click="connect" v-if="username!=''">连接ws://localhost:80/chat</button>
        <span v-bind:style="{backgroundColor:bgColor}" style="color: aliceblue;padding: 5px;">{{tip}}</span>
        <br><br>
        <textarea name="" id="text" cols="30" rows="10" placeholder="请输入内容..." v-model="content"></textarea>
        <button @click="send">发送内容</button>
        <button @click="close">关闭websocket连接</button>
        <div>服务器发送来的内容:</div>
        <div>{{content_back}}</div>
    </div>

script标签内

  var websocket = null;
    var myVue = new Vue({
        el:'#app',
        data:{
            username:'',
            content:'',
            content_back:'',
            bgColor:'#ccc',
            tip:'未连接',
        },
        methods: {
            connect:function(){
                var that = this;
                //建立websocket对象,并直接发出连接申请,可以带传递的参数
                websocket = new WebSocket("ws://localhost:80/chat/"+this.username);
                //当建立起连接时要做的事情
                websocket.onopen = function(){
                    that.bgColor = "green";
                    that.tip = "连接中";
                    that.$message({
                        showClose: true,
                        message: '成功建立websocket连接',
                        type: 'success',
                        duration:1000
                    });
                }
                //当收到服务器发送来的消息时要做的事情
                websocket.onmessage = function(res){
                    that.content_back = res.data;
                }
                //当websocket断开时做的事情【任意一方断开连接】
                websocket.onclose = function(){
                    that.$message({
                        showClose:true,
                        message: '成功关闭websocket连接',
                        type: 'success',
                        duration:1000
                    });
                    websocket=null;
                }
                //当发生错误时要做的事情
                websocket.onerror = function(){
                    
                }
            },
            close:function(){
                if(websocket!=null){
                    //浏览器主动关闭websocket连接
                    websocket.close();
                    this.bgColor="#ccc";
                    this.tip = "未连接";
                }
            },
            send:function(){
                if(websocket!=null && this.content!=''){
                    //如果内容不为空,websocket成功建立连接,便发送信息到服务器
                    websocket.send(this.content);
                    this.$message({
                        showClose: true,
                        message: '信息发送成功',
                        type: 'success',
                        duration:1000
                    });
                }else if(websocket==null){
                    this.$message({
                        showClose: true,
                        message: '未建立websocket连接!',
                        type: 'error',
                        duration:1000
                    });
                }else{
                    this.$message({
                        showClose: true,
                        message: '发送信息不能为空!',
                        type: 'error',
                        duration:1000
                    });
                }
            }
        }
    })

上面写好后的前端页面长这样
在这里插入图片描述

用户只有输入姓名后才能建立连接,当服务器关于websocket连接的服务写好后,就可以建立连接了,输入姓名后建立连接长这样

在这里插入图片描述

此时的f12打开后不再是一对一的请求和响应了,而变成了下面这样

在这里插入图片描述

红框圈住的部分就是比较重要的部分,请读者自行了解。

在这里插入图片描述

2.4.2 后端基于springboot操作websocket

第一步需要导入下面这个依赖

<!--   WebSocket启动器   -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>

如果不配置端口号,那么默认的端口就是8080,但是我这里的前端页面请求的路径是80端口,所以我配置了一下端口号,在application.properties文件中书写

server.port=80

然后创建一个config类,这里的config类主要配置ServerEndpointExporter类,这个类能够扫描@ServerEndPoint注解,不配置的话注解将无法生效

package com.hr.config;

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

/**
 * @author
 * @description
 */
@Configuration
public class MyWebSocketConfig {

    @Bean
//    注入ServerEndpointExporter对象,此对象能够扫描@ServerEndPoint注解
    public ServerEndpointExporter serverEndpointExporter(){
        return new ServerEndpointExporter();
    }
}

下面的这个类是主要的类,也是接受客户端发起ws连接主要的服务类

ChatEndPoint

package com.hr.ws;

import lombok.SneakyThrows;
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.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @author
 * @description 每当一个用户建立websocket连接时,服务器都会在内存中创建一个对应的ChatEndPoint对
 *  			象来提供服务,每个对象的session是唯一的
 */
// 前端传递过来的参数需要用rest风格去接收
@ServerEndpoint("/chat/{username}")
@Component
public class ChatEndPoint {

    //并发情况下的安全map,建议用这个map
    private static Map<String,ChatEndPoint> clients = new ConcurrentHashMap<>();
    private Session session;
    private String username;



    //通过@PathParam注解来获取用户发送的请求参数,第二个session对象指代建立连接的用户端,通过
    //这个session对象可以识别客户端身份并可以给它发送信息
    @OnOpen
    public void onOpen(@PathParam("username") String username, Session session, EndpointConfig config){
        System.out.println("用户"+username+"已经建立起websocket连接");
        this.username = username;
        this.session = session;

        clients.put(username,this);
    }
    //当收到客户端发来的消息时会做的事情
    @OnMessage
    public void OnMessage(String message,Session session){
        System.out.println("====================================");
//        异步发送,当收到客户端发来的消息时就告诉客户端一声
        session.getAsyncRemote().sendText("已收到"+username+"发来的:"+message);
//        try {
//            session.getBasicRemote().sendText("已收到您发来的:"+message+"消息");
//        } catch (IOException e) {
//            e.printStackTrace();
//        }
        for (String key : clients.keySet()) {
            ChatEndPoint value = clients.get(key);
            System.out.println("{username="+key+",item="+value+"}");
            System.out.println("session="+value.session);
            System.out.println("username="+value.username);
            System.out.println("====================================");
        }
    }
    //当关闭websocket连接时要做的事情
    @OnClose
    @SneakyThrows//lombok的注解,用来过滤掉异常
    public void onClose(Session session) throws IOException {
        session.close();
    }

}

然后直接启动springboot即可

2.5 总结

​ 没有其他能像 WebSocket 一样实现双向通信的技术了,迄今为止,大部分开发者还是使用 Ajax 轮询来实现,但这是个不太优雅的解决办法,WebSocket 虽然用的人不多,可能是因为协议刚出来的时候有安全性的问题以及兼容的浏览器比较少,但现在都有解决。如果你有这些需求可以考虑使用 WebSocket:

1 、多个用户之间进行交互;

2、需要频繁地向服务端请求更新数据。

比如弹幕、消息订阅、多玩家游戏、协同编辑、股票基金实时报价、视频会议、在线教育等需要高实时的场景。

;