WebSocket原理及模板(附java源码)

 

原理方面,参考其他大牛博客。

参考链接:

1、https://www.zhihu.com/question/20215561

2、http://www.ruanyifeng.com/blog/2017/05/websocket.html

目录

先说一下Http

websocket优点

websocket协议建立过程

代码部分

后端

前端代码

源码目录


先说一下Http

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

有交集,但是并不是全部。
另外Html5是指的一系列新的API,或者说新规范,新技术。Http协议本身只有1.0和1.1,而且跟Html本身没有直接关系。。
通俗来说,你可以用HTTP协议传输非Html数据,就是这样=。=
再简单来说,层级不一样
Htpp是一个非持久化协议。

HTTP的生命周期通过Request来界定,也就是一个Request 一个Response,那么HTTP1.0,这次HTTP请求就结束了。
在HTTP1.1中进行了改进,使得有一个keep-alive,也就是说,在一个HTTP连接中,可以发送多个Request,接收多个Response。
但是请记住 Request = Response , 在HTTP中永远是这样,也就是说一个request只能有一个response。而且这个response也是被动的,不能主动发起。

那么问题来了,我要想服务器这边发生了什么变化,怎么通知客户端呢?http轮询问是可以做到的。但是,频繁的建立连接删除连接非常的消耗资源,而且并不能达到实时通知,因为你轮询总会有事件间隔。

websocket就是解决这种问题的一种服务器推送技术。

websocket优点

websocket是一个全双通的协议,会在服务器与客户端建立一个长连接,相互之间可以通讯。

WebSocket是HTML5下一种新的协议。它实现了浏览器与服务器全双工通信,能更好的节省服务器资源和带宽并达到实时通讯的目的。它与HTTP一样通过已建立的TCP连接来传输数据,但是它和HTTP最大不同是:

  • WebSocket是一种双向通信协议。在建立连接后,WebSocket服务器端和客户端都能主动向对方发送或接收数据,就像Socket一样;
  • WebSocket需要像TCP一样,先建立连接,连接成功后才能相互通信。

它的特点:

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

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

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

(4)可以发送文本,也可以发送二进制数据。

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

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

例如:

ws://example.com:80/some/path

传统HTTP客户端与服务器请求响应模式如下图所示:

WebSocket模式客户端与服务器请求响应模式如下图:

上图对比可以看出,相对于传统HTTP每次请求-应答都需要客户端与服务端建立连接的模式,WebSocket是类似Socket的TCP长连接通讯模式。一旦WebSocket连接建立后,后续数据都以帧序列的形式传输。在客户端断开WebSocket连接或Server端中断连接前,不需要客户端和服务端重新发起连接请求。在海量并发及客户端与服务器交互负载流量大的情况下,极大的节省了网络带宽资源的消耗,有明显的性能优势,且客户端发送和接受消息是在同一个持久连接上发起,实时性优势明显。

websocket协议建立过程

Websocket握手

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
Origin: http://example.com

其中

Upgrade: websocket
Connection: Upgrade

这个就是Websocket的核心了,告诉Apache、Nginx等服务器:注意啦,窝发起的是Websocket协议,快点帮我找到对应的助理处理~不是那个老土的HTTP。

Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13

首先,Sec-WebSocket-Key 是一个Base64 encode的值,这个是浏览器随机生成的,告诉服务器:泥煤,不要忽悠窝,我要验证尼是不是真的是Websocket助理。
然后,Sec_WebSocket-Protocol 是一个用户定义的字符串,用来区分同URL下,不同的服务所需要的协议。简单理解:今晚我要服务A,别搞错啦~
最后,Sec-WebSocket-Version 是告诉服务器所使用的Websocket Draft(协议版本),在最初的时候,Websocket协议还在 Draft 阶段,各种奇奇怪怪的协议都有,而且还有很多期奇奇怪怪不同的东西,什么Firefox和Chrome用的不是一个版本之类的,当初Websocket协议太多可是一个大难题。。不过现在还好,已经定下来啦~大家都使用的一个东西~ 脱水:服务员,我要的是13岁的噢→_→

然后服务器会返回下列东西,表示已经接受到请求, 成功建立Websocket啦!
 

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
Sec-WebSocket-Protocol: chat

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

Upgrade: websocket
Connection: Upgrade

依然是固定的,告诉客户端即将升级的是Websocket协议,而不是mozillasocket,lurnarsocket或者shitsocket。
然后,Sec-WebSocket-Accept 这个则是经过服务器确认,并且加密过后的 Sec-WebSocket-Key。服务器:好啦好啦,知道啦,给你看我的ID CARD来证明行了吧。。
后面的,Sec-WebSocket-Protocol 则是表示最终使用的协议。

至此,HTTP已经完成它所有工作了,接下来就是完全按照Websocket协议进行了。

代码部分

后端

这是后台接受请求的代码,当然这里我写成了一个子包的形式。需要的时候,在你的项目里面引入一下即可使用。

package com.haogenmin.websocket.service.impl;

import com.alibaba.fastjson.JSONObject;
import com.haogenmin.websocket.manager.IWebSocketManager;
import com.haogenmin.websocket.manager.impl.WebSocketManager;
import com.haogenmin.websocket.service.IWebSocketServer;
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;

/**
 * @author :HaoGenmin
 * @Title :WebSocketServer
 * @date :Created in 2020/6/13 16:41
 * @description :websocket
 */
public abstract class WebSocketServer implements IWebSocketServer {

    private Logger logger = LoggerFactory.getLogger(WebSocketServer.class);

    private IWebSocketManager webSocketManager;

    //与某个客户端的连接会话,需要通过它来给客户端发送数据
    private Session session;

    //用户标记
    private String userId;


    @OnOpen
    public void open(@PathParam("userId") String userId, Session session) {
        // 添加初始化操作
        logger.info("---初始化----userId:" + userId);
        this.session = session;

        //获取当前登录用户的id
        this.userId = userId;

        this.webSocketManager = WebSocketManager.getInstance();

        webSocketManager.addWebSocketServer(userId, this);     //加入map中
    }


    @OnMessage
    public abstract void getMessage(String message, Session session);


    @OnClose
    public void close() {
        // 添加关闭会话时的操作
        webSocketManager.removeWebSocketServer(userId);
        logger.info("用户" + userId + "的连接关闭!当前在线人数为" + webSocketManager.getOnlineCount());
    }


    @OnError
    public void error(Throwable t) {
        logger.info("websocket发生错误");
        t.printStackTrace();
    }


    public synchronized void sendMessageAsync(String message) {
        this.session.getAsyncRemote().sendText(message);//非阻塞式的
    }


    public synchronized void sendMessage(String message) {
        try {
            this.session.getBasicRemote().sendText(message);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public synchronized void sendMessage(String message, boolean isLast) {
        try {
            this.session.getBasicRemote().sendText(message, isLast);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }


    public Session getSession() {
        return session;
    }


    public String getUserId() {
        return userId;
    }


}

 这里是写了一个管理类,可以向所有的连接或者个别连接发送请求。

package com.haogenmin.websocket.manager.impl;

import com.haogenmin.websocket.manager.IWebSocketManager;
import com.haogenmin.websocket.service.IWebSocketServer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @author :HaoGenmin
 * @Title :WebSocketManager
 * @date :Created in 2020/6/13 16:57
 * @description:
 */
public class WebSocketManager implements IWebSocketManager {
    private static final WebSocketManager instance = new WebSocketManager();

    private WebSocketManager() {
    }

    public static WebSocketManager getInstance() {
        return instance;
    }

    private Logger logger = LoggerFactory.getLogger(WebSocketManager.class);

    // 存放客户端的socket
    private ConcurrentHashMap<String, IWebSocketServer> webSocketMap = new ConcurrentHashMap<String, IWebSocketServer>();

    @Override
    public int getOnlineCount() {
        return webSocketMap.size();
    }

    @Override
    public void sendMessageToAll(String message) {
        for (Map.Entry<String, IWebSocketServer> entry : webSocketMap.entrySet()) {
            IWebSocketServer webSocketServer = entry.getValue();
            if (webSocketServer.getSession().isOpen()) {
                webSocketServer.sendMessageAsync(message);
            } else {
                logger.info("与账号" + entry.getKey() + "的websocket连接已经关闭,执行删除连接操作!");
                webSocketMap.remove(entry.getKey(), entry.getValue());
            }
        }

    }

    @Override
    public void sendMessageToOne(String userId, String message) {
        IWebSocketServer webSocketServer = webSocketMap.get(userId);
        if (webSocketServer == null) {
            logger.info("没有与账号" + userId + "的websocket连接!");
            return;
        }
        if (webSocketServer.getSession().isOpen()) {
            webSocketServer.sendMessageAsync(message);
        } else {
            logger.info("与账号" + userId + "的websocket连接已经关闭,执行删除连接操作!");
            webSocketMap.remove(userId);
        }
    }

    @Override
    public void addWebSocketServer(String userId, IWebSocketServer webSocketServer) {
        webSocketMap.put(userId, webSocketServer);
    }

    @Override
    public void removeWebSocketServer(String userId) {
        webSocketMap.remove(userId);

    }
}

前端代码

前端的一个样例,通过reconnecting-websocket.js包实现了断线重连。这里给出GitHub上的地址,相关设置可参考使用文档。

https://github.com/pladaria/reconnecting-websocket

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <title>简易聊天Demo</title>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1, maximum-scale=1, user-scalable=no">
    <link href="https://cdn.bootcss.com/bootstrap/3.3.2/css/bootstrap.min.css" rel="stylesheet">
    <style type="text/css">
        <!--
        html, body {
            min-height: 100%;
        }

        body {
            margin: 0;
            padding: 0;
            width: 100%;
            font-family: "Microsoft Yahei", sans-serif, Arial;
        }

        .container {
            text-align: center;
        }

        .title {
            font-size: 16px;
            color: rgba(0, 0, 0, 0.3);
            position: fixed;
            line-height: 30px;
            height: 30px;
            left: 0px;
            right: 0px;
            background-color: white;
        }

        .content {
            background-color: #f1f1f1;
            border-top-left-radius: 6px;
            border-top-right-radius: 6px;
            margin-top: 30px;
        }

        .content .show-area {
            text-align: left;
            padding-top: 8px;
            padding-bottom: 168px;
        }

        .content .show-area .message {
            width: 70%;
            padding: 5px;
            word-wrap: break-word;
            word-break: normal;
        }

        .content .write-area {
            position: fixed;
            bottom: 0px;
            right: 0px;
            left: 0px;
            background-color: #f1f1f1;
            z-index: 10;
            width: 100%;
            height: 160px;
            border-top: 1px solid #d8d8d8;
        }

        .content .write-area .send {
            position: relative;
            top: -28px;
            height: 28px;
            border-top-left-radius: 55px;
            border-top-right-radius: 55px;
        }

        .content .write-area #name {
            position: relative;
            top: -20px;
            line-height: 28px;
            font-size: 13px;
        }

        -->
    </style>
</head>
<body>
<div class="container">
    <div class="title">简易聊天demo</div>
    <div class="content">
        <div class="show-area"></div>
        <div class="write-area">
            <div>
                <button class="btn btn-default send">发送</button>
            </div>
            <div><input name="name" id="name" type="text" placeholder="input your name"></div>
            <div>
                <textarea name="message" id="message" cols="38" rows="4" placeholder="input your message..."></textarea>
            </div>
        </div>
    </div>
</div>

<script src="http://libs.baidu.com/jquery/1.9.1/jquery.min.js"></script>
<script src="https://cdn.bootcss.com/bootstrap/3.3.2/js/bootstrap.min.js"></script>
<script src="/js/reconnecting-websocket.js"></script>
<script>
    $(function () {
        var r=Math.round(Math.random()*100);
        var wsurl = 'ws://localhost:8080/myWebsocket/'+r;
        var websocket;
        var i = 0;
        if (window.WebSocket) {
            websocket = new ReconnectingWebSocket(wsurl);
            websocket.debug = true;
            websocket.reconnectInterval = 3000;
            websocket.maxReconnectAttempts = 20;

            //连接建立
            websocket.onopen = function (event) {
                console.log("Connected to WebSocket server.");
                $('.show-area').append('<p class="bg-info message"><i class="glyphicon glyphicon-info-sign"></i>Connected to WebSocket server!</p>');
            }
            //收到消息
            websocket.onmessage = function (event) {
                // var msg = JSON.parse(event.data); //解析收到的json消息数据
                // var umsg = msg.message; //消息文本
                // var uname = msg.name; //发送人
                // var date = msg.date;
                i++;
               // $('.show-area').append('<p class="bg-success message"><i class="glyphicon glyphicon-user"></i><a name="' + i + '"></a><span class="label label-primary">' +"<"+date+">"+ uname + ' : </span>' + umsg + '</p>');
                $('.show-area').append('<p class="bg-success message"><i class="glyphicon glyphicon-user"></i><a name="' + i + '"></a><span class="label label-primary">'+ ' : </span>' + event.data + '</p>');

                window.location.hash = '#' + i;
            }

            //发生错误
            websocket.onerror = function (event) {
                i++;
                console.log("Connected to WebSocket server error");
                $('.show-area').append('<p class="bg-danger message"><a name="' + i + '"></a><i class="glyphicon glyphicon-info-sign"></i>Connect to WebSocket server error.</p>');
                window.location.hash = '#' + i;
            }

            //连接关闭
            websocket.onclose = function (event) {
                i++;
                console.log('websocket Connection Closed. ');
                $('.show-area').append('<p class="bg-warning message"><a name="' + i + '"></a><i class="glyphicon glyphicon-info-sign"></i>websocket Connection Closed.</p>');
                window.location.hash = '#' + i;
            }

            function send() {
            	i++;
                var name = 10086;
                var message = $('#message').val();
                if (!message) {
                    alert('发送消息不能为空!');
                    return false;
                }
                var msg = {
                    message: message,
                    name: name
                };
                try {
                    websocket.send(JSON.stringify(msg));
                    $('.show-area').append('<p class="bg-warning message"><i class="glyphicon glyphicon-user"></i><a name="' + i + '"></a><span class="label label-primary">' + name + ' : </span>' + message + '</p>');
                    window.location.hash = '#' + i;
                } catch (ex) {
                    console.log(ex);
                }
            }

            //按下enter键发送消息
            $(window).keydown(function (event) {
                if (event.keyCode == 13) {
                    console.log('user enter');
                    send();
                }
            });

            //点发送按钮发送消息
            $('.send').bind('click', function () {
                send();
            });

        }
        else {
            alert('该浏览器不支持web socket');
        }

    });
</script>
</body>
</html>

源码目录

使用方法说明:

第一步:注意,自己的项目的包也要扫描,不然自己写的东西不会被spring纳入容器。

第二步:

第三步:发送消息给客户端

        WebSocketManager.getInstance().sendMessageToAll("hello");
        WebSocketManager.getInstance().sendMessageToOne("55","hello");

结束。

如果您觉得对您有帮助的话,请点个赞鼓励一下,感谢。

源码:https://github.com/haogenmin/websocket

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值