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

socket.io官网地址

案例

前端代码(socket.io.js)

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Document</title>
  <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 = '系统消息:<span>'+ data +'</span>'
		  document.getElementById('content').appendChild(html)
		  console.log(data);
		});
	}
	
    
  </script>
</body>
</html>

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

后端代码(Netty SocketIO Server)

maven依赖

<dependency>
    <groupId>com.corundumstudio.socketio</groupId>
    <artifactId>netty-socketio</artifactId>
    <version>1.7.7</version>
</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

负载均衡策略

将该值配置成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
  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
使用Node.js和Socket.IO进行前后通信可以实现实时的双向通信。下面是一个简单的示例,演示了如何在Node.js中使用Socket.IO与前建立连接和传递消息: 1. 安装Socket.IO: 在你的Node.js项目中,使用以下命令安装Socket.IO: ``` npm install socket.io ``` 2. 在Node.js中创建服务器: 在你的Node.js应用中,创建一个服务器,并将Socket.IO与之关联: ```javascript const app = require('express')(); const server = require('http').createServer(app); const io = require('socket.io')(server); // 监听连接事件 io.on('connection', (socket) => { console.log('A user connected'); // 监听客户发送的消息 socket.on('message', (data) => { console.log('Received message:', data); // 可以在这里处理收到的消息,并将回复发送给客户 socket.emit('reply', 'Hello from server'); }); // 监听断开连接事件 socket.on('disconnect', () => { console.log('A user disconnected'); }); }); // 启动服务器 server.listen(3000, () => { console.log('Server is running on port 3000'); }); ``` 3. 前连接到服务器: 在前HTML文件中,使用以下代码连接到服务器并发送/接收消息: ```html <script src="/socket.io/socket.io.js"></script> <script> const socket = io(); // 监听连接成功事件 socket.on('connect', () => { console.log('Connected to server'); // 发送消息给服务socket.emit('message', 'Hello from client'); }); // 监听服务器回复消息事件 socket.on('reply', (data) => { console.log('Received reply:', data); }); </script> ``` 以上示例中,当客户连接到服务器时,会打印出连接成功的消息。然后,客户会发送一条消息给服务器,并打印出服务器回复的消息。 你可以根据实际需求在服务器和客户之间传递更多的消息和数据。Socket.IO提供了丰富的API和事件来简化实时通信的开发。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

来了就走下去

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值