Skip to content
This repository was archived by the owner on Apr 28, 2025. It is now read-only.

Commit 61dc28e

Browse files
committed
SpringFramework 組み込みのWebSocket APIを使ったデモ作成
1 parent abf42fe commit 61dc28e

File tree

7 files changed

+285
-4
lines changed

7 files changed

+285
-4
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ SpringBoot2.x系で非同期メッセージングを使ったチャットアプ
44
内容:
55

66
* 2018-12 : Spring MVC で SseEmitter を使った Server-Sent Events のサンプルを作成。
7+
* 2019-03 : Spring MVC で WebSocket API を使ったデモを作成。
78

89
## 実行環境
910

pom.xml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@
55
<parent>
66
<groupId>org.springframework.boot</groupId>
77
<artifactId>spring-boot-starter-parent</artifactId>
8-
<version>2.1.1.RELEASE</version>
8+
<version>2.1.3.RELEASE</version>
99
<relativePath/> <!-- lookup parent from repository -->
1010
</parent>
1111
<groupId>com.secureskytech</groupId>
1212
<artifactId>springboot2-async-chat-demo</artifactId>
13-
<version>v201812.27.1</version>
13+
<version>v201903.26.1</version>
1414
<name>springboot2-async-chat-demo</name>
1515
<description>sample chat demo applications using asynchronous api with SpringBoot 2.x.</description>
1616

@@ -36,6 +36,10 @@
3636
<groupId>org.springframework.boot</groupId>
3737
<artifactId>spring-boot-starter-web</artifactId>
3838
</dependency>
39+
<dependency>
40+
<groupId>org.springframework.boot</groupId>
41+
<artifactId>spring-boot-starter-websocket</artifactId>
42+
</dependency>
3943

4044
<dependency>
4145
<groupId>org.springframework.boot</groupId>
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package com.secureskytech.demochatasync;
2+
3+
import org.springframework.context.annotation.Configuration;
4+
import org.springframework.web.socket.WebSocketHandler;
5+
import org.springframework.web.socket.config.annotation.EnableWebSocket;
6+
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
7+
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
8+
import org.springframework.web.socket.handler.ExceptionWebSocketHandlerDecorator;
9+
import org.springframework.web.socket.handler.LoggingWebSocketHandlerDecorator;
10+
11+
import lombok.AllArgsConstructor;
12+
13+
@Configuration
14+
@EnableWebSocket
15+
@AllArgsConstructor
16+
public class PlainWsConfig implements WebSocketConfigurer {
17+
18+
private final PlainWsDemoHandler plainWsDemoHandler;
19+
20+
@Override
21+
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
22+
final WebSocketHandler wrapped = new ExceptionWebSocketHandlerDecorator(
23+
new LoggingWebSocketHandlerDecorator(plainWsDemoHandler));
24+
registry.addHandler(wrapped, "/plain-ws-demo");
25+
}
26+
}
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
package com.secureskytech.demochatasync;
2+
3+
import java.io.IOException;
4+
import java.net.URI;
5+
import java.time.Duration;
6+
7+
import org.slf4j.Logger;
8+
import org.slf4j.LoggerFactory;
9+
import org.springframework.beans.factory.annotation.Autowired;
10+
import org.springframework.stereotype.Component;
11+
import org.springframework.util.MultiValueMap;
12+
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
13+
import org.springframework.web.socket.CloseStatus;
14+
import org.springframework.web.socket.TextMessage;
15+
import org.springframework.web.socket.WebSocketSession;
16+
import org.springframework.web.socket.handler.ConcurrentWebSocketSessionDecorator;
17+
import org.springframework.web.socket.handler.TextWebSocketHandler;
18+
import org.springframework.web.socket.handler.WebSocketSessionDecorator;
19+
import org.springframework.web.util.UriComponentsBuilder;
20+
21+
import com.secureskytech.demochatasync.service.SpringManagedActorSystem;
22+
23+
import akka.actor.AbstractActorWithTimers;
24+
import akka.actor.ActorSystem;
25+
import akka.actor.Props;
26+
import akka.event.Logging;
27+
import akka.event.LoggingAdapter;
28+
29+
@Component
30+
public class PlainWsDemoHandler extends TextWebSocketHandler {
31+
protected static final Logger LOG = LoggerFactory.getLogger(PlainWsDemoHandler.class);
32+
33+
@Autowired
34+
SpringManagedActorSystem managedActorSystem;
35+
36+
@Override
37+
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
38+
LOG.info("websocket connection established.");
39+
LOG.info("WebSocketSession.getAcceptedProtocol()={}", session.getAcceptedProtocol());
40+
session.getAttributes().forEach((k, v) -> {
41+
LOG.info("WebSocketSession.getAttributes()=[{},{}]", k, v);
42+
43+
});
44+
LOG.info("WebSocketSession.getBinaryMessageSizeLimit()={}", session.getBinaryMessageSizeLimit());
45+
session.getHandshakeHeaders().forEach((headerName, headerValues) -> {
46+
headerValues.forEach(headerValue -> {
47+
LOG.info("WebSocketSession.getHandshakeHeaders()=[{}, {}]", headerName, headerValue);
48+
49+
});
50+
});
51+
LOG.info("WebSocketSession.getId()={}", session.getId());
52+
LOG.info("WebSocketSession.getLocalAddress()={}", session.getLocalAddress());
53+
LOG.info("WebSocketSession.getPrincipal()={}", session.getPrincipal());
54+
LOG.info("WebSocketSession.getRemoteAddress()={}", session.getRemoteAddress());
55+
LOG.info("WebSocketSession.getTextMessageSizeLimit()={}", session.getTextMessageSizeLimit());
56+
LOG.info("WebSocketSession.getUri()={}", session.getUri());
57+
LOG.info("WebSocketSession.isOpen()={}", session.isOpen());
58+
59+
// これ、ここでwrapして正しいんだろうか・・・
60+
final WebSocketSession concurrentSession = new ConcurrentWebSocketSessionDecorator(session, 1024 * 200, 200);
61+
62+
final ActorSystem actorSystem = managedActorSystem.getActorSystem();
63+
final URI uri = session.getUri();
64+
MultiValueMap<String, String> parameters = UriComponentsBuilder.fromUri(uri).build().getQueryParams();
65+
// 細かいエラーは無視する。
66+
final long numOfCount = Long.parseLong(parameters.getFirst("numOfCount"));
67+
final long intervalSec = Long.parseLong(parameters.getFirst("intervalSec"));
68+
final int errval = Integer.parseInt(parameters.getFirst("errval"));
69+
actorSystem.actorOf(
70+
Props.create(AsyncCountUpTimerDemoActor.class, numOfCount, intervalSec, concurrentSession, errval));
71+
}
72+
73+
static class Tick0 {
74+
}
75+
76+
/**
77+
* N秒おきにカウントアップして {@link SseEmitter#send(Object)} し、指定カウントになったら終了する。
78+
* コンストラクタオプションによっては途中で例外を発生させて、ビジネスロジック中での例外をエミュレートすることも可能。
79+
*/
80+
static class AsyncCountUpTimerDemoActor extends AbstractActorWithTimers {
81+
final LoggingAdapter log = Logging.getLogger(getContext().getSystem(), this);
82+
int count = 0;
83+
final long numOfCount;
84+
final WebSocketSessionDecorator session;
85+
final int errval;
86+
87+
/**
88+
* @param numOfCount カウントアップ最大値
89+
* @param intervalSec 何秒おきにカウントアップするかのスリープ秒数
90+
* @param sseEmitter
91+
* @param errval カウントがこの値に到達したら div by zero を発生させる。
92+
*/
93+
AsyncCountUpTimerDemoActor(final long numOfCount, final long intervalSec,
94+
final WebSocketSessionDecorator session, final int errval) {
95+
getTimers().startPeriodicTimer("tick0", new Tick0(), Duration.ofSeconds(intervalSec));
96+
this.numOfCount = numOfCount;
97+
this.session = session;
98+
this.errval = errval;
99+
}
100+
101+
@Override
102+
public void postStop() {
103+
if (session.isOpen()) {
104+
log.info("WebSocketSession is open, normal close.");
105+
try {
106+
session.close(CloseStatus.NORMAL);
107+
} catch (IOException e) {
108+
log.info("WebSocketSession close error: {}", e.getMessage());
109+
}
110+
}
111+
log.info("async count-down timer stopped, called WebSocketSession.close(CloseStatus.NORMAL).");
112+
}
113+
114+
@Override
115+
public Receive createReceive() {
116+
return receiveBuilder().match(Tick0.class, tick -> {
117+
count++;
118+
log.info("tick {}/{}", count, numOfCount);
119+
try {
120+
// カウンタがerrvalに到達したら、div by zero を発生させる。
121+
if (errval == count) {
122+
count = count / (errval - count);
123+
}
124+
} catch (ArithmeticException e) {
125+
log.info("div/zero => stop");
126+
session.close(CloseStatus.SERVER_ERROR);
127+
getContext().stop(getSelf());
128+
}
129+
if (count >= numOfCount) {
130+
log.info("tick count max => stop");
131+
getContext().stop(getSelf());
132+
}
133+
try {
134+
session.sendMessage(new TextMessage("count-" + count));
135+
} catch (IOException e) {
136+
log.info("I/O error between peer connection : {} => stop", e.getMessage());
137+
getContext().stop(getSelf());
138+
}
139+
}).build();
140+
}
141+
}
142+
143+
@Override
144+
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
145+
LOG.info("websocket text message received.");
146+
LOG.info("WebSocketSession.getId()={}", session.getId());
147+
LOG.info("WebSocketSession.getUri()={}", session.getUri());
148+
LOG.info("WebSocketSession.isOpen()={}", session.isOpen());
149+
// クライアントからテキストメッセージが送信されたら、"thx:" prefixを付けてecho-backする。
150+
session.sendMessage(new TextMessage("thx:" + message.getPayload()));
151+
}
152+
153+
@Override
154+
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
155+
LOG.info("websocket connection established.");
156+
LOG.info("WebSocketSession.getId()={}", session.getId());
157+
LOG.info("WebSocketSession.getUri()={}", session.getUri());
158+
LOG.info("WebSocketSession.isOpen()={}", session.isOpen());
159+
}
160+
}

src/main/java/com/secureskytech/demochatasync/WebSecurityConfig.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@
1919
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
2020
@Override
2121
protected void configure(HttpSecurity http) throws Exception {
22-
http.authorizeRequests().antMatchers("/", "/sse-demo/**").permitAll().anyRequest().authenticated().and()
23-
.formLogin().loginPage("/login").permitAll().and().logout().permitAll();
22+
http.authorizeRequests().antMatchers("/", "/sse-demo/**", "/plain-ws-demo*").permitAll().anyRequest()
23+
.authenticated().and().formLogin().loginPage("/login").permitAll().and().logout().permitAll();
2424
}
2525

2626
/* https://docs.spring.io/spring-security/site/docs/5.1.2.RELEASE/reference/htmlsingle/#hello-web-security-java-configuration
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
<!DOCTYPE html>
2+
<html lang="ja">
3+
<head>
4+
<meta charset="utf-8">
5+
<title>Spring Framework 組み込みのWebSocket APIのデモ</title>
6+
</head>
7+
<body>
8+
<h2>Spring Framework 組み込みのWebSocket APIのデモ</h2>
9+
<p>
10+
<code>/plain-ws-demo</code>エンドポイントに接続し、シンプルなecho機能とサーバからの非同期送信メッセージを表示します。
11+
</p>
12+
<form>
13+
<table>
14+
<tr><th>カウントアップ最大値</th><td><input type="number" name="numOfCount" value="10" step="1" min="1" max="999"></td></tr>
15+
<tr><th>何秒おきにカウントアップするかのスリープ秒数</th><td><input type="number" name="intervalSec" value="1" step="1" min="0" max="999"></td></tr>
16+
<tr><th>カウントがこの値に到達したら div by zero を発生させる</th><td><input type="number" name="errval" value="999" step="1" min="1" max="999"></td></tr>
17+
</table>
18+
<button id="connect-to-server" onclick="return false;">接続</button>
19+
&nbsp;
20+
<button id="close-from-server" onclick="return false;">切断</button>
21+
<br>
22+
送信メッセージ:
23+
<input type="text" name="message" value="">
24+
&nbsp;
25+
<button id="send-to-server" onclick="return false;">送信</button>
26+
</form>
27+
<br>
28+
<div>
29+
<textarea id="ta-out" rows="30" cols="100"></textarea>
30+
</div>
31+
</body>
32+
<script>
33+
function talog(msg) {
34+
const curr = document.querySelector('#ta-out').value;
35+
document.querySelector('#ta-out').value = curr + '\n' + msg;
36+
}
37+
function talogEvent(event) {
38+
const eventInfo = {
39+
data: event.data,
40+
id: event.lastEventId,
41+
};
42+
talog('event=' + JSON.stringify(eventInfo));
43+
}
44+
const endpoint = 'ws://' + location.host + '/plain-ws-demo';
45+
let websocket = null;
46+
47+
document.querySelector('#connect-to-server').addEventListener('click', function(event) {
48+
event.stopPropagation();
49+
const numOfCount = document.querySelector('input[name=numOfCount]').value;
50+
const intervalSec = document.querySelector('input[name=intervalSec]').value;
51+
const errval = document.querySelector('input[name=errval]').value;
52+
websocket = new WebSocket(endpoint + '?numOfCount=' + numOfCount + '&intervalSec=' + intervalSec + '&errval=' + errval);
53+
websocket.onopen = function(event) {
54+
talog('websocket open');
55+
console.log(event);
56+
websocket.send('hello, websocket');
57+
};
58+
websocket.onmessage = function(event) {
59+
talog('websocket message received: ' + event.data);
60+
console.log(event.data);
61+
}
62+
websocket.onclose = function(event) {
63+
talog('websocket close, code=' + event.code);
64+
console.log(event);
65+
websocket = null;
66+
}
67+
websocket.onerror = function(event) {
68+
talog('websocket error');
69+
console.log(event);
70+
}
71+
return false;
72+
});
73+
74+
document.querySelector('#close-from-server').addEventListener('click', function(event) {
75+
event.stopPropagation();
76+
if (websocket) {
77+
websocket.close();
78+
}
79+
});
80+
81+
document.querySelector('#send-to-server').addEventListener('click', function(event) {
82+
event.stopPropagation();
83+
if (websocket) {
84+
const msg = document.querySelector('input[name=message]').value;
85+
websocket.send(msg);
86+
}
87+
});
88+
</script>
89+
</html>

src/main/resources/templates/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ <h2>SpringBoot2系で非同期メッセージングを使ったチャットア
99
<ul>
1010
<li><a th:href="@{/sse-demo/}" target="_blank">SseEmitterを使った Server-Sent Events のデモ</a></li>
1111
<li><a th:href="@{/sse-chatroom}" target="_blank">SseEmitterとServer-Sent Eventsによるチャットのサンプル</a>(ログインチェック付き)</li>
12+
<li><a th:href="@{/plain-ws-demo.html}" target="_blank">Spring Framework 組み込みのWebSocket APIのデモ</a></li>
1213
</ul>
1314
</body>
1415
</html>

0 commit comments

Comments
 (0)