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的高并发,可以考虑。

posted @ 2020-02-28 00:44  胖墩哥  阅读(2514)  评论(0编辑  收藏  举报