SpringBoot 和 websocket集成

场景

websocket的典型使用场景:

  • 在线客服、聊天
  • 在线支付完成后通知客户端

了解websocket

WebSocket 是一种全双工的通信协议,使用 ws 或 wss 的统一资源标志符(URI),属于应用层协议。wss 表示基于 TLS 的 WebSocket,默认情况下 WebSocket 协议使用 80 端口;若运行在 TLS 之上时,则默认使用 443 端口。
WebSocket 与 HTTP 的关系:

  • WebSocket 与 HTTP 协议一样都是基于 TCP 的,二者均为可靠的通信协议。
  • WebSocket 与 HTTP 协议均为应用层协议,但 WebSocket 是一种独立于 HTTP 的协议。
  • WebSocket 在建立握手连接时,数据是通过 HTTP 协议传输的。
  • WebSocket 建立好连接后,真正通信阶段的数据传输不依赖于 HTTP 协议。

websocket连接请求报文:

GET /websocket/ HTTP/1.1
Host: localhost
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: xqBt3ImNzJbYqRINxEFlkg==
Origin: http://localhost:8080
Sec-WebSocket-Version: 13

服务端应答报文:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept:K7DJLdLooIwIG/MOpvWFB3y3FE8=

了解这两个报文格式对我们后续诊断问题和配置有帮助。

原料

  • Spring framework websocket library
  • SockJS
  • Vue3 & Nuxt3

Java后端

Spring Framework 4包括一个全新的WebSocket支持的spring-websocket模块。它与Java WebSocket API标准(JSR-356)兼容,并且还提供额外的功能。Spring和websocket集成有四种方式:

  • 基于JSR-356规范
  • spring自己的封装
  • STOMP over websocket
  • TIO: t-io作为国内知名的开源网络编程框架,性能高,匠心打造,用t-io写的程序每秒能处理1000+万条消息,1.9G内存能够支撑30万TCP长连接。

采用Java websocket API

@ServerEndpoint("/websocket/{name}")
public class WebSocket {
    
    
    private String name;
    private Session session;private static Map<String,Session>  sessions =new ConcurrentHashMap();//用于记录所有的用户和连接@OnOpen
    public void onOpen(@PathParam("name") String name, Session session) throws  Exception{
    
    
        this.name=name;
        this.session = session;
        sessions.put(name, session);
    }@OnMessage
    public void onMessage(Session session ,String message) {
    
           
        sendMessage(toName, "", session);
    }@OnError
    public void onError(Session session,Throwable e) {
    
    
        try {
    
    
            session.close();
            sessions.remove(name);
        } catch (IOException ex) {
    
                
        }        
    }@OnClose
    public void onClose(Session session) {
    
    
        sessions.remove(name);
    }public void sendMessage(String name, String message, Session session) {
    
    
         Session toSession = sessions.get(name);if (toSession != null) {
    
    
             toSession.getAsyncRemote().sendText(message);
             return;
         }
        if (session != null) {
    
    ​
            session.getAsyncRemote().sendText("");
        }}

采用SpringBoot提供的websocket API

在Spring中使用较低层次的API处理消息,必须编写一个实现WebSocketHandler的类。或者继承Spring提供了一些更方便的实现WebSocketHandler的类,如:AbstractWebSocketHandler、TextWebSocketHandler、BinaryWebSocketHandler等。

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer
{
    
    

        @Override
        public void configureMessageBroker(MessageBrokerRegistry config)
        {
    
    
                config.enableSimpleBroker("/topic");
                config.setApplicationDestinationPrefixes("/ws/");
        }

        @Override
        public void registerStompEndpoints(StompEndpointRegistry registry)
        {
    
    
                registry.addEndpoint("/test"); // This will allow you to use ws://localhost:8080/test to establish websocket connection
                registry.addEndpoint("/test").withSockJS(); // This will allow you to use http://localhost:8080/test to establish websocket connection
        }

}

STOMP代理中继

简单代理由于是基于内存的,所以不适合集群,因为如果集群的话,每个节点也只能管理自己的代理和自己的那部分消息。对于生产环境,一般使用真正支持STOMP的代理来支撑WebSocket消息,如RabbitMQ或ActiveMQ。这样的代理提供了可扩展性和健壮性的消息功能。STOMP搭建代理后,可以通过如下配置使用STOMP代理替换内存代理。

STOMP Over WebSocket Messaging Architecture
The WebSocket protocol defines two types of messages, text and binary, but their content is undefined. It’s expected that the client and server may agree on using a sub-protocol (i.e. a higher-level protocol) to define message semantics. While the use of a sub-protocol with WebSocket is completely optional either way client and server will need to agree on some kind of protocol to help interpret messages.

@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
    
    
    //配置消息代理 处理以"/queue","topic"为前缀的消息、host、端口、用户名、密码
    registry.enableStompBrokerRelay("/queue","topic").setRelayHost("rabbit.stomp")
            .setRelayPort(22222).setClientLogin("username").setClientPasscode("password");
    //发往应用程序的消息将会带有/app前缀,所有目的地以"/app"打头的消息将会路由到带有@MessageMapping注解的方法中
    registry.setApplicationDestinationPrefixes("/app");
}

在这里插入图片描述

客户端

原生的WebSocket API

SockJS

SockJS is one of the existing WebSocket browser fallbacks. SockJS includes a protocol, a JavaScript browser client, and Node.js, Erlang, Ruby servers. There are also third-party SockJS clients and servers for different programming languages and platforms. SockJS was designed to emulate the WebSocket API as close as possible to provide the ability to use WebSocket subprotocols on top of it.

简而言之,SockJS是对浏览器能力的补足。它包括一套协议,一套客户端JS库,和server端的一些实现,如NodeJS和Java的实现。

浏览器

SockJS 底层传输层实现分为三种:

  • native WebSocket
  • HTTP streaming:基于HTTP 1.1 chunked transfer encoding
  • HTTP long polling:包括xhr-polling,xdr-polling,iframe-xhr-polling和jsonp-polling

在创建SockJS时,可以指定transport:

let sock = new SockJS('url', null, {
    
    transports: 'websocket'})

如果不指定,SockJS会逐一检查可用的传输方式,然后选择一种。

let sockJS = null;
// 'Connect' button click handler
function connect() {
    
    
   const option = $("#transports").find('option:selected').val();
   const transports = (option === 'all') ? [] : [option];
   sockJS = new SockJS('http://localhost:8080/websocket-sockjs',
       'subprotocol.demo.websocket', {
    
    debug: true, transports: transports});
   sockJS.onopen = function () {
    
    
       log('Client connection opened');
       console.log('Subprotocol: ' + sockJS.protocol);
       console.log('Extensions: ' + sockJS.extensions);
   };
   sockJS.onmessage = function (event) {
    
    
       log('Client received: ' + event.data);
   };
   sockJS.onerror = function (event) {
    
    
       log('Client error: ' + event);
   };
   sockJS.onclose = function (event) {
    
    
       log('Client connection closed: ' + event.code);
   };
}
// 'Disconnect' button click handler
function disconnect() {
    
    
   if (sockJS != null) {
    
    
       sockJS.close();
       sockJS = null;
   }
}

SockJS Java client

也可以采用Java的实现:

List<Transport> transports = new ArrayList<>(2);
transports.add(new WebSocketTransport(new StandardWebSocketClient()));
transports.add(new RestTemplateXhrTransport());

SockJsClient sockJsClient = new SockJsClient(transports);
sockJsClient.doHandshake(new MyWebSocketHandler(), "ws://example.com:8080/sockjs");

Nginx转发websocket请求

#nginx server中配置
# 转发ws
location /cms/ws/ {
    
    
    # 后台准备的websocket地址端口
    proxy_pass http://api_server/cms/ws/;
    # 其他参数都一样
    proxy_read_timeout 3600s;
    proxy_send_timeout 300s;
    proxy_set_header  Host $host;
    proxy_set_header  X-Real-IP  $remote_addr;
    proxy_set_header  X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header  X-Forwarded-Proto $scheme;
    proxy_http_version 1.1;
    proxy_set_header X-NginX-Proxy true;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";

    proxy_redirect off;
}

转发wss请求:

map $http_upgrade $connection_upgrade {
    
     
	default upgrade; 
	'' close; 
} 
upstream wsbackend{
    
     
	server ip1:port1; 
	server ip2:port2; 
	keepalive 1000; 
} 
server{
    
    
	listen 20038 ssl;
	server_name localhost;
	ssl_certificate    /usr/local/nginx-1.17.8/conf/keys/binghe.com.pem;
	ssl_certificate_key /usr/local/nginx-1.17.8/conf/keys/binghe.com.key;
	ssl_session_timeout 20m;
	ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4;
	ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
	ssl_prefer_server_ciphers on;
	ssl_verify_client off;
	location / {
    
    
	  proxy_http_version 1.1;
	  proxy_pass http://wsbackend;
	  proxy_redirect off; 
	  proxy_set_header Host $host; 
	  proxy_set_header X-Real-IP $remote_addr; 
	  proxy_read_timeout 3600s; 
	  proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 
	  proxy_set_header Upgrade $http_upgrade; 
	  proxy_set_header Connection $connection_upgrade; 
	}
}

推荐客户端在创建SockJS对象时,采用同一domain下URL,避开跨域问题。然后在Nginx里做转发。

相关工具

  • wscat: nodejs工具

npm install wscat

测试:

wscat --connect ws://192.168.100.20:8020

其它

nodejs实现websocket服务端

var Msg = '';
var WebSocketServer = require('ws').Server
    , wss = new WebSocketServer({
    
    port: 8010});
    wss.on('connection', function(ws) {
    
    
        ws.on('message', function(message) {
    
    
        console.log('Received from client: %s', message);
        ws.send('Server received from client: ' + message);
    });
 });

Golang中构建 WebSocket服务

可以采用gorilla/websocket库进行构建WebSocket服务

 var upgrader = websocket.Upgrader{
    
    
	ReadBufferSize:  1024,
	WriteBufferSize: 1024,
}

func handler(w http.ResponseWriter, r *http.Request) {
    
    
	conn, err := upgrader.Upgrade(w, r, nil)
	if err != nil {
    
    
		log.Println(err)
		return
	}

	for {
    
    
		messageType, p, err := conn.ReadMessage()
		if err != nil {
    
    
			log.Println(err)
			return
		}
		if err := conn.WriteMessage(messageType, p); err != nil {
    
    
			log.Println(err)
			return
		}
	}
}

常见问题

SockJS请求404

在这里插入图片描述
检查nginx转发配置是否正确。

WebSocket is closed before the connection is established

可能是客户端和服务端websocket版本一致导致。

参考链接

相关开源项目

猜你喜欢

转载自blog.csdn.net/jgku/article/details/129744162
今日推荐