Springboot整合websocket


一、短链接与长连接

1、短连接

客户端和服务器每进行一次通讯,就建立一次连接,通讯结束就中断连接。

 HTTP是一个简单的请求-响应协议,它通常运行在TCP之上。HTTP/1.0使用的TCP默认是短连接。

2、长连接

是指在建立连接后可以连续多次发送数据,直到双方断开连接。

 HTTP从1.1版本起,底层的TCP使用的长连接。使用长连接的HTTP协议,会在响应头加入代码:Connection:keep-alive

3、短链接与长连接的区别

1)、通讯流程

短连接:创建连接 -> 传输数据 -> 关闭连接

长连接:创建连接 -> 传输数据 -> 保持连接 -> 传输数据 -> …… -> 关闭连接

2)、适用场景

短连接:并发量大,数据交互不频繁情况。

长连接:数据交互频繁,点对点的通讯。

3)、通讯方式

方式说明
短连接 我跟你发信息,必须等到你回复我或者等了一会等不下去了,就结束通讯了
长连接 我跟你发信息,一直保持通讯,在保持通讯这个时段,我去做其他事情的当中你回复我了,我能立刻你回复了我什么,然后可以回应或者不回应,继续做事

二、websocket协议(全双工,即允许服务器向客户端发送数据)

1、何为websocket协议

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

何谓全双工:全双工(Full Duplex)是通讯传输的一个术语。双方在通信时允许数据在两个方向上同时传输,它在能力上相当于两个单工通信方式的结合。全双工指可以同时进行信号的双向传输。指A→B的同时B→A,就像是双向车道。

单工就就像是汽车的单行道,是在只允许甲方向乙方传送信息,而乙方不能向甲方传送 。

单工:信息只能单向传送。

  http1.0:单工。因为是短连接,客户端发起请求之后,服务端处理完请求并收到客户端的响应后即断开连接。

半双工:信息能双向传送但不能同时双向传送。

  http1.1:半双工。默认开启长连接keep-alive,开启一个连接可发送多个请求。

全双工:信息能够同时双向传送。

  http2.0:全双工,允许服务端主动向客户端发送数据。

(2)、在 WebSocket中,浏览器和服务器只需要完成一次握手,就可以创建持久性的连接,并进行双向数据传输

(3)、在推送功能的实现技术上,相比使用Ajax 定时轮询的方式(setInterval),WebSocket 更节省服务器资源和带宽

(4)、服务器向客户端发送数据的功能是websocket协议的典型使用场景

 2、websocket常用事件方法

以下 API 用于创建 WebSocket 对象。

var Socket = new WebSocket(url, [protocol] );

WebSocket 事件

以下是 WebSocket 对象的相关事件。假定我们使用了以上代码创建了 Socket 对象:

事件事件处理程序描述
open Socket.onopen 连接建立时触发
message Socket.onmessage 客户端接收服务端数据时触发
error Socket.onerror 通信发生错误时触发
close Socket.onclose 连接关闭时触发

WebSocket 方法

方法描述
Socket.send() 使用连接发送数据
Socket.close() 关闭连接

三、创建一个springboot工程

1、添加依赖

    
        org.springframework.boot
        spring-boot-starter-parent
        2.1.5.RELEASE
        
    
    
        
            org.springframework.boot
            spring-boot-starter-websocket
        
        
            org.springframework.boot
            spring-boot-starter-web
        
        
            org.projectlombok
            lombok
        
        
            org.apache.commons
            commons-lang3
            3.8.1
        
        
            com.alibaba
            fastjson
            1.2.58
        
    

2、创建启动类

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class);
    }
}

3、WebSocketConfig开启WebSocket的支持

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

//开启WebSocket的支持,并把该类注入到spring容器中
@Configuration
public class WebSocketConfig {

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

4、WebSocketServer

(1)、因为WebSocket是类似客户端服务端的形式(采用ws协议),那么这里的WebSocketServer其实就相当于一个ws协议的Controller。

(2)、直接@ServerEndpoint("/imserver/{userId}") 、@Component启用即可,然后在里面实现@OnOpen开启连接,@onClose关闭连接,@onMessage接收消息等方法。

(3)、新建一个ConcurrentHashMap webSocketMap 用于接收当前userId的WebSocket,方便IM之间对userId进行推送消息。单机版实现到这里就可以。

(4)、集群版(多个ws节点)还需要借助mysql或者redis等进行处理,改造对应的sendMessage方法即可。

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;

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

@ServerEndpoint("/wsServer/{userId}")
@Component
@Slf4j
public class WebSocketServer {

    /**
     * 静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
     */
    private static int onlineCount = 0;
    /**
     * concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。
     */
    private static ConcurrentHashMap webSocketMap = new ConcurrentHashMap<>();
    /**
     * 与某个客户端的连接会话,需要通过它来给客户端发送数据
     */
    private Session session;
    /**
     * 接收userId
     */
    private String userId = "";

    /**
     * 连接建立成功调用的方法
     */
    @OnOpen
    public void onOpen(Session session, @PathParam("userId") String userId) {
        this.session = session;
        this.userId = userId;
        if (webSocketMap.containsKey(userId)) {
            webSocketMap.remove(userId);
            webSocketMap.put(userId, this);
            //加入set中
        } else {
            webSocketMap.put(userId, this); // 连接建立成功后,将userId作为key,将websocket作为value存入Map中
            //加入set中
            addOnlineCount();
            //在线数加1
        }

        log.info("用户连接:" + userId + ",当前在线人数为:" + getOnlineCount());

        try {
            sendMessage("连接成功"); // 服务器主动推送数据到客户端
        } catch (IOException e) {
            log.error("用户:" + userId + ",网络异常!!!!!!");
        }
    }

    /**
     * 连接关闭调用的方法
     */
    @OnClose
    public void onClose() {
        if (webSocketMap.containsKey(userId)) {
            webSocketMap.remove(userId);
            //从set中删除
            subOnlineCount();
        }
        log.info("用户退出:" + userId + ",当前在线人数为:" + getOnlineCount());
    }

    /**
     * 收到客户端消息后调用的方法
     *
     * @param message 客户端发送过来的消息
     */
    @OnMessage
    public void onMessage(String message, Session session) {
        log.info("用户消息:" + userId + ",报文:" + message);
        //可以群发消息
        //消息保存到数据库、redis
        if (StringUtils.isNotBlank(message)) {
            try {
                //解析发送的报文
                JSONObject jsonObject = JSON.parseObject(message);
                //追加发送人(防止串改)
                jsonObject.put("fromUserId", this.userId);
                String toUserId = jsonObject.getString("toUserId");
                // 传送给对应toUserId用户的websocket
                if (StringUtils.isNotBlank(toUserId) && webSocketMap.containsKey(toUserId)) {
                    webSocketMap.get(toUserId).sendMessage(jsonObject.toJSONString());
                } else {
                    log.error("请求的userId:" + toUserId + "不在该服务器上");
                    //否则不在这个服务器上,发送到mysql或者redis
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 出现错误
     *
     * @param session
     * @param error
     */
    @OnError
    public void onError(Session session, Throwable error) {
        log.error("用户错误:" + this.userId + ",原因:" + error.getMessage());
        error.printStackTrace();
    }

    /**
     * 实现服务器主动推送
     */
    public void sendMessage(String message) throws IOException {
        this.session.getBasicRemote().sendText(message);
    }


    /**
     * 发送自定义消息
     */
    public static void sendInfo(String message, @PathParam("userId") String userId) throws IOException {
        log.info("发送消息到:" + userId + ",报文:" + message);
        if (StringUtils.isNotBlank(userId) && webSocketMap.containsKey(userId)) {
            webSocketMap.get(userId).sendMessage(message);
        } else {
            log.error("用户" + userId + ",不在线!");
        }
    }

    public static synchronized int getOnlineCount() {
        return onlineCount;
    }

    public static synchronized void addOnlineCount() {
        WebSocketServer.onlineCount++;
    }

    public static synchronized void subOnlineCount() {
        WebSocketServer.onlineCount--;
    }
}

5、编写Controller推送新消息

import com.zwh.WebSocketServer;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.ModelAndView;
import java.io.IOException;

@RestController
public class DemoController {

    @GetMapping("index")
    public ResponseEntity index(){
        return ResponseEntity.ok("请求成功");
    }

    @GetMapping("page")
    public ModelAndView page(){
        return new ModelAndView("websocket");
    }

    @RequestMapping("/push/{toUserId}")
    public ResponseEntity pushToWeb(@RequestParam String message, @PathVariable String toUserId) throws IOException {
        WebSocketServer.sendInfo(message,toUserId);
        return ResponseEntity.ok("MSG SEND SUCCESS:" + message);
    }
}

效果如下:

四、创建一个VUE工程

效果如下:

1、编写WebSocket.vue





2、编写WebSocket1.vue





3、启动项目

1)、点击WebSocket按钮时,就会建立连接,userId为2的websocket对象就会存入Map中。

2)、点击WebSocket1按钮时,就会建立连接,userId为2的websocket对象就会存入Map中。

3)、点击“发消息”按钮,用户2就会给用户3发送一条消息,

{"fromUserId":"2","message":"你好,我是2","toUserId":"3"}

4)、点击“发消息1”按钮,用户3就会给用户2发送一条消息

{"fromUserId":"3","message":"你好,我是3","toUserId":"2"}

页面控制台打印如下:

4、发送消息

用户2:

用户3:

浏览器访问:http://localhost:8080/push/2?message=hello,即给用户2发送消息

此时,用户2的控制台如下:

再打开一个页面,访问:http://localhost:8080/push/3?message=hello,how%20are%20you

此时3的控制台如下: