websocket详解

概述

WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。

WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。

首先,Websocket是一个持久化的协议,相对于HTTP这种非持久的协议来说。HTML5 定义的 WebSocket 协议,能更好的节省服务器资源和带宽,并且能够更实时地进行通讯,看下图:
在这里插入图片描述
首先我们来看个典型的 Websocket 握手

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
Origin: http://example.com

熟悉HTTP的童鞋可能发现了,这段类似HTTP协议的握手请求中,多了几个东西。我会顺便讲解下作用

Upgrade: websocket
Connection: Upgrade

这个就是Websocket的核心了,告诉 Apache 、 Nginx 等服务器:注意啦,我发起的是Websocket协议,快点帮我找到对应的助理处理~不是那个老土的HTTP。

Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13

首先, Sec-WebSocket-Key 是一个 Base64 encode 的值,这个是浏览器随机生成的,告诉服务器:泥煤,不要忽悠窝,我要验证尼是不是真的是Websocket助理。

然后, Sec_WebSocket-Protocol 是一个用户定义的字符串,用来区分同URL下,不同的服务所需要的协议。简单理解:今晚我要服务A,别搞错啦~

最后, Sec-WebSocket-Version 是告诉服务器所使用的 Websocket Draft (协议版本),在最初的时候,Websocket协议还在 Draft 阶段,各种奇奇怪怪的协议都有,而且还有很多期奇奇怪怪不同的东西,什么Firefox和Chrome用的不是一个版本之类的,当初Websocket协议太多可是一个大难题。。不过现在还好,已经定下来啦大家都使用的一个东西 脱水: 服务员,我要的是13岁的噢→_→
然后服务器会返回下列东西,表示已经接受到请求, 成功建立Websocket啦!

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
Sec-WebSocket-Protocol: chat

这里开始就是HTTP最后负责的区域了,告诉客户,我已经成功切换协议啦~

Upgrade: websocket
Connection: Upgrade

依然是固定的,告诉客户端即将升级的是 Websocket 协议。
然后, Sec-WebSocket-Accept 这个则是经过服务器确认,并且加密过后的 Sec-WebSocket-Key 。
后面的, Sec-WebSocket-Protocol 则是表示最终使用的协议。

tomcat代码实现

项目结构

项目结构

<?xml version="1.0" encoding="UTF-8"?>
<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">
    <parent>
        <artifactId>websocket</artifactId>
        <groupId>com.websocket</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>websocket-tomcat</artifactId>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.12</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>


</project>
package com.ax.config;


import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

@Configuration
public class WebSocketConfig {

    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }

}

websocket连接握手

package com.ax.config;

import org.springframework.util.StringUtils;

import javax.servlet.http.HttpSession;
import javax.websocket.HandshakeResponse;
import javax.websocket.server.HandshakeRequest;
import javax.websocket.server.ServerEndpointConfig;

public class WsHandshake extends ServerEndpointConfig.Configurator {

    /**
     * 握手,可以从session中获取属性,前提是先调用 com.ax.controller.LoginController#login(javax.servlet.http.HttpSession)
     * @param sec
     * @param request
     * @param response
     */
    @Override
    public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {

        Object httpSession = request.getHttpSession();
        if (httpSession == null) {
            return;
        }
        HttpSession session = (HttpSession) request.getHttpSession();
        String username = (String) session.getAttribute("username");
        if (StringUtils.isEmpty(username) || !"123456".equals(username)) {
            System.out.println("用户已经登录");
        }
    }
}

简单的登录接口,把登录用户名放入session

package com.ax.controller;

import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpSession;

@RestController
public class LoginController {

    @GetMapping("/login")
    public String login(HttpSession httpSession, String username) {

        if (!StringUtils.isEmpty(username)) {
            httpSession.setAttribute("username", username);
            return "success";
        }
        return "fail";
    }

}

定时任务模拟推送消息,分别是广播推送和队列推送

package com.ax.service;

import com.ax.websocket.WebSocketServer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

@Component
public class OrderTask {

    @Autowired
    private WebSocketServer webSocketServer;

    @Scheduled(cron = "0/1 * * * * ?")
    public void send() {
        webSocketServer.sendAll("发送全部订单");
        webSocketServer.sendToUser("123456", "发送单个订单");
    }

}

WebSocketServer,连接路径指定了username和握手,username可以用来鉴权,添加了广播推送和队列推送两种常见的方式。

package com.ax.websocket;

import com.ax.config.WsHandshake;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@ServerEndpoint(value = "/hello/{username}", configurator = WsHandshake.class)
@Component
@Slf4j
public class WebSocketServer {

    private static Map<String, Session> sessions = new ConcurrentHashMap<>();

    private static Map<String, String> users = new ConcurrentHashMap<>();

    @OnOpen
    public void onOpen(Session session, @PathParam("username") String username) {
        log.info("建立连接, username: " + username);
        sessions.put(session.getId(), session);
        users.put(username, session.getId());
    }

    @OnClose
    public void onClose(Session session) {
        sessions.remove(session.getId());
        for (Map.Entry<String, String> entry : users.entrySet()) {
            if (entry.getValue().equals(session.getId())) {
                users.remove(entry.getKey());
            }
        }
        log.info("关闭连接");
    }

    @OnMessage
    public void onMessage(Session session, String message) {
        log.info("接收消息: " + message);
        try {
            session.getBasicRemote().sendText("hello, client");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @OnError
    public void onError(Session session, Throwable ex) {
        log.error("发生异常: " + ex);
    }

    public void sendAll(String message) {

        try {
            for (Session session : sessions.values()) {
                session.getBasicRemote().sendText(message);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void sendToUser(String username, String message) {
        try {
            String sessionId = users.get(username);
            if (!StringUtils.isEmpty(sessionId)) {
                sessions.get(sessionId).getBasicRemote().sendText(message);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
package com.ax;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication
@EnableScheduling
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

<h1>hello</h1>

<button id="btn">发送</button>

<a href="/login" onclick="return false">登录</a>


<script>
    var ed = null;

    if('WebSocket' in window){
        ed = new WebSocket("ws://localhost:8080/hello/123456");
    } else {
        alert('Not support ed')
    }

    ed.onError = function(){
        console.log('error')
    };

    ed.onOpen = function(event){
        console.log('onOpen' + event)
    }

    ed.onMessage = function(event){
        console.log('接收消息 ' + event)
    }

    ed.onClose = function(){
        console.log('关闭连接')
    }

    var btn = document.getElementById('btn')
    btn.onclick = function(){
        ed.send('hello, server')
    }

</script>


</body>
</html>

优缺点

优点

  • 代码实现简单

缺点

  • 比较原始,如tcp和http的关系,这个websocket就像tcp,很多功能需要自己实现
  • 握手拦截缺少需要的方法和参数
  • 心跳等功能需要自己实现
  • 发布订阅功能需要自定义协议实现
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值