Spring Boot入门教程(五十五): WebSocket

一:HTTP和WebSocket

HTTP : 客户端 -> 服务器端

传统客户端(浏览器)向服务器获取数据只能使用http主动向服务器拉数据,因为http只能从客户端发起请求,没办法从服务器端发起请求。项目中如果要即时的获取消息就只能写个定时器,每隔几秒钟去向服务器发一个http请求,获取最新的数据。http虽然是短连接,但是定时每隔几秒钟去定时发请求,会不断的占用请求,当客户端从服务端没有拉取到数据时,此时这次连接就显的浪费。

WebSocket: 客户端 <-> 服务器端

WebSocket与http最大的不同就是客户端可以主动向服务器端拉取数据,服务器端也可以主动向客户端推送数据,当客户端启动的时候会首先建立一个长连接,当需要的时候服务器端就可以通过该长连接向客户端推送数据。

HTTP与WebSocket的区别

  • HTTP采用http协议,WebSocket采用ws协议
  • HTTP是短连接,连接响应后即断开;WebSocket是长连接(因是长连接所以同时连接的数量不能太大)
  • HTTP只能客户端向服务器发送请求,不能服务器端向客户端发起请求;WebSocket都可以

WebSocket常见使用场景

  1. 聊天
  2. 服务器端向客户端推送消息(如常见的右下角向上弹出个消息、新的未读消息数量等)

二:示例

在这里插入图片描述

1. pom.xml

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<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>com.alibaba</groupId>
   <artifactId>fastjson</artifactId>
   <version>1.2.51</version>
</dependency>
<dependency>
   <groupId>org.projectlombok</groupId>
   <artifactId>lombok</artifactId>
   <optional>true</optional>
</dependency>

2. Configuration

/**
 * 开启WebSocket支持
 */
@Configuration
public class WebSocketConfiguration {

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

3. WebSocketServer

@Data
@ToString
@RequiredArgsConstructor
public class Payload {
    private String from;
    private String to;
    private String content;
}
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.thymeleaf.util.DateUtils;

import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.Date;
import java.util.Locale;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * ServerEndpoint 用户定义客户端连接的地址, 可以在路径上指定路径参数
 */
@Slf4j
@Component
@ServerEndpoint("/websocket/{userId}")
public class WebSocketServer {

    /** 在线连接数量 */
    private static final AtomicInteger onlineCount = new AtomicInteger(0);

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

    /** 当前连接的用户id */
    private String userId;


    /**
     * 连接成功建立时调用该方法
     * @param session
     * @param userId
     */
    @OnOpen
    public void onOpen(Session session, @PathParam("userId") String userId) {
        sessionMap.put(userId, session);
        this.userId = userId;
        int currentOnlineCount = onlineCount.incrementAndGet();
        log.info("{} 连接创建成功,当前用户id为{}, 当前在线人数{}", now() , userId, currentOnlineCount);

        JSONObject succesJson = new JSONObject();
        succesJson.put("from", "服务器");
        succesJson.put("to", userId);
        succesJson.put("content", "WebSocket服务器连接成功!");

        sendMessage(session, succesJson.toJSONString());
    }

    @OnMessage
    public void onMessage(String message, Session session) {
        sendMessage(session, message);
    }

    @OnError
    public void onError(Session session, Throwable throwable) {
        throwable.printStackTrace();
        log.error(throwable.toString());
    }

    @OnClose
    public void onClose(Session session) {
        sessionMap.remove(this.userId);
        int currentOnlineCount = onlineCount.decrementAndGet();

        log.info("用户:{} 退出连接,当前连接数为:{}", this.userId, currentOnlineCount);
    }


    /**
     * 向指定会话发送消息
     * @param session 当前会话session
     * @param message 消息内容
     */
    public void sendMessage(Session session, String message) {
        Payload payload = JSONObject.parseObject(message, Payload.class);
        String toUserId = payload.getTo();
        Session targetSession = sessionMap.get(toUserId);
        if (targetSession == null) {
            log.info("目标用户{} 已退出连接", toUserId);
            return;
        }

        try {
            targetSession.getBasicRemote().sendText(now() + " " + message);
        } catch (IOException e) {
            log.error("发送消息失败, 失败原因为:{}", e);
        }
    }

    /**
     * 向指定用户发送消息
     * @param userId 用户id
     * @param message 消息
     */
    public void sendMessage(String userId, String message) {
        Session session = sessionMap.get(userId);
        if (session != null) {
            JSONObject succesJson = new JSONObject();
            succesJson.put("from", "服务器");
            succesJson.put("to", userId);
            succesJson.put("content", message);
            
            this.sendMessage(session, succesJson.toJSONString());
        } else {
            log.info("用户{} 已退出连接,无法发送消息", userId);
        }
    }

    private String now() {
        return DateUtils.format(new Date(), "yyyy-MM-dd HH:mm:ss", Locale.CHINESE);
    }
}

4. Controller

@Controller
@RequestMapping("/im")
public class WebSocketController {

    @Autowired
    private WebSocketServer webSocketServer;

    @RequestMapping("/users/{from}/{to}")
    public ModelAndView index(@PathVariable("from") String from, @PathVariable("to") String to){
        ModelAndView modelAndView = new ModelAndView("index");
        modelAndView.addObject("from", from);
        modelAndView.addObject("to", to);
        return modelAndView;
    }

    /**
     * 模拟服务器端向客户端推送消息
     * @param userId
     * @param message
     */
    @ResponseBody
    @RequestMapping("/push/{userId}")
    public void mockPushMessageToClient(@PathVariable String userId, String message) {
        webSocketServer.sendMessage(userId, message);
    }
}

5. html

templates/index.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<script>
    var webSocket = null;
    var from = [[${from}]];
    var to = [[${to}]];

    function openSocket() {
        if(typeof(WebSocket) == "undefined") {
            console.log("您的浏览器不支持WebSocket, 请升级您的浏览器的版本");
        }else{
            // 连接WebSocket服务器端
            var userId = [[${from}]];
            webSocket = new WebSocket("ws://localhost:8080/websocket/" + userId);

            // 打开事件
            webSocket.onopen = function() {
                console.log("连接服务器成功。");
            };

            // 获得消息事件
            webSocket.onmessage = function(msg) {
                console.log(msg.data);
                document.getElementById('message').innerHTML += msg.data + '<br/>';
            };

            // 关闭事件
            webSocket.onclose = function() {
                console.log("websocket已关闭");
            };

            // 错误事件
            webSocket.onerror = function(socket, event) {
                console.log("websocket发生了错误");
            };

            // 监听浏览器窗口关闭事件,当关闭窗口时关闭websocket连接,节省连接资源
            window.onbeforeunload = function () {
                websocket.close();
            }
        }
    }

    function sendMessage() {
        if(typeof(WebSocket) == "undefined") {
            console.log("您的浏览器不支持WebSocket");
        }else {
            var content = document.getElementById('msg').value;
            var payload = {'from': from, 'to': to, 'content': content}
            webSocket.send(JSON.stringify(payload));
        }
    }

</script>
<body>
<input id="msg" type="text"/>

<button onclick="sendMessage()">发送消息</button>
<button onclick="openSocket()">连接WebSocket(只需执行一次)</button>

<div id="message"></div>
</body>
</html>

在这里插入图片描述

发布了308 篇原创文章 · 获赞 936 · 访问量 133万+

猜你喜欢

转载自blog.csdn.net/vbirdbest/article/details/89160559