- Today
- Total
개발하는 고라니
[Spring MVC] Web Socket(웹 소켓)과 Chatting(채팅) 본문
기존 공부 용도의 게시판(?)에 여러 기능을 추가하던 차, 관리자와 멤버 간 채팅 기능을 구현하고 싶었다.
채팅을 하려면 웹 소켓이 필요하다고 한다. 간단하게 구현하는 것은 어렵지 않으므로 웹 소켓이 무엇인지 짚어본다.
(+추가)
Spring Boot 환경에서 구현 및 구체적으로 학습 진행중..
1장 - 웹소켓만으로 구현 (완)
2장 - 웹소켓 + SockJS 구현 (완)
3장 - STOMP + 채팅방 나누기 (완)
4장 - RabbitMQ 메세지 브로커 (완)
# Web Socket
Web Socket이란?
- 웹 소켓 프로토콜인 RFC 6455는 단일 TCP 연결을 통해 Client와 Server 사이에 전이중 방향 통신(Full Duplex, 2-way Communication) 채널을 설정하는 표준화된 방법을 제공한다.
- HTTP 환경에서, HTTP와는 다른 TCP 프로토콜이나, Port 80 및 443을 사용해 HTTP를 통해 작동하며, 기존 방화벽 규칙을 재사용할 수 있도록 설계되었다.
- Spring 스펙에서 'Web'에 속해있으며, Spring 4.0에서 등장한 네트워크 서비스이다. 기존에 채팅을 구현하려면 일반적인 Java Socket을 사용해야 했다. Java Socket으로 소켓 통신의 과정을 일일이 구현해야 했다. HTTP 통신은 기본적으로 비연결성(Connectless) 통신이므로, Client에게 한 번 보내고 나면 연결이 끊겨 지속적으로 데이터를 주고 받을 수 없다.
- *(임시 방편으로 Ajax를 사용한 비동기적 통신을 통해 주기적으로 한 페이지 안에서 Server한테 자신에게 보낼 정보가 있는지 요청(Request)하거나, 페이지가 이동될 때 마다 자신에게 온 정보가 있는지에 대한 질문을 요청에 포함할 수 있었다.)
Web Socket과 TCP
- 웹 소켓은 연결 요청에 대해 HTTP를 통해 Switching 및 HandShaking이 이루어진다.
- TCP는 이진(Binary)데이터만 주고 받을 수 있으나, 웹 소켓은 Binary와 Text 데이터도 주고 받을 수 있다.
Web Socket과 HTTP
* 웹 소켓은 HTTP 호환이 가능하게 설계되었고, HTTP 요청으로 시작하나 두 Protocol의 아키텍쳐와 Application Programming Model은 매우 다르다 *
- HTTP와 REST에서 Application은 여러 URL(Uniform Resource Location)로 모델링 된다. 반면에 웹 소켓은 일반적으로 초기 연결을 위한 URL이 한 개만 존재한다. 결과적으로 모든 Application 내 메세지는 동일한 TCP 연결을 통해 흐른다. 이는 완전히 다른 '비동기식 이벤트' 중심의 메세지 전달 아키텍쳐를 나타낸다.
- 웹 소켓은 HTTP와 달리 메세지 내용에 의미를 규정하지 않는 저수준 전송 프로토콜이다. 즉, Client와 Server가 메세지 시멘틱에 동의하지 않으면 메세지를 라우팅하거나 처리할 수 없다.
Web Socket의 특징
- HTTP 통신의 단점 개선
- 영구적 양방향 통신
- HTML 5의 주요 API
- HTTP 프로토콜을 기반으로 하는 웹 브라우저의 웹 서버간 양방향 통신을 지원하기 위한 표준
- Client/Server가 실시간으로 데이터를 주고 받을 수 있다.
Spring MVC에서 채팅창 만들기
1. Web Socket을 사용하려면
# <pom.xml>
Maven Repository에서 'Spring websocket', 'jackson-databind'를 검색 후 복사해서 pom.xml에 붙여도 좋고, 아래의 코드를 붙여도 좋다.
웹 소켓의 데이터 통신은 내부적으로 JSON을 사용하기 때문에 JSON 라이브러리를 추가하지 않으면 Error가 발생한다.
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-websocket</artifactId>
<version>5.0.7.RELEASE</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.11.3</version>
</dependency>
# <servlet-context.xml>
Servlet-context의 Namespace에서 위 사진의 하단과 같이 'websocket'을 활성화 시키고,
<websocket:handlers>
<websocket:mapping handler="chattingHandler" path="/chatting"/>
<websocket:sockjs></websocket:sockjs>
</websocket:handlers>
위와 같이 핸들러를 등록한다. 그리고 핸들러를 Bean으로 등록한다. (핸들러의 이름은 자유롭게 해도 괜찮다.)
<beans:bean id="chattingHandler" class="org.practice.handler.ChattingHandler"/>
# <web.xml>
<servlet>
<servlet-name>appServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/spring/appServlet/servlet-context.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
<async-supported>true</async-supported>
</servlet>
앱 서블릿 부분에 <async-supported>태그를 설정해준다. 이는 1:1 대화에서는 문제없지만, 클라이언트가 다수일 경우 클라이언트들이 동시에 데이터를 전송할 수 있기에 해당 제어를 위해 비동기 처리를 한다.
# <ChattingHandler>
핸들러는 보통 TextWebSocketHandler를 상속받아 구현한다. 핸들러 클래스를 구현해야 클라이언트 관리를 할 수 있다. 본 채팅 구현은 1:1이 아닌 다수와 채팅이 가능하도록 하겠다.
@Log4j
public class ChattingHandler extends TextWebSocketHandler{
private List<WebSocketSession> sessionList = new ArrayList<WebSocketSession>();
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
log.info("#ChattingHandler, afterConnectionEstablished");
sessionList.add(session);
log.info(session.getPrincipal().getName() + "님이 입장하셨습니다.");
}
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
log.info("#ChattingHandler, handleMessage");
log.info(session.getId() + ": " + message);
for(WebSocketSession s : sessionList) {
s.sendMessage(new TextMessage(session.getPrincipal().getName() + ":" + message.getPayload()));
}
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
log.info("#ChattingHandler, afterConnectionClosed");
sessionList.remove(session);
log.info(session.getPrincipal().getName() + "님이 퇴장하셨습니다.");
}
}
* afterafterConnectionEstablished()
- 채팅을 위해 해당 페이지에 들어오면(본 포스트에서는 /chat) 클라이언트가 연결된 후 해당 클라이언트의 세션을 sessionList에 add한다.
* handleTextMessage()
- 웹 소켓 서버로 메세지를 전송했을 때 이 메서드가 호출된다. 현재 웹 소켓 서버에 접속한 Session모두에게 메세지를 전달해야 하므로 loop를 돌며 메세지를 전송한다.
* afterConnectionClosed()
- 클라이언트와 연결이 끊어진 경우(채팅방을 나간 경우) remove로 해당 세션을 제거한다.
# <ChatController>
@Controller
@Log4j
public class ChatController {
@GetMapping("/chat")
public void chat(Model model) {
CustomUser user = (CustomUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
log.info("==================================");
log.info("@ChatController, GET Chat / Username : " + user.getUsername());
model.addAttribute("userid", user.getUsername());
}
}
- 필자는 Spring Security를 적용했다. User 클래스를 상속받은 CustomUser 클래스의 정보(로그인한 ID)를 Model에 담아 뷰로 보냈다.
# <chat.jsp>
<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 class="col-6">
</div>
</div>
- <input id="msg" type="text> 태그에 메세지를 작성해 <button id="button-send">를 누르면 메세지가 전송된다. 이는 아래 JavaScript로 작동한다.
# <JS>
! sockJS의 CDN을 추가해야한다.
<script src="https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js"></script>
<script type="text/javascript">
//전송 버튼 누르는 이벤트
$("#button-send").on("click", function(e) {
sendMessage();
$('#msg').val('')
});
var sock = new SockJS('http://localhost:8080/chatting');
sock.onmessage = onMessage;
sock.onclose = onClose;
sock.onopen = onOpen;
function sendMessage() {
sock.send($("#msg").val());
}
//서버에서 메시지를 받았을 때
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 = '${userid}'; //현재 세션에 로그인 한 사람
console.log("cur_session : " + cur_session);
sessionId = arr[0];
message = arr[1];
//로그인 한 클라이언트와 타 클라이언트를 분류하기 위함
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);
}
}
//채팅창에서 나갔을 때
function onClose(evt) {
var user = '${pr.username}';
var str = user + " 님이 퇴장하셨습니다.";
$("#msgArea").append(str);
}
//채팅창에 들어왔을 때
function onOpen(evt) {
var user = '${pr.username}';
var str = user + "님이 입장하셨습니다.";
$("#msgArea").append(str);
}
</script>
- JavaScript 모듈의 자세한 내용은 Github 링크를 참조한다.
github.com/sockjs/sockjs-client
# 결과
- test1과 admin으로 각각 접속해서 채팅을 쳤을 때 채팅 말풍선의 배경색이 다름으로 구분을 지을 수 있다.
채팅방의 참여자인 test1, test2, admin이 동시에 채팅을 할 수 있는 것을 확인할 수 있다.
# References
'Framework > Spring' 카테고리의 다른 글
[Spring] 크롤링된 이미지 다운로드 (0) | 2020.12.16 |
---|---|
[Spring] 크롤링 해온 정보 DB에 저장 (0) | 2020.12.14 |
[Spring MVC] JSON 출력하기 (0) | 2020.12.08 |
[Spring MVC] @RestController String의 한글 처리 (0) | 2020.12.08 |
[Spring] 메일 송신 (0) | 2020.12.02 |