WebSocket+SockJs+STMOP

应用场景

websocket 是 Html5 新增加特性之一,目的是浏览器与服务端建立全双工的通信方式,解决 http 请求-响应带来过多的资源消耗,同时对特殊场景应用提供了全新的实现方式,比如聊天、股票交易、游戏等对对实时性要求较高的行业领域。

1.WebSocket

WebSocket 是发送和接收消息的底层API,WebSocket 协议提供了通过一个套接字实现全双工通信的功能。也能够实现 web 浏览器和 server 间的异步通信,全双工意味着 server 与浏览器间可以发送和接收消息。需要注意的是必须考虑浏览器是否支持,浏览器的支持情况如下:

1.1 编写Handler类

方法一:实现 WebSocketHandler 接口,WebSocketHandler 接口如下

public interface WebSocketHandler {
    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

@Service
public class ChatHandler extends AbstractWebSocketHandler {
    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        session.sendMessage(new TextMessage("hello world."));
    }
}

该类中的方法我们都可以按需求重载

1.2 拦截器的实现

@Component
public class WebSocketHandshakeInterceptor implements HandshakeInterceptor {
    @Override
    public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
        if (request instanceof ServletServerHttpRequest) {
            attributes.put("username",userName);
        }
        return true;
    }

    @Override
    public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {

    }
}
  • beforeHandshake()方法,在调用 handler 前调用。常用来注册用户信息,绑定 WebSocketSession,在 handler 里根据用户信息获取WebSocketSession发送消息

1.3 WebSocketConfig配置

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer{
    @Autowired
    private ChatHandler chatHandler;
    @Autowired
    private WebSocketHandshakeInterceptor webSocketHandshakeInterceptor;
    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {  
    registry.addHandler(chatHandler,"/chat")
    .addInterceptors(webSocketHandshakeInterceptor);
    }
     @Bean
    public ServletServerContainerFactoryBean createWebSocketContainer() {
        ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean();
        container.setMaxTextMessageBufferSize(8192*4);
        container.setMaxBinaryMessageBufferSize(8192*4);
        return container;
    }

}
  • 实现 WebSocketConfigurer 接口,重写 registerWebSocketHandlers 方法,这是一个核心实现方法,配置 websocket 入口,允许访问的域、注册 Handler、SockJs 支持和拦截器。
  • registry.addHandler()注册和路由的功能,当客户端发起 websocket 连接,把 /path 交给对应的 handler 处理,而不实现具体的业务逻辑,可以理解为收集和任务分发中心。
  • addInterceptors,顾名思义就是为 handler 添加拦截器,可以在调用 handler 前后加入我们自己的逻辑代码。
  • ServletServerContainerFactoryBean可以添加对WebSocket的一些配置

1.4 客户端配置

var  wsServer = 'ws://127.0.0.1:8080/chat'; 
var  websocket = new WebSocket(wsServer); 
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 onOpen(evt) { 
     console.log("Connected to WebSocket server."); 
} 
function onClose(evt) { 
     console.log("Disconnected"); 
} 
function onMessage(evt) { 
     console.log('Retrieved data from server: ' + evt.data); 
} 
function onError(evt) { 
     console.log('Error occured: ' + evt.data); 
}
websocket.send(“test”);//客户端向服务器发送消息

注意:
其中 wsServer = ‘ws://127.0.0.1:8080/chat’中的地址要根据自己的实际情况来定,一般形式为:ws://域名:端口/应用路径/WebSocket 配置的 path。“应用路径”是应用部署在 tomcat 中的文件夹路径,“WebSocket 配置的 path”是配置文件中这条配置项配置的路径。

后台输出:

- Connection established
- getId:1
- getLocalAddress:/127.0.0.1:8080
- getUri:/chat

1.5 Bad Code

  • 1006
    nginx配置添加
  proxy_connect_timeout 500s;
  • 1009
    内容长度超限,将MaxTextMessageBufferSize增大

2. SockJs

为了应对许多浏览器不支持WebSocket协议的问题,设计了备选SockJs

SockJS 是 WebSocket 技术的一种模拟。SockJS 会 尽可能对应 WebSocket API,但如果 WebSocket 技术不可用的话,就会选择另外的通信方式协议。

2.1 WebSocketConfig配置

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer{
    @Autowired
    private ChatHandler chatHandler;
    @Autowired
    private WebSocketHandshakeInterceptor webSocketHandshakeInterceptor;
    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        
    // withSockJS() 方法声明我们想要使用 SockJS 功能,如果WebSocket不可用的话,会使用 SockJS; 
       registry.addHandler(chatHandler,"/chat").addInterceptors(webSocketHandshakeInterceptor).withSockJS();
    }
}

客户端配置

具体做法是 依赖于 JavaScript 模块加载器(如 require.js or curl.js) 还是简单使用 script 标签加载 JavaScript 库。最简单的方法是 使用 script 标签从 SockJS CDN 中进行加载,如下所示:

//加载sockjs
<script src="http://cdn.sockjs.org/sockjs-0.3.min.js"></script>
var url = '/chat';
var sock = new SockJS(url);
//.....

对以上代码分析

  • SockJS 所处理的 URL 是 “http://“ 或 “https://“ 模式,而不是 “ws://“ or “wss://“;
  • 其他的函数如 onopen, onmessage, and onclose ,SockJS 客户端与 WebSocket 一样,在此代码省略

后台输出:

- Connection established
- getId: qtfwdtti**(注意:此处和直接利用websocket API有区别)**
- getLocalAddress:/127.0.0.1:8080
- getUri: /chat/668/qtfwdtti/websocket**(注意:此处和直接利用websocket API有区别)**

3.STOMP

SockJS 为 WebSocket 提供了 备选方案。但无论哪种场景,对于实际应用来说,这种通信形式层级过低。下面看一下如何 在 WebSocket 之上使用 STOMP协议,来为浏览器 和 server 间的 通信增加适当的消息语义。(STOMP—— Simple Text Oriented Message Protocol——面向消息的简单文本协议)

3.1 WebSocket、SockJs、STOMP三者关系

简而言之,WebSocket 是底层协议,SockJS 是WebSocket 的备选方案,也是 底层协议,而 STOMP 是基于 WebSocket(SockJS) 的上层协议

  1. 假设HTTP协议并不存在,只能使用TCP套接字来编写web应用,你可能认为这是一件疯狂的事情。
  2. 不过幸好,我们有HTTP协议,它解决了 web 浏览器发起请求以及 web 服务器响应请求的细节。
  3. 直接使用 WebSocket(SockJS) 就很类似于 使用 TCP 套接字来编写 web 应用;因为没有高层协议,因此就需要我们定义应用间所发送消息的语义,还需要确保 连接的两端都能遵循这些语义。
  4. 同HTTP在TCP套接字上添加请求-响应模型层一样,STOMP在 WebSocket之上提供了一个基于帧的线路格式层,用来定义消息语义。

3.2 STOMP

STOMP帧由命令,一个或多个头信息以及负载所组成。如下就是发送数据的一个STOMP帧:

SEND
destination:/app/room-message
content-length:20

{\"message\":\"Hello!\"}

对以上代码分析:

  1. SEND:STOMP命令,表明会发送一些内容;
  2. destination:头信息,用来表示消息发送到哪里;
  3. content-length:头信息,用来表示 负载内容的 大小;
  4. 空行;
  5. 帧内容(负载)内容

3.3 WebSockConfig配置

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        //定义了一个客户端订阅地址的前缀信息,也就是客户端接收服务端发送消息的前缀信息
        config.enableSimpleBroker("/topic");
        //定义了服务端接收地址的前缀,也即客户端给服务端发消息的地址前缀
        config.setApplicationDestinationPrefixes("/app");
    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        //添加一个/chat端点,客户端就可以通过这个端点来进行连接;withSockJS作用是添加SockJS支持
        registry.addEndpoint("/gs-guide-websocket")).withSockJS();
    }

}

对以上代码分析:

  1. EnableWebSocketMessageBroker 注解表明: 这个配置类不仅配置了 WebSocket,还配置了基于代理的 STOMP 消息;
  2. 它复写了 registerStompEndpoints() 方法:添加一个服务端点,来接收客户端的连接。将 “/gs-guide-websocket
  3. ” 路径注册为 STOMP 端点。这个路径与之前发送和接收消息的目的路径有所不同, 这是一个端点,客户端在订阅或发布消息到目的地址前,要连接该端点,即用户发送请求 :url=’/127.0.0.1:8080/gs-guide-websocket
  4. ’ 与 STOMP server 进行连接,之后再转发到订阅url;
  5. 它复写了 configureMessageBroker() 方法:配置了一个 简单的消息代理,通俗一点讲就是设置消息连接请求的各种规范信息。
  6. 发送应用程序的消息将会带有 “/app” 前缀。

3.4 Controller

@Controller
public class GreetingController {


    @Autowired
    private SimpMessagingTemplate template;

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

}

对以上代码分析

  1. @MessageMapping 标识客户端发来消息的请求地址,前面我们全局配置中制定了服务端接收的地址以“/app”开头,所以客户端发送消息的请求连接是:/app/hello;
  2. @SendToUser可以将消息只返回给发送者
  3. @SendTo会将消息广播给所有订阅/hello这个路径的用户。

3.5 客户端代码

function connect() {
    var socket = new SockJS('/gs-guide-websocket');
    stompClient = Stomp.over(socket);
    stompClient.connect({}, function (frame) {
        setConnected(true);
        console.log('Connected: ' + frame);
        stompClient.subscribe('/user/topic/greetings', function (greeting) {
            showGreeting(JSON.parse(greeting.body).content);
        });
    });
}

3.6 获取用户信息,定向推送

虽然STMOP的协议很好的实现了订阅,返回的模式,但是没法定向的从服务端推送消息个某个用户,要解决这个问题就需要获取用户的信息,使得我们可以对其推送。

3.6.1 MyHandsHandler
public class MyHandsHandler extends DefaultHandshakeHandler {


    public MyHandsHandler() {
    }

    public MyHandsHandler(RequestUpgradeStrategy requestUpgradeStrategy) {
        super(requestUpgradeStrategy);
    }

    @Override
    protected Principal determineUser(ServerHttpRequest request, WebSocketHandler wsHandler, Map<String, Object> attributes) {
        URI uri = request.getURI();
        String id = null;
        if (uri != null) {
            String query = uri.getPath();
            String[] pairs = query.split("/");
            id = pairs[3];
        }
        MyPrincipal principal = new MyPrincipal();
        principal.setName(id);
        System.out.println(id);
        return principal;
    }

    class MyPrincipal implements Principal {
        private String name;

        @Override
        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;

            MyPrincipal that = (MyPrincipal) o;

            return name != null ? name.equals(that.name) : that.name == null;

        }

        @Override
        public int hashCode() {
            return name != null ? name.hashCode() : 0;
        }
    }
}

继承DefaultHandshakeHandler,重写其determineUser方法,根据需要自定义Principal的返回值,其name就是用来标记返回对象的id。更进一步可以用一个List来维护用户的登陆状态等。

3.6.2 注册MyHandsHandler
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        //定义了一个客户端订阅地址的前缀信息,也就是客户端接收服务端发送消息的前缀信息
        config.enableSimpleBroker("/topic");
        //定义了服务端接收地址的前缀,也即客户端给服务端发消息的地址前缀
        config.setApplicationDestinationPrefixes("/app");
    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        //添加一个/chat端点,客户端就可以通过这个端点来进行连接;withSockJS作用是添加SockJS支持
        registry.addEndpoint("/gs-guide-websocket").setHandshakeHandler(new MyHandsHandler()).withSockJS();
    }

}
3.6.3 Controller
@Controller
public class GreetingController {


    @Autowired
    private SimpMessagingTemplate template;


    @RequestMapping("/xxx")
    public String greetingx(String id) throws Exception {
        template.convertAndSendToUser(id, "/topic/greetings", new Greeting("Hello, " + id + "!"));
        return "success";
    }
}

以上代码分析

  1. 通过依赖注入SimpMessagingTemplate我们可以在服务端的任何地方发送消息
  2. template.convertAndSendToUser(id, "/topic/greetings", new Greeting("Hello, " + id + "!"))可以将消息发送给我们指定id的用户,此处的id就是Principal中的name值

3.7 其他Annotation说明

3.7.1@DestinationVariable
@MessageMapping("/hello/{roomId}")
    public void roomMessage(HelloMessage message, @DestinationVariable String roomId){
        String dest = "/topic/" + roomId + "/" + "greetings";
        this.template.convertAndSend(dest, message);
    }
  1. @DestinationVariable 用以取出请求地址中的房间 id 参数 roomId;

参考文献:

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

推荐阅读更多精彩内容