개발

[스프링] Websocket 사용하여 1:1 채팅 구현하기 (1) - 프로젝트 생성

꾸릉스토리 2024. 5. 28. 18:36

 

 

기본적인 pom.xml버전 설정 및 인코딩 필터 설정을 진행합니다.

 

https://nameguhyeon.tistory.com/2

 

[스프링] 프로젝트 생성 및 기본 설정

스프링을 사용하기 위해서 sts3 툴을 다운받아서 사용했습니다.이클립스의 marketplace에서 지원되는 sts3는 2019년인가부터 지원이 되지 않는다고 하여 요새는 sts3를 사용한다고합니다.https://docs.spri

nameguhyeon.tistory.com

 

위 게시글 확인하시면 됩니다.

 

 

 

 

설정 후 제대로 설정이 되었는지 서버로 동작시켜서 확인합니다.

 

이전 게시글을 하고 이번 게시글도 하시면 오류가 발생할 수 있습니다.

 

Details 를 눌러서 내용을 확인해 보시면 

Could not publish server configuration for Tomcat v9.0 Server at localhost. Multiple Contexts have a path of "/root"

번역해 보면

로컬 호스트에서 Tomcat v9.0 Server에 대한 서버 구성을 게시할 수 없습니다. 여러 컨텍스트에 "/root" 경로가 있습니다

라고 합니다. 

 

원인을 해결하려면 서버쪽 설정을 수정해야 합니다.

 

 

 

좌측하단을 보면 Servers 라는 폴더가 있습니다.

 

여기서 server.xml을 확인하시면

 

하단 부분 대략 163번째 줄을 보시면 Context라는 부분이 있습니다. 

 

서버가 어떤 프로젝트에서 어떤 경로를 실행할지 설정이 되어있는 부분인데 

1_Practice 의 path부분과 2_Chat의 path부분이 /root로 동일해서 발생하는 문제입니다.

 

1_Practice는 사용하지 않는 프로젝트이기 때문에 주석처리를 하겠습니다.

 

우선 한 줄로 되어있는 Context를 정리를 해주고 1_Practice부분을 주석처리하겠습니다.

주의하실 점은 </Host> 위에 Context코드가 있어야 합니다. </Host>가 없어져도 에러가 발생합니다.

 

 

저장 후 다시 한번 서버로 프로젝트를 실행해 봅니다.

 

에러 없이 잘 동작됩니다.

 

 

다음으로 채팅서비스를 구현하기 위한 라이브러리를 추가하겠습니다.

 

모두 https://mvnrepository.com/ 에서 적용 가능합니다.

 

spring websocket을 검색하여 나오는 라이브러리를 들어가서 아무 버전이나 사용합니다.

 

 

pom.xml에 추가합니다.

 

 

 

한 가지 라이브러리를 추가로 설치합니다.

 

해당 라이브러리는 JSON데이터를 주고받기 위해 사용되는 라이브러리입니다.

Jackson Databind 를 검색해서 나오는 첫 번째 라이브러리를 적용합니다.

 

버전은 2.12.1 을 사용합니다.

 

pom.xml에 추가합니다.

 

 

 

라이브러리 설치는 완료되었습니다.

 

 

다음은 설정입니다.

 

servlet-context.xml 파일에서 설정을 진행합니다.

 

 

파일을 오픈한 후 하단 Namespaces를 클릭하고 websocket을 체크합니다.

 

 

다시 Source를 클릭한 후 위 내용을 작성합니다. (28~32번째 줄)

 

 

extends로 TextWebSocketHandler 을 상속받고 ctrl + alt + s 를 클릭하면 안내 창이 하나가 표시됩니다.

 

그중 중간하단쯤 Override/Implement Methods... 를 클릭하시면 아래와 같은 창이 표시됩니다.

 

 

AbstractWebSocketHandler 에서

afterConnectionClosed 체크 -> 소켓연결이 끊어졌을 때 동작되는 코드

afterConnectionEstablished 체크 -> 소켓이 연결되었을 때 동작되는 코드

handleTextMessage 체크 -> 소켓 간 메세지를 주고받을 때 동작되는 코드

 

위 세 개를 체크하고 OK를 클릭합니다.

 

위 이미지처럼 override가 진행됩니다.

 

 

 

우선 사용자가 연결되고 끊어지는 것을 확인하기 위해서 코드를 수정해 줍니다.

 

 

 

chat.jsp 파일을 하나 생성하고 닉네임을 설정하고 채팅 페이지로 이동합니다..

 

 

 

 

더보기
package com.chat.root;

import java.text.DateFormat;
import java.util.Date;
import java.util.Locale;

import javax.servlet.http.HttpSession;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;

/**
 * Handles requests for the application home page.
 */
@Controller
public class HomeController {
	
	private static final Logger logger = LoggerFactory.getLogger(HomeController.class);
	
	/**
	 * Simply selects the home view to render by returning its name.
	 */
	@RequestMapping(value = "/", method = RequestMethod.GET)
	public String home(Locale locale, Model model) {
		logger.info("Welcome home! The client locale is {}.", locale);
		
		Date date = new Date();
		DateFormat dateFormat = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG, locale);
		
		String formattedDate = dateFormat.format(date);
		
		model.addAttribute("serverTime", formattedDate );
		
		return "home";
	}
	@GetMapping("chat")
	public String chat() {
		return "chat";
	}
	@GetMapping("chatForm")
	public String chatForm(HttpSession session, @RequestParam String name) {
		session.setAttribute("name", name);
		return "chat_form";
	}
	
}

 

홈컨트롤러의 하단부에 getMapping 으로 chat요청과 charForm을 만들어줍니다.

 

chat페이지에서 닉네임을 작성한 후 입장을 누르면 chatForm으로 연결이 됩니다.'

 

chatForm으로 전환이 될 때는 작성한 닉네임이 세션값으로 전달이 됩니다.

 

이제 서버를 실행시키고 웹에서 http://localhost:8080/root/chat 경로로 요청을 하면 페이지로 이동됩니다.

 

 

닉네임을 작성 후 채팅하러 가기를 누르면 닉네임을 포함하여 chatForm으로 이동합니다.

 

채팅이 보일 간단한 폼입니다.

 

 

접속을 할 때마다 서버 측 콘솔을 확인하면 소켓정보와 함께 연결 유무를 확인할 수 있습니다.

 

 

이제는 실제로 채팅을 주고받을 수 있게 설정을 변경합니다.

 

더보기
package com.chat.root;

import java.util.ArrayList;
import java.util.List;

import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;

public class ChatSocket extends TextWebSocketHandler{
	List<WebSocketSession> sessionList = new ArrayList<WebSocketSession>();

	@Override
	public void afterConnectionEstablished(WebSocketSession session) throws Exception {
		System.out.println("session id " + session.getId() + "인 사용자가 연결됨");
		sessionList.add(session);
		System.out.println(session.getAttributes().get("name"));

		
	}

	@Override
	protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
		String sender = (String) session.getAttributes().get("name");
		for (WebSocketSession  s : sessionList) {
			TextMessage text = new TextMessage(sender + ":" + message.getPayload());
			s.sendMessage(text);			
		}
		
	}

	@Override
	public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
		System.out.println("session id " + session.getId() + "인 사용자의 연결이 끊어짐");
		sessionList.remove(session);
	}

}

 

ChatSocket.java 파일 내용입니다.

 

클래스 하단에 List를 하나 만들었습니다.

 

사용자가 접근할 때마다 List에 추가하여 메시지를 전달할 때 사용합니다.

 

사용자가 연결이 끊어지면 list에서 세션정보를 삭제합니다. 그렇지 않으면 에러가 발생합니다.

 

 

사용자가 메세지를 전달하면 handleTextMessage가 동작합니다.

 

메세지 전달 부분에서는 우선적으로 메세지를 보낸 사용자가 누구인지 속성값을 불러와 확인합니다.

 

그리고 List를 반복문을 통해 반복하면서 List에 속해있는 모든 사용자에게 sendMessage로 내용을 전달합니다.

 

이때 보내는 사람이 누구인지도 포함하여 전달합니다. 

 

for문을 통해서 보내기 때문에 내가 보냈어도 나한테 메시지가 다시 돌아오게 됩니다.

 

그렇기 때문에 jsp에서 보낸 사용자에 따라 우측 또는 좌측에 메시지를 정렬할 수 있도록 하기 위해서 사용됩니다.

이런 식으로 표현이 될 예정입니다.

 

 

다음은 chatForm.jsp 파일입니다.

 

우선 바디 부분은 간단합니다.

 

전달받은 메시지가 보일 div를 만듭니다.

보낼 채팅내용을 적을 textarea와 전송 버튼을 생성합니다.

 

 

 

 

더보기
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
<script src="https://code.jquery.com/jquery-3.4.1.min.js"></script>
<script type="text/javascript">
	let sock = null;
	window.onload = () => {
		console.log("test")
		let wsUri = "ws://localhost:8080/root/chat/websocket";
		sock = new WebSocket(wsUri);
		console.log("sock : ", sock)
		sock.onmessage = onMessage;
		
		$("#sendBtn").click( () => {
			sendMessage();
			$("#msg").val("");
		})
	}
	
	function sendMessage() {
		sock.send($("#msg").val());
	}
	function onMessage( msg ) {
		const message = msg.data.split(":")
		if (message[0] == `${name}`) {
			var msg = '<div class="sendMsg">' + message[1] + " : " + message[0] + "<br>";
			$("#msgWrapper").append(msg)
			$(".sendMsg").css("text-align", "right")
		} else {
			var msg = '<div class="recMsg">' + message[0] + " : " + message[1] + "<br>";
			$("#msgWrapper").append(msg)
			$(".recMsg").css("text-align", "left")		
		}
	}
</script>
</head>
<body>
	<h1>chat.jsp 페이지</h1>
	<b>${ name } 님 반갑습니다.</b>
	<hr>
	
	<div id="msgWrapper" style="width:400px; min-height:200px; overflow: hidden">
	</div>
	<hr>
	내용 입력 <textarea rows="" cols="" id="msg"></textarea>
	<input type="button" id="sendBtn" value="전송">
</body>
</html>

 

스크립트 부분이 조금 내용이 있습니다.

 

window.onload를 통해 페이지가 완전히 불러와진 후 sock을 불러옵니다.

 

uri를 통해 websocket에 접속을 합니다. (이때 서버 측의 afterConnectionEstablished 가 동작됩니다.)

 

그리고 사용자가 메세지를 작성하여 전송버튼을 누르면 $("#sendBtn").click() 부분이 동작됩니다.

 

클릭하면 sock.send를 통해 서버 측의 handleTextMessage 메서드로 전달이 되고 작성한 코드대로 진행됩니다.

 

sender를 String값으로 저장하여 메세지와 함께 모든 session으로 전달합니다.

 

 

다시 chatForm으로 돌아가서 sock.onmessage를 통해 메세지를 전달받습니다.

 

onMessage함수로 연결이 되고 매개변수로 msg라는 값을 받습니다.

 

sender과 메세지를 구분하기 위해 split을 사용하여 ":"를 구분자로 분리합니다.

 

그렇게 되면 message[0]에는 sender정보가 저장이 되고 message[1]에는 메세지가 저장이 됩니다.

 

 

if문을 통해 sender값과 나의 닉네임을 비교합니다.

 

내가 보낸 메세지라면 class이름이 sendMsg값인 div를 생성합니다. 

 

메세지를 좌측에 배치하고 아이디를 우측에 표시되게 작성합니다.

 

그리고 css옵션으로 배치는 오른쪽 정렬로 설정합니다.

 

 

내가 보낸 메세지가 아니라면 class이름이 recMsg값인 div를 생성합니다.

 

메세지를 우측에 배치하고 아이디를 좌측에 표시되게 작성합니다.

 

css옵션은 왼쪽 정렬로 설정합니다.

 

 

결과물을 보면 아래와 같습니다.

서로 다른 브라우저에서 실행한 결과입니다.

 

이런 식으로 채팅 서비스를 개발할 수 있을 것 같습니다.

 

 

다음번엔 채팅내용 저장 및 불러오기나 설정한 높이를 넘어가는 내용에 대해서 hidden으로 설정 후

스크롤 최하단으로 이동하기 같은 부분을 작성해보려 합니다.

 

 

 

 

 

 

 

 

 

 

 

 

 

반응형