WebSocket protocol 是HTML5一种新的协议。它实现了浏览器与服务器全双工通信(full-duplex)。
在 WebSocket API,浏览器和服务器只需要要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。在此WebSocket 协议中,为我们实现即时服务带来了两大好处:
1. Header
互相沟通的Header是很小的-大概只有 2 Bytes
2. Server Push
目前支持websocket的浏览器有:
Chrome | Supported in version 4+ |
Firefox | Supported in version 4+ |
Internet Explorer | Supported in version 10+ |
Opera | Supported in version 10+ |
Safari | Supported in version 5+ |
对于那些不支持websocket的浏览器,可以使用SockJS来协调支持,该库目前提供的模拟方式已经支持绝大多是浏览器了。具体情况如下:
Browser | Websockets | Streaming | Polling |
---|---|---|---|
IE 6, 7 | no | no | jsonp-polling |
IE 8, 9 (cookies=no) | no | xdr-streaming † | xdr-polling † |
IE 8, 9 (cookies=yes) | no | iframe-htmlfile | iframe-xhr-polling |
IE 10 | rfc6455 | xhr-streaming | xhr-polling |
Chrome 6-13 | hixie-76 | xhr-streaming | xhr-polling |
Chrome 14+ | hybi-10 / rfc6455 | xhr-streaming | xhr-polling |
Firefox <10 | no ‡ | xhr-streaming | xhr-polling |
Firefox 10+ | hybi-10 / rfc6455 | xhr-streaming | xhr-polling |
Safari 5 | hixie-76 | xhr-streaming | xhr-polling |
Opera 10.70+ | no ‡ | iframe-eventsource | iframe-xhr-polling |
Opera 12.10+ | rfc6455 | xhr-streaming | xhr-polling |
Konqueror | no | no | jsonp-polling |
从spring4开始,加入了对websocket的支持,同时也加入了对SockJS、STOMP的支持,现在通过Spring可以非常方便的创建我们的WebSocket应用服务。
进入正题,使用maven搭建spring的webmvc环境。pom.xml的内容如下:
<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/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.zhm.spring</groupId>
<artifactId>ws_test</artifactId>
<packaging>war</packaging>
<version>1.0-SNAPSHOT</version>
<name>ws_test Maven Webapp</name>
<url>http://maven.apache.org</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<spring.version>4.1.6.RELEASE</spring.version>
<jackson.version>2.5.4</jackson.version>
<jetty.version>6.1.23</jetty.version>
</properties>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.10</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-websocket</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-messaging</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet.jsp.jstl</groupId>
<artifactId>jstl-api</artifactId>
<version>1.2</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>javax.servlet.jsp-api</artifactId>
<version>2.3.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>${jackson.version}</version>
</dependency>
</dependencies>
<build>
<finalName>ROOT</finalName>
<plugins>
<plugin>
<artifactId>maven-war-plugin</artifactId>
<version>2.6</version>
<configuration>
<packagingExcludes>
%regex[WEB-INF/lib/.*]
</packagingExcludes>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.tomcat.maven</groupId>
<artifactId>tomcat7-maven-plugin</artifactId>
<version>2.2</version>
<configuration>
<port>9090</port>
<path>/</path>
<uriEncoding>UTF-8</uriEncoding>
<finalName>ROOT</finalName>
<server>tomcat7</server>
</configuration>
</plugin>
</plugins>
</build>
</project>
web.xml的配置(其中<async-supported>true</async-supported>配置在使用非W3C WebSocket连接的时候需要加上):
<web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_1.xsd">
<filter>
<filter-name>encodingFilter</filter-name>
<filter-class>
org.springframework.web.filter.CharacterEncodingFilter
</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>utf-8</param-value>
</init-param>
<init-param>
<param-name>forceEncoding</param-name>
<param-value>true</param-value>
</init-param>
<async-supported>true</async-supported>
</filter>
<filter-mapping>
<filter-name>encodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<servlet>
<servlet-name>dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring/dispatcher-servlet.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
<async-supported>true</async-supported>
</servlet>
<servlet-mapping>
<servlet-name>dispatcher</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<display-name>Archetype Created Web Application</display-name>
</web-app>
dispatcher-server.xml的配置如下:
<?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:context="http://www.springframework.org/schema/context"
xmlns:websocket="http://www.springframework.org/schema/websocket"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.1.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.1.xsd
http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.1.xsd
http://www.springframework.org/schema/websocket http://www.springframework.org/schema/websocket/spring-websocket.xsd">
<!-- websocket消息接收与处理类 -->
<bean id="websocket" class="com.zhm.spring.ws.websocket.WebSocketEndPoint"/>
<!-- 定义客户端与服务器握手的拦截器,可以做一些预处理 -->
<!-- 该拦截器专门为SockJS客户端服务的 -->
<websocket:handlers>
<websocket:mapping path="/sockjs/websocket" handler="websocket"/>
<websocket:handshake-interceptors>
<bean class="com.zhm.spring.ws.websocket.HandshakeInterceptor"/>
</websocket:handshake-interceptors>
<!-- 开启sockjs支持 -->
<websocket:sockjs />
</websocket:handlers>
<!-- 定义客户端与服务器握手的拦截器,可以做一些预处理 -->
<!-- 该拦截器专门为WebSocket客户端服务的 -->
<websocket:handlers>
<websocket:mapping path="/websocket" handler="websocket"/>
<websocket:handshake-interceptors>
<bean class="com.zhm.spring.ws.websocket.HandshakeInterceptor"/>
</websocket:handshake-interceptors>
</websocket:handlers>
<mvc:view-resolvers>
<mvc:jsp />
</mvc:view-resolvers>
<mvc:annotation-driven />
<context:component-scan base-package="com.zhm.spring" />
</beans>
WebSocketEndPoint代码:
package com.zhm.spring.ws.websocket;
import com.zhm.spring.ws.utils.Constants;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;
import java.util.ArrayList;
/**
* Created by zhm on 2015/7/14.
*/
public class WebSocketEndPoint extends TextWebSocketHandler{
private static final ArrayList<WebSocketSession> users;
static {
users = new ArrayList<WebSocketSession>();
}
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
users.add(session);
super.afterConnectionEstablished(session);
}
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
users.remove(session);
super.handleTransportError(session, exception);
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
users.remove(session);
super.afterConnectionClosed(session, status);
}
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
super.handleTextMessage(session, message);
TextMessage returnMessage = new TextMessage(session.getAttributes().get(Constants.SESSION_USERNAME.value())+" : "+message.getPayload());
// session.sendMessage(returnMessage);
sendToAllClients(returnMessage, session);
}
private void sendToAllClients(TextMessage msg,WebSocketSession curSession) {
try
{
for(WebSocketSession user : users)
{
if(user.isOpen()) {
if (!user.getId().equals(curSession.getId())){
user.sendMessage(msg);
}
}
else
{
users.remove(user.getId());
}
}
}catch(Exception e)
{
e.printStackTrace();
}
}
}
继承TextWebSocketHandler,并复写一些方法。很好理解。
HandshakeInterceptor的源码如下:
package com.zhm.spring.ws.websocket;
import com.zhm.spring.ws.utils.Constants;
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.support.HttpSessionHandshakeInterceptor;
import javax.servlet.http.HttpSession;
import java.util.Map;
/**
* Created by zhm on 2015/7/14.
*/
public class HandshakeInterceptor extends HttpSessionHandshakeInterceptor {
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
if (request instanceof ServletServerHttpRequest) {
ServletServerHttpRequest servletRequest = (ServletServerHttpRequest) request;
HttpSession session = servletRequest.getServletRequest().getSession(false);
if (session != null) {
//使用userName区分WebSocketHandler,以便定向发送消息
String userName = (String) session.getAttribute(Constants.SESSION_USERNAME.value());
attributes.put(Constants.SESSION_USERNAME.value(),userName);
}
}
return super.beforeHandshake(request, response, wsHandler, attributes);
}
@Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception ex) {
super.afterHandshake(request, response, wsHandler, ex);
}
}
主要做一些握手的之前的预处理工作。
最后就是写个HomeController加载客户端页面了。
package com.zhm.spring.ws.controller;
import com.zhm.spring.ws.utils.Constants;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpSession;
/**
* Created by zhm on 2015/7/14.
*/
@Controller
public class HomeController {
@RequestMapping(value="/webchat/{username}")
public String webchat(@PathVariable String username,HttpSession session){
session.setAttribute(Constants.SESSION_USERNAME.value(),username);
return "websocket";
}
}
websocket.jsp的代码如下:
<%@ page import="com.zhm.spring.ws.utils.Constants" %>
<%--
Created by IntelliJ IDEA.
User: zhm
Date: 2015/7/14
Time: 10:35
To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<!DOCTYPE html>
<html>
<head>
<title>Spring4 websocket实例</title>
<meta charset="utf-8">
<style type="text/css">
#connect-container {
float: left;
width: 400px
}
#connect-container div {
padding: 5px;
}
#console-container {
float: left;
margin-left: 15px;
width: 400px;
}
#console {
border: 1px solid #CCCCCC;
border-right-color: #999999;
border-bottom-color: #999999;
height: 170px;
overflow-y: scroll;
padding: 5px;
width: 100%;
}
#console p {
padding: 0;
margin: 0;
}
</style>
<script src="http://cdn.sockjs.org/sockjs-0.3.min.js"></script>
<script type="text/javascript">
var ws = null;
var url = null;
var transports = [];
function setConnected(connected) {
document.getElementById('connect').disabled = connected;
document.getElementById('disconnect').disabled = !connected;
document.getElementById('echo').disabled = !connected;
}
function connect() {
if (!url) {
alert('请选择使用W3C的websocket还是SockJS');
return;
}
ws = (url.indexOf('sockjs') != -1) ?
new SockJS(url, undefined, {protocols_whitelist: transports}) : new WebSocket(url);
ws.onopen = function () {
setConnected(true);
log('Info: 连接成功.');
};
ws.onmessage = function (event) {
log(event.data);
};
ws.onclose = function (event) {
setConnected(false);
log('Info: 断开连接.');
log(event);
};
}
function disconnect() {
if (ws != null) {
ws.close();
ws = null;
}
setConnected(false);
}
function echo() {
if (ws != null) {
var message = document.getElementById('message').value;
log('<%=session.getAttribute(Constants.SESSION_USERNAME.value())%>:' + message);
ws.send(message);
} else {
alert('没有建立连接,请连接服务!');
}
}
function updateUrl(urlPath) {
if (urlPath.indexOf('sockjs') != -1) {
url = urlPath;
document.getElementById('sockJsTransportSelect').style.visibility = 'visible';
}
else {
if (window.location.protocol == 'http:') {
url = 'ws://' + window.location.host + urlPath;
} else {
url = 'wss://' + window.location.host + urlPath;
}
document.getElementById('sockJsTransportSelect').style.visibility = 'hidden';
}
}
function updateTransport(transport) {
alert(transport);
transports = (transport == 'all') ? [] : [transport];
}
function log(message) {
var console = document.getElementById('console');
var p = document.createElement('p');
p.style.wordWrap = 'break-word';
p.appendChild(document.createTextNode(message));
console.appendChild(p);
while (console.childNodes.length > 25) {
console.removeChild(console.firstChild);
}
console.scrollTop = console.scrollHeight;
}
</script>
</head>
<body>
<noscript><h2 style="color: #ff0000">Seems your browser doesn't support Javascript! Websockets
rely on Javascript being enabled. Please enable
Javascript and reload this page!</h2></noscript>
<div>
<div id="connect-container">
<input id="radio1" type="radio" name="group1" onclick="updateUrl('/websocket');">
<label for="radio1">W3C WebSocket</label>
<br>
<input id="radio2" type="radio" name="group1" onclick="updateUrl('/sockjs/websocket');">
<label for="radio2">SockJS</label>
<div id="sockJsTransportSelect" style="visibility:hidden;">
<span>SockJS transport:</span>
<select onchange="updateTransport(this.value)">
<option value="all">all</option>
<option value="websocket">websocket</option>
<option value="xhr-polling">xhr-polling</option>
<option value="jsonp-polling">jsonp-polling</option>
<option value="xhr-streaming">xhr-streaming</option>
<option value="iframe-eventsource">iframe-eventsource</option>
<option value="iframe-htmlfile">iframe-htmlfile</option>
</select>
</div>
<div>
<button id="connect" onclick="connect();">连接服务器</button>
<button id="disconnect" disabled="disabled" onclick="disconnect();">断开连接</button>
</div>
<div>
<textarea id="message" style="width: 350px">测试消息!</textarea>
</div>
<div>
<button id="echo" onclick="echo();" disabled="disabled">发送消息</button>
</div>
</div>
<div id="console-container">
<div id="console"></div>
</div>
</div>
</body>
</html>
完整实例下载地址:https://git.oschina.net/zhmlvft/ws_test