一、为什么需要stomp?

        websocket 协议是一种相当低级的协议。它定义了如何将字节流转换为帧。帧可以包含文本或二进制消息。由于消息本身不提供有关如何路由或处理它的任何其他信息,因此很难在不编写其他代码的情况下实现更复杂的应用程序。幸运的是,websocket 规范允许在更高的应用程序级别上使用子协议。

        另外,单单使用websocket完成群聊、私聊功能时,需要自己管理session信息,通过stomp协议时,spring已经封装好,开发者只需要关注自己的主题、订阅关系即可。

二、stomp详解

        stomp 中文为“面向消息的简单文本协议”,stomp 提供了能够协作的报文格式,以至于 stomp 客户端可以与任何 stomp 消息代理(brokers)进行通信,从而为多语言,多平台和 brokers 集群提供简单且普遍的消息协作。stomp 协议可以建立在 websocket 之上,也可以建立在其他应用层协议之上。通过 websocket建立 stomp 连接,也就是说在 websocket 连接的基础上再建立 stomp 连接。最终实现如上图所示,这一点可以在代码中有一个良好的体现。

业界已经有很多优秀的 stomp 的服务器/客户端的开源实现

  • stomp 服务器:activemq、rabbitmq、stompserver、…
  • stomp 客户端库:stomp.js(javascript)

        stomp 的特点是客户端的实现很容易,服务端相当于消息队列的 broker 或者是 server,一般不需要我们去实现,所以重点关注一下客户端如何使用

  • connect 启动与服务器的流或 tcp 连接
  • send发送消息
  • subscribe 订阅主题
  • unsubscribe 取消订阅
  • begin 启动事物
  • commit提交事物
  • abort回滚事物
  • ack确认来自订阅的消息的消费
  • nack告诉服务器客户端没有消费该消息
  • disconnect断开连接

        其实stomp协议并不是为ws所设计的, 它其实是消息队列的一种协议, 和amqp,jms是平级的。 只不过由于它的简单性恰巧可以用于定义ws的消息体格式。 目前很多服务端消息队列都已经支持了stomp, 比如rabbitmq, apache activemq等。很多语言也都有stomp协议的客户端解析库,像java的gozirra,c的libstomp,python的pyactivemq,javascript的stomp.js等等。

stomp协议官方文档

三、springboot集成stomp代码示例

3.1、功能示例

 

 

3.2、架构图

3.3、服务端代码

pom文件引入jar

<?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>org.example</groupid>
    <artifactid>websocket-demo</artifactid>
    <version>1.0-snapshot</version>

    <parent>
        <groupid>org.springframework.boot</groupid>
        <artifactid>spring-boot-starter-parent</artifactid>
        <version>2.3.10.release</version>
        <relativepath/> <!-- lookup parent from repository -->
    </parent>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupid>org.springframework.boot</groupid>
            <artifactid>spring-boot-starter-web</artifactid>
        </dependency>
        <dependency>
            <groupid>org.apache.commons</groupid>
            <artifactid>commons-lang3</artifactid>
        </dependency>

        <dependency>
            <groupid>org.projectlombok</groupid>
            <artifactid>lombok</artifactid>
            <version>1.18.12</version>
        </dependency>

        <dependency>
            <groupid>org.springframework.boot</groupid>
            <artifactid>spring-boot-starter-websocket</artifactid>
        </dependency>

        <dependency>
            <groupid>org.webjars</groupid>
            <artifactid>webjars-locator-core</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>
        <dependency>
            <groupid>org.webjars</groupid>
            <artifactid>bootstrap</artifactid>
            <version>3.3.7</version>
        </dependency>
        <dependency>
            <groupid>org.webjars</groupid>
            <artifactid>jquery</artifactid>
            <version>3.1.0</version>
        </dependency>


        <dependency>
            <groupid>com.alibaba</groupid>
            <artifactid>fastjson</artifactid>
            <version>1.2.62</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupid>org.apache.maven.plugins</groupid>
                <artifactid>maven-compiler-plugin</artifactid>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

websocketmessagebroker配置类

@configuration
@enablewebsocketmessagebroker
public class websocketconfig implements websocketmessagebrokerconfigurer
{
	// 启用一个简单的基于内存的消息代理
	@override
	public void configuremessagebroker(messagebrokerregistry config) {
		//通过/topic 开头的主题可以进行订阅
		config.enablesimplebroker("/topic");
		//send命令时需要带上/app前缀
		config.setapplicationdestinationprefixes("/app");
        //修改convertandsendtouser方法前缀, 稍后解释作用
//		config.setuserdestinationprefix ("/myuserprefix");
	}
	@override
	public void registerstompendpoints(stompendpointregistry registry) {
		//连接前缀
		registry.addendpoint("/gs-guide-websocket")
				.setallowedorigins("*")  // 跨域处理
				.withsockjs();  //支持socketjs
	}
}

@enablewebsocketmessagebroker注解启用 websocket 消息处理,由消息代理支持。

sockjs 有一些浏览器中缺少对 websocket 的支持,而 sockjs 是一个浏览器的 javascript库,它提供了一个类似于网络的对象,sockjs 提供了一个连贯的,跨浏览器的javascriptapi,它在浏览器和 web 服务器之间创建了一个低延迟、全双工、跨域通信通道。sockjs 的一大好处在于提供了浏览器兼容性。即优先使用原生websocket,如果浏览器不支持 websocket,会自动降为轮询的方式。如果你使用 java 做服务端,同时又恰好使用 spring framework 作为框架,那么推荐使用sockjs。

控制器代码

@slf4j
@restcontroller
public class testcontroller
{
	@autowired
	private simpmessagingtemplate simpmessagingtemplate;

	@messagemapping("/hello")
	@sendto ("/topic/greetings")
	public greeting greeting(hellomessage message) throws exception {
		thread.sleep(1000); // simulated delay
		return new greeting("hello, " + htmlutils.htmlescape(message.getname()) + "!");
	}

	@messagemapping("/topic/greetings")
	public greeting greeting2(hellomessage message) throws exception {
		thread.sleep(1000); // simulated delay
		log.info ("hello, " + htmlutils.htmlescape(message.getname()) + "!");
		return new greeting("hello, " + htmlutils.htmlescape(message.getname()) + "!");
	}
	@getmapping ("/hello2")
	public void greeting3(hellomessage message) throws exception {
		thread.sleep(1000); // simulated delay
		simpmessagingtemplate.convertandsend ("/topic/greetings",
				new greeting("hello, " + htmlutils.htmlescape(message.getname()) + "!"));
	}


	@messagemapping("/sendtouser")
	public void sendtouser(hellomessage message) throws exception {
		thread.sleep(1000); // simulated delay
		log.info ("userid:{},msg:{}",message.getuserid (),message.getname ());
//		simpmessagingtemplate.convertandsendtouser (message.getuserid (),"/sendtouser",
//				new greeting("hello, " + htmlutils.htmlescape(message.getname()) + "!"));
//		simpmessagingtemplate.convertandsend ("/user/1/sendtouser",
//				new greeting("hello, " + htmlutils.htmlescape(message.getname()) + "!"));
		simpmessagingtemplate.convertandsend ("/topic/user/"+message.getuserid ()+"/sendtouser",
				new greeting("hello, " + htmlutils.htmlescape(message.getname()) + "!"));
	}
}
  • @messagemapping 功能与requestmapping注解类似。send指令发送信息时添加此注解
  • @sendto/@sendtouser 将信息输出到该主题。客户端订阅同样的主题后就会收到信息。
  • 在只有指定@messagemapping@messagemapping == “/topic” + @sendto
  • 如果想使用rest接口发送消息。可以通过simpmessagingtemplate进行发送。
  • 点对点聊天时,可以使用simpmessagingtemplate.convertandsendtouser方法发送。个人意味比注解@sendtouser更加容易理解,更加方便
  • convertandsendtouser方法和convertandsend类似,区别在于convertandsendtouser方法会在主题默认添加/user/为前缀。因此,示例代码中convertandsend方法直接传入"/topic/user/"+message.getuserid ()+"/sendtouser" 也是点对点发送。topic其中是默认前缀。
  • 如果想修改convertandsendtouser默认前缀可在配置类进行配置,可在websocketconfig类中查看。

3.4、h5代码

<!doctype html>
<html>
<head>
    <title>hello websocket</title>
    <link href="/webjars/bootstrap/css/bootstrap.min.css" rel="external nofollow"  rel="stylesheet">
    <link href="/main.css" rel="external nofollow"  rel="stylesheet">
    <script type="text/javascript" src="https://cdn.bootcss.com/jquery/3.2.1/jquery.min.js"></script>

    <script src="/webjars/sockjs-client/sockjs.min.js"></script>
    <script src="/webjars/stomp-websocket/stomp.min.js"></script>
    <script src="/app.js"></script>
</head>
<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">websocket connection:</label>
                    <button id="connect" class="btn btn-default" type="submit">connect</button>
                    <button id="disconnect" class="btn btn-default" type="submit" disabled="disabled">disconnect
                    </button>
                </div>
            </form>
        </div>
        <div class="col-md-6">
            <form class="form-inline">
                <div class="form-group">
                    <label for="name">what is your name?</label>
                    <input type="text" id="name" class="form-control" placeholder="your name here...">
                    <input type="text" id="userid" class="form-control" placeholder="userid">
                </div>
                <button id="send" class="btn btn-default" type="submit">send</button>
                <button id="send2" class="btn btn-default" type="submit">send2</button>

                <button id="send3" class="btn btn-default" type="submit">sendtouser</button>

            </form>
        </div>
    </div>
    <div class="row">
        <div class="col-md-12">
            <table id="conversation" class="table table-striped">
                <thead>
                <tr>
                    <th>greetings</th>
                </tr>
                </thead>
                <tbody id="greetings">
                </tbody>
            </table>
        </div>
    </div>
</div>
</body>
</html>

app.js

var stompclient = null;
var userid = null;
function setconnected(connected) {
    $("#connect").prop("disabled", connected);
    $("#disconnect").prop("disabled", !connected);
    if (connected) {
        $("#conversation").show();
    }
    else {
        $("#conversation").hide();
    }
    $("#greetings").html("");
}

function connect() {
    var socket = new sockjs('/gs-guide-websocket');
    stompclient = stomp.over(socket);
    stompclient.connect({}, function (frame) {
        setconnected(true);
        console.log('connected: ' + frame);
        stompclient.subscribe('/topic/greetings', function (greeting) {
            showgreeting(json.parse(greeting.body).content);
        });
        //对应controller greeting2方法  注意,这儿有两个topic
         stompclient.subscribe('/topic/topic/greetings', function (greeting) {
                showgreeting(json.parse(greeting.body).content);
          });
  stompclient.subscribe('/topic/user/'+userid+'/sendtouser', function (greeting) {
                          showgreeting(json.parse(greeting.body).content);
                    });
           stompclient.subscribe('/user/'+userid+'/sendtouser', function (greeting) {
                          showgreeting(json.parse(greeting.body).content);
                    });
    });
}

function disconnect() {
    if (stompclient !== null) {
        stompclient.disconnect();
    }
    setconnected(false);
    console.log("disconnected");
}

function sendname() {
    stompclient.send("/app/hello", {}, json.stringify({'name': $("#name").val()}));
//    stompclient.send("/hello", {}, json.stringify({'name': $("#name").val()}));
}
function sendname2() {
    stompclient.send("/app/topic/greetings", {}, json.stringify({'name': $("#name").val()}));
//    stompclient.send("/topic/greetings", {}, json.stringify({'name': $("#name").val()}));
}
function sendname3() {
    stompclient.send("/app/sendtouser", {}, json.stringify({'userid':$("#userid").val(),'name': $("#name").val()}));
}

function showgreeting(message) {
    $("#greetings").append("<tr><td>" + message + "</td></tr>");
}
function getquerystring(name) {
	var reg = new regexp("(^|&)" + name + "=([^&]*)(&|$)", "i");
	var r = window.location.search.substr(1).match(reg); //获取url中"?"符后的字符串并正则匹配
	var context = "";
	if (r != null)
		context = r[2];
	reg = null;
	r = null;
	return context == null || context == "" || context == "undefined" ? "" : context;
}
$(function () {
    userid =  getquerystring("userid");
    $("form").on('submit', function (e) {
        e.preventdefault();
    });
    $( "#connect" ).click(function() { connect(); });
    $( "#disconnect" ).click(function() { disconnect(); });
    $( "#send" ).click(function() { sendname(); });
    $( "#send2" ).click(function() { sendname2(); });
    $( "#send3" ).click(function() { sendname3(); });

});

一些无关紧要的类

public class greeting
{
   private string content;

   public greeting() {
   }

   public greeting(string content) {
      this.content = content;
   }

   public string getcontent() {
      return content;
   }
}
public class hellomessage
{
   private string userid;
   private string name;
    // 省去get/set
}
name3(); });

});

到此这篇关于springboot+stomp协议实现私聊、群聊的文章就介绍到这了,更多相关springboot stomp私聊、群聊内容请搜索www.887551.com以前的文章或继续浏览下面的相关文章希望大家以后多多支持www.887551.com!