J2EE及Spring Websocket基本运用

一、现状分析

目前,我们的项目中还是有很多场景用得到消息推送,目前大都采用的是Ajax轮询、dwr、amq等方式,这些本质上其实都是http请求轮询,而这种方式是比较占用宽带且容易给服务器造成压力的。
WebSocket 是 HTML5 一种新的协议。它实现了浏览器与服务器全双工通信,能更好的节省服务器资源和带宽并达到实时通讯,它建立在 TCP 之上,同 HTTP 一样通过 TCP 来传输数据,但是它和 HTTP 最大不同是:

  1. WebSocket 是一种双向通信协议,在建立连接后,WebSocket 服务器和 Browser/Client Agent 都能主动的向对方发送或接收数据,就像 Socket 一样。
  2. WebSocket 需要类似 TCP 的客户端和服务器端通过握手连接,连接成功后才能相互通信。

那么传统的http轮回模式和websocket模式客户端与服务器的交互如下:
这里写图片描述
上图对比可以看出,相对于传统 HTTP 每次请求-应答都需要客户端与服务端建立连接的模式,WebSocket 是类似 Socket 的 TCP 长连接的通讯模式,一旦 WebSocket 连接建立后,后续数据都以帧序列的形式传输。在客户端断开 WebSocket 连接或 Server 端断掉连接前,不需要客户端和服务端重新发起连接请求。在海量并发及客户端与服务器交互负载流量大的情况下,极大的节省了网络带宽资源的消耗,有明显的性能优势,且客户端发送和接受消息是在同一个持久连接上发起,实时性优势明显。
早期 HTML5 并没有形成业界统一的规范,各个浏览器和应用服务器厂商有着各异的类似实现,如 IBM 的 MQTT,Comet 开源框架等,直到 2014 年,HTML5 在 IBM、微软、Google 等巨头的推动和协作下终于尘埃落地,正式从草案落实为实际标准规范,各个应用服务器及浏览器厂商逐步开始统一,在 JavaEE7 中也实现了 WebSocket 协议,从而无论是客户端还是服务端的 WebSocket 都已完备。

二、Websocket支持

WebSocket 的实现分为客户端和服务端两部分,客户端(通常为浏览器)发出 WebSocket 连接请求,服务端响应,实现类似 TCP 握手的动作,从而在浏览器客户端和 WebSocket 服务端之间形成一条 HTTP 长连接快速通道。两者之间后续进行直接的数据互相传送,不再需要发起连接和相应。

WebSocket 服务端

WebSocket 服务端在各个主流应用服务器厂商中已基本获得符合 JEE JSR356 标准规范 API 的支持,以下列举了部分常见的商用及开源应用服务器对 WebSocket Server 端的支持情况:

厂商应用服务器备注
IBMWebSphereWebSphere 8.0 以上版本支持,7.X 之前版本结合 MQTT 支持类似的 HTTP 长连接
甲骨文WebLogicWebLogic 12c 支持,11g 及 10g 版本通过 HTTP Publish 支持类似的 HTTP 长连接
微软IISIIS 7.0+支持
ApacheTomcatTomcat 7.0.5+支持,7.0.2X 及 7.0.3X 通过自定义 API 支持
ApacheJettyJetty 7.0+支持
WebSocket 客户端

对于 WebSocket 客户端,主流的浏览器(包括 PC 和移动终端)现已都支持标准的 HTML5 的 WebSocket API,这意味着客户端的 WebSocket JavaScirpt 脚本具备良好的一致性和跨平台特性,以下列举了常见的浏览器厂商对 WebSocket 的支持情况:

厂商版本支持
ChromeChrome version 4+支持
FirefoxFirefox version 5+支持
IEIE version 10+支持
SafariIOS 5+支持
Android BrowerAndroid 4.5+支持

三、 Websocket实现

客户端实现

客户端 WebSocket API 基本上已经在各个主流浏览器厂商中实现了统一,因此使用标准 HTML5 定义的 WebSocket 客户端的 JavaScript API 即可,Demo实例如下:

<!DOCTYPE html>
<html>
<head>
<title>Testing websockets</title>
</head>
<body>
    <div>
        <input type="submit" value="Start" onclick="start()" />
    </div>
    <div id="messages"></div>
    <script type="text/javascript">
        var webSocket = 
            new WebSocket('ws://localhost:8088/byteslounge/websocket');

        webSocket.onerror = function(event) {//发生异常的处理
            alert(event.data);
        };

        webSocket.onopen = function(event) {//建立连接的处理
            document.getElementById('messages').innerHTML = 'Connection established';
        };

        webSocket.onmessage = function(event) {//接收到消息的处理
            document.getElementById('messages').innerHTML + '<br />' + event.data;
        };
        function start() {//页面发送消息给服务器
            webSocket.send('hello');
            return false;
        }
    </script>
</body>
</html>
服务端实现

服务端的实现可以分为两种:J2EE的websocket和Spring提供的websocket支持。下面分别介绍下两种实现方式的代码:

J2EE的实现
import java.io.IOException;
import java.util.concurrent.CopyOnWriteArraySet;

import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;

@ServerEndpoint(value = "/websocket")
public class WebSocketTest {

    public static CopyOnWriteArraySet<Session> sessions = new CopyOnWriteArraySet<Session>();


    @OnMessage
    public void onMessage(String message, Session session) throws IOException, InterruptedException {
        System.out.println("Got your message (" + message + ").Thanks !");
        System.out.println("Received: " + message);

        for(Session s: WebSocketTest.sessions){
            if(message!=null&&!"".equals(message)){
                System.out.println("批量发送:"+message);
                s.getBasicRemote().sendText(message);
            }
        }
    }

    @OnOpen
    public void onOpen(Session session) {
        sessions.add(session);
    }
    @OnClose
    public void onClose (Session session) {
        sessions.remove(session);
    }
    @OnError
    public void onError(Session session) {
        sessions.remove(session);
    }
}
  1. 只要对一个类进行@ServerEndpoint注解,既可以标示该类为一个websocket服务。我们可以选择实现onMessage、onOpen等方法进行相应的消息处理。
  2. 这里的CopyOnWriteArraySet是为了收集所有的连接,以便我们在推送消息时能够发送到所有的客户端。
  3. 环境:JDK1.7+,tomcat7.0.5+,本例中的环境:JDK1.8,tomcat7.0.68
  4. 相关依赖:这里是为了编译而添加的jar包,最好2个jar包都添加上
<dependency>
            <groupId>javax</groupId>
            <artifactId>javaee-api</artifactId>
            <version>7.0</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>javax.websocket</groupId>
            <artifactId>javax.websocket-api</artifactId>
            <version>1.1</version>
            <scope>provided</scope>
        </dependency>
Spring websocket的实现

需要实现三个类,这里简单介绍下每个类的作用及一些注意事项:
1.WebSocketConfig.java

@Configuration
@EnableWebMvc
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {

        /**
         * 支持websocket 的 connection
         */
        registry.addHandler(new WebSocketHander(),"/websocket").addInterceptors(new HandshakeInterceptor());

        /**
         * 不支持websocket的connenction,采用sockjs
         */
        registry.addHandler(new WebSocketHander(),"/sockjs/websocket").addInterceptors(new HandshakeInterceptor()).withSockJS();
    }
}

@Configuration @EnableWebMvc @EnableWebSocket这三个大致意思是使这个类支持以@Bean的方式加载bean,并且支持springmvc和websocket,不是很准确大致这样,试了一下@EnableWebMvc不加也没什么影响,@Configuration本来就支持springmvc的自动扫描。
另外,使用sockjs的方式这里不多讨论。
2.HandshakeInterceptor.java

import java.util.Map;

import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.socket.WebSocketHandler;
public class HandshakeInterceptor implements org.springframework.web.socket.server.HandshakeInterceptor {

    @Override
    public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse serverHttpResponse, WebSocketHandler webSocketHandler, Map<String, Object> map) throws Exception {
        if(request.getHeaders().containsKey("Sec-WebSocket-Extensions")) {
            request.getHeaders().set("Sec-WebSocket-Extensions", "permessage-deflate");
        }
        return true;
    }

    @Override
    public void afterHandshake(ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse, WebSocketHandler webSocketHandler, Exception e) {

    }
}

这个类的作用可以理解为拦截器,在websocket的方法握手前后的处理,这里我并没有实现什么业务,我们可以在这里取一些用户信息等操作
3.WebSocketHander.java

import java.io.IOException;
import java.util.concurrent.CopyOnWriteArraySet;

import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.WebSocketMessage;
import org.springframework.web.socket.WebSocketSession;

public class WebSocketHander implements WebSocketHandler {

    public static CopyOnWriteArraySet<WebSocketSession> sessions = new CopyOnWriteArraySet<WebSocketSession>();

    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        sessions.add(session);
    }
    @Override
    public void handleMessage(WebSocketSession webSocketSession, WebSocketMessage<?> webSocketMessage) throws Exception {
        TextMessage message = new TextMessage(webSocketMessage.getPayload() + "hello");
        for (WebSocketSession user : sessions) {
            try {
                if (user.isOpen()) {
                    user.sendMessage(message);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    @Override
    public void handleTransportError(WebSocketSession webSocketSession, Throwable throwable) throws Exception {
        if(webSocketSession.isOpen()){
            webSocketSession.close();
        }
        sessions.remove(webSocketSession);
    }

    @Override
    public void afterConnectionClosed(WebSocketSession webSocketSession, CloseStatus closeStatus) throws Exception {
        sessions.remove(webSocketSession);
    }

    @Override
    public boolean supportsPartialMessages() {
        return false;
    }
}
  1. 我们可以看到继承了WebsocketHandle之后,其方法类型与J2EE很类似,分别为建立连接,关闭连接,接收消息等。
  2. 如果只考虑Websocket不考虑socketJs,只需添加SpringWebsocket相关依赖即可:
<dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-websocket</artifactId>
            <version>4.3.7.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-messaging</artifactId>
            <version>4.3.7.RELEASE</version>
        </dependency>
效果图

这里写图片描述这里写图片描述
我们在IE11和chrome下分别打开页面,无论是在chrome中发送消息还是在IE中发送消息,两边都能接收到服务器推送的消息。

四、常见错误

  1. spring security与WebSocket结合使用时报302循环重定向错误。spring security的使用
    原因:springsecurity的过滤器DelegatingFilterProxy拦截了我们的ws请求,我们可以在自定义的CsrfSecurityRequestMatcher中添加不过滤我们的请求:
    这里写图片描述
  2. 在使用struts2时,服务器连接不上。
    原因:struts2的拦截器拦截掉了我们的请求,可以添加:
<constant name="struts.action.excludePattern" value="/websocket"></constant>

如果还是有问题的话,就需要对struts2的拦截器拦截范围做一定限制。

一般很多问题都是由于ws的请求被拦截,所以需要注意自己项目中的拦截器定义,并仔细检查是不是被拦截掉了。

评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值