SpringBoot+websocket实现实时的点对点或群发通信(内附详细代码实现)

websocket原理

WebSocket用于在Web浏览器和服务器之间进行任意的双向数据传输的一种技术。WebSocket协议基于TCP协议实现,包含初始的握手过程,以及后续的多次数据帧双向传输过程。其目的是在WebSocket应用和WebSocket服务器进行频繁双向通信时,可以使服务器避免打开多个HTTP连接进行工作来节约资源,提高了工作效率和资源利用率。

1. 什么是keep-alive

keep-alive只是客户端的一种建议

我们打开百度首页,进一步查看header。 
这里写图片描述

如图,我们看到请求header中有一行:

Connection:keep-alive
  •  

1.keep-alive是通知服务器,在这个HTTP Request/Responset结束后,不要立即断开TCP连接(注意是TCP连接,和HTTP没有关系),后面的HTTP Request仍然可以通过这个TCP连接继续传送。

但是!这只是个建议,服务器可能不支持,也可能忽略掉这个建议。也可能因为时间太久而直接断开TCP连接

通俗点解释就是:keep-alive只是通知服务器,您先别挂,一会儿可能还有活儿,至于它挂不挂还是看它心情。 
所以,keep-alive只是客户端建议的一种复用TCP连接的方式,至于服务器支持不支持,就由不得客户端了。

keep-alive只是http协议中的一部分 
keep-alive是http协议中的一部分,也即客户端可以主动的发起request到服务器,服务器只能被动的response给客户端。 
我要想实现服务器主动的push消息给客户端,keep-alive是无能无力的。

2. 服务器的消息如何发给客户端

long long ago~ 服务器端要想主动的push消息给客户端(比如网页聊天室消息的即时收发),这是不可能滴。 
但是,我可以使用ajax轮询、long poll 技术造一个服务端给客户端主动push消息的假象。

①:什么是ajax轮询?

ajax轮询的原理非常简单,让浏览器隔个几秒就发送一次请求,询问服务器是否有新信息。

场景再现:

客户端:啦啦啦,有没有新信息(Request) 服务端:没有(Response) 客户端:啦啦啦,有没有新信息(Request) 
服务端:没有。。(Response) 客户端:啦啦啦,有没有新信息(Request) 服务端:你好烦啊,没有啊。。(Response) 
客户端:啦啦啦,有没有新消息(Request) 服务端:好啦好啦,有啦给你。(Response) 
客户端:啦啦啦,有没有新消息(Request) 服务端:。。。。。没。。。。没。。。没有(Response) —- loop

但是这样,有没有发现,大大增加了服务端的负载,并且速度还慢。

②:什么是long poll?

long poll和ajax差不多,原理都是采用轮询的方式。只不过long poll是采取的阻塞的方式去轮询。 
也即客户端发起一个请求连接,这个连接会阻塞住,直到服务端有了消息,才会response给客户端。

场景再现:

客户端:啦啦啦,有没有新信息,没有的话就等有了才返回给我吧(Request) 服务端:额。。 等待到有消息的时候。。来 
给你(Response) 客户端:啦啦啦,有没有新信息,没有的话就等有了才返回给我吧(Request) -loop

long pull 虽然降低了服务器的负载,但是需要服务器有很高的并发能力才可以。 
而目前处理高并发的模型基本都是异步非阻塞的模型(比如nginx)。

③:总结 
ajax轮询、long poll技术虽然都能实现服务端消息的实时通知,但是各有缺点,都不是根本的解决办法。 
计算机界急需一种新的技术去处理这些需求~

3. websocket来啦

既然ajax轮询、long poll都不怎么样。我们发明一种新的协议吧!

Websocket协议解决了服务器与客户端全双工通信的问题。

注:什么是单工、半双工、全工通信? 信息只能单向传送为单工; 信息能双向传送但不能同时双向传送称为半双工; 
信息能够同时双向传送则称为全双工。

websocket协议解析 
wensocket协议包含两部分:一部分是“握手”,一部分是“数据传输”。 
为了便于演示,我们采用swoole建立一个websocket服务器来演示。

第一步、握手

①客户端向服务端发起连接请求 
这里写图片描述

如图,我们在请求服务器的时候,发送了这样的request header。

下面我们就一些比较重要的字段信息进行说明:

Connection:Upgrade #通知服务器协议升级 Upgrade:websocket #协议升级为websocket协议 
Host:0.0.0.0:9501 #升级协议的服务主机:端口地址 
Sec-WebSocket-Key:K8o1cNIxO2pR6inTIDBSgg== #传输给服务器的key 
Sec-WebSocket-Version:13 #websocket协议版本13

Sec-WebSocket-Key有什么用呢? 
客户端将这个key发送给服务器,服务器将这个key进行处理,将处理后的key返回给客户端,客户端根据这个key是否正确来判断是否建立连接。

②:服务端返回握手应答 
这里写图片描述 
如图,我们看到websocket协议状态码是101.

101表示协议切换成功。
  • 1

我们查看websocket的response header。如图: 
这里写图片描述
下面解释下reponse header字段的含义

Connection:Upgrade #协议升级成功 
Sec-WebSocket-Accept:GnoYH/ip/ZMh+a5rX5P/YR6e68g= #服务端处理之后的key 
Sec-WebSocket-Version:13#websocket 协议版本号 
Upgrade:websocket#协议升级为websocket

至此,websocket握手成功!下面就尽情的传输数据吧!

第二步、数据传输

连接建立成功之后,就可以进行数据传输了,不同的客户端有不同的实现方案,在Android、iOS、网页(通过js)都可以向服务器发送数据,当然服务器可以是java、PHP等,客户端和服务器端都有相应的解决方案。

websocket最伟大之处在于服务器和客户端可以在给定的时间范围内的任意时刻,相互推送信息

       浏览器和服务器只需要要做一个握手的动作,在建立连接之后,服务器可以主动传送数据给客户端,客户端也可以随时向服务器发送数据。

第一、WebSocket是HTML5中的协议,支持持久连接;而Http协议不支持持久连接。
第二、首先,Websocket是一个持久化的协议,相对于HTTP这种非持久的协议来说

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

第三、传统的http请求,其并发能力都是依赖同时发起多个TCP连接访问服务器实现的(因此并发数受限于浏览器允许的并发连接数),而websocket则允许我们在一条ws连接上同时并发多个请求,即在A请求发出后A响应还未到达,就可以继续发出B请求。由于TCP的慢启动特性(新连接速度上来是需要时间的),以及连接本身的握手损耗,都使得websocket协议的这一特性有很大的效率提升。
第四、http协议的头部太大,且每个请求携带的几百上千字节的头部大部分是重复的,很多时候可能响应都远没有请求中的header空间大。如此多无效的内容传递是因为无法利用上一条请求内容,websocket则因为复用长连接而没有这一问题。
第五、当需要实现客户端刷新消息时,传统方案往往通过定时ajax请求实现,实际上对多数用户多数时间下这些请求都是无意义了,并且非常占用资源,websocket资源占用就小很多。

2.在springboot程序当中使用websocket,接下来这套程序能够实现的功能有:

下面我们来演示一个springboot+websocket实现的一对一的通信,详细的代码可以在我的github上下载

第一步:导入websocket等依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.zeng</groupId>
    <artifactId>springboot-websocket</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>springboot-websocket</name>
    <description>Demo project for Spring Boot</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.4.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>


        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <!-- 跳转 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency><!-- 跳转 -->

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.35</version>
        </dependency>
        <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>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>


</project>

第二步:websocket的配置bean

package com.zeng.springbootwebsocket;

/**
 * @author shkstart
 * @create 2018-08-09 16:44
 */


import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
@Configuration
public class WebSocketConfig {
    /**
     * 注入ServerEndpointExporter,
     * 这个bean会自动注册使用了@ServerEndpoint注解声明的Websocket endpoint
     * @return
     */
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}

第三步:建立ServerEndpoint的java类,能够接受客户端发送过来的信息和发送给客户端信息。

package com.zeng.springbootwebsocket;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.thymeleaf.expression.Maps;

import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @author shkstart
 * @create 2018-08-10 16:34
 */
@Component
@ServerEndpoint("/websocket/{username}")
public class WebSocket {
    private Logger logger = LoggerFactory.getLogger(this.getClass());
    /**
     * 在线人数
     */
    public static int onlineNumber = 0;
    /**
     * 以用户的姓名为key,WebSocket为对象保存起来
     */
    private static Map<String, WebSocket> clients = new ConcurrentHashMap<String, WebSocket>();
    /**
     * 会话
     */
    private Session session;
    /**
     * 用户名称
     */
    private String username;
    /**
     * 建立连接
     *
     * @param session
     */
    @OnOpen
    public void onOpen(@PathParam("username") String username, Session session)
    {
        onlineNumber++;
        logger.info("现在来连接的客户id:"+session.getId()+"用户名:"+username);
        this.username = username;
        this.session = session;
        logger.info("有新连接加入! 当前在线人数" + onlineNumber);
        try {
            //messageType 1代表上线 2代表下线 3代表在线名单 4代表普通消息
            //先给所有人发送通知,说我上线了
            Map<String,Object> map1 = new HashMap<String,Object>();
            map1.put("messageType",1);
            map1.put("username",username);
            sendMessageAll(JSON.toJSONString(map1),username);

            //把自己的信息加入到map当中去
            clients.put(username, this);
            //给自己发一条消息:告诉自己现在都有谁在线
            Map<String,Object> map2 = new HashMap<String,Object>();
            map2.put("messageType",3);
            //移除掉自己
            Set<String> set = clients.keySet();
            map2.put("onlineUsers",set);
            sendMessageTo(JSON.toJSONString(map2),username);
        }
        catch (IOException e){
            logger.info(username+"上线的时候通知所有人发生了错误");
        }



    }

    @OnError
    public void onError(Session session, Throwable error) {
        logger.info("服务端发生了错误"+error.getMessage());
        //error.printStackTrace();
    }
    /**
     * 连接关闭
     */
    @OnClose
    public void onClose()
    {
        onlineNumber--;
        //webSockets.remove(this);
        clients.remove(username);
        try {
            //messageType 1代表上线 2代表下线 3代表在线名单  4代表普通消息
            Map<String,Object> map1 =new HashMap<String,Object>();
            map1.put("messageType",2);
            map1.put("onlineUsers",clients.keySet());
            map1.put("username",username);
            sendMessageAll(JSON.toJSONString(map1),username);
        }
        catch (IOException e){
            logger.info(username+"下线的时候通知所有人发生了错误");
        }
        logger.info("有连接关闭! 当前在线人数" + onlineNumber);
    }

    /**
     * 收到客户端的消息
     *
     * @param message 消息
     * @param session 会话
     */
    @OnMessage
    public void onMessage(String message, Session session)
    {
        try {
            logger.info("来自客户端消息:" + message+"客户端的id是:"+session.getId());
            JSONObject jsonObject = JSON.parseObject(message);
            String textMessage = jsonObject.getString("message");
            String fromusername = jsonObject.getString("username");
            String tousername = jsonObject.getString("to");
            //如果不是发给所有,那么就发给某一个人
            //messageType 1代表上线 2代表下线 3代表在线名单  4代表普通消息
            Map<String,Object> map1 =new HashMap<String,Object>();
            map1.put("messageType",4);
            map1.put("textMessage",textMessage);
            map1.put("fromusername",fromusername);
            if(tousername.equals("All")){
                map1.put("tousername","所有人");
                sendMessageAll(JSON.toJSONString(map1),fromusername);
            }
            else{
                map1.put("tousername",tousername);
                sendMessageTo(JSON.toJSONString(map1),tousername);
            }
        }
        catch (Exception e){
            logger.info("发生了错误了");
        }

    }


    public void sendMessageTo(String message, String ToUserName) throws IOException {
        for (WebSocket item : clients.values()) {
            if (item.username.equals(ToUserName) ) {
                item.session.getAsyncRemote().sendText(message);
                break;
            }
        }
    }

    public void sendMessageAll(String message,String FromUserName) throws IOException {
        for (WebSocket item : clients.values()) {
            item.session.getAsyncRemote().sendText(message);
        }
    }

    public static synchronized int getOnlineCount() {
        return onlineNumber;
    }

}

第四步:编写简易的前端页面

<!DOCTYPE html>
<html>
<head>
    <title>websocket</title>
    <script type="text/javascript" src="http://ajax.microsoft.com/ajax/jquery/jquery-1.4.min.js"></script>
    <script src="http://cdn.bootcss.com/stomp.js/2.3.3/stomp.min.js"></script>
    <script src="https://cdn.bootcss.com/sockjs-client/1.1.4/sockjs.min.js"></script>
</head>
 
<body>
    <div style="margin: auto;text-align: center">
        <h1>Welcome to websocket</h1>
    </div>
    <br/>
    <div style="margin: auto;text-align: center">
        <select id="onLineUser">
            <option>--所有--</option>
        </select>
        <input id="text" type="text" />
        <button onclick="send()">发送消息</button>
    </div>
    <br>
    <div style="margin-right: 10px;text-align: right">
        <button onclick="closeWebSocket()">关闭连接</button>
    </div>
    <hr/>
    <div id="message" style="text-align: center;"></div>
    <input  type="text" th:value="${username}" id="username" style="display: none" />
</body>
 
 
<script type="text/javascript">
    var webSocket;
    var commWebSocket;
    if ("WebSocket" in window)
    {
        webSocket = new WebSocket("ws://localhost:9030/websocket/"+document.getElementById('username').value);
 
        //连通之后的回调事件
        webSocket.onopen = function()
        {
            //webSocket.send( document.getElementById('username').value+"已经上线了");
            console.log("已经连通了websocket");
            setMessageInnerHTML("已经连通了websocket");
        };
 
        //接收后台服务端的消息
        webSocket.onmessage = function (evt)
        {
            var received_msg = evt.data;
            console.log("数据已接收:" +received_msg);
            var obj = JSON.parse(received_msg);
            console.log("可以解析成json:"+obj.messageType);
            //1代表上线 2代表下线 3代表在线名单 4代表普通消息
            if(obj.messageType==1){
                //把名称放入到selection当中供选择
                var onlineName = obj.username;
                var option = "<option>"+onlineName+"</option>";
                $("#onLineUser").append(option);
                setMessageInnerHTML(onlineName+"上线了");
            }
            else if(obj.messageType==2){
                $("#onLineUser").empty();
                var onlineName = obj.onlineUsers;
                var offlineName = obj.username;
                var option = "<option>"+"--所有--"+"</option>";
                for(var i=0;i<onlineName.length;i++){
                    if(!(onlineName[i]==document.getElementById('username').value)){
                        option+="<option>"+onlineName[i]+"</option>"
                    }
                }
                $("#onLineUser").append(option);
 
                setMessageInnerHTML(offlineName+"下线了");
            }
            else if(obj.messageType==3){
                var onlineName = obj.onlineUsers;
                var option = null;
                for(var i=0;i<onlineName.length;i++){
                    if(!(onlineName[i]==document.getElementById('username').value)){
                        option+="<option>"+onlineName[i]+"</option>"
                    }
                }
                $("#onLineUser").append(option);
                console.log("获取了在线的名单"+onlineName.toString());
            }
            else{
                setMessageInnerHTML(obj.fromusername+"对"+obj.tousername+"说:"+obj.textMessage);
            }
        };
 
        //连接关闭的回调事件
        webSocket.onclose = function()
        {
            console.log("连接已关闭...");
            setMessageInnerHTML("连接已经关闭....");
        };
    }
    else{
        // 浏览器不支持 WebSocket
        alert("您的浏览器不支持 WebSocket!");
    }
    //将消息显示在网页上
    function setMessageInnerHTML(innerHTML) {
        document.getElementById('message').innerHTML += innerHTML + '<br/>';
    }
 
    function closeWebSocket() {
        //直接关闭websocket的连接
        webSocket.close();
    }
 
    function send() {
        var selectText = $("#onLineUser").find("option:selected").text();
        if(selectText=="--所有--"){
            selectText = "All";
        }
        else{
            setMessageInnerHTML(document.getElementById('username').value+"对"+selectText+"说:"+ $("#text").val());
        }
        var message = {
            "message":document.getElementById('text').value,
            "username":document.getElementById('username').value,
            "to":selectText
        };
        webSocket.send(JSON.stringify(message));
        $("#text").val("");
 
    }
</script>
 
</html>

第五步:创建一个controller,通过接口跳转到websocket页面上去

package com.zeng.springbootwebsocket;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;

@Controller
public class HiController {

    private Logger logger = LoggerFactory.getLogger(this.getClass());


    @RequestMapping("/websocket/{name}")
    public String webSocket(@PathVariable String name,Model model){
        try{
            logger.info("跳转到websocket的页面上");
            //通过Model进行对象数据的传递
            model.addAttribute("username",name);
            return "socket";
        }
        catch (Exception e){
            logger.info("跳转到websocket的页面上发生异常,异常信息是:"+e.getMessage());
            return "error";
        }
    }
}

第六步:application.xml的配置

spring.thymeleaf.prefix: classpath:/templates/

项目完整的路径如下:

开始测试:

在浏览器上输入:localhost:8080/websocket/Rose连接:

再添加一个用户:localhost:8080/websocket/Jack

上线通知:

下线通知:

在线人数统计:

对所有人聊天:

一对一聊天:

猜你喜欢

转载自blog.csdn.net/qq_25843323/article/details/81585648