WebSocket 이란?
기존의 단방향 HTTP 프로토콜과 호환되어 양방향 통신을 제공하기 위해 개발된 프로토콜.
일반 Socket통신과 달리 HTTP 80 Port를 사용하므로 방화벽에 제약이 없으며 통상 WebSocket으로 불린다.
접속까지는 HTTP 프로토콜을 이용하고, 그 이후 통신은 자체적인 WebSocket 프로토콜로 통신하게 된다.
언제 쓰면 좋을까?
Spring Reference을 참조하면, 자주 + 많은 양의 + 지연이 짧아야 하는 통신을 할 수록 WebSocket이 적합하다고 설명하고 있다. 주로 채팅이나 게임이 이러한 요구 사항을 가질 것이다. 단순한 알림 성격의 뉴스 피드 같은 정보에는 polling이나 streaming 방식이 더욱 단순하고 효율적인 솔루션이 될 수 있다.
websocket에 대해 아주 잘 정리 해놓은 두개의 블로그를 공유해 드립니다.
자세한 내용은 아래 블로그를 방문하셔서 확인 부탁드립니다.
구현
build.gradle
아래 프로젝트를 실행하기 위해서는
thymeleaf, websocket, lombok 에 대한 dependency만 설치하면 됩니다.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2'
runtimeOnly 'mysql:mysql-connector-java'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
// websocket //
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-websocket'
}
@Configuration
@RequiredArgsConstructor
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
private final ChatHandler chatHandler;
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(chatHandler, "ws/chat").setAllowedOrigins("*");
}
}
WebSocketHandlerRegistry에 WebSocketHandler의 구현체를 등록한다.
핸들러를 이용해 WebSocket을 활성화하기 위한 Config를 작성할 것이다.
@EnableWebSocket 어노테이션을 사용해 WebSocket을 활성화 하도록 한다.
WebSocket에 접속하기 위한 Endpoint는 /chat으로 설정하고,
도메인이 다른 서버에서도 접속 가능하도록 CORS : setAllowedOrigins("*"); 를 추가해준다.
이제 클라이언트가 ws://localhost:8080/chat으로 커넥션을 연결하고 메세지 통신을 할 수 있는 준비를 마쳤다.
다음은 websocket을 test할 view를 rendering 해주는 controller를 먼저 만들어줍니다.
@Controller
@Log4j2
public class ChatController {
@GetMapping("/chat")
public String chatGET(){
log.info("@ChatController, chat GET()");
return "chat";
}
}
익명의 사용자가 localhost:8080/chat 으로 접속하게 되면
ChatController를 통해 아래 chat.html로 이동하게 됩니다.
chat.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<th:block th:fragment="content">
<div class="container">
<div class="col-6">
<label><b>채팅방</b></label>
</div>
<div>
<div id="msgArea" class="col"></div>
<div class="col-6">
<div class="input-group mb-3">
<input type="text" id="msg" class="form-control" aria-label="Recipient's username" aria-describedby="button-addon2">
<div class="input-group-append">
<button class="btn btn-outline-secondary" type="button" id="button-send">전송</button>
</div>
</div>
</div>
</div>
</div>
</th:block>
</th:block>
<script th:inline="javascript">
$(document).ready(function(){
const username = (1 + Math.random()) * 0x10000
$("#disconn").on("click", (e) => {
disconnect();
})
$("#button-send").on("click", (e) => {
send();
});
const websocket = new WebSocket("ws://localhost:8080/ws/chat");
websocket.onmessage = onMessage;
websocket.onopen = onOpen;
websocket.onclose = onClose;
function send(){
let msg = document.getElementById("msg");
console.log(username + ":" + msg.value);
websocket.send(username + ":" + msg.value);
msg.value = '';
}
//채팅창에서 나갔을 때
function onClose(evt) {
var str = username + ": 님이 방을 나가셨습니다.";
websocket.send(str);
}
//채팅창에 들어왔을 때
function onOpen(evt) {
var str = username + ": 님이 입장하셨습니다.";
websocket.send(str);
}
function onMessage(msg) {
var data = msg.data;
var sessionId = null;
//데이터를 보낸 사람
var message = null;
var arr = data.split(":");
for(var i=0; i<arr.length; i++){
console.log('arr[' + i + ']: ' + arr[i]);
}
var cur_session = username;
//현재 세션에 로그인 한 사람
console.log("cur_session : " + cur_session);
sessionId = arr[0];
message = arr[1];
console.log("sessionID : " + sessionId);
console.log("cur_session : " + cur_session);
//로그인 한 클라이언트와 타 클라이언트를 분류하기 위함
if(sessionId == cur_session){
var str = "<div class='col-6'>";
str += "<div class='alert alert-secondary'>";
str += "<b>" + sessionId + " : " + message + "</b>";
str += "</div></div>";
$("#msgArea").append(str);
}
else{
var str = "<div class='col-6'>";
str += "<div class='alert alert-warning'>";
str += "<b>" + sessionId + " : " + message + "</b>";
str += "</div></div>";
$("#msgArea").append(str);
}
}
})
</script>
</html>
- new WebSocket("ws://localhost:8080/ws/chat") : handshake를 한다. (위에서 우리는WebsocketConfig에서 handshake를 요청받을 준비를 완료해 놓은 상태이다.)
- websocket.onoepn : handshake가 완료되고 connection이 맺어지면 실행된다.
- websocket.send(string) : socket을 대상으로 문자열을 전송한다.
- websocket.onmessage : socket에서 정보를 수신했을 때 실행된다
front에서 websocket을 이용하여 data를 계속 보낼 것이다.
이제는 서버에서 받은 data를 이용하여 처리해보자.
@Component
@Log4j2
public class ChatHandler extends TextWebSocketHandler {
private static List<WebSocketSession> list = new ArrayList<>();
@Override
// 채팅을 전송 할 때 호출되는 메서드
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
String payload = message.getPayload();
log.info("payload : " + payload);
for(WebSocketSession sess: list) {
sess.sendMessage(message);
}
}
/* Client가 접속 시 호출되는 메서드 */
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
list.add(session);
log.info(session + " 클라이언트 접속");
}
/* Client가 접속 해제 시 호출되는 메서드드 */
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
log.info(session + " 클라이언트 접속 해제");
list.remove(session);
}
}
ChatHandler는 WebSocketHandler의 구현체이다.
WebSocketHandler는 다음 메서드를 가지고 있다.
- afterConnectionEstablished(WebSocketSession) : connection이 맺어진 후 실행된다.
- handleMessage(WebSocketSession, WebSocketMessage<?>) : session에서 메시지를 수신했을 때 실행된다.
- message 타입에 따라 handleTextMessage(), handleBinaryMessage()를 실행한다.
- afterConnectionClosed(WebSocketSession, CloseStatus) : close 이후 실행된다.
결과
Reference
https://dev-gorany.tistory.com/212
https://supawer0728.github.io/2018/03/30/spring-websocket/
https://ko.javascript.info/websocket