Skip to content

Commit 060d197

Browse files
committed
feat: sse 示例
1 parent 6b003ce commit 060d197

File tree

9 files changed

+282
-0
lines changed

9 files changed

+282
-0
lines changed

codes/web/pom.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
<module>https</module>
2727
<module>connections</module>
2828
<module>websocket</module>
29+
<module>sse</module>
2930
<module>fastjson</module>
3031
<module>view</module>
3132
<module>client</module>

codes/web/sse/pom.xml

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
3+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
4+
<modelVersion>4.0.0</modelVersion>
5+
6+
<parent>
7+
<groupId>org.springframework.boot</groupId>
8+
<artifactId>spring-boot-starter-parent</artifactId>
9+
<version>2.7.7</version>
10+
</parent>
11+
12+
<groupId>io.github.dunwu.spring</groupId>
13+
<artifactId>spring-web-sse</artifactId>
14+
<version>1.0.0</version>
15+
<packaging>jar</packaging>
16+
<name>Spring::Web::SSE</name>
17+
18+
<dependencies>
19+
<dependency>
20+
<groupId>org.springframework.boot</groupId>
21+
<artifactId>spring-boot-starter-web</artifactId>
22+
</dependency>
23+
<dependency>
24+
<groupId>org.springframework.boot</groupId>
25+
<artifactId>spring-boot-starter-test</artifactId>
26+
<scope>test</scope>
27+
</dependency>
28+
<dependency>
29+
<groupId>org.projectlombok</groupId>
30+
<artifactId>lombok</artifactId>
31+
</dependency>
32+
</dependencies>
33+
34+
<build>
35+
<plugins>
36+
<plugin>
37+
<groupId>org.springframework.boot</groupId>
38+
<artifactId>spring-boot-maven-plugin</artifactId>
39+
</plugin>
40+
</plugins>
41+
</build>
42+
</project>
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package example.spring.web.sse;
2+
3+
import org.springframework.web.bind.annotation.CrossOrigin;
4+
import org.springframework.web.bind.annotation.GetMapping;
5+
import org.springframework.web.bind.annotation.PathVariable;
6+
import org.springframework.web.bind.annotation.RequestMapping;
7+
import org.springframework.web.bind.annotation.RequestParam;
8+
import org.springframework.web.bind.annotation.RestController;
9+
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
10+
11+
/**
12+
* @author <a href="mailto:[email protected]">Zhang Peng</a>
13+
* @date 2024-04-16
14+
*/
15+
@CrossOrigin
16+
@RestController
17+
@RequestMapping("/sse")
18+
public class SseController {
19+
20+
public static final String PREFIX = "user:";
21+
public static final String[] WORDS = "The quick brown fox jumps over the lazy dog.".split(" ");
22+
23+
@GetMapping(value = "/connect/{userId}", produces = "text/event-stream;charset=UTF-8")
24+
public SseEmitter connect(@PathVariable String userId) {
25+
return SseUtil.connect(PREFIX + userId);
26+
}
27+
28+
@GetMapping("/close/{userId}")
29+
public boolean close(@PathVariable String userId) {
30+
return SseUtil.close(PREFIX + userId);
31+
}
32+
33+
@GetMapping("/send/{userId}")
34+
public boolean send(@PathVariable String userId, @RequestParam("msg") String msg) {
35+
SseUtil.send(PREFIX + userId, "收到消息:" + msg);
36+
return true;
37+
}
38+
39+
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package example.spring.web.sse;
2+
3+
import lombok.extern.slf4j.Slf4j;
4+
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
5+
6+
import java.util.Map;
7+
import java.util.concurrent.ConcurrentHashMap;
8+
import java.util.concurrent.atomic.AtomicInteger;
9+
import java.util.function.Consumer;
10+
11+
/**
12+
* @author <a href="mailto:[email protected]">Zhang Peng</a>
13+
* @date 2024-04-16
14+
*/
15+
@Slf4j
16+
public class SseUtil {
17+
18+
public static final long SSE_TIMEOUT = 30000L;
19+
20+
private static final AtomicInteger COUNT = new AtomicInteger(0);
21+
private static final Map<String, SseEmitter> SSE_MAP = new ConcurrentHashMap<>();
22+
23+
public static synchronized SseEmitter connect(String key) {
24+
25+
if (SSE_MAP.containsKey(key)) {
26+
return SSE_MAP.get(key);
27+
}
28+
29+
try {
30+
SseEmitter sseEmitter = new SseEmitter(SSE_TIMEOUT);
31+
sseEmitter.onCompletion(handleCompletion(key));
32+
sseEmitter.onError(handleError(key));
33+
sseEmitter.onTimeout(handleTimeout(key));
34+
SSE_MAP.put(key, sseEmitter);
35+
COUNT.getAndIncrement();
36+
log.info("【SSE】创建连接成功!key: {}, 当前连接数:{}", key, COUNT.get());
37+
return sseEmitter;
38+
} catch (Exception e) {
39+
log.error("【SSE】创建连接异常!key: {}", key, e);
40+
return null;
41+
}
42+
}
43+
44+
public static synchronized boolean close(String key) {
45+
SseEmitter sseEmitter = SSE_MAP.get(key);
46+
if (sseEmitter == null) {
47+
return false;
48+
}
49+
sseEmitter.complete();
50+
SSE_MAP.remove(key);
51+
COUNT.getAndDecrement();
52+
log.info("【SSE】key: {} 断开连接!当前连接数:{}", key, COUNT.get());
53+
return true;
54+
}
55+
56+
private static Runnable handleCompletion(String key) {
57+
return () -> {
58+
log.info("【SSE】连接结束!key: {}", key);
59+
close(key);
60+
};
61+
}
62+
63+
private static Consumer<Throwable> handleError(String key) {
64+
return t -> {
65+
log.warn("【SSE】连接异常!key: {}", key, t);
66+
close(key);
67+
};
68+
}
69+
70+
private static Runnable handleTimeout(String key) {
71+
return () -> {
72+
log.info("【SSE】连接超时!key: {}", key);
73+
close(key);
74+
};
75+
}
76+
77+
public static void send(String key, Object message) {
78+
if (SSE_MAP.containsKey(key)) {
79+
try {
80+
SseEmitter sseEmitter = SSE_MAP.get(key);
81+
sseEmitter.send(message);
82+
} catch (Exception e) {
83+
log.error("【SSE】发送消息异常!key: {}, message: {}", key, message, e);
84+
close(key);
85+
}
86+
} else {
87+
log.warn("【SSE】发送消息失败!key: {}, message: {}", key, message);
88+
}
89+
}
90+
91+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package example.spring.web.sse;
2+
3+
import org.springframework.boot.SpringApplication;
4+
import org.springframework.boot.autoconfigure.SpringBootApplication;
5+
import org.springframework.web.bind.annotation.CrossOrigin;
6+
7+
@CrossOrigin
8+
@SpringBootApplication
9+
public class WebSseApplication {
10+
11+
public static void main(String[] args) {
12+
SpringApplication.run(WebSseApplication.class, args);
13+
}
14+
15+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
spring.mvc.async.request-timeout = 30000
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
${AnsiColor.BRIGHT_YELLOW}${AnsiStyle.BOLD}
2+
________ ___ ___ ________ ___ __ ___ ___
3+
|\ ___ \|\ \|\ \|\ ___ \|\ \ |\ \|\ \|\ \
4+
\ \ \_|\ \ \ \\\ \ \ \\ \ \ \ \ \ \ \ \ \\\ \
5+
\ \ \ \\ \ \ \\\ \ \ \\ \ \ \ \ __\ \ \ \ \\\ \
6+
\ \ \_\\ \ \ \\\ \ \ \\ \ \ \ \|\__\_\ \ \ \\\ \
7+
\ \_______\ \_______\ \__\\ \__\ \____________\ \_______\
8+
\|_______|\|_______|\|__| \|__|\|____________|\|_______|
9+
${AnsiColor.CYAN}${AnsiStyle.BOLD}
10+
:: Java :: (v${java.version})
11+
:: Spring Boot :: (v${spring-boot.version})
12+
${AnsiStyle.NORMAL}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?xml version="1.0" encoding="UTF-8" ?>
2+
<configuration scan="true" scanPeriod="60 seconds" debug="false">
3+
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
4+
<encoder>
5+
<pattern>%d{HH:mm:ss.SSS} [%boldYellow(%thread)] [%highlight(%-5level)] %boldGreen(%c{36}.%M) - %boldBlue(%m%n)
6+
</pattern>
7+
</encoder>
8+
</appender>
9+
10+
<logger name="example.spring" level="INFO" />
11+
12+
<root level="WARN">
13+
<appender-ref ref="CONSOLE" />
14+
</root>
15+
</configuration>
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<!DOCTYPE html>
2+
<html lang='en'>
3+
4+
<head>
5+
<title>SSE 示例</title>
6+
<meta charset='UTF-8'>
7+
</head>
8+
9+
<body>
10+
<h1>SSE 示例</h1>
11+
<div>
12+
userId: <input type='text' id='userId' value=''>
13+
<button id='connectBtn' onclick='connect()'>connect</button>
14+
<br />
15+
msg: <input type='text' id='msg'> <br />
16+
<button id='sendBtn' onclick='send()'>send</button>
17+
<button id='closeBtn' onclick='disconnect()'>close</button>
18+
</div>
19+
<div id='result'></div>
20+
</body>
21+
22+
<script>
23+
24+
let eventSource
25+
26+
const connect = () => {
27+
28+
let userId = document.getElementById('userId').value
29+
eventSource = new EventSource(`/sse/connect/${userId}`)
30+
31+
eventSource.onmessage = function(event) {
32+
console.log('msg', event.data)
33+
document.getElementById('result').innerHTML += '<span>' + event.data + '</span><br />'
34+
}
35+
36+
eventSource.onopen = function(event) {
37+
console.log('onopen', eventSource.readyState)
38+
document.getElementById('result').innerHTML = ''
39+
}
40+
41+
eventSource.onerror = function(error) {
42+
console.error('onerror', error)
43+
}
44+
}
45+
46+
const send = async () => {
47+
let userId = document.getElementById('userId').value
48+
let msg = document.getElementById('msg').value
49+
const response = await fetch(`/sse/send/${userId}?msg=${msg}`)
50+
response.text().then((data) => {
51+
if (data !== 'true') {
52+
console.error('发送失败')
53+
}
54+
})
55+
}
56+
57+
const disconnect = () => {
58+
eventSource.close()
59+
console.log('连接关闭')
60+
}
61+
</script>
62+
</html>
63+
64+
65+
66+

0 commit comments

Comments
 (0)