zoukankan      html  css  js  c++  java
  • WebSocket的介绍

    WebSocket

    关于websocket的一个小demo,是聊天室,源代码地址:

    聊天室的github源代码

    websocket的背景

    现在,很多网站为了实现推送技术,所用的技术都是 Ajax 轮询或者long poll 。这种传统的模式带来很明显的缺点,即浏览器需要不断的向服务器发出请求,然而HTTP请求可能包含较长的头部,其中真正有效的数据可能只是很小的一部分,显然这样会浪费很多的带宽等资源。

    websocket的特点

    • WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。能更好的节省服务器资源和带宽,并且能够更实时地进行通讯。
    • WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。
    • 在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
    • 浏览器通过 JavaScript 向服务器发出建立 WebSocket 连接的请求,连接建立以后,客户端和服务器端就可以通过 TCP 连接直接交换数据。

    Ajax轮询

    ajax轮询 的原理非常简单,让浏览器隔个几秒就发送一次请求,询问服务器是否有新信息。
    场景再现:
    客户端:啦啦啦,有没有新信息(Request)
    服务端:没有(Response)
    客户端:啦啦啦,有没有新消息(Request)
    服务端:好啦好啦,有啦给你。(Response)
    客户端:啦啦啦,有没有新消息(Request)
    服务端:。。。。。没。。。。没。。。没有(Response) ---- loop

    long poll

    long poll 其实原理跟 ajax轮询 差不多,都是采用轮询的方式,不过采取的是阻塞模型(一直打电话,没收到就不挂电话),也就是说,客户端发起连接后,如果没消息,就一直不返回Response给客户端。直到有消息才返回,返回完之后,客户端再次建立连接,周而复始。
    场景再现:
    客户端:啦啦啦,有没有新信息,没有的话就等有了才返回给我吧(Request)
    服务端:额。。 等待到有消息的时候。。来 给你(Response)
    客户端:啦啦啦,有没有新信息,没有的话就等有了才返回给我吧(Request) -loop

    从上面可以看出其实这两种方式,都是在不断地建立HTTP连接,然后等待服务端处理,可以体现HTTP协议的另外一个特点,被动性。何为被动性呢,其实就是,服务端不能主动联系客户端,只能有客户端发起。

    从上面很容易看出来,不管怎么样,上面这两种都是非常消耗资源的。
    ajax轮询 需要服务器有很快的处理速度和资源。(速度)
    long poll 需要有很高的并发,也就是说同时接待客户的能力。(场地大小)
    所以ajax轮询 和long poll 缺点非常明显。

    websocket 与Http的关系

    WebSocket是HTML5出的东西(协议),也就是说HTTP协议没有变化,或者说没关系,但HTTP是不支持持久连接的(长连接,循环连接的不算)

    首先HTTP有1.1和1.0之说,也就是所谓的keep-alive,把多个HTTP请求合并为一个,但是Websocket其实是一个新协议,跟HTTP协议基本没有关系,只是为了兼容现有浏览器的握手规范而已,也就是说它是HTTP协议上的一种补充,可以通过这样一张图理解

    首先,相对于HTTP这种非持久的协议来说,Websocket是一个持久化的协议。

    • HTTP还是一个无状态协议。通俗的说就是,服务器因为每天要接待太多客户了,是个健忘鬼,你一挂电话,他就把你的东西全忘光了,把你的东西全丢掉了。你第二次还得再告诉服务器一遍。
    • HTTP的生命周期通过Request来界定,也就是一个Request 一个Response,那么HTTP1.0,这次HTTP请求就结束了。
    • 在HTTP1.1中进行了改进,使得有一个keep-alive,也就是说,在一个HTTP连接中,可以发送多个Request,接收多个Response。
    • 但是 Request = Response , 在HTTP中永远是这样,也就是说一个request只能有一个response。而且这个response也是被动的,不能主动发起。

    所以在这种情况下出现了,Websocket出现了。他解决了HTTP的难题。

    websocket协议建立

    WebSocket并不是全新的协议,而是利用了HTTP协议来建立连接。我们来看看WebSocket连接是如何创建的。

    首先,WebSocket连接必须由浏览器发起,因为请求协议是一个标准的HTTP请求,格式如下:

    GET ws://localhost:3000/ws/chat HTTP/1.1
    Host: localhost
    Upgrade: websocket
    Connection: Upgrade
    Origin: http://localhost:3000
    Sec-WebSocket-Key: client-random-string
    Sec-WebSocket-Version: 13
    

    该请求和普通的HTTP请求有几点不同:

    1. GET请求的地址不是类似/path/,而是以ws://开头的地址;
    2. 请求头Upgrade: websocketConnection: Upgrade表示这个连接将要被转换为WebSocket连接;
    3. Sec-WebSocket-Key是用于标识这个连接,并非用于加密数据;
    4. Sec-WebSocket-Version指定了WebSocket的协议版本。

    服务器如果接受该请求,就会返回如下响应:

    HTTP/1.1 101 Switching Protocols
    Upgrade: websocket
    Connection: Upgrade
    Sec-WebSocket-Accept: server-random-string
    

    该响应代码101表示本次连接的HTTP协议即将被更改,更改后的协议就是Upgrade: websocket指定的WebSocket协议。

    这里开始就是HTTP最后负责的区域了,告诉客户端,已经成功切换协议啦~

    websocket的客户端简单实例

    var ws = new WebSocket("wss://localhost:8080/ws/asset");
    
    //连接建立成功调用的方法
    ws.onopen = function(evt) { 
      console.log("Connection open ..."); 
      //向服务端发送消息
      ws.send("Hello WebSockets!");
    };
    //收到服务端消息后调用的方法
    ws.onmessage = function(evt) {
      console.log( "Received Message: " + evt.data);
      ws.close();
    };
    //连接关闭调用的方法
    ws.onclose = function(evt) {
      console.log("Connection closed.");
    };
    

    webSocket的服务端简单实例

    @ServerEndpoint(value = "/ws/asset")
    @Component
    @Slf4j
    public class WebSocketServer {
        
    
        private static AtomicInteger OnlineCount = new AtomicInteger(0);
        // concurrent包的线程安全Set,用来存放每个客户端对应的Session对象。
        private static CopyOnWriteArraySet<Session> SessionSet = new CopyOnWriteArraySet<Session>();
    
    
        /**
         * 连接建立成功调用的方法
         */
        @OnOpen
        public void onOpen(Session session) {
            SessionSet.add(session);
            int cnt = OnlineCount.incrementAndGet(); // 在线数加1
            log.info("有连接加入,当前连接数为:{}", cnt);
            SendMessage(session, "连接成功");
        }
    
        /**
         * 连接关闭调用的方法
         */
        @OnClose
        public void onClose(Session session) {
            SessionSet.remove(session);
            int cnt = OnlineCount.decrementAndGet();
            log.info("有连接关闭,当前连接数为:{}", cnt);
        }
    
        /**
         * 收到客户端消息后调用的方法
         *
         * @param message
         *            客户端发送过来的消息
         */
        @OnMessage
        public void onMessage(String message, Session session) {
            log.info("来自客户端的消息:{}",message);
            System.out.println(session.toString());
            SendMessage(session, "收到消息,消息内容:"+message+session.getId());
    
        }
    
        /**
         * 出现错误
         * @param session
         * @param error
         */
        @OnError
        public void onError(Session session, Throwable error) {
            log.error("发生错误:{},Session ID: {}",error.getMessage(),session.getId());
            error.printStackTrace();
        }
    
        /**
         * 发送消息,实践表明,每次浏览器刷新,session会发生变化。
         * @param session
         * @param message
         */
        public static void SendMessage(Session session, String message) {
            try {
                session.getBasicRemote().sendText(message);
            } catch (IOException e) {
                log.error("发送消息出错:{}", e.getMessage());
                e.printStackTrace();
            }
        }
    
        /**
         * 群发消息
         * @param message
         * @throws IOException
         */
        public static void BroadCastInfo(String message) throws IOException {
            for (Session session : SessionSet) {
                if(session.isOpen()){
                    SendMessage(session, message);
                }
            }
        }
    
        /**
         * 指定Session发送消息
         * @param sessionId
         * @param message
         * @throws IOException
         */
        public static void SendMessage(String message,String sessionId) throws IOException {
            Session session = null;
            for (Session s : SessionSet) {
                if(s.getId().equals(sessionId)){
                    session = s;
                    break;
                }
            }
            if(session!=null){
                SendMessage(session, message);
            }
            else{
                log.warn("没有找到你指定ID的会话:{}",sessionId);
            }
        }
    }
    
    

    websocket的心跳机制

    websockt心跳机制,不得不说很形象;那何为心跳机制,就是表明client与server的连接是否还在的检测机制;

    如果不存在检测,那么网络突然断开,造成的后果就是client、server可能还在傻乎乎的发送无用的消息,浪费了资源;怎样检测呢?原理就是定时向server发送消息,如果接收到server的响应就表明连接依旧存在;
    这个心跳机制在分布式中也很常见,

    demo

    聊天室demo

    (1)Client:客户端说明

    ​ 客户端的代码主要是使用H5的WebSocket进行实现,在前端网页中使用WebSocket进行连接服务端,然后建立Socket连接进行通讯。

    (2)Server:服务端说明

    ​ 服务端主要是建立多个客户端的关系,进行消息的中转等。客户端成功连接到服务端之后,就可以通过建立的通道进行发送消息到服务端,服务端接收到消息之后在群发给所有的客户端。

    (3)客户端和服务端连接

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

    (4)客户端和服务端怎么发送消息?

    客户端可以使用webSocket提供的send()方法,如下代码:

    var message = document.getElementById('text').value;  
    websocket.send(message);  
    

    服务端怎么发送消息呢?主要是使用在成功建立连接的时候,创建的Session对象进行发送,如下代码:

    session.getAsyncRemote().sendText("恭喜您成功连接上WebSocket");  
    

    (5)客户端和服务端怎么接受消息?

    客户端接收消息消息使用的是websocket的onmessage回调方法,如下代码:

    websocket.onmessage = function(event) {  
               //文本信息直接显示,如果是json信息,需要转换下在显示.  
           var data = event.data;  
           document.getElementById('message').innerHTML += data;  
    }  
    

    服务端:

    @OnMessage  
    public void onMessage(String message, Session session) {  
            System.out.println("来自客户端的消息:" + message);  
    }
    
    

    (6)群聊原理(群发消息)

    服务端在和客户端建立连接的时候,会创建一个webSocket对象,我们会将每个连接创建的对象进行报错到一个列表中,比如:CopyOnWriteArraySet(这是线程安全的);在要进行群发的时候,编写我们的列表对象进行群发消息。

    (7)单聊原理(一对一消息)

    聊的时候,就无需遍历列表,而是需要知道发送者和接受者各自的Session对象,这个Session对象怎么获取呢?Session可以获取到sessionId,发送者在发送消息的时候,携带接收消息的sessionId,那么问题就演变成了:发送者怎么知道接受者的sessionId,那就是加入一个在线用户列表即可,在线用户列表中有用户的基本信息,包括sessionId。

    websocket的实时推送

    对比聊天室的demo,不同之处在于,客户端连入服务器时候,会开启一个线程,在线程中对客户端进行推送数据。

    关键代码:

    /**
         * 接收到消息
         *
         * @param text
         */
        @OnMessage
        public void onMsg(Session session,@PathParam("param") String param) throws IOException {
            //记录客户端
            webSocketMaps.put(session, param);
            //实例化工作任务
            Operater operater =new Operater(session,param);
            //开启线程
            Thread thread = new Thread(operater);
            thread.start();
            logger.info("发送线程启动成功");
        }
    
    

    目前业务还不是很复杂,后期功能添加时候,再进行扩展,关于这个实时推送,大概开了50个窗口就连接失败了。关于websocket的高并发,可以考虑。

  • 相关阅读:
    Attach Files to Objects 将文件附加到对象
    Provide Several View Variants for End-Users 为最终用户提供多个视图变体
    Audit Object Changes 审核对象更改
    Toggle the WinForms Ribbon Interface 切换 WinForms 功能区界面
    Change Style of Navigation Items 更改导航项的样式
    Apply Grouping to List View Data 将分组应用于列表视图数据
    Choose the WinForms UI Type 选择 WinForms UI 类型
    Filter List Views 筛选器列表视图
    Make a List View Editable 使列表视图可编辑
    Add a Preview to a List View将预览添加到列表视图
  • 原文地址:https://www.cnblogs.com/jimlau/p/12375447.html
Copyright © 2011-2022 走看看