socket.io官网地址
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
<dependency>
<groupId>com.corundumstudio.socketiogroupId>
<artifactId>netty-socketioartifactId>
<version>1.7.7version>
dependency>
@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请求被转发到不同的服务中,因为一个服务不具备其它服务中所建立的连接信息,所以此时消息发送会发生错误,
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模块
将该值配置成ip_hash即可。
参考地址
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