WebSocket实践:建立一个Web QQ应用

前言

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

相关阅读: 简单易懂理解WebSocket       轮询和长连接的优缺点

1、Web QQ项目结构

2、pom.xml依赖

<?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">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.1.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.scb</groupId>
    <artifactId>websocketdemo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>websocketdemo</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </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.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <!-- JSP -->
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>jstl</artifactId>
            <version>1.2</version>
        </dependency>
        <dependency>
            <groupId>org.apache.tomcat.embed</groupId>
            <artifactId>tomcat-embed-jasper</artifactId>
        </dependency>

    </dependencies>

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

</project>

3、因为使用JSP作为视图,所以在application.yml中进行配置视图解析器

spring:
  mvc:
    view:
      prefix: /WEB-INF/views/
      suffix: .jsp

4、创建WebSocketConfig配置类,配置ServerEndpointExporter为bean,放入Spring IOC容器中。ServerEndpointExporter对象用来定义WebSocket服务器的端点,这样客户端就能请求服务器的端点了。

package com.scb.websocketdemo.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();
    }
}

       有了这个bean,就可以使用@ServerEndpoint 定义一个端点服务类。在这个端点服务类中,还能定义WebSocket的打开、关闭、错误和发送消息的方法,如下所示:

5、定义WebSocket服务端站点

package com.scb.websocketdemo.service;

import org.springframework.stereotype.Service;

import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.concurrent.CopyOnWriteArraySet;

@ServerEndpoint("/ws")
@Service
public class WebSocketServiceImpl {
    // 静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
    private static int onlineCount=0;
    // concurrent包的线程安全Set,用来存放每个客户端对应的WebSocketServiceImpl对象。
    // 若要实现服务端与单一客户端通信的话,可以使用Map来存放,其中Key可以为用户标识
    private static CopyOnWriteArraySet<WebSocketServiceImpl> webSocketSet=new CopyOnWriteArraySet<>();
    // 与某个客户端的连接会话,需要通过它来给客户端发送数据
    private Session session;

    /**
     * 发送消息
     * @param message 客户端消息
     * @throws IOException
     */
    private void sendMessage(String message) throws IOException{
        this.session.getBasicRemote().sendText(message);
    }

    /**
     *
     * @return 返回在线人数
     */
    private static synchronized int getOnlineCount(){
        return onlineCount;
    }

    /**
     * 当连接人数增加时
     */
    private static synchronized void addOnlineCount(){
        WebSocketServiceImpl.onlineCount++;
    }

    /**
     * 当连接人数减少时
     */
    private static synchronized void subOnlineCount(){
        WebSocketServiceImpl.onlineCount--;
    }

    /**
     * 连接建立成功调用的方法
     * @param session
     */
    @OnOpen
    public void onOpen(Session session){
        this.session=session;
        webSocketSet.add(this);
        addOnlineCount();
        System.out.println("New connection join. Now count is "+getOnlineCount());
        try{
            sendMessage("has new connection join");
        }catch (IOException e){
            System.out.println(e);
        }
    }

    /**
     * 连接关闭调用的方法
     */
    @OnClose
    public void onClose(){
        webSocketSet.remove(this);
        subOnlineCount();
        System.out.println("A connection has closed. Now count is "+getOnlineCount());
    }

    /**
     * 收到客户端消息后调用的方法
     * @param message 客户端发送过来的消息
     * @param session
     */
    @OnMessage
    public void onMessage(String message,Session session){
        System.out.println("Message from client:"+message);

        // 群发消息(首先,服务器接收到客户端的message,然后在这里遍历所有的客户端WebSocketServiceImpl对象,群发消息给他们。)
        for(WebSocketServiceImpl item:webSocketSet){
            try{
                item.sendMessage(message);
            }catch (IOException e){
                e.printStackTrace();
            }
        }
    }

    /**
     * 发生错误时调用
     * @param session
     * @param error
     */
    @OnError
    public void onError(Session session,Throwable error){
        System.out.println("Error");
        error.printStackTrace();
    }
}

@ServerEndpoint 注解是一个类层次的注解,它的功能主要是将目前的类定义成一个websocket服务器端。注解的值将被用于监听用户连接的终端访问URL地址。

onOpen 和 onClose 方法分别被@OnOpen和@OnClose 所注解。这两个注解的作用不言自明:他们定义了当一个新用户连接和断开的时候所调用的方法。

onMessage 方法被@OnMessage所注解。这个注解定义了当服务器接收到客户端发送的消息时所调用的方法。注意:这个方法可能包含一个javax.websocket.Session可选参数(在我们的例子里就是session参数)。如果有这个参数,容器将会把当前发送消息客户端的连接Session注入进去。

@OnError注解是标注客户端请求WebSocket服务端点发生异常时的调用方法。

注意:在onMessage方法中,我们通过轮询对所有的客户端连接都给予发送信息,所以当一个客户端发送消息时,所有的客户端连接都会接收到消息。但是有时候可能只是需要发送给特定的用户,则需要得到用户的信息,可以通过以下方式得到,然后在发送给特定用户。

    /**
     * 收到客户端消息后调用的方法
     * @param message 客户端发送过来的消息
     * @param session
     */
    @OnMessage
    public void onMessage(String message,Session session){
        System.out.println("Message from client:"+message);

        // 群发消息(首先,服务器接收到客户端的message,然后在这里遍历所有的客户端WebSocketServiceImpl对象,群发消息给他们。)
        for(WebSocketServiceImpl item:webSocketSet){
            try{
                /**
                 * 获取当前用户名称
                 */
                String userName=item.session.getUserPrincipal().getName();
                System.out.println("UserName:"+userName);
                item.sendMessage(message);
            }catch (IOException e){
                e.printStackTrace();
            }
        }
    }

6、WebSocket页面开发

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<!DOCTYPE html>
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <title>My WebSocket</title>
    <script type="text/javascript" src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
    <script type="text/javascript" src="./../js/websocket.js"></script>
</head>
<body>
    test WebSocket endpoint
    <br />
    <input id="message" type="text" />
    <button onclick="sendMessage()">send message</button>
    <button onclick="closeWebSocket()">close websocket connection</button>
    <div id="context"></div>
</body>
</html>

       这段代码中定义了一个文本框和两个按钮和一个div。其中文本框是给予用户输入消息的,两个按钮一个是发送消息,另一个是关闭WebSocket连接。JavaScript则是引入了JQuery和自定义的一个脚本。代码如下:

var websocket=null;
// 判断当前浏览器是否支持websocket
if ('WebSocket' in window){
    // 创建websocket对象,连接服务器端点
    websocket=new WebSocket("ws://localhost:8080/ws");
} else {
    alert('Not support websocket');
}

// 连接发生错误时的回调方法
websocket.onerror=function () {
    appendMessage("error");
}

// 连接成功建立的回调方法
websocket.onopen=function (event) {
    appendMessage("open");
}

// 接收到消息时的回调方法
websocket.onmessage=function (event) {
    appendMessage(event.data);
}

// 连接关闭时的回调方法
websocket.onclose=function () {
    appendMessage("close");
}

// 监听窗口关闭事件,当窗口关闭时,主动关闭websocket连接
// 防止连接还没断开就关闭窗口,srever端会抛出异常
window.onbeforeunload=function (ev) {
    websocket.close();
}

// 将消息显示在id为context的区域内
function appendMessage(message){
    var context=$("#context").append("<br/>"+message);
}

// 关闭连接
function closeWebSocket(){
    websocket.close();
}

// 发送消息
function sendMessage(){
    var message=$("#message").val();
    websocket.send(message);
}

7、WebSocket控制器

package com.scb.websocketdemo.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("/websocket")
public class WebSocketController {
    // 跳转到websocket界面
    @GetMapping("/index")
    public String websocket(){
        return "websocket";
    }
}

这样,当我们运行项目,访问http://localhost:8080/websocket/index 时,就可以打开WebSocket连接了。

8、结果截图

这里打开两个页面,访问http://localhost:8080/websocket/index

服务端控制台此时截图如下:

当我们在其中一个客户端发送消息时:

此时服务端控制台输出如下:

后记

目前很多浏览器已经实现了WebSocket协议,但是依然存在很多浏览器没有实现该协议,为了兼容那些没有实现该协议的浏览器,往往还需要通过STOMP协议来完成这些兼容,有关STOMP协议的内容在下一章,我们在继续讨论。

 

STOMP实践:点对点和广播通信(系统推送公告和用户聊天功能)

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值