WebSocket

WebSocket

一、什么是WebSocket?

  • WebSocket是HTML5下一种新的协议(websocket协议本质上是一个基于tcp的协议)
  • 它实现了浏览器与服务器全双工通信,能更好的节省服务器资源和带宽并达到实时通讯的目的
  • WebSocket是一个持久化的协议

WebSocket允许我们创建“实时”应用程序,与传统API协议相比,该应用程序速度更快且开销更少。
在这里插入图片描述

二、websocket的原理

  • websocket约定了一个通信的规范,通过一个握手的机制,客户端和服务器之间能建立一个类似tcp的连接,从而方便它们之间的通信
  • 在websocket出现之前,web交互一般是基于http协议的短连接或者长连接
  • websocket是一种全新的协议,不属于http无状态协议,协议名为"ws"

三、WebSocket是如何工作的

按照传统的定义,WebSocket是一种双工协议,主要用于客户端-服务器通信通道。它本质上是双向的,这意味着通信在客户端与服务器之间来回发生。

使用 WebSocket 开发的连接只要任何参与方中断连接就会持续存在。一旦一方断开连接,另一方将无法进行通信,因为连接会在其前面自动断开。

WebSocket需要HTTP的支持来发起连接。说到它的实用性,当涉及到数据的无缝流和各种不同步流量时,它是现代 Web 应用程序开发的支柱。

四、为什么需要WebSocket以及何时应该避免使用

WebSocket 是一种重要的客户端-服务器通信工具,人们需要充分了解其实用性并避免使用其最大潜力的场景。

在以下情况下使用 WebSocket:

1.开发实时网络应用程序

WebSocket 最常见的用途是实时应用程序开发,其中它有助于在客户端连续显示数据。当后端服务器不断发回这些数据时,WebSocket 允许在已经打开的连接中不间断地推送或传输这些数据。WebSocket 的使用使此类数据传输变得快速并充分利用了应用程序的性能。

此类 WebSocket 实用程序的一个现实示例是比特币交易网站。在这里,WebSocket 协助部署的后端服务器向客户端发送数据处理。

‍ 2.创建聊天应用程序

聊天应用程序开发人员在一次性交换和发布/广播消息等操作中向 WebSocket 寻求帮助。由于使用相同的 WebSocket 连接来发送/接收消息,因此通信变得简单快捷。

‍ 3.正在开发游戏应用程序

在游戏应用程序开发过程中,服务器必须不间断地接收数据,而不要求 UI 刷新。WebSocket 可以在不影响游戏应用程序 UI 的情况下实现这一目标。

既然已经清楚了应该在哪里使用 WebSocket,请不要忘记了解应该避免使用 WebSocket 的情况,让自己远离大量的操作麻烦。

当需要获取旧数据或仅需要一次性处理数据时,不应该使用 WebSocket。在这些情况下,使用 HTTP 协议是明智的选择。

五、WebSocekt与HTTP的关系

  • 相同点
    • 都是基于tcp的,都是可靠性传输协议
    • 都是应用层协议
  • 不同点
    • WebSocket是双向通信协议,模拟Socket协议,可以双向发送或接受信息
    • HTTP是单向的
    • WebSocket是需要浏览器和服务器握手进行建立连接的
    • 而http是浏览器发起向服务器的连接,服务器预先并不知道这个连接
  • 两者之间的联系
    • WebSocket在建立握手时,数据是通过HTTP传输的。但是建立之后,在真正传输时候是不需要HTTP协议的。

总结:

  1. 首先,客户端发起http请求,经过3次握手后,建立起TCP连接;http请求里存放WebSocket支持的版本号等信息,如:Upgrade、Connection、WebSocket-Version等;
  2. 然后,服务器收到客户端的握手请求后,同样采用HTTP协议回馈数据;
  3. 最后,客户端收到连接成功的消息后,开始借助于TCP传输信道进行全双工通信。

由于 HTTP 和 WebSocket 都用于应用程序通信,因此人们经常感到困惑,并且很难从这两者中选择一个。看一下下面提到的文本,可以更清楚地了解 HTTP 和 WebSocket。

如前所述,WebSocket 是一种框架式双向协议。相反,HTTP 是一个在 TCP 协议之上运行的单向协议。

由于WebSocket协议能够支持连续的数据传输,因此主要用于实时应用程序开发。HTTP 是无状态的,用于开发RESTful和 SOAP 应用程序。Soap仍然可以使用HTTP来实现,但是REST被广泛传播和使用。

在 WebSocket 中,通信发生在两端,这使其成为更快的协议。在 HTTP 中,连接是在一端建立的,这使得它比 WebSocket 有点慢。

WebSocket使用统一的TCP连接,需要一方终止连接。在发生这种情况之前,连接将保持活动状态。HTTP 需要为单独的请求构建不同的连接。请求完成后,连接会自动断开。

六、使用WebSocket

6.1 Java原生实现

6.1.1 使用注解
6.1.1.1 @ServerEndpoint

表示当前类是一个websocket的服务器,values属性指定一个连接服务器的url地址

@ServerEndpoint("/connect")
public class WebSocketServer {
}
6.1.1.2 @OnOpen

打开连接的方法,只有在客户端连接服务端的时候这个方法会调用一次

// session表示一个websocket客服端的连接会话每一个客户端连接就会创建一个Session会话
// 注意:它不是HtppSession中的会话,而是websocket里的
@OnOpen
public void onOpen(Session session) {
    log.info("客服端已连接");
    // 将session添加到用户列表中
    users.add(session);
}
6.1.1.3 @OnMessage

接受客户端发送的消息方法

@OnMessage
public void onMessage(String message, Session session) throws IOException {
    log.info("消息:" + message);
    // 向当前客户端发送一个消息
    session.getBasicRemote().sendText("Hello client");
}
6.1.1.4 @OnClose

当客户端断开连接后调用此方法

@OnClose
public void onClose(Session session){
    log.info("客户端已断开连接");
}
6.1.2 群发信息案列

1)添加WebSocket依赖

<!-- spring websocket在spring4.0版本开始支持 -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-websocket</artifactId>
    <version>5.3.23</version>
</dependency>

2)编写消息对象

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Message {
    /**
     * 发送人
     */
    private String fromUser;
    /**
     * 发送时间
     */
    private String sendTime;
    /**
     * 发送内容
     */
    private String content;
}

3)编写用户登录的controller

@WebServlet("/login")
public class LoginServlet extends HttpServlet {

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String userName = req.getParameter("username");
        // 将用户名保存到HttpSession中
        req.getSession().setAttribute("username",userName);
        // 重定向到聊天首页
        resp.sendRedirect("chat.html");
    }
}

4)编写握手连接处理类

由于websocket在交互得第一次请求是基于HTTP协议进行握手的,因此可以在这个类中得到握手请求对象,从而得到HttpSession的信息

public class WebSocketHandshake extends Configurator {

    /**
     * 重写握手处理方法
     * @param sec
     * @param request 请求对象
     * @param response 响应对象
     */
    @Override
    public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {
        // 获取HttpSession对象
        HttpSession httpSession = (HttpSession) request.getHttpSession();
        // 获取用户名
        String userName = (String) httpSession.getAttribute("username");
        // 将用户保存到当前用户连接websocket的Session中
        sec.getUserProperties().put("user",userName);
    }
}

5)编写WebSocket服务器

@Slf4j
@ServerEndpoint(value = "/connect", configurator = WebSocketHandshake.class)
public class CharServer {

    /**
     * 用户列表
     * key为用户id或者name,
     * value则是每一个客户端的Session
     */
    private static final Map<String, Session> users = new HashMap<>();

    @OnOpen
    public void onOpen(Session session) {
        // 获取Session中的用户名
        String userName = (String) session.getUserProperties().get("user");
        // 添加到用户列表中
        users.put(userName, session);
    }

    @OnMessage
    public void onMessage(String message, Session session) throws Exception {
        // 获取发送人
        String fromUser = (String) session.getUserProperties().get("user");
        // 创建发送时间
        String sendTime = new SimpleDateFormat("hh:mm").format(new Date());
        // 封装消息对象并序列化为json
        Message msg = new Message(fromUser, sendTime, message);
        String jsonMessage = new ObjectMapper().writeValueAsString(msg);
        // 群发给所有人
        for (String userName : users.keySet()){
            Session s = users.get(userName);
            s.getBasicRemote().sendText(jsonMessage);
        }
    }

    @OnClose
    public void onClose(Session session) {
        // 将用户移除在线列表
        String userName = (String) session.getUserProperties().get("user");
        users.remove(userName);
    }
}

6)编写登录页面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>用户登录</h1>
<form name="f1" method="post" action="login">
    Name:<input type="text" name="username"/><br>
    <input type="submit" value="登录">
</form>

</body>
</html>

7)编写聊天室页面实现信息发送

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script src="js/jquery-1.8.2.js"></script>
</head>
<body>
<h1>聊天室</h1>
<div id="msg"></div>
<input type="text" id="message"/><br>
<input type="button" value="发送">

<script>
    // 构建websocket实例,连接后台server的请求地址
    // websocket在第一次请求时使用http协议连接服务端,告诉服务器
    // 接下来要使用websocket进行通信,此时将进行协议升级,会在
    // http的请求头中带有upgrade:websocket的头信息
    let ws = new WebSocket('ws://localhost:8080/connect');
    // 接收服务端的消息
    ws.onmessage = function (event){
        // 将消息填充到div中
        let data = event.data;
        // 将json字符串转换为json对象
        data = $.parseJSON(data);
        $('#msg').append(data.fromUser + ':' + data.sendTime + '<br>');
        $('#msg').append(data.content + '<br>');
    }
    $(function() {
        $(':button').on('click', function() {
            let msg = $('#message').val();
            // 发送消息
            ws.send(msg);
            $('#message').val('');
        })
    })
</script>
</body>
</html>

6.2 结合spring实现

6.2.1 使用注解
6.2.1.1 @EnableWebSocket

启动websocket支持,一般放在websocket配置类上。

@Configuration
// 启用websocket支持
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
  // ...
}
6.2.2 具体案列

1)添加websocket依赖

2)编写消息对象类

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Message {
    private String fromUser;
    private String sendTime;
    private String content;
}

3)编写websocket配置类

@Configuration
// 启用websocket支持
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
    /**
     * 装配服务端
     *
     * @return
     */
    @Bean
    public WebSocketHandler webSocketHandler() {
        return new ChatServer();
    }

    /**
     * 装配HttpSession的拦截器,这样就可以在握手阶段
     * 获取HttpSession的内容,在使用WebSocketSession时
     * 就能直接得到HttpSession的数据
     *
     * @return
     */
    @Bean
    public HandshakeInterceptor handshakeInterceptor() {
        return new HttpSessionHandshakeInterceptor();
    }

    /**
     * 给服务端注册请求的端点(映射连接地址)
     *
     * @param registry
     */
    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
                // 给ChatServer设置连接的url
        registry.addHandler(webSocketHandler(), "/connect")
                // 设置握手拦截器
                .addInterceptors(handshakeInterceptor());
    }
}

4)编写MvcConfig、WebConfig以及主配置AppConfig

可以参考以前编写的配置类

5)编写spring封装的websocket服务端

需要继承一个TextWebSocketHandler接口,表示一个服务端用户处理文本数据的消息。

public class ChatServer extends TextWebSocketHandler {

    /**
     * 用户列表
     * 每一个用户连接时都会创建一个WebSocketSession对象
     */
    private static final Map<String, WebSocketSession> users = new HashMap<>();

    /**
     * 客户端建立连接后执行的方法,等效于onOpen方法
     *
     * @param session
     * @throws Exception
     */
    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        // 获取登录的用户信息
        String username = (String) session.getAttributes().get("user");
        // 保存到用户列表
        users.put(username, session);
    }

    /**
     * 接收客户端的消息,等效于onMessage方法
     *
     * @param session
     * @param message
     * @throws Exception
     */
    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        // 获取消息载体,也就是客户端发送的文本内容
        String msgContent = message.getPayload();
        // 获取发送人
        String fromUser = (String) session.getAttributes().get("user");
        // 发送时间
        String sendTime = new SimpleDateFormat("hh:mm").format(new Date());
        // 封装消息对象
        Message msg = new Message(fromUser, sendTime, msgContent);
        // 序列化为json字符串
        String json = new ObjectMapper().writeValueAsString(msg);
        // 群发给所有人
        for (String userName : users.keySet()) {
            WebSocketSession s = users.get(userName);
            // 发送消息,必须是一个TextMessage对象
            s.sendMessage(new TextMessage(json));
        }
    }

    /**
     * 连接关闭后执行的方法,等效于onClose方法
     *
     * @param session
     * @param status
     * @throws Exception
     */
    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        String userName = (String) session.getAttributes().get("user");
        // 将用户移出在线列表
        users.remove(userName);
    }
}

6)编写一个简单的用户登录Controller

@Controller
public class UserController {
    @PostMapping("/user/login")
    public String login(String username, HttpSession session) {
        // 将用户信息保存到会话作用域
        session.setAttribute("user", username);
        // 重定向到聊天的首页
        return "redirect:/static/chat.html";
    }
}

7)编写登录页面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>用户登录</h1>
<form name="f1" method="post" action="../user/login">
    Name:<input type="text" name="username"/><br>
    <input type="submit" value="登录">
</form>
</body>
</html>

8)编写聊天室页面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script src="js/jquery-1.8.2.js"></script>
</head>
<body>
<h1>聊天室</h1>
<div id="msg"></div>
<input type="text" id="message"/><br>
<input type="button" value="发送">

<script>
    // 创建websocket对象
    let ws = new WebSocket('ws://localhost:8080/connect');
    // 接收服务端的消息
    ws.onmessage = function (event){
        // 将消息填充到div中
        let data = event.data;
        // 将json字符串转换为json对象
        data = $.parseJSON(data);
        $('#msg').append(data.fromUser + ':' + data.sendTime + '<br>');
        $('#msg').append(data.content + '<br>');
    }
    $(function() {
        $(':button').on('click', function() {
            let msg = $('#message').val();
            // 发送消息
            ws.send(msg);
            $('#message').val('');
        })
    })
</script>
</body>
</html>

七、STOMP协议

7.1 简介

STOMP允许消息客户端(生产者、消费者)与任意消息代理(Broker)之间进行异步消息传输的简单文本定向消息协议。但STOMP并不是为WebSocket而设计的,它是属于消息队列的一种协议(AMQP、JMS等都属于消息队列协议)。许多消息队列都支持STOMP协议(例如:RabbitMQ、ActiveMQ)。由于它的简单性,因此可以用于定义websocket的消息体格式。我们先建立了webscoket连接, 接下来我只需要在webscoket连接的基础上建立stomp连接,因此STOMP协议格式的消息就会写入到websocket的payload中。

7.2 协议格式

STOMP协议由命令(Command)、头(Header)、消息体(Body)组成,与http协议结构相似。结构如下:

COMMAND
header1:value1
header2:value2

Body^@

其中Command包含SEND, SUBSCRIBE, MESSAGE, CONNECT, CONNECTED等命令。header则类似于http的content-length, content-type等。Body就是具体的消息内容,可以是二进制或者文本。其中^@代表null结尾。

八、WebSocket的消息代理

8.1 使用注解

8.1.1 @EnableWebSocketMessageBroker

启用websocket消息代理中间件,作用于WebSocket配置类中。

@Configuration
// 启用websocket消息代理中间件
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
  // ...
}

8.2 具体案列

1)添加spring-websocket的消息代理依赖

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-messaging</artifactId>
    <version>5.3.23</version>
</dependency>

2)编写消息对象类

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Message {
    private String content;
    private String sendTime;
}

3)编写WebSocketConfig配置类

@Configuration
// 启用websocket消息代理中间件
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    /**
     * 注册一个连接消息中间件的端点(路径url)
     *
     * @param registry
     */
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("broker");
    }

    /**
     * 配置消息代理,主要是设置相关的主题
     * 消息代理是服务中心的核心,spring-websocket内置了
     * 一个简单的消息代理,但也只是能够满足基本要求,如果
     * 需要强大的消息中心的功能,通常都会集成第三方的消息队列
     * 例如:RabbitMQ等
     *
     * @param registry
     */
    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        // 启用spring内置简单的消息代理并设置一个主图(topic)的前缀,用于消息的发送和订阅
        // enableSimpleBroker:启动内部的消息代理
        // enableStompBrokerRelay:启用第三方的消息代理
        registry.enableSimpleBroker("/news", "/video");
        // 如果需要集成外部其他的消息代理,使用下的方法
        // registry.enableStompBrokerRelay();
    }
}

4)编写余下的配置类(MvcConfig、WebConfig、AppConfig等)

可以参考之前的配置类

5)编写发布消息的controller

注意:需要注入一个SimpMessagingTemplate类(消息处理模版),用于发布消息

@RestController
@RequiredArgsConstructor
public class PublishController {

    private final SimpMessagingTemplate template;

    @PostMapping("/publish/{topic}/{sub}")
    public void publish(String message, @PathVariable("topic") String topic,@PathVariable("sub") String sub) {
        String sendTime = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(new Date());
        Message msg = new Message(message, sendTime);
        // 将消息发布到消息代理指定的主题中
        template.convertAndSend("/" + topic + "/" + sub, msg);
    }
}

6)编写页面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script src="js/jquery-1.8.2.js"></script>
</head>
<body>
<h1>后台管理</h1>
<form id="f1">
    <select id="topic">
        <option>--请选择--</option>
        <option value="sport">体育</option>
        <option value="recreation">娱乐</option>
    </select>
    <input type="text" name="message" id="message"/>
    <input type="button" value="发布"/><br>
</form>
 <script>
     $(function(){
         $(':button').on('click',function() {
             let param = $('#f1').serialize();
             let subTopic = $('#topic').val();
             $.ajax({
                 url: '/publish/news/' + subTopic,
                 type: 'post',
                 data: param,
                 success: function(data){
                    $('#message').val('');
                 }
             })
         })
     })
 </script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script src="js/jquery-1.8.2.js"></script>
    <script src="js/stomp.min.js"></script>
</head>
<body>
<h1>体育新闻页</h1>
<div id="msg"></div>
<script>
    $(function() {
        // 创建websocket实例
        let ws = new WebSocket('ws://localhost:8080/broker');
        // 将websocket包装成stomp客户端
        let stompClient = Stomp.over(ws);
        // 连接服务器并订阅消息
        // {}:放入请求头 - 键值对
        stompClient.connect({},function() {
            // 执行订阅
            stompClient.subscribe('/news/sport', function(data) {
                // 接收发布的通知内容
                // 取出STOMP中的body部分并解析为json对象
                let msg = $.parseJSON(data.body);
                $('#msg').append(msg.sendTime + '<br>')
                $('#msg').append(msg.content + '<br>')
            });
        });
    })
</script>
</body>
</html>
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

WyuanY.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值