欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

WebSocket应用

程序员文章站 2024-03-23 10:44:22
...

WebSocket应用

WebSocket协议是基于TCP的一种新的网络协议。它实现了浏览器与服务器全双工(full-duplex)通讯——允许服务器主动发送消息给客户端,这样就可以实现从客户端发送消息到服务器,而服务器又可以转发消息到客户端,这样就能够实现客户端之间的交互。

目前很多浏览器已经实现了WebSocket协议,但是依旧存在着很多浏览器没有实现该协议,为了兼容哪些没有实现该协议的浏览器,往往还需要通过STOMP协议来完成这些兼容。

本篇基于Spring Boot2.x进行开发WebSocket应用

Maven依赖

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

这里加入了spring security的依赖,因为有时候对于WebSocket而言,需要的是点对点通信,这一点需要用户进行登陆。

服务端点配置

ServerEndpointExporter对象可以定义WebSocket服务器的端点,这样客户端就能够请求服务器的端点。

package com.lay.websocket.config;

/**
 * @Description:
 * @Author: lay
 * @Date: Created in 11:39 2018/11/22
 * @Modified By:IntelliJ IDEA
 */
@Configuration
public class WebSocketConfig {
    //创建服务端点
    @Bean
    public ServerEndpointExporter serverEndpointExporter(){
        return new ServerEndpointExporter();
    }
}

有了这个bean,就可以使用@ServerEndPoint定义一个端点服务类。

在这个端点服务类种还可以定义WebSocket的打开、关闭、错误和发送消息的方法。

服务端点开发

接口

package com.lay.websocket.service;

import javax.websocket.Session;

/**
 * @Description:
 * @Author: lay
 * @Date: Created in 11:43 2018/11/22
 * @Modified By:IntelliJ IDEA
 */
public interface WebSocketService {
    //连接成功调用的方法
    public void onOpen(Session session);

    //连接关闭调用的方法
    public void onClose();

    //收到客户端消息后调用的方法
    public void onMessage(String message,Session session);

    //发生异常时调用的方法
    public void onError(Session session,Throwable error);
    
}

实现类

package com.lay.websocket.service.impl;

import com.lay.websocket.service.WebSocketService;
import org.springframework.stereotype.Service;

import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CopyOnWriteArraySet;

/**
 * @Description:
 * @Author: lay
 * @Date: Created in 11:51 2018/11/22
 * @Modified By:IntelliJ IDEA
 */
@Service
@ServerEndpoint("/ws")
public class WebSocketServiceImpl implements WebSocketService {
    //静态变量,用来记录当前在线连接数,应该把它涉及成线程安全的
    private static int onlineCount = 0;
    // concurrent包含的线程安全Set,用来存放每个客户端对应的WebSocketServiceImpl对象
    private static CopyOnWriteArraySet<WebSocketServiceImpl> webSocketSet = new CopyOnWriteArraySet<>();
    //与某个客户端的连接会话,需要通过它来给客户端发送数据
    private Session session;

    //连接加你成功调用的方法
    @Override
    @OnOpen
    public void onOpen(Session session) {
        this.session = session;
        webSocketSet.add(this);//加入set中
        addOnllineCount();
        System.out.println("有新连接加入!当前在线人数为:" + getOnlineCount());
        try {
            sendMessage("有新的连接加入了!!");
        } catch (IOException e) {
            System.out.println("IO异常");
        }
    }


    //连接关闭调用方法
    @Override
    @OnClose
    public void onClose() {
        webSocketSet.remove(this);
        subOnlineCount();
        System.out.println("有一连接关闭!当前在线人数为:" + getOnlineCount());
    }

    //收到客户端消息后调用方法
    @Override
    @OnMessage
    public void onMessage(String message, Session session) {
        System.out.println("来自客户端的消息:" + message);

        webSocketSet.forEach(p -> {
            //获取当前用户名称
            String userName = p.getSession().getUserPrincipal().getName();
            System.out.println(userName);
            try {
                p.sendMessage(message);
            } catch (IOException e) {
                e.printStackTrace();
            }
        });

    }


    //当发生错误时调用方法
    @Override
    @OnError
    public void onError(Session session, Throwable error) {
        System.out.println("发生错误");
        error.printStackTrace();
    }

    //发送消息
    private void sendMessage(String message) throws IOException {
        this.session.getBasicRemote().sendText(message);
    }

    //返回在线人数
    private static synchronized int getOnlineCount() {
        return onlineCount;
    }

    //当连接人数增加时
    private static synchronized void addOnllineCount() {
        WebSocketServiceImpl.onlineCount++;
    }

    //当连接人数减少时
    private static synchronized void subOnlineCount() {
        WebSocketServiceImpl.onlineCount--;
    }

    public Session getSession() {
        return session;
    }
}

这里说明下重要的几点

  • @ServerEndpoint("/ws"):表示让Spring创建WebSocket的服务端点,其中请求地址是/ws
  • @OnOpen:标注客户端打开WebSocket服务端点调用方法。
  • @OnClose:标注客户端关闭WebSocket服务端点调用方法。
  • @OnMessage:标注客户端发送消息,WebSocket服务端点调用方法。
  • @OnError:标注客户端请求WebSocket服务端点发生异常调用方法。

因为每个客户端打开时,都会为其创建一个WebSocketServiceImpl对象,素以这里的打开方法终都会去计数并且将这个对象保存到CopyOnWriteArraySet中,这样你就可以知道有多少连接。对于关闭方法则是清除这个对象,并且计数减一。对于消息发送方法,则是通过轮询对所有客户端的连接都给予发送消息,所以所有的连接都可以接收这个消息,但是有时候可能只是需要发送给特定的用户,则需要得到用户信息,然后再发送给特定用户。

页面开发

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">

<head>
    <meta charset="UTF-8">
    <meta charset="UTF-8">
    <title>websocket</title>
    <script type="text/javascript" src="https://code.jquery.com/jquery-3.2.0.js"></script>
    <script type="text/javascript" th:src="@{/websocket.js}"></script>
</head>
<body>
测试WebSocket站点
<br />
<input id="message" type="text"/>
<button onclick="sendMessage()">发送消息</button>
<button onclick="closeWebSocket()">关闭WebSocket连接</button>
<div id="context"></div>
</body>
</html>

引入相关JS

alert("coming");
var websocket=null;
//判断当前浏览器是否支持WebSocket
if('WebSocket' in window){
    //创建WebSocket对象,连接服务器端点
    websocket=new WebSocket("ws://localhost:8080/ws");
}else {
    alert('Not Support WebSocket');
}
//连接发生错误的回调方法
websocket.onerror=function () {
    appendMessage("error");
}
//连接成功建立的回调方法
websocket.onopen=function () {
    appendMessage("open");
}
//接收到消息的回调方法
websocket.onmessage=function (event) {
    appendMessage(event.data);
}

//连接关闭的回调方法
websocket.onclose=function () {
    appendMessage("close");
}

//监听窗口关闭事件,当窗口关闭时,主动关闭webSocket连接
//防止连接还没断开就关闭窗口,server端会抛出异常
window.onbeforeunload=function () {
    websocket.close();
}
//将消息显示在网页上
function appendMessage(message) {
    var context=$('#context').html()+"<br/>"+message;
    $("#context").html(context);
}

// 关闭连接
function closeWebSocket() {
    websocket.close();
}

// 发送消息
function sendMessage(){
    var message=$('#message').val();
    websocket.send(message);
}

控制器

package com.lay.websocket.controller;

/**
 * @Description:
 * @Author: lay
 * @Date: Created in 15:11 2018/11/22
 * @Modified By:IntelliJ IDEA
 */
@Controller
@RequestMapping("/websocket")
public class WebSocketController {

    //跳转websocket页面
    @GetMapping("/index")
    public String websocket(){
        return "websocket";
    }
}

使用STOMP

并不是所有的浏览器都支持WebSocket协议,一些旧版本的浏览器并不能支持WebSocket协议,因此还需要去兼容这些浏览器。为此可以引入WebSocket的子协议STOMP(Simple or Streaming Text Orientated Messaging Protocol),通过它即可兼容那些不支持WebSocket协议的浏览器。

首先需要在配置文件中加入注解@EnableWebSocketMessageBroker,这个注解将会启动WebSocket下的子协议STOMP。为了配置这个协议,可以实现Spring 提供的接口WebSocketMessageBrokerConfigurer。为了更为简单,spring还提供了这个接口的空实现的抽象类AbstractWebSocketMessageBrokerConfigurer。通过覆盖它所定义的方法即可。下面给我具体的实例。

配置STOMP服务端点和请求订阅前缀

package com.lay.websocket.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;

/**
 * @Description:
 * @Author: lay
 * @Date: Created in 17:46 2018/11/22
 * @Modified By:IntelliJ IDEA
 */
//启用STOMP协议
@EnableWebSocketMessageBroker
@Configuration
public class StomopWebSocktconfig implements WebSocketMessageBrokerConfigurer {
    //注册服务器端点
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        //增加一个聊天服务端点
        registry.addEndpoint("/socket").withSockJS();
        //增加一个用户服务端点
        registry.addEndpoint("/wsuser").withSockJS();
    }

    //定义服务器端点请求和订阅前缀
    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        //客户端订阅路径前缀
        registry.enableSimpleBroker("/sub","/queue");

        //服务端点请求前缀
        registry.setApplicationDestinationPrefixes("/request");
    }
}

代码中使用了@EnableWebSocketMessageBroker驱动Spring 启用STOMP协议。然后这个配置类继承了AbstractWebSocketMessageBrokerConfigurer,这样覆盖其定义的方法就可以配置STOMP相关服务。其中registerStompEndpoints方法是用于注册端点的方法。这里定义了"/socket""/wsuser"两个服务端点,而在定义端点时还加入了withSockJS方法,这个方法的声明代表着可以支持SockJS,SockJS它是一个第三方关于支持WebSocket请求的JavaScript框架。configureMessageBroker方法注册请求的前缀和客户端订阅的前缀。

配置之后,spring boot就会自动篡改就SimpMessagingTemplate对象,它是一个可以进行转发消息的模板,通过这个模板可以发送消息到特定的地址,甚至是限制给特定的用户发送消息。

控制器

package com.lay.websocket.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.messaging.simp.broker.SimpleBrokerMessageHandler;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

import java.security.Principal;

/**
 * @Description:
 * @Author: lay
 * @Date: Created in 17:59 2018/11/22
 * @Modified By:IntelliJ IDEA
 */
@Controller
@RequestMapping("stompws")
public class StompWebSocketController {

    //注入spring boot 自动配置消息模板对象
    @Autowired
    private SimpMessagingTemplate simpMessagingTemplate;

    //发送页面
    @GetMapping("/send")
    public String send(){
        return "/stomp/send";
    }

    //接收页面
    @GetMapping("/receive")
    public String receive(){
        return "/stomp/receive";
    }

    //对特定用户发送页面
    @GetMapping("/sendUser")
    public String sendUser(){
        return "/stomp/send-user";
    }

    //接收用户消息页面
    @GetMapping("/receiveUser")
    public String receiveUser(){
        return "/stomp/receive-user";
    }

    //定义消息请求路径
    @MessageMapping("/send")
    //定义结果发送到特定路径
    @SendTo("/sub/chat")
    public String sendMsg(String value){
        return value;
    }

    //将消息发送给特定用户
    @MessageMapping("/sendUser")
    public void sendToUser(Principal principal,String body){
        String srcUser=principal.getName();
        //解析用户和消息
        String[] args=body.split(",");
        String desUser=args[0];
        String message="【"+srcUser+"】给你发来消息:"+args[1];
        //发送到用户和监听地址
        simpMessagingTemplate.convertAndSendToUser(desUser,"/query/customer",message);
    }

}

@MessageMapping注解和@RequestMapping类似,它是定义WebSocket请求的路径,当然需要与WebSocketConfig所定义的前缀"/request"连用。

@SendTo注解配置为"/sub/chat",说明在执行玩这个方法后,会将返回结果发送到订阅的这个目的地中,这样客户端就可以得到消息。

sendToUser方法存在Principal参数,如果使用了spring security,这个参数可以获取当前用户的消息,然后通过simpMessagingTemplateconvertAndSendToUser方法,就可以设置发送给对应的目的地并且限定特定的用户消息。

因为这里涉及到用户,所以需要使用spring security,我们进行配置。

配置Spring Security

package com.lay.websocket;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@SpringBootApplication
@EnableScheduling
public class WebsocketApplication extends WebSecurityConfigurerAdapter {

    public static void main(String[] args) {
        SpringApplication.run(WebsocketApplication.class, args);
    }

    //定义3个可以登陆的内存用户
    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        //密码加密器
        PasswordEncoder passwordEncoder=new BCryptPasswordEncoder();
        //加入3个内存用户,密码分别为加密后的 p1,p2,p3
        //可以通过passwordEncoder.encode("p1")这样获得加密后的密码
        auth.inMemoryAuthentication().passwordEncoder(passwordEncoder)
                .withUser("user1").password(passwordEncoder.encode("p1"))
                .roles("USER").and()
                .withUser("user2").password(passwordEncoder.encode("p2"))
                .roles("USER").and()
                .withUser("user3").password(passwordEncoder.encode("p3"))
                .roles("USER");
    }
}

这样就限定了3个可以登陆的用户和密码。wile测试还需要客户端,这里涉及了WebSocketController所定义的4个映射地址,下面一个一个给出。

页面

首先是WebSocketControllersend方法映射的/stomp/send.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">

<head>
    <meta charset="UTF-8">
    <meta charset="UTF-8">
    <title>websocket</title>
    <script type="text/javascript" src="https://code.jquery.com/jquery-3.2.0.js"></script>
    <script type="text/javascript" src="https://cdn.jsdelivr.net/sockjs/1/sockjs.min.js"></script>
    <!-- stomp.min.js下载地址 https://raw.githubusercontent.com/jmesnil/stomp-websocket/master/lib/stomp.min.js 该地址为文本,需要自行先下载-->
    <script type="text/javascript" th:src="@{/stomp.min.js}"></script>
    <script type="text/javascript">
        var stompClient=null;
        //设置连接
        function setConnected(connected) {
            $("#connect").attr({"disabled":connected});
            $('#disconnect').attr({"disabled":!connected});

            if(connected){
                $('#conversationDiv').show();
            }else {
                $('#conversationDiv').hide();
            }
            $('#response').html("");
        }

        //开启socket连接
        function connect() {
            //定义请求服务器的端点
            var socket=new SockJS('/socket');
            // stomp客户端
            stompClient=Stomp.over(socket);
            //连接服务器端点
            stompClient.connect({},function (frame) {
                //建立连接后的回调
                setConnected(true);
            });
        }

        //断开socket连接
        function disconnect() {
            if(stompClient!=null){
                stompClient.disconnect();
            }
            setConnected(false);
            console.log("Disconnected");
        }
        //向/request/send服务端发送消息
        function sendMsg() {
            var value=$('#message').val();
            //发送消息到"/request/send",其中/request是服务器定义的前缀
            //而send则是@MessageMapping所配置的路径
            stompClient.send("/request/send",{},value);
        }
        //启用连接
        connect();
    </script>
</head>
<body>
    <div>
        <div>
            <button id="connect" onclick="connect();">连接</button>
            <button id="disconnect" onclick="disconnect();" disabled="disabled">断开连接</button>
        </div>
        <div id="conversationDiv">
            <p>
                <label>发送的内容</label>
            </p>
            <p>
                <textarea id="message" rows="5"></textarea>
            </p>
            <button id="sendMsg" onclick="sendMsg();">Send</button>
            <p id="response"></p>
        </div>
    </div>
</body>
</html>

这里加入了socket.min.jsstomp.min.js两个javaSript脚本,这样就可以通过对应JavaScript API进行请求服务器端点。

  • 首先是建立连接,可以看到connect函数
  • 其次是发送消息可以看到sendMsg函数
  • 最后是关闭连接可以看到disconnect函数

这里的sendMsg函数请求额是StompWebSockertControllersendMsg方法,这样这个方法就将消息发送到"/sub/chat"中,所以需要一个客户端去订阅这个地址,者就是WebSocketControlelrreceive方法所映射的/stomp/receive.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">

<head>
    <meta charset="UTF-8">
    <meta charset="UTF-8">
    <title>websocket</title>
    <script type="text/javascript" src="https://code.jquery.com/jquery-3.2.0.js"></script>
    <script type="text/javascript" src="https://cdn.jsdelivr.net/sockjs/1/sockjs.min.js"></script>
    <!-- stomp.min.js下载地址 https://raw.githubusercontent.com/jmesnil/stomp-websocket/master/lib/stomp.min.js 该地址为文本,需要自行先下载-->
    <script type="text/javascript" th:src="@{/stomp.min.js}"></script>
    <script type="text/javascript">
        var noticeSocket=function () {
            //连接服务器端点
            var s=new SockJS('/socket');
            //客户端
            var stompClient=Stomp.over(s);
            stompClient.connect({},function () {
                console.log('notice socket connected');
                //订阅消息地址
                stompClient.subscribe('/sub/chat',function (data) {
                    $('#receive').html(data.body);
                })
            });
        };
        noticeSocket();
    </script>
</head>
<body>
<h1>
    <span id="receive">等待接收消息</span>
</h1>
</body>
</html>

此处,客户端stompClient在服务器端点之后,加入了订阅消息的地址,这样就能够获取StompWebSocketControllersendMsg方法发送到"/sub/chat"的消息。

这里只是可以实现群发的功能,有时候可能仅仅希望只是对一个用户发送,这样就会用到StompWebSocketControllersendToUser方法。这里先定义发送的页面,它对应的StompWebSocketControllersendUser方法

send-user.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">

<head>
    <meta charset="UTF-8">
    <meta charset="UTF-8">
    <title>websocket</title>
    <script type="text/javascript" src="https://code.jquery.com/jquery-3.2.0.js"></script>
    <script type="text/javascript" src="https://cdn.jsdelivr.net/sockjs/1/sockjs.min.js"></script>
    <!-- stomp.min.js下载地址 https://raw.githubusercontent.com/jmesnil/stomp-websocket/master/lib/stomp.min.js 该地址为文本,需要自行先下载-->
    <script type="text/javascript" th:src="@{/stomp.min.js}"></script>
    <script type="text/javascript">
        var stompClient=null;
        //设置连接
        function setConnected(connected) {
            $("#connect").attr({"disabled":connected});
            $('#disconnect').attr({"disabled":!connected});

            if(connected){
                $('#conversationDiv').show();
            }else {
                $('#conversationDiv').hide();
            }
            $('#response').html("");
        }

        //开启socket连接
        function connect() {
            //定义请求服务器的端点/wsuser
            var socket=new SockJS('/wsuser');
            // stomp客户端
            stompClient=Stomp.over(socket);
            //连接服务器端点
            stompClient.connect({},function (frame) {
                //建立连接后的回调
                setConnected(true);
            });
        }

        //断开socket连接
        function disconnect() {
            if(stompClient!=null){
                stompClient.disconnect();
            }
            setConnected(false);
            console.log("Disconnected");
        }
        //向/request/send服务端发送消息
        function sendMsg() {
            var value=$('#message').val();
            var user=$('#user').val();
            //用户和消息组成的字符串
            var text=user+","+value;
            //发送消息到"/request/sendUser",其中/request是服务器定义的前缀
            //而send则是@MessageMapping所配置的路径
            stompClient.send("/request/sendUser",{},text);
        }
        //启用连接
        connect();

    </script>
</head>
<body>
    <div>
        <div>
            <button id="connect" onclick="connect();">连接</button>
            <button id="disconnect" onclick="disconnect();" disabled="disabled">断开连接</button>
        </div>
        <div id="conversationDiv">
            <p><label>发送给用户</label></p>
            <p><input type="text" id="user" /></p>
            <p><label>发送的内容</label></p>
            <p><textarea id="message" rows="5"></textarea>
            </p><button id="sendMsg" onclick="sendMsg();">发送</button>
            <p id="response"></p>
        </div>
    </div>
</body>
</html>

这里连接的是服务器端点"/wsuser",而请求的是"/request/sendUser",这样就对应了StompWebSocketControllersendToUser方法。这里值得注意的是,把用户名和消息都发送给了服务器端点,所以在sendToUser方法里,分离了用户名和小米,然后就通过消息模板(simpMessagingTemplate)的convertAndSendToUser方法,指定了用户的参数,然后发送到地址/queue/customer,这样对应的用户登陆后,通过订阅合格地址就能够得到服务器发送的消息。

下面是订货这个地址的页面代码

receive-user.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">

<head>
    <meta charset="UTF-8">
    <meta charset="UTF-8">
    <title>websocket</title>
    <script type="text/javascript" src="https://code.jquery.com/jquery-3.2.0.js"></script>
    <script type="text/javascript" src="https://cdn.jsdelivr.net/sockjs/1/sockjs.min.js"></script>
    <!-- stomp.min.js下载地址 https://raw.githubusercontent.com/jmesnil/stomp-websocket/master/lib/stomp.min.js 该地址为文本,需要自行先下载-->
    <script type="text/javascript" th:src="@{/stomp.min.js}"></script>
    <script type="text/javascript">
        var noticeSocket=function () {
            //连接服务器端点
            var s=new SockJS('/wsuser');
            //客户端
            var stompClient=Stomp.over(s);
            stompClient.connect({},function () {
                console.log('notice socket connected');
                //订阅消息地址
                //这里/user前缀是不能缺少的,它代表着订阅指定用户的消息
                stompClient.subscribe('/user/queue/customer',function (data) {
                    $('#receive').html(data.body);
                });
            });
        };
        noticeSocket();
    </script>
</head>
<body>
    <h1><span id="receive">等待接收消息</span></h1>
</body>
</html>

代码中的/user/queue/customer/user前缀是不能缺少的,它代表着订阅指定用户的消息。至此,所有的页面和代码都开发完成。我们可以登陆不同的用户去进行测试,这里就不讲了。

相关标签: WebSocket