💻문제점
흠... webflux에서 kafka랑 rsocket 으로 채팅하는 예제를 찾으며 시도, 실패를 계속 반복하다가 webflux에서 websocket과 kafka를 적용하는 것까지 와버렸다.
다른 사람 레퍼런스를 봐도 이해가 안 가는 부분이 많아서... 일단 빈 프로젝트에 kafka producer, consumer를 만들어 테스트하고, websocket 연결도 따로 테스트해서 합쳐보자..!라고 생각했다. 내가 직접 안 해본 것이니 이해가 안되는 거라는 생각이 들어서...
그렇게 kafka 테스트를 따로 끝내고, websocket 전체 채팅을 우선 만들었다.
테스트를 하려고 websocket test client 크롬 확장 프로그램을 열어 url에 ws://localhost:8080/ws-chat을 입력했는데, 연결이 자꾸 실패했다.
🔍해결
build.gradle에 webflux와 websocket에 대한 종속성이 있었는데, 이게 문제였다.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-websocket' //이거!
implementation 'org.springframework.boot:spring-boot-starter-webflux'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'io.projectreactor:reactor-test'
//lombok
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
//json
implementation 'org.json:json:20210307'
}
websocket 구성에 있어서 webflux랑 사용하려면 websocket 의존성을 추가할 필요는 없었다.
implementation 'org.springframework.boot:spring-boot-starter-webflux'
Spring Webflux에서는 Reactor Netty를 사용하기 때문에 이미 websocket에 대한 것이 포함되어 있다고 한다.
톰캣을 사용하는 서블릿 기반, Spring MVC에서 사용해야 하는 것 같다.
implementation 'org.springframework.boot:spring-boot-starter-websocket'
💻구현
WebSocketConfig
@Configuration
public class WebsocketConfig {
@Bean
public SimpleUrlHandlerMapping handlerMapping(WebSocketHandler wsh) {
return new SimpleUrlHandlerMapping(Map.of("/ws-chat", wsh), 1);
}
@Bean
public WebSocketHandlerAdapter webSocketHandlerAdapter() {
return new WebSocketHandlerAdapter();
}
@Bean
public Sinks.Many<String> sink() {
return Sinks.many().multicast().directBestEffort();
}
}
CustomWebsocketHandler
JSON 형태로 메시지가 들어오기 때문에 username과 message를 key값으로 value값을 가져왔다.
username이 공백이라면 익명으로 변경했다.
@Component
@Slf4j
public class CustomWebSocketHandler implements WebSocketHandler {
private final Sinks.Many<String> sink;
public CustomWebSocketHandler(Sinks.Many<String> sink) {
this.sink = sink;
}
@Override
public Mono<Void> handle(WebSocketSession session) {
var output = session.receive()
// 메시지를 JSON 객체로 변환
.map(e -> e.getPayloadAsText())
.map(e -> {
try {
// 메시지를 파싱
JSONObject json = new JSONObject(e);
String username = json.getString("username");
if (username.equals("")) username = "익명";
String message = json.getString("message");
return username + ": " + message;
} catch (JSONException ex) {
ex.printStackTrace();
return "메시지 처리 중 오류 발생";
}
});
output.subscribe(s -> sink.emitNext(s, Sinks.EmitFailureHandler.FAIL_FAST));
return session.send(sink.asFlux().map(session::textMessage));
}
}
HomeController
@Controller
public class HomeController {
@GetMapping("/")
public Mono<Rendering> home() {
return Mono.just(Rendering.view("index").build());
}
}
index.html
style 부분은 길어서 생략했다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Commeow Chat</title>
<style>
// 생략
</style>
</head>
<body>
<div id="chat-container">
<div class="chat-box">
<ul id="message-list"></ul>
<div id="message-input">
<input type="text" id="inputMessage" placeholder="메시지를 입력하세요...">
<button onclick="sendMessage()">전송</button>
</div>
</div>
<div id="nickname-input">
<input type="text" id="username" placeholder="닉네임을 입력하세요...">
<button id="confirmUsername" onclick="confirmUsername()">완료</button>
</div>
</div>
<script>
const chatWebSocket = new WebSocket('ws://' + location.host + '/ws-chat');
const inputMessage = document.getElementById('inputMessage');
const messageList = document.getElementById('message-list');
chatWebSocket.onmessage = function(event) {
const li = document.createElement('li');
li.innerText = event.data;
messageList.appendChild(li);
};
chatWebSocket.onclose = function(event) {
alert('웹소켓이 닫혔습니다! 다시 연결하려면 페이지를 새로고침 해주세요.');
};
function sendMessage() {
if (inputMessage.value.trim() === '') {
return;
}
// chatWebSocket.send(inputMessage.value);
const username = document.getElementById("username").value.trim();
const messageObject = {
username: username,
message: inputMessage.value,
};
chatWebSocket.send(JSON.stringify(messageObject));
inputMessage.value = '';
}
function confirmUsername() {
const usernameInput = document.getElementById("username");
const confirmButton = document.getElementById("confirmUsername");
if (usernameInput.value.trim() !== '') {
usernameInput.disabled = true;
confirmButton.disabled = true;
confirmButton.style.backgroundColor = "gray";
}
}
</script>
</body>
</html>
풀코드는 깃허브에 있다.
https://github.com/O-Wensu/webflux-websocket-chat
'TIL' 카테고리의 다른 글
[TIL - 20230612] OBS 방송 시작 관련 문제 해결 (0) | 2023.06.13 |
---|---|
[TIL - 20230608] Webflux + Websocket + Kafka (0) | 2023.06.08 |
[TIL - 20230605] RSocketRequester Redis 저장 실패 (0) | 2023.06.05 |
[TIL - 20230603] Webflux, Mock 사용한 Channel 도메인, ChannelService 테스트 케이스 작성 (0) | 2023.06.03 |
[TIL - 20230602] RSocket 채팅 전송 시 토큰 검사, 토큰에서 데이터 추출 (0) | 2023.06.03 |