socket.io前后端实践及转发、多服务问题

socket.io官网地址

案例

前端代码(socket.io.js)

DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Documenttitle>
  <style>
    input {
      background-color: #fff;
      background-image: none;
      border-radius: 4px;
      border: 1px solid #bfcbd9;
      box-sizing: border-box;
      color: #1f2d3d;
      font-size: inherit;
      height: 40px;
      line-height: 1;
      outline: 0;
      padding: 3px 10px;
    }
    .el-button--primary {
      color: #fff;
      background-color: #20a0ff;
      border-color: #20a0ff;
    }
    .el-button {
      display: inline-block;
      line-height: 1;
      white-space: nowrap;
      cursor: pointer;
      background: #00aac5;
      border: 1px solid #c4c4c4;
      color: #fff;
      margin: 0;
      padding: 10px 15px;
      border-radius: 4px;
      outline: 0;
      text-align: center;
    }
  style>
head>
<body>
  <div>
    <div id="content">
    div>
  div>
  <div>
	<input type="text" id="userId" value="1"/>
    <input type="text" id="input">
    <button class="el-button el-button--primary el-button--large" type="button" onclick="connect()"><span>建立连接span>button>
  div>
  <script src="./socket.io.js">script>
  <script>
	var socket = null;
	
    // 建立连接
	function connect() {
		let userId = document.getElementById("userId").value;
		socket = io.connect('http://localhost:8088?accessToken=xxxxxx', {
			path: '/socket.io'
		});
		
		socket.emit('setId', userId);
		
		// 监听 message 会话
		socket.on('message', function (data) {
		  let html = document.createElement('p')
		  html.innerHTML = '系统消息:'+ data +''
		  document.getElementById('content').appendChild(html)
		  console.log(data);
		});
	}
	
    
  script>
body>
html>

前端通过后端提供的socket地址,进行授权连接,连接成功后发送用户ID

后端代码(Netty SocketIO Server)

maven依赖

<dependency>
    <groupId>com.corundumstudio.socketiogroupId>
    <artifactId>netty-socketioartifactId>
    <version>1.7.7version>
dependency>

SocketIO 服务

@Bean
public SocketIOServer socketIOServer() {
	SocketConfig socketConfig = new SocketConfig();
	socketConfig.setTcpNoDelay(true);
	socketConfig.setSoLinger(0);
	com.corundumstudio.socketio.Configuration config = new com.corundumstudio.socketio.Configuration();
	config.setSocketConfig(socketConfig);
	BeanUtils.copyProperties(socketIOProperties, config);

	// 连接鉴权
	config.setAuthorizationListener(socketIOAuthorizationListener);

	return new SocketIOServer(config);
}

socketIOProperties设置SocketIOServer相关配置,例如:


# netty-socketio 配置
socketio:
  host: 0.0.0.0
  port: 8088
  # 设置最大每帧处理数据的长度,防止他人利用大数据来攻击服务器
  maxFramePayloadLength: 1048576
  # 设置http交互最大内容长度
  maxHttpContentLength: 1048576
  # socket连接数大小(如只监听一个端口boss线程组为1即可)
  bossCount: 1
  workCount: 100
  allowCustomRequests: true
  # 协议升级超时时间(毫秒),默认10秒。HTTP握手升级为ws协议超时时间
  upgradeTimeout: 1000000
  # Ping消息超时时间(毫秒),默认60秒,这个时间间隔内没有接收到心跳消息就会发送超时事件
  pingTimeout: 6000000
  # Ping消息间隔(毫秒),默认25秒。客户端向服务器发送一条心跳消息间隔
  pingInterval: 25000

socketIOAuthorizationListener对连接进行鉴权:

@Slf4j
@Component
public class SocketIOAuthorizationListener implements AuthorizationListener {

    @Autowired
    private ServiceRedis serviceRedis;

    @Override
    public boolean isAuthorized(HandshakeData handshakeData) {
        log.debug("SocketIO鉴权:{}", new Gson().toJson(handshakeData.getUrlParams()));
        String accessToken = handshakeData.getSingleUrlParam("accessToken");
        if (StrUtil.isEmpty(accessToken)) {
            return false;
        }
        try {
            // 鉴权
			
			return isSuccess;
        } catch (Exception e) {
            log.error("SocketIO鉴权发生异常:{}", accessToken, e);
        }

        return false;
    }
}

SocketIO服务类

@Service
public class SocketIORunner implements CommandLineRunner {

    /**
     * 存储已连接的客户端session
     */
    public static Map<Long, SocketIOClient> clientMap = new ConcurrentHashMap<>();

    @Autowired
    private SocketIOServer socketIOServer;

    @Autowired
    private ServiceRedis serviceRedis;

    @Autowired
    private MsgCenterProperties msgCenterProperties;

    @Override
    public void run(String... args) throws Exception {
        log.info("启动web端socket服务器开始.......");
        socketIOServer.start();
        log.info("启动web端socket服务器完成.......");
    }

    /**
     * 添加connect事件
     *
     * @param client
     */
    @OnConnect
    public void onConnect(SocketIOClient client) {
        log.debug("未知用户" + client.getHandshakeData().getAddress().toString() + "连接到服务器" + DateUtils.format(new Date(), DateUtils.YYYY_MM_DD_HH_mm_SS));
    }

    /**
     * 添加@OnDisconnect事件,客户端断开连接时,刷新客户端信息
     *
     * @param client
     */
    @OnDisconnect
    public void onDisconnect(SocketIOClient client) {
        Long userId = getUserId(clientMap, client);
        if (userId != null) {
            log.debug("用户userId:{}断开服务器连接", userId);
            // 删除用户信息
        }
    }

    /**
     * 当客户端发起事件传递userId,存储session
     *
     * @param client
     * @param request
     * @param data
     */
    @OnEvent(value = "setId")
    public void getUserId(SocketIOClient client, AckRequest request, String data) {

        String userId = data;
        if (userId != null) {
             log.debug("用户userId:{}连接到服务器", userId);
            // 保存用户信息
        }
    }
}

在本地,这一切都看起来非常的顺利,但是部署到正式服,遇到如下两个问题。

问题

域名转发

正式服使用的是域名,且通过k8s进行服务部署,使用nginx官方的nginx-ingress-controller进行http转发。因为使用socket.io,默认的路径是/socket.io,所以针对这个路径进行转发配置,但这里存在一个问题,因为该域名已经用于服务,且/路径已经配置了转发规则且加了一个跨域头,那么此时配置/socket.io的转发规则,因为SocketIOServer会返回跨域头Access-Control-Allow-Origin,导致重复的头问题

尝试解决方案

想着能不能通过nginx.org/location-snippets中判断$request_uri来进行设置不同的头,发现这是不被允许的。

最终方案

新创建一个新的域名用于socket.io的转发,这样子只需要配置/路径转发就行,同时设置websocket升级请求头

proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";

多服务转发

socket.io使用polling:HTTP 长轮询传输在 Socket.IO 会话的生命周期内发送多个 HTTP 请求
如果这些Http请求被转发到不同的服务中,因为一个服务不具备其它服务中所建立的连接信息,所以此时消息发送会发生错误,

只使用websocket传输协议

const socket = io("https://io.yourhost.com", {
  // WARNING: in that case, there is no fallback to long-polling
  transports: [ "websocket" ] // or [ "websocket", "polling" ] (the order matters)
});

要想实现粘性会话,有如下两种解决方案:
(1)基于 cookie 路由客户端(推荐解决方案)
(2)根据客户端的原始地址路由客户端

您将在下面找到一些常见负载平衡解决方案的示例:
NginX(基于 IP)
Apache HTTPD(基于 cookie)
HAProxy(基于 cookie)
Traefik(基于 cookie)
Node.jscluster模块

nginx-ingress-controller:ngxin.org/lb-method

socket.io前后端实践及转发、多服务问题_第1张图片

将该值配置成ip_hash即可。

nginx-ingress-controller:nginx.com/sticky-cookie-services

参考地址

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: cafe-ingress-with-session-persistence
  annotations:
    nginx.com/sticky-cookie-services: "serviceName=coffee-svc srv_id expires=1h path=/coffee;serviceName=tea-svc srv_id expires=2h path=/tea"
spec:
  rules:
  - host: cafe.example.com
    http:
      paths:
      - path: /tea
        pathType: Prefix
        backend:
          service:
            name: tea-svc
            port:
              number: 80
      - path: /coffee
        pathType: Prefix
        backend:
          service:
            name: coffee-svc
            port:
              number: 80

你可能感兴趣的:(问题,socket.io,websocket,nginx-ingress)