敞开成长之旅!这是我参与「日新计划 12 月更文应战」的第25天,点击查看活动详情

前言

咱们在开发时,经常需求在前后端之间传递信息。前端的改变要通知给后端,反之,后端的数据更新也要及时通知给前端。那什么样的技术,能够保证前后端之间的高效安稳通讯呢?今日 壹哥 就带各位学习WebSockets,完成前后端之间的通讯。

一. WebSockets简介

1. 什么是Websockets

WebSocket是HTML5提供的一种新的网络通讯协议。它完成了服务端与客户端的全双工通讯,树立在传输层TCP协议之上,即浏览器与服务端需求先树立TCP协议,再发送WebSocket衔接树立恳求。

2. 为什么要有WebSockets

已然网络通讯已经有了http协议,为什么还需求WebSocket协议呢?

这是因为http协议有一个缺陷,通讯只能由客户端建议恳求,服务器端回来查询成果。

比如说咱们想要获取一个实时的新闻信息,在每次更新新闻信息后,咱们都需求改写页面才干获取到最新的信息,只要再次建议客户端恳求,服务器端才会回来成果。但是服务器端不能做到推送音讯给客户端,当然咱们能够运用轮询,查看服务器有没有新的音讯,比如 “聊天室” 这样的,但是轮询效率是非常低的,因而WebSocket就这样产生了。

SpringBoot2.x系列教程51--SpringBoot中整合WebSockets实现前后端之间的通信

3. WebSocket创立衔接过程

客户端发送恳求信息,服务端接纳到这个恳求并回来响应信息。
当衔接树立后,客户端发送http恳求时,经过Upgrade:webSocket Connection:Upgrade 奉告服务器需求树立的是WebSocket衔接,并且还会传递WebSocket版本号、协议的字版本号、原始地址、主机地址, WebSocket相互通讯的Header很小,大概只要2Bytes。

SpringBoot2.x系列教程51--SpringBoot中整合WebSockets实现前后端之间的通信

4. WebSocket的长处

WebSocket的最大长处便是服务器能够主意向客户端推送音讯,客户端也能够主意向服务器端发送音讯

运用WebSockets能够在服务器与客户端之间树立一个非http的双向衔接。这个衔接是实时的,也是永久的(除非被封闭)。

当服务器想向客户端发送数据时,能够立即将数据推送到客户端的浏览器中,无需重新树立链接,只要客户端有一个被翻开的socket(套接字)并且与服务器树立链接,服务器就能够把数据推送到这个socket上。

5. WebSocket的前端API

5.1 树立衔接

WebSocket需求接纳一个url参数,然后调用WebSocket目标的结构器来树立与服务器之间的通讯链接。

如下代码初始化:

var websocket = new WebSocket('wss://echo.websocket.org');
注:

URL字符串必须以 “ws” 或 “wss”(加密通讯)最初。

利用上面的代码,咱们的通讯衔接树立之后,就能够进行客户端与服务器端的双向通讯了。能够运用WebSocket目标的send办法对服务器发送数据,但是只能发送文本数据(咱们能够运用JSON目标把任何js目标转换成文本数据后再进行发送)。

5.2 发送音讯的办法

websocket.send("data");

5.3 接纳服务器传过来的数据

经过onmessage事情来接纳服务器传过来的数据,如下代码:

websocket.onmessage = function(event) {
   var data = event.data;
}

5.4 监听socket的翻开事情

经过获取onopen事情来监听socket的翻开事情。如下代码:

websocket.onopen = function(event) {
// 开始通讯时的处理
}

5.5 监听socket的封闭事情

经过获取onclose事情来监听socket的封闭事情。如下代码:

websocket.onclose = function(event) {
// 通讯结束时的处理
}

5.6 封闭socket

经过close办法来封闭socket, 如下代码:

websocket.close();

5.6 WebSocket的状态

能够经过读取readyState的特点值来获取WebSocket目标的状态,readyState特点存在以下几种特点值。

  • CONNECTING(数字值为0),表明正在衔接;
  • OPEN(数字值为1),表明已树立衔接;
  • CLOSING(数字值为2),表明正在封闭衔接;
  • CLOSED(数字值为3),表明已封闭链接。

二. SpringBoot2.x整合WebSockets

1. 创立Web项目

咱们按照之前的经历,创立一个Web项目,并将之改造成Spring Boot项目,具体过程略。

SpringBoot2.x系列教程51--SpringBoot中整合WebSockets实现前后端之间的通信

2. 增加依靠包

在pom.xml文件中增加核心依靠包。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.46</version>
</dependency>

3. 创立WebSocket装备文件

咱们创立一个装备类,敞开对WebSockets的支撑。

package com.yyg.boot.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
/**
 * @Author 逐个哥Sun
 * @Date Created in 2020/5/13
 * @Description Description
 */
@Configuration
public class WebSocketConfig {
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}

4. 创立WebSockets的Server端

接着咱们要创立WebSeckets服务端的装备代码,这是通讯的核心。

package com.yyg.boot.websockets;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;
/**
 * @Author 逐个哥Sun
 * @Date Created in 2020/5/13
 * @Description Description
 */
@Component
@ServerEndpoint("/server/{uid}")
@Slf4j
public class WebSocketServer {
    /**
     * 用来记录当时在线衔接数量,应该把它规划成线程安全的。
     */
    private static int onlineCount = 0;
    /**
     * concurrent包是线程安全的Set,用来存放每个客户端对应的WebSocket目标。
     */
    private static ConcurrentHashMap<String, WebSocketServer> webSocketMap = new ConcurrentHashMap<>();
    /**
     * 与某个客户端的衔接会话,需求经过它来给客户端发送数据.
     */
    private Session session;
    /**
     * 接纳客户端音讯的uid
     */
    private String uid = "";
    /**
     * 衔接树立成功调用的办法
     */
    @OnOpen
    public void onOpen(Session session, @PathParam("uid") String uid) {
        this.session = session;
        this.uid = uid;
        if (webSocketMap.containsKey(uid)) {
            webSocketMap.remove(uid);
            //参加到set中
            webSocketMap.put(uid, this);
        } else {
            //参加set中
            webSocketMap.put(uid, this);
            //在线数加1
            addOnlineCount();
        }
        log.info("用户衔接:" + uid + ",当时在线人数为:" + getOnlineCount());
        try {
            sendMsg("衔接成功");
        } catch (IOException e) {
            log.error("用户:" + uid + ",网络异常!!!!!!");
        }
    }
    /**
     * 衔接封闭调用的办法
     */
    @OnClose
    public void onClose() {
        if (webSocketMap.containsKey(uid)) {
            webSocketMap.remove(uid);
            //从set中删去
            subOnlineCount();
        }
        log.info("用户退出:" + uid + ",当时在线人数为:" + getOnlineCount());
    }
    /**
     * 收到客户端音讯后调用的办法
     * @param message 客户端发送过来的音讯
     */
    @OnMessage
    public void onMessage(String message, Session session) {
        log.info("用户id:" + uid + ",接纳到的报文:" + message);
        //能够群发音讯
        //音讯保存到数据库、redis
        if (!StringUtils.isEmpty(message)) {
            try {
                //解析发送的报文
                JSONObject jsonObject = JSON.parseObject(message);
                //追加发送人(避免串改)
                jsonObject.put("fromUID", this.uid);
                String toUID = jsonObject.getString("toUID");
                //传送给对应的toUserId用户的WebSocket
                if (!StringUtils.isEmpty(toUID) && webSocketMap.containsKey(toUID)) {
                    webSocketMap.get(toUID).sendMsg(jsonObject.toJSONString());
                } else {
                    //若果不在这个服务器上,能够考虑发送到mysql或许redis
                    log.error("恳求的UserId:" + toUID + "不在该服务器上");
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    /**
     * 处理过错
     */
    @OnError
    public void onError(Session session, Throwable error) {
        log.error("用户过错:" + this.uid + ",原因:" + error.getMessage());
        error.printStackTrace();
    }
    /**
     * 完成服务器自动推送
     */
    private void sendMsg(String msg) throws IOException {
        this.session.getBasicRemote().sendText(msg);
    }
    /**
     * 发送自定义音讯
     */
    public static void sendInfo(String message, @PathParam("uid") String uid) throws IOException {
        log.info("发送音讯到:" + uid + ",发送的报文:" + message);
        if (!StringUtils.isEmpty(uid) && webSocketMap.containsKey(uid)) {
            webSocketMap.get(uid).sendMsg(message);
        } else {
            log.error("用户" + uid + ",不在线!");
        }
    }
    private static synchronized int getOnlineCount() {
        return onlineCount;
    }
    private static synchronized void addOnlineCount() {
        WebSocketServer.onlineCount++;
    }
    private static synchronized void subOnlineCount() {
        WebSocketServer.onlineCount--;
    }
}

5. 创立Controller接口

编写一个Controller,定义出通讯的接口。

package com.yyg.boot.web;
import com.yyg.boot.websockets.WebSocketServer;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.ModelAndView;
import java.io.IOException;
/**
 * @Author 逐个哥Sun
 * @Date Created in 2020/5/13
 * @Description Description
 */
@RestController
public class WebSocketController {
    @GetMapping("/page")
    public ModelAndView page() {
        return new ModelAndView("webSocket");
    }
    @RequestMapping("/push/{toUID}")
    public ResponseEntity<String> pushToClient(String message, @PathVariable String toUID) throws IOException {
        WebSocketServer.sendInfo(message, toUID);
        return ResponseEntity.ok("Send Success!");
    }
}

6. 创立前端页面发送音讯

编写前端页面,向后端发送通讯恳求。

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>WebSocket通讯</title>
</head>
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.js"></script>
<script>
    var socket;
    //翻开WebSocket
    function openSocket() {
        if (typeof(WebSocket) === "undefined") {
            console.log("您的浏览器不支撑WebSocket");
        } else {
            console.log("您的浏览器支撑WebSocket");
            //完成化WebSocket目标,指定要衔接的服务器地址与端口,树立衔接.
            //等同于socket = new WebSocket("ws://localhost:8080/xxx/im/25");
            //var socketUrl="${request.contextPath}/im/"+$("#uid").val();
            var socketUrl = "http://localhost:8080/socket/server/" + $("#uid").val();
            //将https与http协议替换为ws协议
            socketUrl = socketUrl.replace("https", "ws").replace("http", "ws");
            console.log(socketUrl);
            if (socket != null) {
                socket.close();
                socket = null;
            }
            socket = new WebSocket(socketUrl);
            //翻开事情
            socket.onopen = function () {
                console.log("WebSocket已翻开");
                //socket.send("这是来自客户端的音讯" + location.href + new Date());
            };
            //获得音讯事情
            socket.onmessage = function (msg) {
                console.log(msg.data);
                //发现音讯进入,开始处理前端触发逻辑
            };
            //封闭事情
            socket.onclose = function () {
                console.log("WebSocket已封闭");
            };
            //发生了过错事情
            socket.onerror = function () {
                console.log("WebSocket发生了过错");
            }
        }
    }
    //发送音讯
    function sendMessage() {
        if (typeof(WebSocket) === "undefined") {
            console.log("您的浏览器不支撑WebSocket");
        } else {
            console.log("您的浏览器支撑WebSocket");
            console.log('{"toUID":"' + $("#toUID").val() + '","Msg":"' + $("#msg").val() + '"}');
            socket.send('{"toUID":"' + $("#toUID").val() + '","Msg":"' + $("#msg").val() + '"}');
        }
    }
</script>
<body>
<p>【uid】:
<div><input id="uid" name="uid" type="text" value="5"></div>
<p>【toUID】:
<div><input id="toUID" name="toUID" type="text" value="10"></div>
<p>【Msg】:
<div><input id="msg" name="msg" type="text" value="hello WebSocket"></div>
<p>【第一步操作:】:
<div><button onclick="openSocket()">敞开socket</button></div>
<p>【第二步操作:】:
<div><button onclick="sendMessage()">发送音讯</button></div>
</body>
</html>

7. 装备application.yml

在application.yml装备文件中,对咱们的项目进行必要的装备。

server:
  port: 8080
  servlet:
    context-path: /socket
spring:
  http:
    encoding:
      force: true
      charset: UTF-8
  application:
    name: websocket-demo
  freemarker:
    request-context-attribute: request
    prefix: /templates/
    suffix: .html
    content-type: text/html
    enabled: true
    cache: false
    charset: UTF-8
    allow-request-override: false
    expose-request-attributes: true
    expose-session-attributes: true
    expose-spring-macro-helpers: true

8. 创立进口类

最终创立一个项目进口类,启动项目。

package com.yyg.boot;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
 * @Author 逐个哥Sun
 * @Date Created in 2020/5/13
 * @Description Description
 */
@SpringBootApplication
public class SocketApplication {
    public static void main(String[] args){
        SpringApplication.run(SocketApplication.class,args);
    }
}

9. 完整项目结构

此时完整的项目结构如下图所示,大家能够参考创立。

SpringBoot2.x系列教程51--SpringBoot中整合WebSockets实现前后端之间的通信

三. 启动项目进行测验

1. 测验page接口

咱们需求在浏览器中翻开另个页面,网址都是:
http://localhost:8080/socket/page

SpringBoot2.x系列教程51--SpringBoot中整合WebSockets实现前后端之间的通信

2. 发送音讯

首先咱们在第一个页面中,输入如下参数信息,点击发送音讯按钮:

SpringBoot2.x系列教程51--SpringBoot中整合WebSockets实现前后端之间的通信

能够看到Console控制台显现的日志信息。然后咱们去Intellij idea中看看后台打印的日志信息:

SpringBoot2.x系列教程51--SpringBoot中整合WebSockets实现前后端之间的通信

3. 再次发送音讯

然后咱们在第二个页面中,输入如下参数信息,点击发送音讯按钮:

SpringBoot2.x系列教程51--SpringBoot中整合WebSockets实现前后端之间的通信

然后咱们去Intellij idea中看看后台打印的日志信息:

SpringBoot2.x系列教程51--SpringBoot中整合WebSockets实现前后端之间的通信

4. 回来之前页面

接着咱们回到第一个页面,能够看到log中展示了如下信息:

SpringBoot2.x系列教程51--SpringBoot中整合WebSockets实现前后端之间的通信

阐明接纳到了从另一个客户端发来的信息。如果在第一个页面中再次点击发送信息的按钮,相同的在第二个页面中能够收到日志信息:
SpringBoot2.x系列教程51--SpringBoot中整合WebSockets实现前后端之间的通信

至此,咱们就完成了WebSocket通讯。

结语

经过上面的一系列代码,咱们就完成了前后端之间的通讯。现在你有么有学会WebSocket的运用呢?如果有什么问题,能够在评论区给壹哥留言哦!