SpringCloud GateWay + WebSocket分享

2,641 阅读18分钟

前端(192.168.1.99)

引入js(需自行搜索咯)

	<script src="/socket.io/socket.io.js"></script>

连接例子

	// 连接例子
	function () {
		try {
			// url为gateway地址,如:http://192.168.1.100:8001
			var socket = io.connect(url);
			socket.on('connect', function () {
			  console.log('ws推送onconnect:websocket连接成功');
			});
			socket.on('disconnect', function () {
			  console.log('ws推送ondisconnect:已从websocket服务器断开连接');
			});
			socket.on('getTestData', function (data) {
			  console.log('获取取getTestData内容');
			});
		} catch (e) {
			alert("连接实时数据通讯服务失败!");
		}
	}
	
	// 发送消息例子
	socket.emit('setTestData', "{'roleId':2, 'projectId':2}");
	// 给指定的客户端发送消息
	io.sockets.socket(socketid).emit('setTestData', data);
	

前端域名https(证书自签)访问但后端是http的情况下,需要安装nginx

加入如下配置

	server {
	   listen 443 ssl;
	   server_name  localhost;
	   ssl_certificate /ssl/rootCA.pem;
	   ssl_certificate_key /ssl/yunNX.key;
	   
	   location ^~ /socket.io/ {
					   #启用支持websocket连接
					   proxy_read_timeout 300s;
					   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_http_version 1.1;
					   proxy_set_header Upgrade $http_upgrade;
					   proxy_set_header Connection "upgrade";
					proxy_pass http://192.168.1.100:8001/socket.io/;
				 }
	}
	// 前端带参连接
	var socket2 = io.connect("https://192.168.1.99:443", {
		'query''type=myTestParam'
	});

后端微服务GateWay

过滤器WsFilter,根据bootstrap.yml配置文件转发/socket.io请求

import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.cloud.gateway.config.LoadBalancerProperties;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.support.DelegatingServiceInstance;
import org.springframework.cloud.gateway.support.NotFoundException;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.net.URI;
import java.net.URISyntaxException;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.*;
@Component
@AllArgsConstructor
@Slf4j
public class WsFilter implements GlobalFilter, Ordered {

    public static final int LOAD_BALANCER_CLIENT_FILTER_ORDER = 10100;
//    private static final Log log = LogFactory.getLog(LoadBalancerClientFilter.class);
    protected final LoadBalancerClient loadBalancer;
    private LoadBalancerProperties properties;
    @Override
    public int getOrder() {
        return LOAD_BALANCER_CLIENT_FILTER_ORDER;
    }
    @Override
    @SuppressWarnings("Duplicates")
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        URI url = exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR);
        String schemePrefix = exchange.getAttribute(GATEWAY_SCHEME_PREFIX_ATTR);
        if (url == null
                || (!"lb".equals(url.getScheme()) && !"lb".equals(schemePrefix))) {
            return chain.filter(exchange);
        }
        // preserve the original url
        addOriginalRequestUrl(exchange, url);

        if (log.isTraceEnabled()) {
            log.trace("LoadBalancerClientFilter url before: " + url);
        }

        final ServiceInstance instance = choose(exchange);

        if (instance == null) {
            throw NotFoundException.create(properties.isUse404(),
                    "Unable to find instance for " + url.getHost());
        }
        URI uri = exchange.getRequest().getURI();
        // if the `lb:<scheme>` mechanism was used, use `<scheme>` as the default,
        // if the loadbalancer doesn't provide one.s
        String overrideScheme = instance.isSecure() ? "https" : "http";
        if (schemePrefix != null) {
            overrideScheme = url.getScheme();
        }
        URI requestUrl = loadBalancer.reconstructURI(
                new DelegatingServiceInstance(instance, overrideScheme), uri);

        if (log.isTraceEnabled()) {
            log.trace("LoadBalancerClientFilter url chosen: " + requestUrl);
        }
        if(requestUrl.getPath().startsWith("/socket.io"))
        {
           if(!exchange.getRequest().getMethod().equals("OPTIONS"))
           {
               try {
                   if(StringUtils.isNotEmpty(requestUrl.getQuery())){
                       // 带参socket,默认端口为对应微服务端口+1
                       requestUrl  = new URI(overrideScheme + "://" + requestUrl.getHost() + ":" + (requestUrl.getPort()+1) + "/socket.io/?"+requestUrl.getQuery());
                   }

                   exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, requestUrl);
                   log.info("转向地址 socket.io "+requestUrl);
               } catch (URISyntaxException e) {
                   e.printStackTrace();
               }
           }
        }
        return chain.filter(exchange);
    }

    protected ServiceInstance choose(ServerWebExchange exchange) {
        return loadBalancer.choose(
                ((URI) exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR)).getHost());
    }

}

设置请求路径/socket.io/**免token校验

@AllArgsConstructor
@Configuration
@EnableWebFluxSecurity
public class ResourceServerConfig {
	......
	@Bean
    public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity httpSecurity){

        httpSecurity.oauth2ResourceServer().jwt().
                jwtAuthenticationConverter(jwtAuthenticationConverter());

        httpSecurity.authorizeExchange()
                .pathMatchers("/xxx", "/yyy")
                .access(authorizationManager)
                .pathMatchers("/socket.io/**")
                .permitAll()
                .pathMatchers("/**")
                .access(authorizationManager)
                .and().exceptionHandling()
                .accessDeniedHandler(accessDeniedHandler)
                .authenticationEntryPoint(authenticationEntryPoint)
                .and().csrf().disable();
        httpSecurity.oauth2ResourceServer().authenticationEntryPoint(authenticationEntryPoint);
        return httpSecurity.build();
    }
	......
}

配置文件bootstrap.yml

server:
  port: 8001
spring:
  application:
    name: my-gateway
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true
          lower-case-service-id: true
      routes:
        - id: service111-socket
          uri: lb://my-service111
          predicates:
            - Path=/socket.io/**
            - Query=type,myTestParam
        - id: service222-socket
          uri: lb://my-service222
          predicates:
            - Path=/socket.io/**
            - Query=type,222
      default-filters:
        - DedupeResponseHeader=Access-Control-Allow-Origin, RETAIN_UNIQUE

    nacos:
      server-addr: 192.168.1.88:8848
      config:
        file-extension: yaml
        namespace: bc6ede3b-aac6-4f43-82e0-130bd2333026
        extension-configs:
          - data-id: redis.yml
      discovery:
        server-addr: 192.168.1.88:8848
        namespace: bc6ede3b-aac6-4f43-82e0-130bd2333026
        ip: 192.168.1.100

  security:
    oauth2:
      resourceserver:
        jwt:
          jwk-set-uri: 'http://192.168.1.101:8001/my-authentication/rsa/publicKey'

微服务模块my-service111

pom.xml新增依赖

        <dependency>
            <groupId>com.corundumstudio.socketio</groupId>
            <artifactId>netty-socketio</artifactId>
            <version>1.7.18</version>
        </dependency>

bootstrap.yml配置socket端口号为服务端口+1

server:
  port: 44446
  max-http-header-size: 1024KB
spring:
  application:
    name: my-service111
  main:
    allow-bean-definition-overriding: true

  cloud:
    nacos:
      server-addr: 192.168.1.88:8848
      config:
        file-extension: yaml
        namespace: xxxxxxxxxx
        extension-configs:
          - data-id: mysql.yml
      discovery:
        server-addr: 192.168.1.88:8848
        namespace: xxxxxxxxxx
        ip: 192.168.1.102

ws:
  server:
    port: 44447
    host: localhost

定义配置类

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties(prefix = "ws.server")
@Data
public class SocketIOWsConfig {
    private int port;
    private String host;
}
import com.corundumstudio.socketio.SocketIOServer;
import com.corundumstudio.socketio.annotation.SpringAnnotationScanner;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnableConfigurationProperties(SocketIOWsConfig.class)
public class SocketIOServerConfig {

    @Bean
     public SocketIOServer GetServer(SocketIOWsConfig socketIOWsConfig)
     {
         com.corundumstudio.socketio.Configuration configuration = new com.corundumstudio.socketio.Configuration();
         configuration.setPort(socketIOWsConfig.getPort());

         configuration.setOrigin(null);
         configuration.setKeyStorePassword("1234");

         configuration.setKeyStore(this.getClass().getResourceAsStream("www.xxx"));

         configuration.setRandomSession(true);
         return  new SocketIOServer(configuration);
     }

    @Bean
    public SpringAnnotationScanner springAnnotationScanner(SocketIOServer server) {
        return new SpringAnnotationScanner(server);
    }

}

随服务启动socket服务器

import com.corundumstudio.socketio.SocketIOServer;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

@Slf4j
@Component
@AllArgsConstructor
public class SocketIOServerRunner implements CommandLineRunner {

    private final SocketIOServer socketIOServer;

    @Override
    public void run(String... args) throws Exception {
          socketIOServer.start();
          log.info("SocketIOServer started !");
    }
}

socket处理类


@Component
@Slf4j
public class SocketIOMsgHandler {
	@OnConnect
    public void onConnection(SocketIOClient client) {
        if (client != null) {
            UUID sessionId = client.getSessionId();
            log.info("用户{}连接成功", sessionId);
        } else {
            log.error("用户连接异常");
        }

    }

    @OnDisconnect
    public void OnDisconnect(SocketIOClient client) {
        if (client != null) {
            UUID sessionId = client.getSessionId();
            log.info("客户端断开连接,【sessionId】= {}", sessionId);
            client.disconnect();
        } else {
            log.error("客户端断开连接异常");
        }
    }
	
	@OnEvent(value = "setTestData")
    public void setTestData(SocketIOClient client, AckRequest request, String paramStr) {
        if (StrUtil.isNotBlank(paramStr)) {
            JSONObject jsonObject = JSONUtil.parseObj(paramStr);
            Integer roleId = jsonObject.get("roleId", Integer.class);
            Integer projectId = jsonObject.get("projectId", Integer.class);
			......
		}
	}
}
			

另一种socket实现

微服务模块my-service111

pom.xml新增依赖

		<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>

配置类

import org.springframework.beans.factory.annotation.Autowired;
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();
    }

}

socket服务器(前端连接)

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@ServerEndpoint(value = "/websockets/{sid}/info")
@Component
@Slf4j
public class WebSocketHandler {

    // 存放每个用户对应的WebSocket连接对象,key为custId_HHmmss,确保一个登录用户只建立一个连接
    private static Map<String, Session> webSocketSessionMap = new ConcurrentHashMap<String, Session>();

    // 与某个客户端的连接会话,需要通过它来给客户端发送数据
    private Session session;

    // 当前在线连接数
    private static int onlineCount = 0;

    private static WebSocketHandler webSocketHandler;

    // 通过@PostConstruct实现初始化bean之前进行的操作
    @PostConstruct
    public void init() {
        // 初使化时将已静态化的webSocketServer实例化
        webSocketHandler = this;
    }

    @OnOpen
    public void onOpen(Session session, @PathParam("sid") String sid) {
        try {
            if (!webSocketSessionMap.containsKey(sid)) {
                this.session = session;
                webSocketSessionMap.put(sid, session);
                addOnlineCount();
                log.info("sid:{}连接成功", sid);
            }
            sendMessage(sid, sid + "连接成功");
        } catch (Exception e) {
            log.error("客户端连接websocket服务异常", e);
        }
    }

    @OnClose
    public void onClose(@PathParam("sid") String sid) {
        if (webSocketSessionMap.containsKey(sid)) {
            try {
                webSocketSessionMap.get(sid).close();
                webSocketSessionMap.remove(sid);
            } catch (IOException e) {
                log.error("连接[{}]关闭失败。", sid);
                e.printStackTrace();
            }
            subOnlineCount();
            log.info("连接[{}]关闭,当前websocket连接数:{}", sid, onlineCount);
        }
    }

    @OnMessage
    public void onMessage(String message, Session session) throws Exception {
        log.info("推送内容为:{}", message);
        session.getBasicRemote().sendText(message);
    }

    /**
     * 发生错误时调用
     *
     * @OnError
     */
    @OnError
    public void onError(Session session, Throwable error) {
        try {
            session.close();
        } catch (IOException e) {
            log.error("发生错误,连接[{}]关闭失败。");
            e.printStackTrace();
        }
    }

    public boolean sendMessage(String sessionKeys, String message) {
        boolean result = true;
        if (StrUtil.isNotBlank(sessionKeys)) {
            String[] sessionKeyArr = sessionKeys.split(",");
            for (String key : sessionKeyArr) {
                try {
                    // 可能存在一个账号多点登录
                    List<Session> sessionList = getLikeByMap(webSocketSessionMap, key);
                    if (CollUtil.isEmpty(sessionList)) {
                        result = false;
                    }
                    for (Session session : sessionList) {
                        synchronized (session) {
                            session.getBasicRemote().sendText(message);
                        }
                        result = true;
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                    result = false;
                    continue;// 某个客户端发送异常,不影响其他客户端发送
                }
            }
        } else {
            result = false;
            log.info("sessionKeys为空,没有目标客户端");
        }
        return result;
    }

    /**
     * 给当前客户端推送消息,首次建立连接时调用
     */
    public void sendMessage(String message)
            throws IOException {
        synchronized (session) {
            this.session.getBasicRemote().sendText(message);
        }
    }

    private List<Session> getLikeByMap(Map<String, Session> map, String keyLike) {
        List<Session> list = new ArrayList<>();
        for (String key : map.keySet()) {
            if (key.contains(keyLike)) {
                list.add(map.get(key));
            }
        }
        return list;
    }

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

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

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

前端连接,懒所以用在线工具

ws://ip:44446/websockets/323/info
在线WebSocket

后端推送消息

@Component
@Slf4j
public class PushService {
    @Autowired
    private WebSocketHandler webSocketHandler;

    // 往socket推送数据
    @Async
    public void push(String socketId, String content) {
        String content = "";

        try {
            boolean result = true;
            int i = 0;
            while (result) {
                result = webSocketHandler.sendMessage(socketId, content);
                if(!result && i<5){
                    //推送失败重试3次
                    i++;
                    result =!result;
                }
                Thread.sleep(4000);
                log.info("{}推送内容为:{}", socketId, content);
            }
        }catch (Exception e){
            log.info("{}推送已停止", socketId);
            log.info("", e);
        }
    }
}

socket客户端(连接第三方socket)

@ClientEndpoint
@Component
@Slf4j
public class WebSocketClient {

    // 存放每个用户对应的WebSocket连接对象,key为custId_HHmmss,确保一个登录用户只建立一个连接
    private static Map<String, Session> webSocketSessionMap = new ConcurrentHashMap<String, Session>();

    // 与某个客户端的连接会话,需要通过它来给客户端发送数据
    private Session session;

    // 当前在线连接数
    private static int onlineCount = 0;

    private static WebSocketClient webSocketHandler;

    @Autowired
    private SocketIOServer server;
    // 存放每个用户对应的WebSocket连接对象,key为custId_HHmmss,确保一个登录用户只建立一个连接
    public static Map<UUID, UUID> socketIOServerSessionMap = new ConcurrentHashMap<UUID, UUID>();

    // 通过@PostConstruct实现初始化bean之前进行的操作
    public void init()
    {
        // 初使化时将已静态化的webSocketServer实例化
        webSocketHandler = this;
    }

    @OnOpen
    public void onOpen(Session session){
        try {
            String sid = session.getId();
            if (CollUtil.isEmpty(webSocketSessionMap)) {
                this.session = session;
                webSocketSessionMap.put(sid, session);
                addOnlineCount();
                log.info("sid:{}连接成功", sid);
            }
//            sendMessage(sid, sid+"连接成功");
        }catch (Exception e){
            log.error("客户端连接websocket服务异常", e);
        }
    }

    @OnClose
    public void onClose(Session session) {
        String sid = session.getId();
        if (webSocketSessionMap.containsKey(sid))
        {
            try
            {
                webSocketSessionMap.get(sid).close();
                webSocketSessionMap.remove(sid);
            }
            catch (IOException e)
            {
                log.error("连接[{}]关闭失败。", sid);
                e.printStackTrace();
            }
            subOnlineCount();
            log.info("连接[{}]关闭,当前websocket连接数:{}", sid, onlineCount);
        }
    }

    @OnMessage
    public void onMessage(String message, Session session) throws Exception {
        log.info("推送内容为:{}", message);
        List<UUID> uuidList = new ArrayList<>();
        socketIOServerSessionMap.entrySet().stream().forEach(e -> {
            if(null != server.getClient(e.getKey())){
                server.getClient(e.getKey()).sendEvent("getTestData", message);
            }else{
                uuidList.add(e.getKey());
            }
        });
        // 清除无用的uuid
        uuidList.forEach(u -> {
            socketIOServerSessionMap.remove(u);
        });
    }
	
	/**
     * 发生错误时调用
     *
     * @OnError
     */
    @OnError
    public void onError(Session session, Throwable error) {
        try{
            session.close();
        }catch (IOException e){
            log.error("发生错误,连接[{}]关闭失败。");
            e.printStackTrace();
        }
    }
}

建立连接调用例子

	@Autowired
    WebSocketClient webSocketClient;
	// 获取socket上报数据
    @OnEvent(value = "setTestData")
    public void setTestData(SocketIOClient client, AckRequest request, String paramStr) throws IOException {
        String uri = "ws://IP:7001/ws";  
        WebSocketContainer container = null;
        try {
            container = ContainerProvider.getWebSocketContainer();
            URI r = URI.create(uri);
            webSocketClient.socketIOServerSessionMap.put(client.getSessionId(), client.getSessionId());
            container.connectToServer(webSocketClient, r);
            log.info("连接socket成功");
        } catch (DeploymentException | IOException e) {
            e.printStackTrace();
        }
    }