springmvc+websocket的全部实现方式

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>

执行结果


image.png

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 的方法,将请求交给这个方法处理。

message-flow-simple-broker.png

客户端两种不同的请求发送到RequestChannel ,根据不同前缀,交给不同Handler处理,/app 交给@MessageHandler方法处理,响应信息到SimpleBroker统一响应给客户端。

想说很多,但又怕表示不清,现在就直接发一个github仓库给有兴趣的同学去看看把github

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,233评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,013评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,030评论 0 241
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,827评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,221评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,542评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,814评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,513评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,225评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,497评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,998评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,342评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,986评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,812评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,560评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,461评论 2 266

推荐阅读更多精彩内容

  • Spring Web MVC Spring Web MVC 是包含在 Spring 框架中的 Web 框架,建立于...
    Hsinwong阅读 21,766评论 1 92
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,099评论 18 139
  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,355评论 6 343
  • WebSocket学习 为什么需要WebSocket 以往使用的HTTP协议存在一个缺陷,通信只能由客户端发起。 ...
    ChaLLengerZeng阅读 1,678评论 0 1
  • 跟小米聊起绘画和少儿绘画考级,让我想到了在法国幼儿园实习的经历,并想要给他写下来。这可能使我最珍视的回忆之一。我是...
    一个牛油果阅读 796评论 2 4