java websocket教程

定义

websocket是什么

WebSocket是一种在单个TCP连接上进行全双工通讯的协议.简单来说就是客户端与服务端建立起长连接可以相互发送消息.

websocket使用场景

主要用在对消息实时性比较高的场景.用来替代轮询方案

  • 实时在线聊天
  • 浏览器之间的协同编辑工作
  • 多人在线游戏

浏览器支持websocket的版本

WebSocket通信协议于2011年被修订为RFC 6455的标准.所以对浏览器、后端服务器是有要求的.以下是被支持的版本

image.png

tomcat支持websocket的版本

http://tomcat.apache.org/(7.0.27支持websocket,建议用tomcat8,7.0.27中的接口已经过时)

浏览器与服务器之间连接如何建立(通信协议)

Websocket 通过HTTP/1.1 协议的101状态码进行握手,升级成websocket连接

  • 请求
# Websocket使用ws或wss统一资源标志符(必填)
GET ws://localhost:8090/ws/stomp/561/abkkwlke/websocket HTTP/1.1
# 升级成websocket协议(必填)
Upgrade: websocket
# Connection必须设置Upgrade,表示客户端希望连接升级(必填)
Connection: Upgrade
# Origin字段是可选的,通常用来表示在浏览器中发起此Websocket连接所在的页面
Origin: http://example.com
# Sec-WebSocket-Key 服务端会用来验证该请求是否是websocket请求,尽量避免与http请求被误认为websocket(必填)
Sec-WebSocket-Key: sN9cRrP/n9NdMgdcy2VJFQ==
# Websocket支持的版本(必填)
Sec-WebSocket-Version: 13

  • 响应
# 响应的状态码,必须是101
HTTP/1.1 101 // 
# 升级的协议
Upgrade: websocket
# 表示客户端希望连接升级
Connection: upgrade
# 服务端根据Sec-WebSocket-Key生成,用来验证该请求是websocket请求
Sec-WebSocket-Accept: V395OugSb9uYXr6dA44VGcn/oAM=

浏览器与服务器之间数据如何传输(数据协议)

STOMP 是基于 WebSocket的上层协议,提供了一个基于帧的线路格式层,用来定义消息语义.提供了一套完整websocket数据传输的api.让前后端能够快速变现.

  • 消息发送的格式
# stomp命令
SEND
# 服务端接口
destination:/ws/broadcast
content-length:87

# 内容 可以是json格式
{"destination":"/topic","payload":"1231231","onErrorDestination":"/topic"}

  • 支持的命令
    • SEND
    • SUBSCRIBE
    • UNSUBSCRIBE
    • BEGIN
    • COMMIT
    • ABORT
    • ACK
    • NACK
    • DISCONNECT

浏览器与服务器之间如何实现消息的广播、点对点传输

主要通过发布/订阅的模式来实现

  • 广播思路

    1. 浏览器订阅主题: /topic
    2. 服务器发送消息到主题/topic
    3. 所有订阅的浏览器都能收到消息
  • 点对点的思路(浏览器A->B)

    1. 浏览器B订阅主题: /user/B/topic
    2. 浏览器A发送消息到主题: /user/B/topic
    3. 浏览器B就能收到消息

如何使用

后端使用

spring boot整合websocket

  • pom
 <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
  • stomp的配置
@Configuration
@ComponentScan("com.websocket.test")
@EnableConfigurationProperties(value = {WebSocketProperties.class})
@EnableWebSocketMessageBroker
public class WebSocketConfigurer extends AbstractWebSocketMessageBrokerConfigurer {

    @Autowired
    private WebSocketProperties webSocketProperties;

    @Override
    public void registerStompEndpoints(StompEndpointRegistry stompEndpointRegistry) {
        // 注册一个Stomp的节点(endpoint),并指定使用SockJS协议。
        stompEndpointRegistry
                .addEndpoint(webSocketProperties.getEndPoint())
                .setAllowedOrigins(webSocketProperties.getAllowedOrigins())
                .withSockJS();
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {

        // 定义心跳线程
        ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
        taskScheduler.setThreadNamePrefix("wss-heartbeat-thread-");
        taskScheduler.setDaemon(true);
        taskScheduler.initialize();

        // 服务端发送消息给客户端的域,多个用逗号隔开
        registry.enableSimpleBroker(webSocketProperties.getEnableSimpleBroker())
                // 定义心跳间隔 单位(ms)
                .setHeartbeatValue(new long[]{webSocketProperties.getHeartBeatInterval(), webSocketProperties.getHeartBeatInterval()})
                .setTaskScheduler(taskScheduler);
        // 定义webSocket前缀
        registry.setApplicationDestinationPrefixes(webSocketProperties.getApplicationDestinationPrefixes());
    }

  • yml

把stomp的相关配置做成配置文件,配置在yml中

commons.websocket:
  # 监听的节点
  endPoint: "/ws/stomp"
  # 跨域支持
  allowedOrigins: "*"
  # 可订阅的主题
  enableSimpleBroker:
   - "/topic"
   - "/queue"
   - "/user"
   - "/client"
  # 客户端向服务器发消息时的前缀
  applicationDestinationPrefixes: "/ws"

注册stomp节点

 stompEndpointRegistry.addEndpoint("/ws/stomp")

定义支持订阅的主题列表


  # 可订阅的主题
  enableSimpleBroker:
   - "/topic"
   - "/queue"
   - "/user"
   - "/client"

 registry.enableSimpleBroker(webSocketProperties.getEnableSimpleBroker());
 

定义跨域的支持

 stompEndpointRegistry.setAllowedOrigins("*")

定义心跳的支持


 @Override
public void configureMessageBroker(MessageBrokerRegistry registry) {

    // 定义心跳线程
    ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
    taskScheduler.setThreadNamePrefix("wss-heartbeat-thread-");
    taskScheduler.setDaemon(true);
    taskScheduler.initialize();

    // 服务端发送消息给客户端的域,多个用逗号隔开
    registry.enableSimpleBroker(webSocketProperties.getEnableSimpleBroker())
            // 定义心跳间隔 单位(ms)
            .setHeartbeatValue(new long[]{webSocketProperties.getHeartBeatInterval(), webSocketProperties.getHeartBeatInterval()})
            .setTaskScheduler(taskScheduler);
}


事件的监听

服务器可以监听到websocket的连接、已连接、订阅、退订、断开事件: .然后可以根据事件来做相应的业务处理.

  • 例子

当某个客户端断开连接之后.发送消息到指定的topic

/**
 * 断开事件,当某个客户端断开连接之后.发送消息到指定的topic
 */
@Slf4j
@Component
public class WebSocketOnDisconnectEventListener implements ApplicationListener<SessionDisconnectEvent> {

    @Autowired
    private WebSocketService webSocketService;

    @Override
    public void onApplicationEvent(SessionDisconnectEvent sessionDisconnectEvent) {
        log.info("WebSocketOnDisconnectEventListener ... ");

        StompHeaderAccessor sha = StompHeaderAccessor.wrap(sessionDisconnectEvent.getMessage());

        if (sha.getSessionAttributes().get("onDisconnectTopic") != null) {
            String onDisconnectTopic = (String) sha.getSessionAttributes().get("onDisconnectTopic");
            String clientId = (String) sha.getSessionAttributes().get("clientId");

            webSocketService.send(
                    WebSocketMsgDefaultVo
                            .builder()
                            .payload(clientId + "断开连接")
                            .destination(onDisconnectTopic)
                            .build()
            );
        }
    }
}

session的获取

服务器可以监听浏览器连接成功事件,获取session信息,用来确定哪个浏览器

@Slf4j
@Component
public class WebSocketOnConnectedEventListener implements ApplicationListener<SessionConnectedEvent> {

    @Override
    public void onApplicationEvent(SessionConnectedEvent sessionConnectEvent) {
        String sessionId = (String) sessionConnectEvent.getMessage().getHeaders().get("simpSessionId");
        log.info("sessionId: {} ", sessionId);
        log.info("WebSocketOnConnectedEventListener ...");
    }
}

INFO  c.k.k.k.w.l.WebSocketOnConnectedEventListener - sessionId: 4gfxeh2z 
INFO  c.k.k.k.w.l.WebSocketOnConnectedEventListener - WebSocketOnConnectedEventListener ...


发送消息的接口

  • spring boot中如何开启

浏览器发送消息给服务端,并且广播、点对点的发送给相应的其他浏览器.这里我们使用@MessageMapping注解来开启

  • 自定义路由与封装的方法 例如 广播(broadcast)、点对点单播(unicast)

@Slf4j
@Controller
public class WebSocketController {

    @Autowired
    private WebSocketService webSocketService;

    @MessageMapping("/broadcast")
    public ResponseMessage broadcast(WebSocketMsgDefaultVo vo) throws Exception {
        log.info("/web_socket/broadcast test ... ", vo.toString());
        webSocketService.send(vo);
        return ResponseMessage.ok(vo.getPayload());
    }

    @MessageMapping("/unicast")
    public ResponseMessage unicast(WebSocketMsgDefaultVo vo) throws Exception {
        log.info("/web_socket/unicast test ... {} ", vo.toString());
        webSocketService.send(vo.getUserId(), vo);
        return ResponseMessage.ok(vo.getPayload());
    }
}

做成基础组件

可以把上面整合spring boot的示例.做成基础组件starter.给其他模块调用.这样别人使用就可以不考虑整合的细节.只要关注与业务的实现

pom


<dependency>
      <groupId>com.example</groupId>
      <artifactId>websocket-starter</artifactId>
</dependency>

yml配置

commons.websocket:
  # 监听的节点
  endPoint: "/ws/stomp"
  # 跨域支持
  allowedOrigins: "*"
  # 可订阅的主题
  enableSimpleBroker:
   - "/topic"
   - "/queue"
   - "/user"
   - "/client"
  # 客户端向服务器发消息时的前缀
  applicationDestinationPrefixes: "/ws"
  # 心跳的间隔
  heartBeatInterval: 10000

前端使用

使用stomp js 来操作websocket

官网api地址

https://stomp-js.github.io/stomp-websocket/codo/class/Client.html

引入

<script type="text/javascript" src="http://cdn.jsdelivr.net/sockjs/0.3.4/sockjs.min.js"></script>
<script type="text/javascript" src="http://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.min.js"></script>

连接

// 开启socket连接
    function connect() {
        var socket = new SockJS('/ws/stomp');
        stompClient = Stomp.over(socket);
        stompClient.connect({"userId": "1", "onDisconnectTopic": "/topic", "clientId": "1"}, function (frame) {
            setConnected(true);
            subscribe();
        });
    }

订阅

function subscribe() {
        console.log("subscribe");
        stompClient.subscribe("/topic", function (data) {
            var message = data.body;
            messageList.append("<li>" + message + "</li>");
        });
    }

发送消息

// 向‘/ws/customizedcast’服务端发送消息
    function sendName() {
        var value = document.getElementById('name').value;
        stompClient.send("/ws/clientcast", {}, JSON.stringify({
            "destination": "/topic",
            "payload": "payload " + value,
            "clientId": "1",
            "onErrorDestination":"/topic"
        }));
    }

断开

// 断开socket连接
    function disconnect() {
        if (stompClient != null) {
            stompClient.disconnect(function (frame) {
                setConnected(false);
            }, {"userId": "1", "onDisconnectTopic": "/topic", "clientId": "1"});
        }
        console.log("Disconnected");
    }

心跳

为了使客户端与服务器的连接保活(若客户端、服务器长时间不通信,就会断开)定义了一套维护心跳的机制.就是客户端会起定时任务发送ping帧,服务端收到返回一个pong帧消息.来保证连接的存活

>>> PING stomp.min.js:8 
<<< PONG stomp.min.js:8 

例子

简易聊天室

1. 打开浏览器A,B
2. A广播消息 1
3. B广播消息 2
4. A发送消息a给B
5. B发送消息b给A
  • 最后显示如下
image.png

思考题

  • 客户端如何处理断线重连机制
  • 客户端如何处理事务的发送机制
  • 服务器如何处理统一异常

参考资料

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

推荐阅读更多精彩内容