SpringBoot中的WebSocket搭建详解 通俗易懂

SpringBoot中已经集成了websocket,搭建起来很简单,容易上手,废话少说,开始。

一、基础搭建

导入依赖 (首先需要一个SpringBoot的环境,此文章不再赘述)
<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>
        
        //以下两个依赖用在前端搭建的时候用到
		 <dependency>
            <groupId>org.webjars</groupId>
            <artifactId>sockjs-client</artifactId>
            <version>1.0.2</version>
        </dependency>
        <dependency>
            <groupId>org.webjars</groupId>
            <artifactId>stomp-websocket</artifactId>
            <version>2.3.3</version>
        </dependency>

导入依赖后 第一步完成。

二、基本的WebSocket配置

接下来进行一个WebSocket配置
需要创建一个配置类WebSocketConfig,定义全局的配置信息,使用JavaConfig的形式

package com.example.websocket.config;

import com.example.websocket.intecepter.HttpHandShakeIntecepter;
import com.example.websocket.intecepter.SocketChannelIntecepter;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.ChannelRegistration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.AbstractWebSocketMessageBrokerConfigurer;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;


@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer{

	/**
	 * 注册端点,发布或者订阅消息的时候需要连接此端点
	 * setAllowedOrigins 非必须,*表示允许其他域进行连接
	 * withSockJS  表示开始sockejs支持
	 *
	 * 这个方法的作用是添加一个服务端点,来接收客户端的连接。
	 */
	public void registerStompEndpoints(StompEndpointRegistry registry) {

		//表示添加了一个 /endpoint-websocket 端点,客户端就可以通过这个端点来进行连接。
		registry.addEndpoint("/endpoint-websocket").addInterceptors(new HttpHandShakeIntecepter())
		.setAllowedOrigins("*").withSockJS();
		//withSockJS()的作用是开启SockJS支持,
	}

	/**
	 * 配置消息代理(中介)
	 * enableSimpleBroker 服务端推送给客户端的路径前缀
	 * setApplicationDestinationPrefixes  客户端发送数据给服务器端的一个前缀
	 *
	 * 这个方法的作用是定义消息代理,通俗一点讲就是设置消息连接请求的各种规范信息。
	 */
	@Override
	public void configureMessageBroker(MessageBrokerRegistry registry) {

		//表示客户端订阅地址的前缀信息,也就是客户端接收服务端消息的地址的前缀信息
		registry.enableSimpleBroker("/topic"); //接收
		//指服务端接收地址的前缀,意思就是说客户端给服务端发消息的地址的前缀
		registry.setApplicationDestinationPrefixes("/app");  //发送

		//上面两个方法定义的信息其实是相反的,一个定义了客户端接收的地址前缀,一个定义了客户端发送地址的前缀

	}

	@Override
	public void configureClientInboundChannel(ChannelRegistration registration) {
		registration.interceptors( new SocketChannelIntecepter());
	}

	@Override
	public void configureClientOutboundChannel(ChannelRegistration registration) {
		registration.interceptors( new SocketChannelIntecepter());
	}
}

在上述的配置文件中
registerStompEndpoints 方法详解

registerStompEndpoints
//这个方法的作用是添加一个服务端点,来接收客户端的连接。
可以通俗的理解为 这就像一个屋子(服务端点),而里面配置就是标明屋子的门牌号

registry.addEndpoint("/endpoint-websocket")
// 这里表示添加了一个/endpoint-websocket 的端点,客户端可以通过这个端点来进行连接
可以通俗的理解为 这是上述所说的门牌号

.addInterceptors(new HttpHandShakeIntecepter())
//这个是添加的一个自定义拦截器(基本配置里可以不加)
.setAllowedOrigins("*")
//经查阅官方文档springwebsocket 4.1.5版本前默认支持跨域访问,之后的版本默认不支持跨域,需要设置

.withSockJS();
//withSockJS()的作用是开启SockJS支持

configureMessageBroker 方法详解

configureMessageBroker
//这个方法的作用是定义消息代理,通俗一点讲就是设置消息连接请求的各种规范信息

registry.enableSimpleBroker("/topic"); //接收 topic可自定义
//表示客户端订阅地址的前缀信息,也就是客户端接收服务端消息的地址的前缀信息

registry.setApplicationDestinationPrefixes("/app"); //发送 app可自定义
//指服务端接收地址的前缀,意思就是说客户端给服务端发消息的地址的前缀

上面两个方法定义的信息其实是相反的,一个定义了客户端接收的地址前缀,一个定义了客户端发送地址的前缀。
比较绕,可以看下我下面举的例子。
一个客户端当作一个人,进入了屋子(服务端点)之后,有接收信息和发送信息的能力,但是房子里的人多了起,你一句我一句会让信息错乱,为了要统一规范,发送和接收前都要加上一个标识,也正是上面所定义的“topic”,“app”。

三、前后端编写

上述的基本配置完成后,就可以编写基本的功能了

package com.example.websocket.controller.v3;

import com.example.websocket.model.InMessage;
import com.example.websocket.model.OutMessage;
import com.example.websocket.service.WebSocketService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Controller;



@Controller
public class V3ChatRoomContoller {

	
	/**
	* 这是 调用convertAndSendToUser来推送消息
	*/
	@Autowired
	private SimpMessagingTemplate template;

	/**
	 * @MessageMapping
	 * @param message
	 */
	@MessageMapping("/v3/single/chat")
	public void singleChat(InMessage message) {

		//意思就是“将给定的对象进行序列化,使用‘MessageConverter’进行包装转化成一条消息,发送到指定的目标”,通俗点讲就是我们使用这个方法进行消息的转发发送!
		template.convertAndSend("/topic/single/"+message.getTo(),
				new OutMessage(message.getFrom()+" 发送:"+ message.getContent()));

	}
}

内容详解

SimpMessagingTemplate
这是调用convertAndSendToUser来推送消息,Spring-WebSocket内置的一个消息发送工具,可以将消息发送到指定的客户端

@MessageMapping
//Spring提供的一个注解,功能类似@RequestMapping,存在于Controller中的,定义一个消息的基本请求,支持通配符``的url等
详情用法见Web on Servlet Stack

此后台的基础搭建完成,开始前端部分。

WebSocket前端准备工作

前端需要准备两个js文件:
sockjs.js和stomp.js

<dependency>
            <groupId>org.webjars</groupId>
            <artifactId>sockjs-client</artifactId>
            <version>1.0.2</version>
        </dependency>
        
        <dependency>
            <groupId>org.webjars</groupId>
            <artifactId>stomp-websocket</artifactId>
            <version>2.3.3</version>
        </dependency>

SockJS:
SockJS 是一个浏览器上运行的 JavaScript 库,如果浏览器不支持 WebSocket,该库可以模拟对 WebSocket 的支持,实现浏览器和 Web 服务器之间低延迟、全双工、跨域的通讯通道。

Stomp
Stomp 提供了客户端和代理之间进行广泛消息传输的框架。Stomp 是一个非常简单而且易用的通讯协议实现,尽管代理端的编写可能非常复杂,但是编写一个 Stomp 客户端却是很简单的事情,另外你可以使用 Telnet 来与你的 Stomp 代理进行交互。

HTML代码:

<!DOCTYPE html>
<html>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<head>
    <title>Hello WebSocket</title>
    <link href="/webjars/bootstrap/css/bootstrap.min.css" rel="stylesheet">
    <script src="/webjars/jquery/jquery.min.js"></script>
    <script src="/webjars/sockjs-client/sockjs.min.js"></script>
    <script src="/webjars/stomp-websocket/stomp.min.js"></script>
    <script src="/v3/app.js"></script>
</head>
<style>
    body {
        background-color: #f5f5f5;
    }

    #main-content {
        max-width: 940px;
        padding: 2em 3em;
        margin: 0 auto 20px;
        background-color: #fff;
        border: 1px solid #e5e5e5;
        -webkit-border-radius: 5px;
        -moz-border-radius: 5px;
        border-radius: 5px;
    }
</style>
<body>
<noscript><h2 style="color: #ff0000">Seems your browser doesn't support Javascript! Websocket relies on Javascript being
    enabled. Please enable
    Javascript and reload this page!</h2></noscript>
<div id="main-content" class="container">
    <div class="row">
        <div class="col-md-6">
            <form class="form-inline">
                <div class="form-group">
                    <label for="connect">建立连接通道:</label>
                    <button id="connect" class="btn btn-default" type="submit">连接</button>
                    <button id="disconnect" class="btn btn-default" type="submit" disabled="disabled">断开连接</button>
                </div>
            </form>
        </div>
        <div class="col-md-6">
            <form class="form-inline">
                <div class="form-group">
                    <input type="text" id="from" class="form-control" placeholder="我是">
                    <input type="text" id="to" class="form-control" placeholder="发送给谁">

                    <input type="text" id="content" class="form-control" placeholder="请输入...">


                </div>
                <button id="send" class="btn btn-default" type="submit">发送</button>
            </form>
        </div>
    </div>
    <div class="row">
        <div class="col-md-12">
            <table id="conversation" class="table table-striped">
                <thead>
                <tr>
                    <th>记录</th>
                </tr>
                </thead>
                <tbody id="notice">
                </tbody>
            </table>
        </div>
    </div>
</div>
</body>
</html>

JS代码:

var stompClient = null;

function setConnected(connected) {
    $("#connect").prop("disabled", connected);
    $("#disconnect").prop("disabled", !connected);
    if (connected) {
        $("#conversation").show();
    }
    else {
        $("#conversation").hide();
    }
    $("#notice").html("");
}

function connect() {
	var from = $("#from").val();
	//找到配置好的服务端点(屋子),建立连接
	var socket = new SockJS('/endpoint-websocket');
    stompClient = Stomp.over(socket);
    stompClient.connect({}, function (frame) {
        setConnected(true);
        console.log('Connected: ' + frame);
        stompClient.subscribe('/topic/single/'+from, function (result) {
            //这个subscribe()方法功能就是定义一个订阅地,址用来接收服务端的信息(在服务端代码中,我们在V3ChatRoomContoller中指定了这个订阅地址“/topic/single”),当收到消息后,在回调函数中处理业务逻辑。
        	showContent(JSON.parse(result.body));
        });
    });
}

//断开连接
function disconnect() {
    if (stompClient !== null) {
        stompClient.disconnect();
    }
    setConnected(false);
    console.log("Disconnected");
}

function sendName() {

    stompClient.send("/app/v3/single/chat", {}, JSON.stringify({'content': $("#content").val(), 'to':$("#to").val(), 'from':$("#from").val()}));


}

function showContent(body) {
    //给记录 添加一条聊天记录
    $("#notice").append("<tr><td>" + body.content + "</td> <td>"+new Date(body.time).toLocaleString()+"</td></tr>");
}

$(function () {
    $("form").on('submit', function (e) {
        e.preventDefault();
    });
    $( "#connect" ).click(function() { connect(); });
    $( "#disconnect" ).click(function() { disconnect(); });
    $( "#send" ).click(function() { sendName(); });
});

开启Socket(建立通道)

1.var socket = new SockJS('/endpoint-websocket');    //建立一个SocketJS对象
	2 stompClient = Stomp.over(socket);   //用Stomp将SocketJS进行协议封装
	3.stompClient.connect();   //与服务端进行连接,同时有一个回调函数,处理连接成功后的操作信息。

发送消息

stompClient.send("/app/v3/single/chat", {}, value);
stompClient.send("/app/v3/single/chat", {}, JSON.stringify({'content': $("#content").val(), 'to':$("#to").val(), 'from':$("#from").val()}));
这里的value 封装了一个json将数据拼接后发送,后台进行解析并进行业务处理

演示效果

spring boot socket短连接 spring boot web socket_spring boot


spring boot socket短连接 spring boot web socket_java_02

客户端 小小 和 大大准备进行连接
连接成功后 打印对应的sessionId,业务处理时,sessionId用于唯一标识

spring boot socket短连接 spring boot web socket_客户端_03


连接后进行消息发送,此时可以看到 大大这边可以接收到消息。

spring boot socket短连接 spring boot web socket_websocket_04


spring boot socket短连接 spring boot web socket_客户端_05


此时后台打印的日志

spring boot socket短连接 spring boot web socket_服务端_06


可以看到消息体的相关内容都捕获到,可以进行后续的业务处理。

至此,WebSocket基础开发完毕

下面是对应后台的拦截器,监听器等。

拦截器:

package com.example.websocket.intecepter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.HandshakeInterceptor;
import javax.servlet.http.HttpSession;
import java.util.Map;

/**
 * 功能描述:http握手拦截器,可以通过这个类的方法获取resuest,和response
 */
public class HttpHandShakeIntecepter implements HandshakeInterceptor{

	@Override
	public boolean beforeHandshake(ServerHttpRequest request,
                                   ServerHttpResponse response, WebSocketHandler wsHandler,
                                   Map<String, Object> attributes) throws Exception {

		System.out.println("【握手拦截器】beforeHandshake");
		
		
		if(request instanceof ServletServerHttpRequest) {
			ServletServerHttpRequest servletRequest = (ServletServerHttpRequest)request;
			HttpSession session =  servletRequest.getServletRequest().getSession();
			String sessionId = session.getId();
			System.out.println("【握手拦截器】beforeHandshake sessionId="+sessionId);
			attributes.put("sessionId", sessionId);
		}
		
		return true;
	}

	
	@Override
	public void afterHandshake(ServerHttpRequest request,
                               ServerHttpResponse response, WebSocketHandler wsHandler,
                               Exception exception) {
		System.out.println("【握手拦截器】afterHandshake");
		
		if(request instanceof ServletServerHttpRequest) {
			ServletServerHttpRequest servletRequest = (ServletServerHttpRequest)request;
			HttpSession session =  servletRequest.getServletRequest().getSession();
			String sessionId = session.getId();
			System.out.println("【握手拦截器】afterHandshake sessionId="+sessionId);
		}
		
		
		
	}

}
package com.example.websocket.intecepter;
import com.alibaba.fastjson.JSONObject;
import com.example.websocket.Utils.Millisecond;
import com.example.websocket.controller.v6.UserChatController;
import com.example.websocket.model.InMessage;
import com.fasterxml.jackson.core.type.TypeReference;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.simp.stomp.StompCommand;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.messaging.support.ChannelInterceptorAdapter;
import java.util.Map;
/**
 * 功能描述:频道拦截器 ,类似管道,可以获取消息的一些meta数据
 */
@Slf4j
public class SocketChannelIntecepter extends ChannelInterceptorAdapter{

	/**
	 * 在完成发送之后进行调用,不管是否有异常发生,一般用于资源清理
	 */
	@Override
	public void afterSendCompletion(Message<?> message, MessageChannel channel,
			boolean sent, Exception ex) {
		System.out.println("SocketChannelIntecepter->afterSendCompletion");
		super.afterSendCompletion(message, channel, sent, ex);
	}

	/**
	 * 在消息被实际发送到频道之前调用
	 */
	@Override
	public Message<?> preSend(Message<?> message, MessageChannel channel) {
		System.out.println("SocketChannelIntecepter->preSend");

		return super.preSend(message, channel);
	}

	/**
	 * 发送消息调用后立即调用
	 */
	@Override
	public void postSend(Message<?> message, MessageChannel channel,
			boolean sent) {
		System.out.println("SocketChannelIntecepter->postSend");
		String s = new String((byte[]) message.getPayload());
		//InMessage inMessage = JSONObject.parseObject(s, new TypeReference<InMessage>(){});
		//System.out.println(inMessage);
		System.out.println(s);
		StompHeaderAccessor headerAccessor = StompHeaderAccessor.wrap(message);//消息头访问器
		if (headerAccessor.getCommand() == null ) {
			return ;// 避免非stomp消息类型,例如心跳检测
		}
		Map<String, Object> sessionAttributes = headerAccessor.getSessionAttributes();
		System.out.println(sessionAttributes);
		String sessionId = headerAccessor.getSessionAttributes().get("sessionId").toString();
		System.out.println("SocketChannelIntecepter -> sessionId = "+sessionId);
		StompCommand command = headerAccessor.getCommand();
		System.out.println(headerAccessor.getCommand());
		switch (headerAccessor.getCommand()) {
		case CONNECT:
			connect(sessionId);
			break;
		case DISCONNECT:
			disconnect(sessionId);
			break;
		case SUBSCRIBE:
			break;
		case UNSUBSCRIBE:
			break;
		default:
			break;
		}
	}
	long btime = 0;
	long etime = 0;
	//连接成功
	private void connect(String sessionId){
		System.out.println("连接成功 sessionId="+sessionId);
		System.out.println("connect sessionId="+sessionId);
		btime = System.currentTimeMillis();
		System.out.println(System.currentTimeMillis());
	}
	//断开连接
	private void disconnect(String sessionId){
		System.out.println("断开连接 sessionId="+sessionId);
		System.out.println("disconnect sessionId="+sessionId);
		//用户下线操作
		UserChatController.onlineUser.remove(sessionId);
		etime = System.currentTimeMillis();
		long difference = Millisecond.getDifference(btime, etime, 0);
		System.out.println("通讯时间--------->"+difference+"秒");
	}
}

监听器:

package com.example.websocket.listener;

import org.springframework.context.ApplicationListener;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.messaging.SessionConnectEvent;

@Component
public class ConnectEventListener implements ApplicationListener<SessionConnectEvent> {

	public void onApplicationEvent(SessionConnectEvent event) {
		StompHeaderAccessor headerAccessor =  StompHeaderAccessor.wrap(event.getMessage());
		System.out.println("【ConnectEventListener监听器事件 类型】"+headerAccessor.getCommand().getMessageType());
		
		
	}

}
package com.example.websocket.listener;

import org.springframework.context.ApplicationListener;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.messaging.SessionSubscribeEvent;


/**
 * 功能描述:springboot使用,订阅事件
 */
@Component
public class SubscribeEventListener implements ApplicationListener<SessionSubscribeEvent> {

	/**
	 * 在事件触发的时候调用这个方法
	 * 
	 * StompHeaderAccessor  简单消息传递协议中处理消息头的基类,
	 * 通过这个类,可以获取消息类型(例如:发布订阅,建立连接断开连接),会话id等
	 * 
	 */
	public void onApplicationEvent(SessionSubscribeEvent event) {
		StompHeaderAccessor headerAccessor =  StompHeaderAccessor.wrap(event.getMessage());
		System.out.println("【SubscribeEventListener监听器事件 类型】"+headerAccessor.getCommand().getMessageType());
		System.out.println("【SubscribeEventListener监听器事件 sessionId】"+headerAccessor.getSessionAttributes().get("sessionId"));
		
	}

}