websocket协议简介
WebSocket 是 HTML5 一种新的协议。它实现了浏览器与服务器全双工通信,能更好的节省服务器资源和带宽并达到实时通讯,它建立在 TCP 之上,同 HTTP 一样通过 TCP 来传输数据,但是它和 HTTP 最大不同是:
WebSocket 是一种双向通信协议,在建立连接后,WebSocket 服务器和 Browser/Client Agent 都能主动的向对方发送或接收数据,就像 Socket 一样;
WebSocket 需要类似 TCP 的客户端和服务器端通过握手连接,连接成功后才能相互通信。
搭建环境
使用maven做版本构建工具
<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.springframework.samples.service.service</groupId>
<artifactId>websocket</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>war</packaging>
<properties>
<!-- Generic properties -->
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<!-- Web -->
<jsp.version>2.3.1</jsp.version>
<jstl.version>1.2</jstl.version>
<servlet.version>3.1.0</servlet.version>
<!-- jetty webSocketFactory -->
<jetty.socket.version>9.2.2.v20140723</jetty.socket.version>
<!-- Spring -->
<spring-framework.version>4.3.17.RELEASE</spring-framework.version>
<!-- Logging -->
<logback.version>1.0.13</logback.version>
<slf4j.version>1.7.5</slf4j.version>
<!-- jackson spring json -->
<jackson.version>2.8.11</jackson.version>
<!-- Test -->
<junit.version>4.12</junit.version>
<security.version>4.2.3.RELEASE</security.version>
</properties>
<dependencies>
<!-- Spring MVC -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${spring-framework.version}</version>
</dependency>
<!-- spring security -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>${security.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>${security.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-messaging</artifactId>
<version>${security.version}</version>
</dependency>
<!-- websocket -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-websocket</artifactId>
<version>${spring-framework.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-messaging</artifactId>
<version>${spring-framework.version}</version>
</dependency>
<!-- Need this for json to/from object -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<!-- Other Web dependencies -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>${jstl.version}</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>${servlet.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>javax.servlet.jsp-api</artifactId>
<version>${jsp.version}</version>
<scope>provided</scope>
</dependency>
<!-- Logging with SLF4J & LogBack -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>${logback.version}</version>
<scope>runtime</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.eclipse.jetty.websocket/websocket-client -->
<dependency>
<groupId>org.eclipse.jetty.websocket</groupId>
<artifactId>websocket-client</artifactId>
<version>${jetty.socket.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-webapp</artifactId>
<version>${jetty.socket.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.websocket</groupId>
<artifactId>websocket-server</artifactId>
<version>${jetty.socket.version}</version>
</dependency>
<!-- Test Artifacts -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>${spring-framework.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>2.4</version>
<configuration>
<warSourceDirectory>src/main/webapp</warSourceDirectory>
<warName>websocket</warName>
<failOnMissingWebXml>false</failOnMissingWebXml>
</configuration>
</plugin>
<!-- mvn jetty:run -->
<plugin>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-maven-plugin</artifactId>
<version>9.2.2.v20140723</version>
</plugin>
</plugins>
</pluginManagement>
<finalName>websocket</finalName>
</build>
</project>
原生websocket实现
1.编写一个weboscket Handler 来处理握手,连接,关闭,接收信息,发送信息的处理类,这个与mvc中的controller有点类型。
第一种方法直接实现WebSocketHandler.class 重写下面全部方法,代码有点多。
void afterConnectionEstablished(WebSocketSession session) throws Exception;
void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception;
void handleTransportError(WebSocketSession session, Throwable exception) throws Exception;
void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception;
boolean supportsPartialMessages();
第二种继承AbstractWebSocketHandler 抽象类,根据不用业务重写信息处理。
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception
protected void handleBinaryMessage(WebSocketSession session, BinaryMessage message) throws Exception
protected void handlePongMessage(WebSocketSession session, PongMessage message) throws Exception
第三中直接继承Spring 写好的文本处理类TextWebSocketHandler,写出handleTextMessage()即可。
public class MessageHandler extends TextWebSocketHandler{
Logger log = LoggerFactory.getLogger(MessageHandler.class);
//用来保存连接进来session
private List<WebSocketSession> sessions = new CopyOnWriteArrayList<>();
/**
* 关闭连接进入这个方法处理,将session从 list中删除
*/
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
sessions.remove(session);
log.info("{} 连接已经关闭,现从list中删除 ,状态信息{}", session, status);
}
/**
* 三次握手成功,进入这个方法处理,将session 加入list 中
*/
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
sessions.add(session);
log.info("用户{}连接成功.... ",session);
}
/**
* 处理客户发送的信息,将客户发送的信息转给其他用户
*/
@Override
public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
log.info("reveice client msg: {}",message.getPayload());
session.sendMessage(new TextMessage("i reveice client msg...."+System.nanoTime()));
for(WebSocketSession wss : sessions)
if(!wss.getId().equals(session.getId()))
wss.sendMessage(message);
}
}
注册websocket路由,设置handler处理
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer{
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(new MessageHandler(), "websocket")
.addInterceptors(new HttpSessionHandshakeInterceptor())
.setAllowedOrigins("*"); //允许跨域访问
}
}
我来解析一下上面代码,用CopyOnWriteArrayList 来维护所有成功握手长连接,将客户端发送信息,转发给CopyOnWriteArrayList中客户端,在list中移除关闭的连接。
客户端代码
<%@ page language="java" contentType="text/html; UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<link rel="stylesheet"
href="https://cdn.bootcss.com/bootstrap/4.0.0/css/bootstrap.min.css"
integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm"
crossorigin="anonymous">
<title>websocket调试页面</title>
<style type="text/css">
body {
font-size: 12px;
}
</style>
</head>
<body>
<div style="float: left; padding: 20px">
<strong>location:</strong> <br />
<input type="text" id="serverUrl" size="35" value="" /> <br />
<button onclick="connect()">Connect</button>
<button onclick="wsclose()">disConnect</button>
<br /> <strong>message:</strong> <br /> <input id="txtMsg" type="text" size="50" />
<br />
<button onclick="sendEvent()">send</button>
</div>
<div style="float: left; margin-left: 20px; padding-left: 20px; width: 350px; border-left: solid 1px #cccccc;"> <strong>Log:</strong>
<div style="border: solid 1px #999999;border-top-color: #CCCCCC;border-left-color: #CCCCCC; padding: 5px;width: 100%;height: 172px;overflow-y: scroll;" id="echo-log"></div>
<button onclick="clearLog()" style="position: relative; top: 3px;">Clear log</button>
</div>
</div>
</body>
<!-- 下面是h5原生websocket js写法 -->
<script type="text/javascript">
var output ;
var websocket;
function connect(){ //初始化连接
output = document.getElementById("echo-log")
var inputNode = document.getElementById("serverUrl");
var wsUri = inputNode.value;
try{
websocket = new WebSocket(wsUri);
}catch(ex){
alert("对不起websocket连接异常")
}
connecting();
window.addEventListener("load", connecting, false);
}
function connecting()
{
websocket.onopen = function(evt) { onOpen(evt) };
websocket.onclose = function(evt) { onClose(evt) };
websocket.onmessage = function(evt) { onMessage(evt) };
websocket.onerror = function(evt) { onError(evt) };
}
function sendEvent(){
var msg = document.getElementById("txtMsg").value
doSend(msg);
}
//连接上事件
function onOpen(evt)
{
writeToScreen("CONNECTED");
doSend("WebSocket rocks");
}
//关闭事件
function onClose(evt)
{
writeToScreen("DISCONNECTED");
}
//后端推送事件
function onMessage(evt)
{
writeToScreen('<span style="color: blue;">RESPONSE: ' + evt.data+'</span>');
}
function onError(evt)
{
writeToScreen('<span style="color: red;">ERROR:</span> ' + evt.data);
}
function doSend(message)
{
writeToScreen("SENT: " + message);
websocket.send(message);
}
//清除div的内容
function clearLog(){
output.innerHTML = "";
}
//浏览器主动断开连接
function wsclose(){
websocket.close();
}
function writeToScreen(message)
{
var pre = document.createElement("p");
pre.style.wordWrap = "break-word";
pre.innerHTML = message;
output.appendChild(pre);
}
//
</script>
</html>
执行结果
sockJS实现
因为并不是所有的浏览器都支持websocket,Spring提供了基于SockJS协议尽可能地模拟WebSocket API的后备选项。SockJS被设计用于浏览器。它使用各种技术支持各种浏览器版本。有关SockJS传输类型和浏览器的完整列表,请参阅 SockJS客户端页面。传输分为3大类:WebSocket,HTTP Streaming和HTTP Long Polling。有关这些类别的概述,请参阅 此博客文章。
启用websocket 也比较简单
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(new MessageHandler(),"/sockjs").setAllowedOrigins("*").withSockJS();
}
只要添加一个withSockJS()即可开启sockJS协议,spring api做得很简洁。
stomp实现
直接使用的WebSocket API的太低级的应用-直至假设有关消息的格式作出很少有一个框架可以做解释通过注释的消息或路由他们。这就是为什么应用程序应该考虑使用子协议和Spring的STOMP over WebSocket支持
STOMP是一种简单的面向文本的消息传递协议,最初是为脚本语言(如Ruby,Python和Perl)创建的,用于连接到企业消息代理。它旨在解决常用消息传递模式的一个子集。STOMP可以用于任何可靠的双向流媒体网络协议,如TCP和WebSocket。虽然STOMP是面向文本的协议,但消息的有效载荷可以是文本或二进制。
Spring框架支持通过Spring-messaging和spring-websocket模块在WebSocket上使用STOMP
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
// 这个服务器并不是用ws:// 而是用http:// 或者 https:// 来连接
registry.addEndpoint("endpoint").setAllowedOrigins("*")
.withSockJS();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
// 定义了两个客户端订阅地址的前缀信息,也就是客户端接收服务端发送消息的前缀信息
registry.enableSimpleBroker("/topic", "/queue");
// 定义了服务端接收地址的前缀,也即客户端给服务端发消息的地址前缀
registry.setApplicationDestinationPrefixes("/app");
//使用客户端一对一通信的时候 编配前缀 通常与@SendToUser 搭配使用
registry.setUserDestinationPrefix("/user");
}
因为需要认证用户,我引入spring security来做认证。
@Configuration
public class WebSocketSecurtiyConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {
@Override
protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
messages
// 任何人都可以订阅/ user / queue / errors
.simpSubscribeDestMatchers("/user/queue/errors").permitAll()
//何具有以“/ app /”开头的目标邮件都将要求用户具有角色ROLE_USER
.simpDestMatchers("/app/**").hasRole("USER").anyMessage().authenticated();
}
//允许跨域
@Override
protected boolean sameOriginDisabled() {
return true;
}
}
用@MessageMapping注解支持的方法@Controller类。它可以用于将方法映射到消息目标,还可以与类型级别结合,@MessageMapping以表达控制器内所有注释方法的共享映射。
@Controller
public class MessageController {
final Logger log = LoggerFactory.getLogger(MessageController.class);
private SimpMessagingTemplate template;
@Autowired
public MessageController(SimpMessagingTemplate template) {
this.template = template;
}
@MessageMapping("/hello")
@SendTo("/queue/echo")
public Map<String, Object> echo(String msg) {
Map<String, Object> map = new HashMap<>();
map.put("message", msg);
map.put("from", "server");
map.put("now", new Date().getTime());
log.info("receive msg from client: {}",msg);
return map;
}
@MessageMapping("only")
@SendToUser(value = "topic/myself",broadcast = false)
public Map<String,Object> respMsg(String msg,Principal principal){
log.info("recevie client msg : {} ,user : {}",msg,principal.getName());
Map<String, Object> map = new HashMap<>();
map.put("message", msg);
map.put("now", new Date().getTime());
map.put("from", "server");
return map;
}
}
看见上面的代码是不是一头雾水啊,其实这些代码跟平时写spring mvc差不多而已。@MessageMapping
跟@RequestMapping
作用差不多,根据不同url映射到方法上。例如客户端 发送 /app/hello请求,spring MessageBroker捕获到/app开头的前缀,将/app去除,查找有没有/hello 的方法,将请求交给这个方法处理。
客户端两种不同的请求发送到RequestChannel ,根据不同前缀,交给不同Handler处理,/app 交给@MessageHandler方法处理,响应信息到SimpleBroker统一响应给客户端。
想说很多,但又怕表示不清,现在就直接发一个github仓库给有兴趣的同学去看看把github