WebSocket 구현
👉 해당 포스트를 읽는데 도움을 줍니다.
0. 들어가면서
이번 포스트는 WebSocket 예제 코드를 정리해서 올려보도록 하겠습니다. 다음과 같은 환경에서 테스트하였습니다.
- spring boot 2.2.5.RELEASE
- spring-boot-starter-thymeleaf
- spring-websocket
- SockJS
1. 시나리오 구성도
- 브라우저가 서버에 소켓 연결을 수행합니다.
- 두 번째 브라우저가 서버에 소켓 연결을 수행합니다.
- 처음 연결한 브라우저에서 메시지를 전달합니다.
- 서버는 메시지를 전달받아 자신이 관리하는 세션(session)으로 메시지를 전송합니다.
- 두 브라우저 모두 서버로부터 전달받은 메시지를 출력합니다.
2. 테스트 코드
2.1. 패키지 구조
./
|-- README.md
|-- action-in-blog.iml
|-- mvnw
|-- mvnw.cmd
|-- pom.xml
`-- src
|-- main
| |-- java
| | `-- blog
| | `-- in
| | `-- action
| | |-- ActionInBlogApplication.java
| | |-- component
| | | `-- WebSocketComponent.java
| | |-- configure
| | | `-- CustomWebsocketConfiguration.java
| | `-- controller
| | `-- ChatController.java
| `-- resources
| |-- application.yml
| `-- templates
| `-- chat.html
`-- test
`-- java
2.2. WebSocketComponent 클래스
- TextWebSocketHandler 클래스를 상속받습니다. 몇 개의 메소드를 오버라이드합니다.
- afterConnectionEstablished 메소드 - 연결 후에 수행됩니다. 자신이 관리하는 sessionMap 객체에 연결 정보를 저장합니다.
- handleMessage 메소드 - sessionMap 객체에서 관리되는 session 정보를 이용하여 전달받은 메시지를 전송합니다.
- afterConnectionClosed 메소드 - 연결이 해제된 후에 수행됩니다. 자신이 관리하는 sessionMap 객체에서 연결 정보를 삭제합니다.
package blog.in.action.component;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.WebSocketMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;
@Log4j2
@Component
public class WebSocketComponent extends TextWebSocketHandler {
public static Map<String, WebSocketSession> sessionMap = new ConcurrentHashMap<>();
@Override
public void afterConnectionEstablished(WebSocketSession session) {
sessionMap.put(session.getId(), session);
}
@Override
public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) {
sessionMap.forEach((sessionId, sessionInMap) -> {
try {
sessionInMap.sendMessage(message);
} catch (IOException e) {
e.printStackTrace();
}
});
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
sessionMap.remove(session.getId());
}
}
2.3. CustomWebsocketConfiguration 클래스
- @EnableWebSocket 애너테이션을 추가합니다.
- WebSocketConfigurer 인터페이스를 구현합니다.
- WebSocket Connection 관리를 위해 생성한 WebSocketComponent 빈(bean)을 주입받습니다.
- registerWebSocketHandlers 메소드 - WebSocket 기능을 위해 필요한 정보들을 지정합니다.
/chat
경로의 WebSocket 연결 정보는 WebSocketComponent 객체로 지정합니다.- CORS 문제 해결을 위해 setAllowedOrigins() 메소드를 사용합니다. 테스트이므로 모든 CORS 정보를 허용합니다.
- SockJS fallback option들을 허용합니다.
- SockJS 사용 시 필요한 클라이언트 라이브러리 URL 정보를 입력합니다.
package blog.in.action.configure;
import blog.in.action.component.WebSocketComponent;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
@Configuration
@EnableWebSocket
public class CustomWebsocketConfiguration implements WebSocketConfigurer {
private final WebSocketComponent webSocketComponent;
public CustomWebsocketConfiguration(WebSocketComponent webSocketComponent) {
this.webSocketComponent = webSocketComponent;
}
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry webSocketHandlerRegistry) {
webSocketHandlerRegistry.addHandler(webSocketComponent, "/chat")
.setAllowedOrigins("*")
.withSockJS()
.setClientLibraryUrl("https://cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.1.5/sockjs.min.js");
}
}
2.4. ChatController 클래스
/chat
경로를 통해 전달받는 요청으로chat.html
페이지를 전달합니다.
package blog.in.action.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class ChatController {
@GetMapping("/chat")
public String chat() {
return "chat";
}
}
2.5. chat.html
- connectSocket 함수 - SockJS 객체를 생성 후 연결에 필요한 경로를 입력합니다.
/chat
- onmessage 함수 - 메시지 수신 시 필요한 함수를 지정합니다.
- onerror 함수 - 에러 발생 시 필요한 함수를 지정합니다.
- onclose 함수 - 연결이 닫힐 시 필요한 함수를 지정합니다.
- send 함수 - sock 객체를 이용해 메시지를 전송합니다.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>채팅</title>
</head>
<body>
<h1>채팅방</h1>
<div id="chatConnect">
<button onclick="connectSocket()">채팅 시작하기</button>
</div>
<div id="chat" hidden="hidden">
<input id="message">
<button id="sendBtn">전송</button>
<div id="chatBox"></div>
</div>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.1.5/sockjs.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
<script type="text/javascript">
let sock;
function connectSocket() {
sock = new SockJS("/chat");
sock.onopen = function () {
alert('연결에 성공하였습니다.');
sock.onmessage = (data => {
$("<p>" + data.data + "</p>").prependTo('#chatBox');
});
$('#chatConnect').hide();
$('#chat').show();
}
sock.onerror = function (e) {
alert('연결에 실패하였습니다.');
$('#chatConnect').show();
$('#chat').hide();
}
sock.onclose = function () {
alert('연결을 종료합니다.');
$('#chatConnect').show();
$('#chat').hide();
};
}
function sendMessage() {
sock.send($("#message").val());
$('#message').val("");
}
$("#message").keyup(e => {
if (e.keyCode == 13) {
sendMessage();
}
});
$("#sendBtn").click(() => {
sendMessage();
});
</script>
</body>
</html>
댓글남기기