EventStream和WebSocket


一、技术对比

  • EventSource
    优势:
    服务器推送:基于HTTP协议,服务器主动向客户端推送数据,客户端只能接收服务器发送的数据。
    适用场景:
    实时股票报价、天气预报、社交媒体通知等。

  • WebSocket:
    优势:
    双向通信:基于TCP协议,客户端和服务器可以互相发送数据。
    实时性:更低的延迟和更快的数据传输速度,适用于实时性要求较高的应用场景。
    适用场景:
    在线聊天室、多人游戏和股票市场等需要快速实时响应的应用程序。

二、WebSocket

1. 服务端

  • pom.xml
<dependency>
	<groupId>com.corundumstudio.socketio</groupId>
	<artifactId>netty-socketio</artifactId>
	<version>2.0.9</version>
</dependency>
  • NettySocketRunner.java
package com.example.socket;

import com.corundumstudio.socketio.SocketIOServer;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

/**
 * Socket启动
 */
@Component
class NettySocketRunner implements CommandLineRunner, DisposableBean {
    private static Log logger = LogFactory.getLog(NettySocketRunner.class);

    @Autowired
    private SocketIOServer socketIOServer;

    @Override
    public void run(String... args) {
        socketIOServer.start();
        logger.info("========== SocketIOServer启动成功");
    }

    @Override
    public void destroy() {
        socketIOServer.stop();
        logger.info("========== SocketIOServer关闭成功");
    }
}
  • NettySocketConfig.java
package com.example.socket;

import com.corundumstudio.socketio.SocketConfig;
import com.corundumstudio.socketio.SocketIOServer;
import com.corundumstudio.socketio.annotation.SpringAnnotationScanner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * Socket配置
 */
@Configuration
public class NettySocketConfig {

    /**
     * 创建SocketIO服务
     */
    @Bean
    public SocketIOServer socketIOServer() {
        SocketConfig socketConfig = new SocketConfig();
        socketConfig.setReuseAddress(true);        // 端口复用
        socketConfig.setTcpNoDelay(true);          // 确保包无论大小, 会尽可能发送
        com.corundumstudio.socketio.Configuration config = new com.corundumstudio.socketio.Configuration();
        config.setSocketConfig(socketConfig);
        config.setPort(8116);                      // 端口
        config.setWorkerThreads(1000);             // 连接数
        config.setAllowCustomRequests(true);       // 允许客户请求
        config.setUpgradeTimeout(30000);           // HTTP升级为ws协议超时时间 30秒
        config.setPingTimeout(90000);              // Ping消息超时时间 90秒
        config.setPingInterval(30000);             // Ping消息间隔 30秒
        config.setMaxHttpContentLength(1048576);   // http交互最大内容长度 1MB
        config.setMaxFramePayloadLength(1048576);  // 每帧最大数据长度 1MB
        final SocketIOServer server = new SocketIOServer(config);
        return server;
    }

    /**
     * 扫描SocketIO注解
     */
    @Bean
    public SpringAnnotationScanner springAnnotationScanner() {
        return new SpringAnnotationScanner(socketIOServer());
    }
}
  • NettyEventHandler.java
package com.example.socket;

import com.alibaba.fastjson2.JSONObject;
import com.corundumstudio.socketio.SocketIOClient;
import com.corundumstudio.socketio.annotation.OnConnect;
import com.corundumstudio.socketio.annotation.OnDisconnect;
import com.corundumstudio.socketio.annotation.OnEvent;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * Socket消息事件处理
 */
@Component
public class NettyEventHandler {
    private static final Log logger = LogFactory.getLog(NettyEventHandler.class);

    // 用于保存和客户端的会话
    public static ConcurrentMap<String, SocketIOClient> socketClientMap = new ConcurrentHashMap<>();

    /**
     * 客户端连接
     */
    @OnConnect
    public void onConnect(SocketIOClient client) {
        String id = client.getHandshakeData().getSingleUrlParam("id");
        String token = client.getHandshakeData().getSingleUrlParam("token");
        if (StringUtils.isEmpty(id) || !"flgn920320erg8erg9".equals(token)) {
            return;
        }
        socketClientMap.put(id, client);
        logger.info("客户端 " + client.getSessionId() + " 已连接, id: " + id);
    }

    /**
     * 客户端关闭连接
     */
    @OnDisconnect
    public void onDisconnect(SocketIOClient client) {
        SocketIOClient socketIOClient = socketClientMap.get(client.getHandshakeData().getSingleUrlParam("id"));
        String token = client.getHandshakeData().getSingleUrlParam("token");
        if (!"flgn920320erg8erg9".equals(token)) {
            return;
        }
        if (null!=socketIOClient){
            socketClientMap.remove(client.getHandshakeData().getSingleUrlParam("id"));
        }
        logger.info("客户端 " + client.getSessionId() + " 断开连接");
    }

    /**
     * 监听客户端事件
     */
    @OnEvent(value = "client_message")
    public void clientMessagelListener(SocketIOClient client, JSONObject data) {
        logger.info("客户端消息: " + data);
    }

}
  • NettySocketRest.java
package com.example.web.rest.comm;

import com.alibaba.fastjson2.JSONObject;
import com.corundumstudio.socketio.SocketIOClient;
import com.example.socket.NettyEventHandler;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;

/**
 * @description 测试NettySocket
 */
@RestController
@RequestMapping("/api/comm/nettysocket")
public class NettySocketRest {
    private static Log logger = LogFactory.getLog(NettySocketRest.class);

    @Resource
    private NettyEventHandler nettyEventHandler;

    /**
     * 获取客户端连接数
     */
    @PostMapping(value="getNum",consumes="application/json")
    public String getNum(@RequestBody JSONObject json) {
        int num = nettyEventHandler.socketClientMap.size();
        JSONObject result = new JSONObject();
        result.put("msg", num);
        return result.toString();
    }

    /**
     * 服务端接收消息, 然后发给客户端
     */
    @PostMapping(value="sendMsg",consumes="application/json")
    public String sendMsg(@RequestBody JSONObject json) {
        SocketIOClient client = nettyEventHandler.socketClientMap.get(json.getString("id"));
        JSONObject result = new JSONObject();
        if (client == null) {
            logger.error("消息未处理: " + json);
            result.put("msg", "client is null");
        } else {
            client.sendEvent("server_message", json.getString("content"));
            result.put("msg", "ok");
        }
        return result.toString();
    }
}

2. 客户端

  • socket.html
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <script type="text/javascript" src="https://lib.baomitu.com/socket.io/4.7.2/socket.io.js"></script>
</head>
<body font-size="14px">
<strong>服务端当前连接Socket客户端数量</strong>
<p>
    <span>地址:</span><input id="ip-num" type="text" size="32" value="http://127.0.0.1:8081" />
    &nbsp;&nbsp;<button type="button" onClick="postNum()">获取数量</button>
</p>
<textarea id="receive-num" type="text" rows="1" cols="73" readonly></textarea>
<br><br><br>
<strong>发送消息到服务端,服务端推送给Socket客户端</strong>
<p>
    <span>地址:</span><input id="ip-msg" type="text" size="32" value="http://127.0.0.1:8081" />
    &nbsp;&nbsp;<span>连接ID:</span><input id="id-msg" type="text" size="20" value="123456" />
    &nbsp;&nbsp;<button type="button" onClick="postMsg()">发送消息</button>
</p>
<p>
    <span>发送内容:</span><br>
</p>
<textarea id="send-msg" type="text"  rows="6" cols="73">{"info":"HelloWord"}</textarea>
<br><br><br>
<strong>Socket客户端连接服务端</strong>
<p>
    <span>地址:</span><input id="ip-socket" type="text" size="32" value="http://127.0.0.1:8116" />
    &nbsp;&nbsp;<span>连接ID:</span><input id="id-socket" type="text" size="20" value="123456" />
</p>
<p>
    <span>认证token:</span><input id="token-socket" type="text" size="32" value="flgn920320erg8erg9" />
    &nbsp;&nbsp;<button type="button" onClick="startSocket()">&nbsp;连接&nbsp;</button>
    &nbsp;&nbsp;<button type="button" onClick="stopSocket()">&nbsp;断开&nbsp;</button>
</p>
<p><strong>Socket客户端接收服务端消息</strong></p>
<textarea id="receive-content" type="text" rows="5" cols="73" readonly></textarea>
<script>
    let socket;
    // 启动socket
    function startSocket() {
        const ip = document.getElementById('ip-socket').value;
        const id = document.getElementById('id-socket').value;
        const token = document.getElementById('token-socket').value;
        const wsUrl = `${ip}/?id=${id}&token=${token}`;
        socket = io.connect(wsUrl);
        if (socket) {
            socket.on('connect', function (data) {
                socket.emit('client_message', {msg:'client msg'});
                document.getElementById("receive-content").innerHTML = `连接\n` + document.getElementById("receive-content").innerHTML;
            });
            socket.on('server_message', function (data) {
                document.getElementById("receive-content").innerHTML = `${data}\n` + document.getElementById("receive-content").innerHTML;
            });
            socket.on('disconnect', function (data) {
                document.getElementById("receive-content").innerHTML = `断开\n` + document.getElementById("receive-content").innerHTML;
            });
        }
    }

    // 结束socket
    function stopSocket() {
        if (socket) {
            socket.disconnect();
        }
    }

    // 发送信息
    function postNum() {
        const ip = document.getElementById('ip-num').value;
        const url = `${ip}/test/api/comm/nettysocket/getNum`;
        sendPostRequest(url, {
        }).then((res) => {
            const r = JSON.parse(res);
            document.getElementById("receive-num").innerHTML = `${r.msg}`;
        })
    }

    // 发送信息
    function postMsg() {
        const message = document.getElementById('send-msg').value;
        const id = document.getElementById('id-msg').value;
        const ip = document.getElementById('ip-msg').value;
        const url = `${ip}/test/api/comm/nettysocket/sendMsg`;
        sendPostRequest(url, {
            content: message,
            id: id
        })
    }

    // 发起Post请求
    function sendPostRequest(url, data) {
        return new Promise(function (resolve, reject) {
            var xhr = new XMLHttpRequest();
            xhr.open('post', url, true);
            xhr.setRequestHeader('Content-Type', 'application/json');
            xhr.onload = function () {
                var status = xhr.status;
                if (status == 200) {
                    resolve(xhr.response);
                } else {
                    reject(status);
                }
            };
            xhr.send(JSON.stringify(data));
        });
    };
</script>
</body>

</html>
  • 效果

在这里插入图片描述

3. Nginx中配置

  • server 中
location /socket.io/ {
	proxy_http_version 1.1;
	proxy_set_header Upgrade $http_upgrade;
	proxy_set_header Connection "upgrade";
	proxy_set_header Host $host;
	proxy_pass http://127.0.0.1:8116/socket.io/;
}

三、EventSource

1. 服务端

  • EventStreamRest.java
package com.example.web.rest.comm;

import com.alibaba.fastjson2.JSONObject;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

/**
 * @description 测试EventStream
 */
@RestController
@RequestMapping("/api/comm/eventstream")
public class EventStreamRest {
    private static Log logger = LogFactory.getLog(EventStreamRest.class);

    private static ConcurrentMap<String, SseEmitter> sseEmitterMap = new ConcurrentHashMap<>();

    /**
     * 连接服务端
     */
    @GetMapping(path = "connect", produces = {MediaType.TEXT_EVENT_STREAM_VALUE})
    public SseEmitter connect(String id, String token) {
        if (!"flgn920320erg8erg9".equals(token)) {
            return null;
        }
        SseEmitter sseEmitter = new SseEmitter(0L);
        sseEmitterMap.put(id,sseEmitter);
        sseEmitter.onTimeout(() -> sseEmitterMap.remove(id));
        return sseEmitter;
    }

    /**
     * 关闭连接
     */
    @PostMapping(value="close",consumes="application/json")
    public String close(@RequestBody JSONObject json) {
        if (!"flgn920320erg8erg9".equals(json.getString("token"))) {
            return null;
        }
        SseEmitter sseEmitter = sseEmitterMap.get(json.getString("id"));
        if (sseEmitter != null) {
            logger.info("sseEmitter complete");
            sseEmitter.complete();
            sseEmitterMap.remove(json.getString("id"));
        }
        return "ok";
    }

    /**
     * 服务端接收消息, 然后发给客户端
     */
    @PostMapping(value="sendMsg",consumes="application/json")
    public String send(@RequestBody JSONObject json) {
        try {
            SseEmitter sseEmitter = sseEmitterMap.get(json.getString("id"));
            if (sseEmitter != null) {
                sseEmitter.send(json.get("content"));
            } else {
                logger.error("消息未处理: " + json);
            }
        } catch (Exception e) {
            e.printStackTrace();
            return "error";
        }
        return "ok";
    }

    /**
     * 获取客户端连接数
     */
    @PostMapping(value="getNum",consumes="application/json")
    public String getNum(@RequestBody JSONObject json) {
        int num = sseEmitterMap.size();
        JSONObject result = new JSONObject();
        result.put("msg", num);
        return result.toString();
    }
}

2. 客户端

  • eventstream.html
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <script type="text/javascript" src="https://lib.baomitu.com/socket.io/4.7.2/socket.io.js"></script>
</head>
<body font-size="14px">
<strong>服务端当前连接SSE客户端数量</strong>
<p>
    <span>地址:</span><input id="ip-num" type="text" size="32" value="http://127.0.0.1:8081" />
    &nbsp;&nbsp;<button type="button" onClick="postNum()">获取数量</button>
</p>
<textarea id="receive-num" type="text" rows="1" cols="73" readonly></textarea>
<br><br><br>
<strong>发送消息到服务端,服务端推送给SSE客户端</strong>
<p>
    <span>地址:</span><input id="ip-msg" type="text" size="32" value="http://127.0.0.1:8081" />
    &nbsp;&nbsp;<span>连接ID:</span><input id="id-msg" type="text" size="20" value="123456" />
    &nbsp;&nbsp;<button type="button" onClick="postMsg()">发送消息</button>
</p>
<p>
    <span>发送内容:</span><br>
</p>
<textarea id="send-msg" type="text"  rows="6" cols="73">{"info":"HelloWord"}</textarea>
<br><br><br>
<strong>SSE客户端连接服务端</strong>
<p>
    <span>地址:</span><input id="ip-sse" type="text" size="32" value="http://127.0.0.1:8081" />
    &nbsp;&nbsp;<span>连接ID:</span><input id="id-sse" type="text" size="20" value="123456" />
</p>
<p>
    <span>认证token:</span><input id="token-sse" type="text" size="32" value="flgn920320erg8erg9" />
    &nbsp;&nbsp;<button type="button" onClick="connectSse()">&nbsp;连接&nbsp;</button>
    &nbsp;&nbsp;<button type="button" onClick="closeSse()">&nbsp;断开&nbsp;</button>
</p>
<p><strong>SSE客户端接收服务端消息</strong></p>
<textarea id="receive-content" type="text" rows="5" cols="73" readonly></textarea>
<script>
    let eventSource;
    // 连接服务端
    function connectSse() {
        const ip = document.getElementById('ip-sse').value;
        const id = document.getElementById('id-sse').value;
        const token = document.getElementById('token-sse').value;
        const sseUrl = `${ip}/test/api/comm/eventstream/connect?id=${id}&token=${token}`;
        eventSource = new EventSource(sseUrl);
        eventSource.onmessage = function (event) {
            document.getElementById("receive-content").innerHTML = `${event.data}\n` + document.getElementById("receive-content").innerHTML;
        };
        document.getElementById("receive-content").innerHTML = `连接\n` + document.getElementById("receive-content").innerHTML;
    }

    // 关闭连接
    function closeSse() {
        if (eventSource) {
            eventSource.close()
        }
        const ip = document.getElementById('ip-sse').value;
        const id = document.getElementById('id-sse').value;
        const token = document.getElementById('token-sse').value;
        const url = `${ip}/test/api/comm/eventstream/close`;
        sendPostRequest(url, {
            token: token,
            id: id
        }).then((res) => {
            document.getElementById("receive-content").innerHTML = `断开\n` + document.getElementById("receive-content").innerHTML;
        })
    }

    // 发送信息
    function postNum() {
        const ip = document.getElementById('ip-num').value;
        const url = `${ip}/test/api/comm/eventstream/getNum`;
        sendPostRequest(url, {
        }).then((res) => {
            const r = JSON.parse(res);
            document.getElementById("receive-num").innerHTML = `${r.msg}`;
        })
    }

    // 发送信息
    function postMsg() {
        const message = document.getElementById('send-msg').value;
        const id = document.getElementById('id-msg').value;
        const ip = document.getElementById('ip-msg').value;
        const url = `${ip}/test/api/comm/eventstream/sendMsg`;
        sendPostRequest(url, {
            content: message,
            id: id
        })
    }

    // 发起Post请求
    function sendPostRequest(url, data) {
        return new Promise(function (resolve, reject) {
            var xhr = new XMLHttpRequest();
            xhr.open('post', url, true);
            xhr.setRequestHeader('Content-Type', 'application/json');
            xhr.onload = function () {
                var status = xhr.status;
                if (status == 200) {
                    resolve(xhr.response);
                } else {
                    reject(status);
                }
            };
            xhr.send(JSON.stringify(data));
        });
    };
</script>
</body>

</html>
  • 效果

在这里插入图片描述

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值