Spring4+WebSocket小示例

一、简介
    WebSocket协议为web应用程序定义了一种全新的重要能力:在客户端和服务器端之间可以进行全双工的双向通信。简短来说,Websocket协议是先用http做初始化的握手,之后利用http向服务器发送一个协议升级(或者协议变化)的请求,如果服务器同意会返回一个101的状态码。如果这个握手成功,http请求升级后对应的底层的tcp socket会一直保持着打开状态,服务端和客户端就都可以利用它来发送消息。
二、后备选项
  做WebSocket应用的一个重要挑战就是要考虑不支持WebSocket的浏览器,如IE从IE10开始才支持(具体的浏览器支持情况可以查看 http://caniuse.com/websockets)。后备选项指的就是当需要时能够通过其他方式来模拟Websocket API从而在不支持Websocket的浏览器中利用Websocket的功能。
  Spring FrameWork基于SockJs protocal对此提供的透明的支持,可以通过配置而不用修改应用程序代码。SockJs的具体内容可以参考 https://github.com/sockjs/sockjs-protocol
三、Websocket中的子协议
  Websocket定义了消息的架构,但是没有规定具体的协议。而直接用TPCP把字节流转换成消息流是极其笨重的,而且不像应用层协议的HTTP,对于一个到来的信息,在WebSocket协议中是没有足够的信息来使framework或者容器知道如何去路由它,以及如何处理它。因此Websocket对于应用程序而言太低层太琐碎,就像现在大多数web应用都会采用一种web 框架,而不是直接用servlet api。
  基于这个原因,Websocket RFC定义了子协议。在握手过程中,服务端和客户端可以利用请求头中的 Sec-WebSocket-Protocol来协定一个高层次的应用级别的子协议。虽然子协议不是必须的,但是即便不用,你的应用程序也需要定义一种客户端和服务端都能识别的消息格式。
  在Spring Framework中提供了STOMP-一种简单的面向文本的消息协议。虽然STOMP是面向文本的,但是消息内容不仅仅局限于文本,也可以是二进制类型。STOMP的具体内容可以参考 http://stomp.github.io/stomp-specification-1.2.html
四、什么时候要用Websocket
  web应用中最适合采用Websocket的是服务端和客户端以高频率低延时交换信息的场合。例如金融、股票、游戏等,这些都是对延时非常敏感,而且交换信息的频率很高的行业。
五、示例
本示例没有用maven,采用xml的配置形式
1.web.xml
和普通的spring mvc没有区别,作用
a.定义了一个servlet拦截器,以及Mapping规则
b.引入spring-context配置文件
c.添加log4j配置文件
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns="http://xmlns.jcp.org/xml/ns/javaee"
    xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
    id="WebApp_ID" version="3.1">
    <display-name>SpringTest</display-name>
    <!-- Processes application requests -->
    <servlet>
        <servlet-name>appServlet</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>/WEB-INF/spring/appServlet/servlet-context.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
        <async-supported>true</async-supported>
    </servlet>

    <context-param>
        <param-name>log4jConfigLocation</param-name>
        <param-value>/resources/log4j/log4j.properties</param-value>
    </context-param>
    <listener>
        <listener-class>org.springframework.web.util.Log4jConfigListener</listener-class>
    </listener>
    <servlet-mapping>
        <servlet-name>appServlet</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>

    <!-- Disables Servlet Container welcome file handling. Needed for compatibility 
        with Servlet 3.0 and Tomcat 7.0 -->
    <welcome-file-list>
        <welcome-file>index.html</welcome-file>
    </welcome-file-list>
</web-app>

2.DispatcherServlet Context配置文件,作用
a.激活注解配置功能
b.配置资源文件路径
c.配置视图层技术
d.引入具体组件配置文档
e.配置websocket相关内容
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:mvc="http://www.springframework.org/schema/mvc"
	xmlns:websocket="http://www.springframework.org/schema/websocket"
	xsi:schemaLocation="
		http://www.springframework.org/schema/beans
		http://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/mvc
		http://www.springframework.org/schema/mvc/spring-mvc.xsd
		http://www.springframework.org/schema/websocket
		http://www.springframework.org/schema/websocket/spring-websocket.xsd
		">

	<!-- DispatcherServlet Context: defines this servlet's request-processing 
		infrastructure -->

	<!-- Enables the Spring MVC @Controller programming model -->
	<mvc:annotation-driven/>

	<!-- Handles HTTP GET requests for /resources/** by efficiently serving 
		up static resources in the ${webappRoot}/resources/ directory -->
	<mvc:resources mapping="/resources/**" location="/resources/" />
	<!-- Resolves views selected for rendering by @Controllers to .jsp resources 
		in the /WEB-INF/views directory -->
<!-- 	<bean -->
<!-- 		class="org.springframework.web.servlet.view.InternalResourceViewResolver"> -->
<!-- 		<property name="prefix" value="/WEB-INF/views/" /> -->
<!-- 		<property name="suffix" value=".jsp" /> -->
<!-- 	</bean> -->
	<bean id="templateResolver"
		class="org.thymeleaf.spring4.templateresolver.SpringResourceTemplateResolver">
		<property name="prefix" value="/WEB-INF/views/" />
		<property name="suffix" value=".html" />
		<!-- Template cache is true by default. Set to false if you want -->
		<!-- templates to be automatically updated when modified. -->
		<property name="cacheable" value="false" />
	</bean>
	<!-- SpringTemplateEngine automatically applies SpringStandardDialect and -->
	<!-- enables Spring's own MessageSource message resolution mechanisms. -->
	<bean id="templateEngine" class="org.thymeleaf.spring4.SpringTemplateEngine">
		<property name="templateResolver" ref="templateResolver" />
	</bean>
	
	<bean class="org.thymeleaf.spring4.view.ThymeleafViewResolver">
        <property name="templateEngine" ref="templateEngine" />
    </bean>
	
	<!-- Imports user-defined @Controller beans that process client requests -->
	<import resource="controllers.xml" />
	<!-- Enable STOMP over WebSocket -->
	<websocket:message-broker application-destination-prefix="/app">
		<websocket:stomp-endpoint path="/websocket">
			<websocket:sockjs />
		</websocket:stomp-endpoint>
		<websocket:simple-broker prefix="/topic, /queue" />
	</websocket:message-broker>
</beans>


关键点:
a.头部引入了「xmlns:websocket=“http://www.springframework.org/schema/websocket”」,websocket对应的命名空间
b.「<websocket:message-broker application-destination-prefix=“/app”>」标明以app开头的请求将被认为是websocket请求
c.「<websocket:stomp-endpoint path=“/websocket”>」注册一个websocket的endpoint,并采用stomp作为子协议,这个endpoint会在websocket handshake时用到(js中初始化websocket)
d.「<websocket:sockjs />」表示启用sockjs备用选项,以便在不支持websocket的浏览器端正常模拟出websocket的效果
e.「<websocket:simple-broker prefix="/topic, /queue" />」标明broker采用SimpleBroker,并定义broker请求对应的地址是topic和queue
3.组件配置文档,主要作用
a.配置默认页面路径
b.配置组件扫描路径
具体文件
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:mvc="http://www.springframework.org/schema/mvc"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd
        http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd">

    <!-- Maps '/' requests to the 'home' view -->
    <mvc:view-controller path="/" view-name="views/index.html"/>
    
    <context:component-scan base-package="test.spring.socket"/>
     
</beans>

4.代码
a.controller 接受用户请求,包括http请求和websocket请求
package test.spring.socket;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

import test.spring.socket.service.GreetingService;

@Controller
public class GreetingController {

	private static final Log logger = LogFactory.getLog(GreetingController.class);

	@Autowired
	private GreetingService service;

	@MessageMapping("/hello")
	@SendTo("/topic/greetings")
	public Greeting greeting(HelloMessage message) throws Exception {
		service.greeting();
		return new Greeting("Hello, " + message.getName() + "!");
	}

	@RequestMapping("/index")
	public String hello() {
		logger.info("start to maping request hell");
		return "socket/index";
	}

}


b.service 模拟逻辑处理,处理完成后发送消息给broker
/**
 * 
 */
package test.spring.socket.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Service;

import test.spring.socket.Greeting;

/**
 * @author dwxx-chengaofeng
 *
 */
@Service
public class GreetingService {
	@Autowired
	private SimpMessagingTemplate template;

	public void greeting() throws InterruptedException {
		for (int i = 0; i < 10; i++) {
			Thread.sleep(1000); // simulated delay
			template.convertAndSend("/topic/greetings", new Greeting("the number is" + i));
		}
	}
}


c.html代码
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>Hello WebSocket</title>
    <link th:href="@{/resources/socket/css/main.css}" rel="stylesheet">
    <script type="text/javascript" th:src="@{/resources/js/jquery-2.0.3.js}"></script>
    <script type="text/javascript" th:src="@{/resources/socket/js/sockjs.js}"></script>
    <script type="text/javascript" th:src="@{/resources/socket/js/stomp.js}"></script>
    <script type="text/javascript" th:src="@{/resources/socket/js/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...">
                </div>
                <button id="send" class="btn btn-default" type="submit">Send</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>
    </form>
</div>
</body>
</html>

d.js代码
var stompClient = 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('/SpringWebsocket/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);
        });
    });
}

function disconnect() {
    if (stompClient != null) {
        stompClient.disconnect();
    }
    setConnected(false);
    console.log("Disconnected");
}

function sendName() {
    stompClient.send("/app/hello", {}, JSON.stringify({'name': $("#name").val()}));
}

function showGreeting(message) {
    $("#greetings").append("<tr><td>" + message + "</td></tr>");
}

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


关键点
a.「var socket = new SockJS(‘/SpringWebsocket/websocket’);」初始化一个SockJs对象,其中SpringWebsocket是自己的工程名,websocket是在Servlet-context.xml中配置的websocket的endpoint
b.「stompClient.send("/app/hello", {}, JSON.stringify({'name': $(“#name”).val()}));」发送消息到websocket,对应的方法是controller中的@MessageMapping(“/hello”)注解方法,注意多了个app前缀,是在Servlet-context.xml中配置的application-destination-prefix
c.「stompClient.subscribe(‘/topic/greetings’, function (greeting) {
            showGreeting(JSON.parse(greeting.body).content);
        });」表示从broker中订阅/topic/greetings,当有消息发送到/topic/greetings时,客户端就可以收到通知,并获取消息的内容。
最终的工程目录:



工程的具体代码请参考附件

猜你喜欢

转载自fengyilin.iteye.com/blog/2341052
今日推荐