기능: kotlin server crosstest에 WSS phase를 추가한다
WsServer TLS 지원 완료 후 crosstest에 누락됐던 WSS phase를 4개 orchestrator에 추가. TypeScript는 handshake 안정성을 위해 phase별 서버 인스턴스를 분리했다. WsServer init 강화(setReuseAddr, startError 전파), TypeScript closeWebSocket 안정화 포함.
This commit is contained in:
parent
c055387ace
commit
c3bfb35a72
9 changed files with 926 additions and 8 deletions
231
agent-task/kotlin_wss_crosstest/code_review_0.log
Normal file
231
agent-task/kotlin_wss_crosstest/code_review_0.log
Normal file
|
|
@ -0,0 +1,231 @@
|
|||
<!-- task=kotlin_wss_crosstest plan=0 tag=KOTLIN_WSS_CROSSTEST -->
|
||||
|
||||
# Code Review Reference - KOTLIN_WSS_CROSSTEST
|
||||
|
||||
## 개요
|
||||
|
||||
date=2026-04-27
|
||||
task=kotlin_wss_crosstest, plan=0, tag=KOTLIN_WSS_CROSSTEST
|
||||
|
||||
## 이 파일을 읽는 리뷰 에이전트에게
|
||||
|
||||
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
|
||||
리뷰 완료 후 반드시 아래 순서로 아카이브하세요.
|
||||
|
||||
1. `CODE_REVIEW.md` → `code_review_0.log` (N = 기존 code_review_*.log 수)
|
||||
2. `PLAN.md` → `plan_0.log` (M = 기존 plan_*.log 수)
|
||||
3. PASS인 경우 `complete.log` 작성 후 종료. WARN/FAIL인 경우 새 `PLAN.md` + `CODE_REVIEW.md` 스텁 작성.
|
||||
|
||||
---
|
||||
|
||||
## 구현 항목별 완료 여부
|
||||
|
||||
| 항목 | 완료 여부 |
|
||||
|------|---------|
|
||||
| [KOTLIN_WSS_CROSSTEST-1] kotlin_dart.kt — WSS phase 추가 | [x] |
|
||||
| [KOTLIN_WSS_CROSSTEST-2] kotlin_go.kt — WSS phase 추가 | [x] |
|
||||
| [KOTLIN_WSS_CROSSTEST-3] kotlin_python.kt — WSS phase 추가 | [x] |
|
||||
| [KOTLIN_WSS_CROSSTEST-4] kotlin_typescript.kt — WSS phase 추가 | [x] |
|
||||
|
||||
## 계획 대비 변경 사항
|
||||
|
||||
- 계획은 `WsServer(..., sslContext = serverCtx)`가 이미 존재한다고 전제했으나, 실제 `WsServer`에는 TLS 인자가 없었다. WSS crosstest 컴파일을 위해 `kotlin/src/main/kotlin/com/tokilabs/toki_socket/WsServer.kt`에 `SSLContext?` 인자와 `DefaultSSLWebSocketServerFactory` 설정을 추가했다.
|
||||
- `WsServer.start()`는 Java-WebSocket 내부 시작 실패가 `onError(null, ex)`로 전달되고 호출자에는 숨겨질 수 있어, 실제 `onStart` 또는 시작 오류를 기다리도록 보강했다. 반복 검증 중 fixed port 재사용 실패를 줄이기 위해 `setReuseAddr(true)`도 추가했다.
|
||||
- 기존 `TlsWsTest`가 `server.port()`를 사용하고 있어 `WsServer.port()` 호환 메서드를 추가했다.
|
||||
- TypeScript WSS는 단일 Java-WebSocket TLS 서버에서 `npx tsx` 클라이언트가 연속 접속할 때 다음 handshake가 간헐적으로 멈췄다. `kotlin_typescript.kt`의 WSS phase만 TLS TCP처럼 `send-push`와 `requests`에 서버 인스턴스를 분리했다.
|
||||
- TypeScript `WsClient.close()`가 `ws.close()` 호출 후 close 완료를 기다리지 않아 WSS 재접속 안정성이 떨어졌다. `typescript/src/ws_client.ts`에서 `close/error` 이벤트 또는 1초 timeout까지 기다리도록 보강했다.
|
||||
|
||||
## 주요 설계 결정
|
||||
|
||||
- Dart/Go/Python orchestrator는 계획대로 단일 WSS 서버 인스턴스를 `send-push`와 `requests`에 재사용했다.
|
||||
- TypeScript orchestrator만 클라이언트 close/handshake 특성 때문에 WSS 서버를 phase별로 분리했다. 외부 동작은 동일하게 `mode=wss`, 동일 포트 `29800`, 동일 시나리오 set을 검증한다.
|
||||
- `WsServer` TLS 지원은 기존 WS 호출과 호환되도록 nullable 기본 인자(`sslContext: SSLContext? = null`)로 추가했다.
|
||||
|
||||
## 리뷰어를 위한 체크포인트
|
||||
|
||||
- **포트 충돌 없음**: 각 파일의 `WSS_PORT`가 같은 파일 내 기존 TCP/WS/TLS_TCP 포트와 겹치지 않는지 확인 (dart=29496, go=29396, python=29400, typescript=29800).
|
||||
- **WsServer sslContext 전달**: `WsServer(..., sslContext = serverCtx)` 형태로 생성하는지 확인. 기존 `runWs()`의 WsServer 생성과 비교.
|
||||
- **서버 인스턴스 수**: Dart/Go/Python은 `runWss()` 안에서 서버를 한 번 start하고 `runWssSendPush` → `runWssRequests` 순으로 재사용하는지 확인. TypeScript는 계획 대비 변경 사항에 기록한 이유로 WSS phase별 서버를 사용한다.
|
||||
- **client runner 인수**: `runDartClient("wss", WSS_PORT, ..., certPath)` 처럼 mode=`"wss"` + certPath를 전달하는지 확인.
|
||||
- **PASS 메시지 순서**: `main()`에서 `runWss()`가 `runTlsTcp()` 다음, PASS 출력 직전에 위치하는지 확인.
|
||||
- **4개 파일 일관성**: `runWssSendPush` / `runWssRequests`의 검증 내용과 client runner 인수가 4개 파일에서 동일한 패턴을 따르는지 확인. TypeScript의 서버 lifetime만 예외다.
|
||||
|
||||
## 검증 결과
|
||||
|
||||
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
|
||||
|
||||
### 중간 검증 (컴파일)
|
||||
|
||||
```
|
||||
$ cd kotlin && env JAVA_HOME=/config/opt/jdk/jdk-17.0.10+7 GRADLE_USER_HOME=/tmp/gradle ./gradlew compileCrosstestKotlin
|
||||
> Task :compileCrosstestKotlin
|
||||
|
||||
BUILD SUCCESSFUL in 2s
|
||||
9 actionable tasks: 1 executed, 8 up-to-date
|
||||
```
|
||||
|
||||
### 최종 검증
|
||||
|
||||
```
|
||||
$ cd kotlin && env JAVA_HOME=/config/opt/jdk/jdk-17.0.10+7 GRADLE_USER_HOME=/tmp/gradle ./gradlew run -PmainClass=com.tokilabs.toki_socket.crosstest.KotlinDartKt
|
||||
INFO typeName kotlin=TestData
|
||||
INFO typeName dart=TestData
|
||||
PASS scenario=1 detail=fire-and-forget sent
|
||||
SERVER_RECEIVED index=101 message=fire from dart client
|
||||
PASS scenario=2 detail=received push from kotlin server
|
||||
INFO typeName dart=TestData
|
||||
PASS scenario=3 detail=single request response matched
|
||||
PASS scenario=4 detail=concurrent request responses matched
|
||||
SLF4J: No SLF4J providers were found.
|
||||
SLF4J: Defaulting to no-operation (NOP) logger implementation
|
||||
SLF4J: See https://www.slf4j.org/codes.html#noProviders for further details.
|
||||
INFO typeName dart=TestData
|
||||
PASS scenario=1 detail=fire-and-forget sent
|
||||
SERVER_RECEIVED index=101 message=fire from dart client
|
||||
PASS scenario=2 detail=received push from kotlin server
|
||||
INFO typeName dart=TestData
|
||||
PASS scenario=3 detail=single request response matched
|
||||
PASS scenario=4 detail=concurrent request responses matched
|
||||
INFO typeName dart=TestData
|
||||
PASS scenario=1 detail=fire-and-forget sent
|
||||
SERVER_RECEIVED index=101 message=fire from dart client
|
||||
PASS scenario=2 detail=received push from kotlin server
|
||||
INFO typeName dart=TestData
|
||||
PASS scenario=3 detail=single request response matched
|
||||
PASS scenario=4 detail=concurrent request responses matched
|
||||
INFO typeName dart=TestData
|
||||
PASS scenario=1 detail=fire-and-forget sent
|
||||
SERVER_RECEIVED index=101 message=fire from dart client
|
||||
PASS scenario=2 detail=received push from kotlin server
|
||||
INFO typeName dart=TestData
|
||||
PASS scenario=3 detail=single request response matched
|
||||
PASS scenario=4 detail=concurrent request responses matched
|
||||
PASS all kotlin-server/dart-client crosstests passed
|
||||
|
||||
BUILD SUCCESSFUL in 7s
|
||||
|
||||
$ cd kotlin && env PATH=/config/go-sdk/go/bin:$PATH JAVA_HOME=/config/opt/jdk/jdk-17.0.10+7 GRADLE_USER_HOME=/tmp/gradle GOCACHE=/tmp/go-build GOMODCACHE=/tmp/go-mod ./gradlew run -PmainClass=com.tokilabs.toki_socket.crosstest.KotlinGoKt
|
||||
INFO typeName kotlin=TestData
|
||||
INFO typeName go=TestData
|
||||
PASS scenario=1 detail=fire-and-forget sent
|
||||
SERVER_RECEIVED index=101 message=fire from go client
|
||||
PASS scenario=2 detail=received push from kotlin server
|
||||
INFO typeName go=TestData
|
||||
PASS scenario=3 detail=single request response matched
|
||||
PASS scenario=4 detail=concurrent request responses matched
|
||||
SLF4J: No SLF4J providers were found.
|
||||
SLF4J: Defaulting to no-operation (NOP) logger implementation
|
||||
SLF4J: See https://www.slf4j.org/codes.html#noProviders for further details.
|
||||
INFO typeName go=TestData
|
||||
PASS scenario=1 detail=fire-and-forget sent
|
||||
SERVER_RECEIVED index=101 message=fire from go client
|
||||
PASS scenario=2 detail=received push from kotlin server
|
||||
INFO typeName go=TestData
|
||||
PASS scenario=3 detail=single request response matched
|
||||
PASS scenario=4 detail=concurrent request responses matched
|
||||
INFO typeName go=TestData
|
||||
PASS scenario=1 detail=fire-and-forget sent
|
||||
SERVER_RECEIVED index=101 message=fire from go client
|
||||
PASS scenario=2 detail=received push from kotlin server
|
||||
INFO typeName go=TestData
|
||||
PASS scenario=3 detail=single request response matched
|
||||
PASS scenario=4 detail=concurrent request responses matched
|
||||
INFO typeName go=TestData
|
||||
PASS scenario=1 detail=fire-and-forget sent
|
||||
SERVER_RECEIVED index=101 message=fire from go client
|
||||
PASS scenario=2 detail=received push from kotlin server
|
||||
INFO typeName go=TestData
|
||||
PASS scenario=3 detail=single request response matched
|
||||
PASS scenario=4 detail=concurrent request responses matched
|
||||
PASS all kotlin-server/go-client crosstests passed
|
||||
|
||||
BUILD SUCCESSFUL in 6s
|
||||
|
||||
$ cd kotlin && env JAVA_HOME=/config/opt/jdk/jdk-17.0.10+7 GRADLE_USER_HOME=/tmp/gradle ./gradlew run -PmainClass=com.tokilabs.toki_socket.crosstest.KotlinPythonKt
|
||||
INFO typeName kotlin=TestData
|
||||
SERVER_RECEIVED index=101 message=fire from python client
|
||||
INFO typeName python=TestData
|
||||
PASS scenario=1 detail=fire-and-forget sent
|
||||
PASS scenario=2 detail=received push from kotlin server
|
||||
INFO typeName python=TestData
|
||||
PASS scenario=3 detail=single request response matched
|
||||
PASS scenario=4 detail=concurrent request responses matched
|
||||
SLF4J: No SLF4J providers were found.
|
||||
SLF4J: Defaulting to no-operation (NOP) logger implementation
|
||||
SLF4J: See https://www.slf4j.org/codes.html#noProviders for further details.
|
||||
SERVER_RECEIVED index=101 message=fire from python client
|
||||
INFO typeName python=TestData
|
||||
PASS scenario=1 detail=fire-and-forget sent
|
||||
PASS scenario=2 detail=received push from kotlin server
|
||||
INFO typeName python=TestData
|
||||
PASS scenario=3 detail=single request response matched
|
||||
PASS scenario=4 detail=concurrent request responses matched
|
||||
SERVER_RECEIVED index=101 message=fire from python client
|
||||
INFO typeName python=TestData
|
||||
PASS scenario=1 detail=fire-and-forget sent
|
||||
PASS scenario=2 detail=received push from kotlin server
|
||||
INFO typeName python=TestData
|
||||
PASS scenario=3 detail=single request response matched
|
||||
PASS scenario=4 detail=concurrent request responses matched
|
||||
SERVER_RECEIVED index=101 message=fire from python client
|
||||
INFO typeName python=TestData
|
||||
PASS scenario=1 detail=fire-and-forget sent
|
||||
PASS scenario=2 detail=received push from kotlin server
|
||||
INFO typeName python=TestData
|
||||
PASS scenario=3 detail=single request response matched
|
||||
PASS scenario=4 detail=concurrent request responses matched
|
||||
PASS all kotlin-server/python-client crosstests passed
|
||||
|
||||
BUILD SUCCESSFUL in 4s
|
||||
|
||||
$ cd kotlin && env JAVA_HOME=/config/opt/jdk/jdk-17.0.10+7 GRADLE_USER_HOME=/tmp/gradle ./gradlew run -PmainClass=com.tokilabs.toki_socket.crosstest.KotlinTypescriptKt
|
||||
INFO typeName kotlin=TestData
|
||||
INFO typeName ts=TestData
|
||||
PASS scenario=1 detail=fire-and-forget sent
|
||||
SERVER_RECEIVED index=101 message=fire from typescript client
|
||||
PASS scenario=2 detail=received push from kotlin server
|
||||
INFO typeName ts=TestData
|
||||
PASS scenario=3 detail=single request response matched
|
||||
PASS scenario=4 detail=concurrent request responses matched
|
||||
SLF4J: No SLF4J providers were found.
|
||||
SLF4J: Defaulting to no-operation (NOP) logger implementation
|
||||
SLF4J: See https://www.slf4j.org/codes.html#noProviders for further details.
|
||||
INFO typeName ts=TestData
|
||||
PASS scenario=1 detail=fire-and-forget sent
|
||||
SERVER_RECEIVED index=101 message=fire from typescript client
|
||||
PASS scenario=2 detail=received push from kotlin server
|
||||
INFO typeName ts=TestData
|
||||
PASS scenario=3 detail=single request response matched
|
||||
PASS scenario=4 detail=concurrent request responses matched
|
||||
INFO typeName ts=TestData
|
||||
PASS scenario=1 detail=fire-and-forget sent
|
||||
SERVER_RECEIVED index=101 message=fire from typescript client
|
||||
PASS scenario=2 detail=received push from kotlin server
|
||||
INFO typeName ts=TestData
|
||||
PASS scenario=3 detail=single request response matched
|
||||
PASS scenario=4 detail=concurrent request responses matched
|
||||
INFO typeName ts=TestData
|
||||
PASS scenario=1 detail=fire-and-forget sent
|
||||
SERVER_RECEIVED index=101 message=fire from typescript client
|
||||
PASS scenario=2 detail=received push from kotlin server
|
||||
INFO typeName ts=TestData
|
||||
PASS scenario=3 detail=single request response matched
|
||||
PASS scenario=4 detail=concurrent request responses matched
|
||||
PASS all kotlin-server/typescript-client crosstests passed
|
||||
|
||||
BUILD SUCCESSFUL in 7s
|
||||
```
|
||||
|
||||
### 추가 검증
|
||||
|
||||
```
|
||||
$ cd kotlin && env JAVA_HOME=/config/opt/jdk/jdk-17.0.10+7 GRADLE_USER_HOME=/tmp/gradle ./gradlew test --tests com.tokilabs.toki_socket.TlsWsTest
|
||||
> Task :test
|
||||
|
||||
BUILD SUCCESSFUL in 9s
|
||||
11 actionable tasks: 4 executed, 7 up-to-date
|
||||
|
||||
$ cd typescript && npm run check
|
||||
> toki-socket@1.0.5 check
|
||||
> tsc --noEmit
|
||||
```
|
||||
26
agent-task/kotlin_wss_crosstest/complete.log
Normal file
26
agent-task/kotlin_wss_crosstest/complete.log
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
task=kotlin_wss_crosstest plan=0 tag=KOTLIN_WSS_CROSSTEST
|
||||
date=2026-04-27
|
||||
result=PASS
|
||||
|
||||
## 요약
|
||||
|
||||
Kotlin server orchestrator 4개에 WSS phase 추가. 1 plan 루프.
|
||||
|
||||
## 루프 이력
|
||||
|
||||
| plan | code_review | 판정 |
|
||||
|------|-------------|------|
|
||||
| plan_0.log | code_review_0.log | PASS |
|
||||
|
||||
## 최종 리뷰 요약
|
||||
|
||||
- [KOTLIN_WSS_CROSSTEST-1] kotlin_dart.kt: WSS_PORT=29496, runWss()/runWssSendPush()/runWssRequests() 추가
|
||||
- [KOTLIN_WSS_CROSSTEST-2] kotlin_go.kt: WSS_PORT=29396, 동일 패턴
|
||||
- [KOTLIN_WSS_CROSSTEST-3] kotlin_python.kt: WSS_PORT=29400, 동일 패턴
|
||||
- [KOTLIN_WSS_CROSSTEST-4] kotlin_typescript.kt: WSS_PORT=29800, withWssServer() helper로 phase별 서버 분리
|
||||
- WsServer.kt: setWebSocketFactory → init 블록 이동, setReuseAddr(true), startError 전파, compareAndSet 가드로 강화
|
||||
- TypeScript ws_client.ts: closeWebSocket에 close/error 이벤트 + 1초 timeout 대기 추가
|
||||
|
||||
## 잔여 Nit
|
||||
|
||||
- code_review_0.log 계획 대비 변경 사항: "WsServer에 TLS 인자가 없었다"는 부정확. kotlin_wss_tls에서 기존 추가된 것을 리팩토링한 것이 실제 내용.
|
||||
352
agent-task/kotlin_wss_crosstest/plan_0.log
Normal file
352
agent-task/kotlin_wss_crosstest/plan_0.log
Normal file
|
|
@ -0,0 +1,352 @@
|
|||
<!-- task=kotlin_wss_crosstest plan=0 tag=KOTLIN_WSS_CROSSTEST -->
|
||||
|
||||
# Kotlin Server WSS Crosstest 추가
|
||||
|
||||
## 이 파일을 읽는 구현 에이전트에게
|
||||
|
||||
각 항목의 체크리스트를 완료 후 `[x]`로 표시한다.
|
||||
중간 검증 명령을 실제로 실행하고 출력을 `CODE_REVIEW.md`의 검증 결과 섹션에 붙여 넣는다.
|
||||
계획과 다르게 구현한 부분은 반드시 `CODE_REVIEW.md`의 "계획 대비 변경 사항"에 이유와 함께 기록한다.
|
||||
|
||||
## 배경
|
||||
|
||||
`kotlin_wss_tls` task에서 `WsServer`에 `sslContext` 파라미터를 추가해 WSS를 지원하게 됐다.
|
||||
그러나 Kotlin server orchestrator 4개(`kotlin_dart.kt`, `kotlin_go.kt`, `kotlin_python.kt`, `kotlin_typescript.kt`)는
|
||||
TLS TCP phase만 추가됐고 WSS phase는 누락된 상태다.
|
||||
4개 client helper(`dart_kotlin_client.dart`, `go_kotlin_client Main.kt`, `python_kotlin_client.py`, `typescript_kotlin_client.ts`)는
|
||||
모두 `"wss"` case가 이미 구현되어 있으므로 orchestrator쪽만 수정하면 된다.
|
||||
|
||||
## 포트 배정
|
||||
|
||||
| 파일 | TCP | WS | TLS TCP | WSS (신규) |
|
||||
|------|-----|----|---------|-----------|
|
||||
| kotlin_dart.kt | 29490 | 29492 | 29494 | **29496** |
|
||||
| kotlin_go.kt | 29390 | 29392 | 29394 | **29396** |
|
||||
| kotlin_python.kt | 29394 | 29396 | 29398 | **29400** |
|
||||
| kotlin_typescript.kt | 29794 | 29796 | 29798 | **29800** |
|
||||
|
||||
각 WSS 포트는 해당 파일 내 기존 포트와 겹치지 않는다. 타 파일과 동일 포트가 있어도 orchestrator는 순차 실행이므로 충돌 없음.
|
||||
|
||||
---
|
||||
|
||||
## [KOTLIN_WSS_CROSSTEST-1] kotlin_dart.kt — WSS phase 추가
|
||||
|
||||
### 문제
|
||||
|
||||
`kotlin/crosstest/kotlin_dart.kt`의 `main()`이 `runTlsTcp()` 호출 후 바로 PASS를 출력한다.
|
||||
WSS (kotlin WsServer + dart `"wss"` client) phase가 없다.
|
||||
|
||||
### 해결 방법
|
||||
|
||||
1. `WSS_PORT = 29496` 상수 추가 (line 37 다음)
|
||||
2. `main()`에 `runWss()` 호출 뒤 `runTlsTcp()` 전에 `runWss()` 사이에 `runWss()` 실행 후 WSS phase를 호출한다.
|
||||
정확히는 `runTlsTcp()` 다음, PASS 출력 전에 `runWss()` 대신 별도 `runWss()` WSS 함수를 추가한다.
|
||||
3. `runWss()`는 기존 WS 패턴과 동일하게 단일 `WsServer` 인스턴스를 유지하고 `sslContext = serverCtx`를 전달한다.
|
||||
4. `runWssSendPush` / `runWssRequests` 2개 내부 함수 추가.
|
||||
|
||||
#### Before (kotlin_dart.kt:42-57)
|
||||
|
||||
```kotlin
|
||||
fun main() = runBlocking {
|
||||
println("INFO typeName kotlin=${typeNameOf<TestData>()}")
|
||||
try {
|
||||
runTcpSendPush()
|
||||
delay(150)
|
||||
runTcpRequests()
|
||||
delay(150)
|
||||
runWs()
|
||||
delay(150)
|
||||
runTlsTcp()
|
||||
println("PASS all kotlin-server/dart-client crosstests passed")
|
||||
} catch (error: Throwable) {
|
||||
System.err.println("FAIL crosstest error=${error.message ?: error}")
|
||||
exitProcess(1)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### After
|
||||
|
||||
```kotlin
|
||||
fun main() = runBlocking {
|
||||
println("INFO typeName kotlin=${typeNameOf<TestData>()}")
|
||||
try {
|
||||
runTcpSendPush()
|
||||
delay(150)
|
||||
runTcpRequests()
|
||||
delay(150)
|
||||
runWs()
|
||||
delay(150)
|
||||
runTlsTcp()
|
||||
delay(150)
|
||||
runWss()
|
||||
println("PASS all kotlin-server/dart-client crosstests passed")
|
||||
} catch (error: Throwable) {
|
||||
System.err.println("FAIL crosstest error=${error.message ?: error}")
|
||||
exitProcess(1)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Before (kotlin_dart.kt:37)
|
||||
|
||||
```kotlin
|
||||
private const val TLS_TCP_PORT = 29494
|
||||
```
|
||||
|
||||
#### After
|
||||
|
||||
```kotlin
|
||||
private const val TLS_TCP_PORT = 29494
|
||||
private const val WSS_PORT = 29496
|
||||
```
|
||||
|
||||
#### 추가할 함수 (runTlsTcp() 이후에 삽입)
|
||||
|
||||
```kotlin
|
||||
private suspend fun runWss() = coroutineScope {
|
||||
val certFile = kotlinResourceFile("server.crt")
|
||||
val keyFile = kotlinResourceFile("server.key")
|
||||
val serverCtx = buildServerSslContext(certFile.path, keyFile.path)
|
||||
val server = WsServer(HOST, WSS_PORT, WS_PATH, sslContext = serverCtx) { conn ->
|
||||
WsClient.forServer(conn, 0, 0, parserMap())
|
||||
}
|
||||
server.start()
|
||||
delay(100)
|
||||
try {
|
||||
runWssSendPush(server, certFile.path)
|
||||
delay(150)
|
||||
runWssRequests(server, certFile.path)
|
||||
} finally {
|
||||
server.stop()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun runWssSendPush(server: WsServer, certPath: String) = coroutineScope {
|
||||
val received = CompletableDeferred<Boolean>()
|
||||
server.onClientConnected = { client ->
|
||||
addListenerTyped<TestData>(client.communicator) { data ->
|
||||
println("SERVER_RECEIVED index=${data.index} message=${data.message}")
|
||||
val valid = data.index == 101 && data.message == "fire from dart client"
|
||||
received.complete(valid)
|
||||
if (valid) {
|
||||
launch {
|
||||
client.send(
|
||||
TestData.newBuilder()
|
||||
.setIndex(200)
|
||||
.setMessage("push from kotlin server")
|
||||
.build(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
runDartClient("wss", WSS_PORT, "send-push", setOf("1", "2"), certPath)
|
||||
val ok = withTimeoutOrNull(SERVER_OBSERVATION_MS) { received.await() } ?: false
|
||||
check(ok) { "WSS send-push server did not receive expected data" }
|
||||
}
|
||||
|
||||
private suspend fun runWssRequests(server: WsServer, certPath: String) {
|
||||
server.onClientConnected = { client ->
|
||||
addRequestListenerTyped<TestData, TestData>(client.communicator) { req ->
|
||||
TestData.newBuilder()
|
||||
.setIndex(req.index * 2)
|
||||
.setMessage("echo: ${req.message}")
|
||||
.build()
|
||||
}
|
||||
}
|
||||
runDartClient("wss", WSS_PORT, "requests", setOf("3", "4"), certPath)
|
||||
}
|
||||
```
|
||||
|
||||
### 수정 파일 및 체크리스트
|
||||
|
||||
- [x] `kotlin/crosstest/kotlin_dart.kt`
|
||||
- [x] `WSS_PORT = 29496` 상수 추가
|
||||
- [x] `main()`에 `delay(150)` + `runWss()` 추가 (runTlsTcp() 뒤)
|
||||
- [x] `runWss()`, `runWssSendPush()`, `runWssRequests()` 함수 추가
|
||||
|
||||
### 테스트 작성
|
||||
|
||||
신규 파일 불필요. orchestrator 자체가 e2e 검증이다.
|
||||
|
||||
### 중간 검증
|
||||
|
||||
```bash
|
||||
$ cd kotlin && env JAVA_HOME=/config/opt/jdk/jdk-17.0.10+7 GRADLE_USER_HOME=/tmp/gradle ./gradlew compileCrosstestKotlin
|
||||
# BUILD SUCCESSFUL
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## [KOTLIN_WSS_CROSSTEST-2] kotlin_go.kt — WSS phase 추가
|
||||
|
||||
### 문제
|
||||
|
||||
`kotlin/crosstest/kotlin_go.kt`에 WSS phase 없음. `go_kotlin_client` Main.kt에는 `"wss"` case 구현됨.
|
||||
|
||||
### 해결 방법
|
||||
|
||||
`kotlin_dart.kt`와 동일한 패턴. 차이점:
|
||||
- `WSS_PORT = 29396`
|
||||
- 클라이언트 runner: `runGoClient("wss", WSS_PORT, ...)` (이미 존재하는 함수)
|
||||
|
||||
#### Before (kotlin_go.kt:37)
|
||||
|
||||
```kotlin
|
||||
private const val TLS_TCP_PORT = 29394
|
||||
```
|
||||
|
||||
#### After
|
||||
|
||||
```kotlin
|
||||
private const val TLS_TCP_PORT = 29394
|
||||
private const val WSS_PORT = 29396
|
||||
```
|
||||
|
||||
`main()` 변경, `runWss()` / `runWssSendPush()` / `runWssRequests()` 추가는 kotlin_dart.kt와 동일 패턴.
|
||||
`runGoClient("wss", ...)` 호출.
|
||||
|
||||
### 수정 파일 및 체크리스트
|
||||
|
||||
- [x] `kotlin/crosstest/kotlin_go.kt`
|
||||
- [x] `WSS_PORT = 29396` 상수 추가
|
||||
- [x] `main()`에 `delay(150)` + `runWss()` 추가
|
||||
- [x] `runWss()`, `runWssSendPush()`, `runWssRequests()` 함수 추가 (Go client 호출)
|
||||
|
||||
### 테스트 작성
|
||||
|
||||
orchestrator e2e로 충분.
|
||||
|
||||
### 중간 검증
|
||||
|
||||
```bash
|
||||
$ cd kotlin && env JAVA_HOME=/config/opt/jdk/jdk-17.0.10+7 GRADLE_USER_HOME=/tmp/gradle ./gradlew compileCrosstestKotlin
|
||||
# BUILD SUCCESSFUL
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## [KOTLIN_WSS_CROSSTEST-3] kotlin_python.kt — WSS phase 추가
|
||||
|
||||
### 문제
|
||||
|
||||
`kotlin/crosstest/kotlin_python.kt`에 WSS phase 없음. `python_kotlin_client.py`에는 `"wss"` case 구현됨.
|
||||
|
||||
### 해결 방법
|
||||
|
||||
동일 패턴. 차이점:
|
||||
- `WSS_PORT = 29400`
|
||||
- 클라이언트 runner: `runPythonClient("wss", WSS_PORT, ...)`
|
||||
|
||||
#### Before (kotlin_python.kt:37)
|
||||
|
||||
```kotlin
|
||||
private const val TLS_TCP_PORT = 29398
|
||||
```
|
||||
|
||||
#### After
|
||||
|
||||
```kotlin
|
||||
private const val TLS_TCP_PORT = 29398
|
||||
private const val WSS_PORT = 29400
|
||||
```
|
||||
|
||||
### 수정 파일 및 체크리스트
|
||||
|
||||
- [x] `kotlin/crosstest/kotlin_python.kt`
|
||||
- [x] `WSS_PORT = 29400` 상수 추가
|
||||
- [x] `main()`에 `delay(150)` + `runWss()` 추가
|
||||
- [x] `runWss()`, `runWssSendPush()`, `runWssRequests()` 함수 추가 (Python client 호출)
|
||||
|
||||
### 테스트 작성
|
||||
|
||||
orchestrator e2e로 충분.
|
||||
|
||||
### 중간 검증
|
||||
|
||||
```bash
|
||||
$ cd kotlin && env JAVA_HOME=/config/opt/jdk/jdk-17.0.10+7 GRADLE_USER_HOME=/tmp/gradle ./gradlew compileCrosstestKotlin
|
||||
# BUILD SUCCESSFUL
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## [KOTLIN_WSS_CROSSTEST-4] kotlin_typescript.kt — WSS phase 추가
|
||||
|
||||
### 문제
|
||||
|
||||
`kotlin/crosstest/kotlin_typescript.kt`에 WSS phase 없음. `typescript_kotlin_client.ts`에는 `"wss"` case 구현됨.
|
||||
|
||||
### 해결 방법
|
||||
|
||||
동일 패턴. 차이점:
|
||||
- `WSS_PORT = 29800`
|
||||
- 클라이언트 runner: `runTypescriptClient("wss", WSS_PORT, ...)`
|
||||
|
||||
#### Before (kotlin_typescript.kt:37)
|
||||
|
||||
```kotlin
|
||||
private const val TLS_TCP_PORT = 29798
|
||||
```
|
||||
|
||||
#### After
|
||||
|
||||
```kotlin
|
||||
private const val TLS_TCP_PORT = 29798
|
||||
private const val WSS_PORT = 29800
|
||||
```
|
||||
|
||||
### 수정 파일 및 체크리스트
|
||||
|
||||
- [x] `kotlin/crosstest/kotlin_typescript.kt`
|
||||
- [x] `WSS_PORT = 29800` 상수 추가
|
||||
- [x] `main()`에 `delay(150)` + `runWss()` 추가
|
||||
- [x] `runWss()`, `runWssSendPush()`, `runWssRequests()` 함수 추가 (TypeScript client 호출)
|
||||
|
||||
### 테스트 작성
|
||||
|
||||
orchestrator e2e로 충분.
|
||||
|
||||
### 중간 검증
|
||||
|
||||
```bash
|
||||
$ cd kotlin && env JAVA_HOME=/config/opt/jdk/jdk-17.0.10+7 GRADLE_USER_HOME=/tmp/gradle ./gradlew compileCrosstestKotlin
|
||||
# BUILD SUCCESSFUL
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 의존 관계 및 구현 순서
|
||||
|
||||
1-4번은 서로 독립적이나 중간 검증을 공유하므로 4개 모두 수정 후 한 번에 컴파일 검증.
|
||||
|
||||
## 수정 파일 요약
|
||||
|
||||
| 파일 | 항목 |
|
||||
|------|------|
|
||||
| `kotlin/crosstest/kotlin_dart.kt` | KOTLIN_WSS_CROSSTEST-1 |
|
||||
| `kotlin/crosstest/kotlin_go.kt` | KOTLIN_WSS_CROSSTEST-2 |
|
||||
| `kotlin/crosstest/kotlin_python.kt` | KOTLIN_WSS_CROSSTEST-3 |
|
||||
| `kotlin/crosstest/kotlin_typescript.kt` | KOTLIN_WSS_CROSSTEST-4 |
|
||||
|
||||
## 최종 검증
|
||||
|
||||
```bash
|
||||
$ cd kotlin && env JAVA_HOME=/config/opt/jdk/jdk-17.0.10+7 GRADLE_USER_HOME=/tmp/gradle \
|
||||
./gradlew run -PmainClass=com.tokilabs.toki_socket.crosstest.KotlinDartKt
|
||||
# PASS all kotlin-server/dart-client crosstests passed
|
||||
|
||||
$ cd kotlin && env PATH=/config/go-sdk/go/bin:$PATH JAVA_HOME=/config/opt/jdk/jdk-17.0.10+7 \
|
||||
GRADLE_USER_HOME=/tmp/gradle GOCACHE=/tmp/go-build GOMODCACHE=/tmp/go-mod \
|
||||
./gradlew run -PmainClass=com.tokilabs.toki_socket.crosstest.KotlinGoKt
|
||||
# PASS all kotlin-server/go-client crosstests passed
|
||||
|
||||
$ cd kotlin && env JAVA_HOME=/config/opt/jdk/jdk-17.0.10+7 GRADLE_USER_HOME=/tmp/gradle \
|
||||
./gradlew run -PmainClass=com.tokilabs.toki_socket.crosstest.KotlinPythonKt
|
||||
# PASS all kotlin-server/python-client crosstests passed
|
||||
|
||||
$ cd kotlin && env JAVA_HOME=/config/opt/jdk/jdk-17.0.10+7 GRADLE_USER_HOME=/tmp/gradle \
|
||||
./gradlew run -PmainClass=com.tokilabs.toki_socket.crosstest.KotlinTypescriptKt
|
||||
# PASS all kotlin-server/typescript-client crosstests passed
|
||||
```
|
||||
|
|
@ -35,6 +35,7 @@ private const val HOST = "127.0.0.1"
|
|||
private const val TCP_PORT = 29490
|
||||
private const val WS_PORT = 29492
|
||||
private const val TLS_TCP_PORT = 29494
|
||||
private const val WSS_PORT = 29496
|
||||
private const val WS_PATH = "/"
|
||||
private const val PROCESS_TIMEOUT_MS = 20_000L
|
||||
private const val SERVER_OBSERVATION_MS = 200L
|
||||
|
|
@ -49,6 +50,8 @@ fun main() = runBlocking {
|
|||
runWs()
|
||||
delay(150)
|
||||
runTlsTcp()
|
||||
delay(150)
|
||||
runWss()
|
||||
println("PASS all kotlin-server/dart-client crosstests passed")
|
||||
} catch (error: Throwable) {
|
||||
System.err.println("FAIL crosstest error=${error.message ?: error}")
|
||||
|
|
@ -208,6 +211,60 @@ private suspend fun runTlsTcpRequests(serverCtx: SSLContext, certPath: String) {
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun runWss() = coroutineScope {
|
||||
val certFile = kotlinResourceFile("server.crt")
|
||||
val keyFile = kotlinResourceFile("server.key")
|
||||
val serverCtx = buildServerSslContext(certFile.path, keyFile.path)
|
||||
val server = WsServer(HOST, WSS_PORT, WS_PATH, sslContext = serverCtx) { conn ->
|
||||
WsClient.forServer(conn, 0, 0, parserMap())
|
||||
}
|
||||
server.start()
|
||||
delay(100)
|
||||
try {
|
||||
runWssSendPush(server, certFile.path)
|
||||
delay(150)
|
||||
runWssRequests(server, certFile.path)
|
||||
} finally {
|
||||
server.stop()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun runWssSendPush(server: WsServer, certPath: String) = coroutineScope {
|
||||
val received = CompletableDeferred<Boolean>()
|
||||
server.onClientConnected = { client ->
|
||||
addListenerTyped<TestData>(client.communicator) { data ->
|
||||
println("SERVER_RECEIVED index=${data.index} message=${data.message}")
|
||||
val valid = data.index == 101 && data.message == "fire from dart client"
|
||||
received.complete(valid)
|
||||
if (valid) {
|
||||
launch {
|
||||
client.send(
|
||||
TestData.newBuilder()
|
||||
.setIndex(200)
|
||||
.setMessage("push from kotlin server")
|
||||
.build(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
runDartClient("wss", WSS_PORT, "send-push", setOf("1", "2"), certPath)
|
||||
val ok = withTimeoutOrNull(SERVER_OBSERVATION_MS) { received.await() } ?: false
|
||||
check(ok) { "WSS send-push server did not receive expected data" }
|
||||
}
|
||||
|
||||
private suspend fun runWssRequests(server: WsServer, certPath: String) {
|
||||
server.onClientConnected = { client ->
|
||||
addRequestListenerTyped<TestData, TestData>(client.communicator) { req ->
|
||||
TestData.newBuilder()
|
||||
.setIndex(req.index * 2)
|
||||
.setMessage("echo: ${req.message}")
|
||||
.build()
|
||||
}
|
||||
}
|
||||
runDartClient("wss", WSS_PORT, "requests", setOf("3", "4"), certPath)
|
||||
}
|
||||
|
||||
private suspend fun withServer(
|
||||
start: () -> Unit,
|
||||
stop: () -> Unit,
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ private const val HOST = "127.0.0.1"
|
|||
private const val TCP_PORT = 29390
|
||||
private const val WS_PORT = 29392
|
||||
private const val TLS_TCP_PORT = 29394
|
||||
private const val WSS_PORT = 29396
|
||||
private const val WS_PATH = "/"
|
||||
private const val PROCESS_TIMEOUT_MS = 20_000L
|
||||
private const val SERVER_OBSERVATION_MS = 200L
|
||||
|
|
@ -49,6 +50,8 @@ fun main() = runBlocking {
|
|||
runWs()
|
||||
delay(150)
|
||||
runTlsTcp()
|
||||
delay(150)
|
||||
runWss()
|
||||
println("PASS all kotlin-server/go-client crosstests passed")
|
||||
} catch (error: Throwable) {
|
||||
System.err.println("FAIL crosstest error=${error.message ?: error}")
|
||||
|
|
@ -208,6 +211,60 @@ private suspend fun runTlsTcpRequests(serverCtx: SSLContext, certPath: String) {
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun runWss() = coroutineScope {
|
||||
val certFile = kotlinResourceFile("server.crt")
|
||||
val keyFile = kotlinResourceFile("server.key")
|
||||
val serverCtx = buildServerSslContext(certFile.path, keyFile.path)
|
||||
val server = WsServer(HOST, WSS_PORT, WS_PATH, sslContext = serverCtx) { conn ->
|
||||
WsClient.forServer(conn, 0, 0, parserMap())
|
||||
}
|
||||
server.start()
|
||||
delay(100)
|
||||
try {
|
||||
runWssSendPush(server, certFile.path)
|
||||
delay(150)
|
||||
runWssRequests(server, certFile.path)
|
||||
} finally {
|
||||
server.stop()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun runWssSendPush(server: WsServer, certPath: String) = coroutineScope {
|
||||
val received = CompletableDeferred<Boolean>()
|
||||
server.onClientConnected = { client ->
|
||||
addListenerTyped<TestData>(client.communicator) { data ->
|
||||
println("SERVER_RECEIVED index=${data.index} message=${data.message}")
|
||||
val valid = data.index == 101 && data.message == "fire from go client"
|
||||
received.complete(valid)
|
||||
if (valid) {
|
||||
launch {
|
||||
client.send(
|
||||
TestData.newBuilder()
|
||||
.setIndex(200)
|
||||
.setMessage("push from kotlin server")
|
||||
.build(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
runGoClient("wss", WSS_PORT, "send-push", setOf("1", "2"), certPath)
|
||||
val ok = withTimeoutOrNull(SERVER_OBSERVATION_MS) { received.await() } ?: false
|
||||
check(ok) { "WSS send-push server did not receive expected data" }
|
||||
}
|
||||
|
||||
private suspend fun runWssRequests(server: WsServer, certPath: String) {
|
||||
server.onClientConnected = { client ->
|
||||
addRequestListenerTyped<TestData, TestData>(client.communicator) { req ->
|
||||
TestData.newBuilder()
|
||||
.setIndex(req.index * 2)
|
||||
.setMessage("echo: ${req.message}")
|
||||
.build()
|
||||
}
|
||||
}
|
||||
runGoClient("wss", WSS_PORT, "requests", setOf("3", "4"), certPath)
|
||||
}
|
||||
|
||||
private suspend fun withServer(
|
||||
start: () -> Unit,
|
||||
stop: () -> Unit,
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ private const val HOST = "127.0.0.1"
|
|||
private const val TCP_PORT = 29394
|
||||
private const val WS_PORT = 29396
|
||||
private const val TLS_TCP_PORT = 29398
|
||||
private const val WSS_PORT = 29400
|
||||
private const val WS_PATH = "/"
|
||||
private const val PROCESS_TIMEOUT_MS = 20_000L
|
||||
private const val SERVER_OBSERVATION_MS = 200L
|
||||
|
|
@ -49,6 +50,8 @@ fun main() = runBlocking {
|
|||
runWs()
|
||||
delay(150)
|
||||
runTlsTcp()
|
||||
delay(150)
|
||||
runWss()
|
||||
println("PASS all kotlin-server/python-client crosstests passed")
|
||||
} catch (error: Throwable) {
|
||||
System.err.println("FAIL crosstest error=${error.message ?: error}")
|
||||
|
|
@ -208,6 +211,60 @@ private suspend fun runTlsTcpRequests(serverCtx: SSLContext, certPath: String) {
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun runWss() = coroutineScope {
|
||||
val certFile = kotlinResourceFile("server.crt")
|
||||
val keyFile = kotlinResourceFile("server.key")
|
||||
val serverCtx = buildServerSslContext(certFile.path, keyFile.path)
|
||||
val server = WsServer(HOST, WSS_PORT, WS_PATH, sslContext = serverCtx) { conn ->
|
||||
WsClient.forServer(conn, 0, 0, parserMap())
|
||||
}
|
||||
server.start()
|
||||
delay(100)
|
||||
try {
|
||||
runWssSendPush(server, certFile.path)
|
||||
delay(150)
|
||||
runWssRequests(server, certFile.path)
|
||||
} finally {
|
||||
server.stop()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun runWssSendPush(server: WsServer, certPath: String) = coroutineScope {
|
||||
val received = CompletableDeferred<Boolean>()
|
||||
server.onClientConnected = { client ->
|
||||
addListenerTyped<TestData>(client.communicator) { data ->
|
||||
println("SERVER_RECEIVED index=${data.index} message=${data.message}")
|
||||
val valid = data.index == 101 && data.message == "fire from python client"
|
||||
received.complete(valid)
|
||||
if (valid) {
|
||||
launch {
|
||||
client.send(
|
||||
TestData.newBuilder()
|
||||
.setIndex(200)
|
||||
.setMessage("push from kotlin server")
|
||||
.build(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
runPythonClient("wss", WSS_PORT, "send-push", setOf("1", "2"), certPath)
|
||||
val ok = withTimeoutOrNull(SERVER_OBSERVATION_MS) { received.await() } ?: false
|
||||
check(ok) { "WSS send-push server did not receive expected data" }
|
||||
}
|
||||
|
||||
private suspend fun runWssRequests(server: WsServer, certPath: String) {
|
||||
server.onClientConnected = { client ->
|
||||
addRequestListenerTyped<TestData, TestData>(client.communicator) { req ->
|
||||
TestData.newBuilder()
|
||||
.setIndex(req.index * 2)
|
||||
.setMessage("echo: ${req.message}")
|
||||
.build()
|
||||
}
|
||||
}
|
||||
runPythonClient("wss", WSS_PORT, "requests", setOf("3", "4"), certPath)
|
||||
}
|
||||
|
||||
private suspend fun withServer(
|
||||
start: () -> Unit,
|
||||
stop: () -> Unit,
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ private const val HOST = "127.0.0.1"
|
|||
private const val TCP_PORT = 29794
|
||||
private const val WS_PORT = 29796
|
||||
private const val TLS_TCP_PORT = 29798
|
||||
private const val WSS_PORT = 29800
|
||||
private const val WS_PATH = "/"
|
||||
private const val PROCESS_TIMEOUT_MS = 20_000L
|
||||
private const val SERVER_OBSERVATION_MS = 200L
|
||||
|
|
@ -49,6 +50,8 @@ fun main() = runBlocking {
|
|||
runWs()
|
||||
delay(150)
|
||||
runTlsTcp()
|
||||
delay(150)
|
||||
runWss()
|
||||
println("PASS all kotlin-server/typescript-client crosstests passed")
|
||||
} catch (error: Throwable) {
|
||||
System.err.println("FAIL crosstest error=${error.message ?: error}")
|
||||
|
|
@ -208,6 +211,71 @@ private suspend fun runTlsTcpRequests(serverCtx: SSLContext, certPath: String) {
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun runWss() = coroutineScope {
|
||||
val certFile = kotlinResourceFile("server.crt")
|
||||
val keyFile = kotlinResourceFile("server.key")
|
||||
val serverCtx = buildServerSslContext(certFile.path, keyFile.path)
|
||||
withWssServer(serverCtx) { server ->
|
||||
runWssSendPush(server, certFile.path)
|
||||
}
|
||||
delay(150)
|
||||
withWssServer(serverCtx) { server ->
|
||||
runWssRequests(server, certFile.path)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun withWssServer(
|
||||
serverCtx: SSLContext,
|
||||
body: suspend (WsServer) -> Unit,
|
||||
) {
|
||||
val server = WsServer(HOST, WSS_PORT, WS_PATH, sslContext = serverCtx) { conn ->
|
||||
WsClient.forServer(conn, 0, 0, parserMap())
|
||||
}
|
||||
server.start()
|
||||
delay(100)
|
||||
try {
|
||||
body(server)
|
||||
} finally {
|
||||
server.stop()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun runWssSendPush(server: WsServer, certPath: String) = coroutineScope {
|
||||
val received = CompletableDeferred<Boolean>()
|
||||
server.onClientConnected = { client ->
|
||||
addListenerTyped<TestData>(client.communicator) { data ->
|
||||
println("SERVER_RECEIVED index=${data.index} message=${data.message}")
|
||||
val valid = data.index == 101 && data.message == "fire from typescript client"
|
||||
received.complete(valid)
|
||||
if (valid) {
|
||||
launch {
|
||||
client.send(
|
||||
TestData.newBuilder()
|
||||
.setIndex(200)
|
||||
.setMessage("push from kotlin server")
|
||||
.build(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
runTypescriptClient("wss", WSS_PORT, "send-push", setOf("1", "2"), certPath)
|
||||
val ok = withTimeoutOrNull(SERVER_OBSERVATION_MS) { received.await() } ?: false
|
||||
check(ok) { "WSS send-push server did not receive expected data" }
|
||||
}
|
||||
|
||||
private suspend fun runWssRequests(server: WsServer, certPath: String) {
|
||||
server.onClientConnected = { client ->
|
||||
addRequestListenerTyped<TestData, TestData>(client.communicator) { req ->
|
||||
TestData.newBuilder()
|
||||
.setIndex(req.index * 2)
|
||||
.setMessage("echo: ${req.message}")
|
||||
.build()
|
||||
}
|
||||
}
|
||||
runTypescriptClient("wss", WSS_PORT, "requests", setOf("3", "4"), certPath)
|
||||
}
|
||||
|
||||
private suspend fun withServer(
|
||||
start: () -> Unit,
|
||||
stop: () -> Unit,
|
||||
|
|
|
|||
|
|
@ -4,32 +4,64 @@ import com.google.protobuf.MessageLite
|
|||
import kotlinx.coroutines.runBlocking
|
||||
import org.java_websocket.WebSocket
|
||||
import org.java_websocket.handshake.ClientHandshake
|
||||
import org.java_websocket.server.DefaultSSLWebSocketServerFactory
|
||||
import org.java_websocket.server.WebSocketServer
|
||||
import java.io.IOException
|
||||
import java.net.InetSocketAddress
|
||||
import java.nio.ByteBuffer
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import javax.net.ssl.SSLContext
|
||||
|
||||
class WsServer(
|
||||
host: String,
|
||||
port: Int,
|
||||
private val path: String = "/",
|
||||
private val sslContext: SSLContext? = null,
|
||||
private val newClient: (WebSocket) -> WsClient,
|
||||
) : WebSocketServer(InetSocketAddress(host, port)) {
|
||||
private val clientByConn = ConcurrentHashMap<WebSocket, WsClient>()
|
||||
private val clients = CopyOnWriteArrayList<WsClient>()
|
||||
private val startedFlag = AtomicBoolean(false)
|
||||
private val startLatch = CountDownLatch(1)
|
||||
@Volatile
|
||||
private var startError: Exception? = null
|
||||
|
||||
var onClientConnected: (WsClient) -> Unit = {}
|
||||
|
||||
init {
|
||||
setReuseAddr(true)
|
||||
if (sslContext != null) {
|
||||
setWebSocketFactory(DefaultSSLWebSocketServerFactory(sslContext))
|
||||
}
|
||||
}
|
||||
|
||||
fun started(): Boolean = startedFlag.get()
|
||||
|
||||
fun clients(): List<WsClient> = clients.toList()
|
||||
|
||||
fun port(): Int = port
|
||||
|
||||
override fun start() {
|
||||
startedFlag.set(true)
|
||||
super.start()
|
||||
if (!startedFlag.compareAndSet(false, true)) return
|
||||
try {
|
||||
super.start()
|
||||
} catch (error: RuntimeException) {
|
||||
startedFlag.set(false)
|
||||
throw error
|
||||
}
|
||||
if (!startLatch.await(5, TimeUnit.SECONDS)) {
|
||||
startedFlag.set(false)
|
||||
throw IOException("websocket server start timed out")
|
||||
}
|
||||
val error = startError
|
||||
if (error != null) {
|
||||
startedFlag.set(false)
|
||||
throw IOException("websocket server failed to start", error)
|
||||
}
|
||||
}
|
||||
|
||||
override fun stop() {
|
||||
|
|
@ -80,11 +112,18 @@ class WsServer(
|
|||
}
|
||||
|
||||
override fun onError(conn: WebSocket?, ex: Exception) {
|
||||
if (conn == null) return
|
||||
if (conn == null) {
|
||||
if (startLatch.count > 0) {
|
||||
startError = ex
|
||||
startLatch.countDown()
|
||||
}
|
||||
return
|
||||
}
|
||||
clientByConn[conn]?.handleFailure()
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
startLatch.countDown()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,11 +10,7 @@ export class WsClient extends BaseClient {
|
|||
private readonly ws: WebSocket;
|
||||
|
||||
constructor(ws: WebSocket, intervalSec: number, waitSec: number, parserMap: ParserMap) {
|
||||
super(intervalSec, waitSec, async () => {
|
||||
if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
|
||||
ws.close();
|
||||
}
|
||||
});
|
||||
super(intervalSec, waitSec, () => closeWebSocket(ws));
|
||||
this.ws = ws;
|
||||
this.initBase(parserMap);
|
||||
|
||||
|
|
@ -119,3 +115,38 @@ function toUint8Array(data: RawData): Uint8Array {
|
|||
}
|
||||
return new Uint8Array(data);
|
||||
}
|
||||
|
||||
async function closeWebSocket(ws: WebSocket): Promise<void> {
|
||||
if (ws.readyState === WebSocket.CLOSED) {
|
||||
return;
|
||||
}
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
const cleanup = () => {
|
||||
clearTimeout(timer);
|
||||
ws.off("close", onDone);
|
||||
ws.off("error", onDone);
|
||||
};
|
||||
const onDone = () => {
|
||||
cleanup();
|
||||
resolve();
|
||||
};
|
||||
const timer = setTimeout(() => {
|
||||
ws.terminate();
|
||||
onDone();
|
||||
}, 1000);
|
||||
|
||||
ws.once("close", onDone);
|
||||
ws.once("error", onDone);
|
||||
|
||||
if (ws.readyState === WebSocket.CLOSING) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
ws.close();
|
||||
} catch {
|
||||
ws.terminate();
|
||||
onDone();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue