Spring/EgovFrameWork(전자정부프레임워크) - 웹소켓 메신저 채팅 구현

2022. 12. 6. 10:40웹/SPRING

프로젝트 중 채팅 서비스를 만들어야하는 일이 발생하여 공부시작

 

전자정부 프레임워크의 웹소켓 메신저 기능을 이용하여 채팅을 구현해볼 예정이다.

 

출처 : 

https://www.egovframe.go.kr/wiki/doku.php?id=egovframework:com:cop:%EC%9B%B9%EC%86%8C%EC%BC%93%EB%A9%94%EC%8B%A0%EC%A0%80

 

egovframework:com:cop:웹소켓메신저 [eGovFrame]

웹소켓 메신저는 HTML5 WebSocket와 Java Websocket을 이용하여 메신저 기능을 제공한다. (Spring 4.X버전은 Java websocket을 지원하지만 Spring 3.X버전은 websocket을 따로 지원하지 않는다. 현재 egov 3.1버전은 Sprin

www.egovframe.go.kr

 

 

들어가기전에 웹소켓이 무엇인지부터 알고가야할듯 하다.

예전에 잠깐 본 적은 있는데 정확한 내용은 모르니 간단하게 짚고 넘어가도록 하자

웹소켓이란??

 

----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

출처:https://choseongho93.tistory.com/266

 

[웹소켓] WebSocket의 개념 및 사용이유, 작동원리, 문제점

오늘은 웹소켓에 대해 알아보겠습니다. ● 웹소켓(WebSocket)의 배경 : 인터넷이 나오고 HTTP를 통해서 서버로부터 데이터를 가져오기 위해서는 오로지 URL을 통한 요청이 유일한 방법이었습니다. 때

choseongho93.tistory.com

 

웹소켓이란?

Transport protocol의 일종으로 서버와 클라이언트 간의 효율적인 양방향 통신을 실현하기 위한 구조입니다.
웹소켓은 단순한 API로 구성되어있으며, 웹소켓을 이용하면 하나의 HTTP 접속으로 양방향 메시지를 자유롭게 주고받을 수 있습니다.

위 배경에서 웹소켓이 나오기 이전에는 모두 클라이언트의 요청이 없다면, 서버로부터 응답을 받을 수 없는 구조였습니다.
웹소켓은 이러한 문제를 해결하는 새로운 약속이었습니다.

웹소켓에서는 서버와 브라우저 사이에 양방향 소통이 가능합니다. 브라우저는 서버가 직접 보내는 데이터를 받아들일 수 있고, 사용자가 다른 웹사이트로 이동하지 않아도 최신 데이터가 적용된 웹을 볼 수 있게 해줍니다. 

 

웹소켓(WebSocket)의 배경

인터넷이 나오고 HTTP를 통해서 서버로부터 데이터를 가져오기 위해서는 오로지 URL을 통한 요청이 유일한 방법이었습니다. 때문에 아이디 중복 확인과 같은 유효성 검사는 서버로 데이터를 보내는 중간과정에서 새로운 페이지 요청을 하게 되었습니다.

여기서 발전된 방식이 Ajax통신으로 클라이언트에서 XMLHttpRequest 객체를 이용하여 서버에 요청을 보내면 서버가 응답을 하는 방식입니다.

페이지 요청이 아닌 데이터 요청이라 부분적으로 정보를 갱신할 수 있게 됩니다.
Ajax를 사용하면 새로운 HTML을 서버로부터 받아야하는 것이 아닌 동일한 페이지의 일부를 수정할 수 있는 가능성이 생기고, 사용자 입장에서는 페이지 이동이 발생되지 않고 페이지 내부 변화만 일어나게 해주므로 그만큼의 자원과 시간을 아낄수 있습니다.


하지만, Ajax도 결국 HTTP를 이용하기 때문에 요청을 보내야 응답이 옵니다. 
변경된 데이터를 가져오기 위해서 버튼을 누른다거나 일정 시간 주기로 요청을 보낸다면 번거로울 뿐더러 자원 낭비입니다.

이러한 문제들을 해결하기 위해 웹소켓이 탄생합니다.

 

문제점

1. 프로그램 구현에 보다 많은 복잡성을 초래합니다.
- 웹 소켓은 HTTP와 달리 Stateful protocol이기 때문에 서버와 클라이언트 간의 연결을 항상 유지해야 하며 만약 비정상적으로 연결이 끊어졌을때 적절하게 대응해야 한다. 이는 기존의 HTTP 사용시와 비교했을때 코딩의 복잡성을 가중시키는 요인이 될 수 있습니다.
2. 서버와 클라이언트 간의 Socket 연결을 유지하는 것 자체가 비용이 듭니다.
- 특히나 트래픽 양이 많은 서버같은 경우에는 CPU에 큰 부담이 될 수 있습니다.
3. 오래된 버전의 웹 브라우저에서는 지원하지 않습니다. (물론 SockJS 라이브러리 같은 경우에는 Fallback option을 제공하고 있습니다.)

 HTTP프로토콜을 통해 이루어집니다.

연결이 정상적으로 이루어진다면 서버와 클라이언트 간에 웹소켓 연결(TCP/IP기반)이 이루어지고 일정 시간이 지나면

HTTP연결은 자동으로 끊어집니다.

 

 

위와 같은 설명으로 요약하면

 

1. Transport protocol의 일종으로 서버와 클라이언트 간의 효율적인 양방향 통신을 실현하기 위한 구조

2. HTTP프로토콜을 통해서 웹소켓 연결(TCP/IP)하여 서버와 클라이언트 간 양방향 통신

3. Ajax 는 클라이언트에서 요청을 보내야 응답이 오는데 서버와 브라우저 사이 양방향 소통으로 서버가 직접 보내는 데이터를 받아들일 수 있음.

 

 

원래 전자정부 프레임워크의 소스를 사용하려고 했는데 달린게 많아서 사용법을 익히고 간단하게 샘플로 구현해보기로 하였다.

 

출처:

웹소켓으로 채팅 프로그램 만들기 (tistory.com)

 

웹소켓으로 채팅 프로그램 만들기

사전 지식 소켓(Socket) 소켓(Socket)은 네트워크에서 동작하는 프로그램의 종착점(endpoint)이라고 주로 표현한다. IP 주소와 포트 번호로 이루어져 있으며, 서버와 클라이언트가 양방향 통신을 할 수

mildous.tistory.com

 

 

소켓통신의 절차

  1. 서버에서 서버용 소켓(ServerSocket)을 생성하고, 클라이언트가 접속하기를 기다린다.
  2. 클라이언트가 소켓(Socket)을 생성하여 서버로 연결을 요청한다.
  3. 서버가 접속을 허가(accept)한다.
  4. 서버와 클라이언트는 각각 통신을 위한 I/O 스트림을 생성한다.
  5. 스트림을 통해 서버와 클라이언트가 통신(write → read)한다.
  6. 클라이언트가 모든 작업을 마친 후 소켓을 종료(close)한다.
  7. 서버는 새로운 클라이언트의 접속을 위해 대기(accept)하거나, 종료(close)할 수 있다.

 

 웹소켓(WebSocket)

 

일반적인 웹 환경은 클라이언트의 요청을 받으면 응답 후 바로 연결을 종료하는 비연결(connectionless) 동기 소켓 방식을 사용한다.

하지만 웹소켓(WebSocket)은 클라이언트의 요청에 응답한 후에도 연결을 그대로 유지하는 연결 지향(connection oriented) 방식이다. 따라서 별도의 요청이 없어도 서버는 원하면 언제든 클라이언트로 데이터를 전송할 수 있다.

 

소켓의 통신 절차는 꽤나 복잡하지만, 웹소켓은 복잡한 절차를 아주 간단히 구현할 수 있는 장치를 가지고 있다.

웹소켓 서버의 구현은 애너테이션을 이용하며, 그 종류는 다음과 같다.

  • @ServerEndpoint : 웹소켓 서버의 요청명을 설정한다.
  • @OnOpen : 클라이언트가 접속했을 때 요청되는 메서드를 정의한다.
  • @OnMessage : 클라이언트로부터 메시지가 전송되었을 때 실행되는 메서드를 정의한다.
  • @OnClose : 클라이언트의 접속이 종료되면 실행되는 메서드를 정의한다.
  • @OnError : 에러 발생 시 실행되는 메서드를 정의한다.

----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

 

 

웹소켓을 샘플로 구현해보도록 하자 전자정부프레임워크 환경에서 프로젝트를 신규로 만들어 테스트 진행하였다.

1. 신규프로젝트 생성

이름은 WebSocket으로 생성하였다.

Next > 버튼을 선택 후 Generate Example체크를 선택해 샘플을 구성하도록한다.

생성 완료

생성이 완료되면 서버에 등록하고 index화면을 켜본다.

서버 클릭 > modeules > add web module로 추가

 

 

기본화면이 오픈되는지 확인 이후에 웹소켓을 구현해보겠다

 

 

 

2. 웹소켓 구현 시작

 

위와 같은 파일들을 복사하여 환경에 맞게 수정해서 사용하였다.

전자정부프레임워크의 

 

1. EgovWebSocketMessengerController.java 부분을 복사하여 컨트롤러를 구성한다.

 

기존의 EgovWebSocketMessengerController.java에서 필요없는 부분을 빼고 복사하여 간단하게 구성하였다.

 

MessengerController.java

package egovframework.example.sample.web;


import java.util.HashMap;
import java.util.Map;

import javax.servlet.http.HttpSession;

import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;


@Controller
public class MessengerController {

	/**
	 * 웹소켓 메신저 접속화면으로 이동한다.
	 * @param session 사용자세션
	 * @param model 모델
	 * @return view name
	 */
	@RequestMapping(value="/messengerView.do")
	public String messengerView(HttpSession session, ModelMap model) {
		return "EgovMessenger";
	}

	/**
	 * 웹 소켓 메신저 메인화면(대화상대 리스트화면)으로 이동한다.
	 * @param session 사용자세션
	 * @param model 모델
	 * @return view name
	 */
	@RequestMapping(value="/messengerMain.do")
	public String messengerMain(   @RequestParam("name") String name, ModelMap model) {
		Map<String,Object> loginVO = new HashMap<String,Object>();
		loginVO.put("name", name);
		System.out.println("--------loginVO : " + (String)loginVO.get("name"));
		model.addAttribute("loginVO", loginVO);
		return "EgovMessengerMain";
	}

	/**
	 * 대화창을 새로 띄운다.
	 * @param roomId 대화창 아이디
	 * @param username 대화상대 이름
	 * @param session 사용자세션
	 * @param model 모델
	 * @return view name
	 */
	@RequestMapping(value="/messengerPopup.do")
	public String messengerPopup(@RequestParam(value="roomId") String roomId,
										 @RequestParam(value="username") String username,
										 HttpSession session, ModelMap model) {
		model.addAttribute("roomId", roomId);
		model.addAttribute("username", username);
		return "chatPopupBubble";
	}
}

 

2. 가장 처음 접속되는 화면인 /messengerView.do 부분의

EgovMessenger.jsp를 복사하고 화면에 나올수 있도록 수정한다.

 

 

버튼 css위치를 변경하고 기존의 VO로 되어있던 소스와 다르게 사용자 명만 입력해서 테스트로 접속할 수 있게 

text박스를 추가하였다.

 

EgovMessenger.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html lang="ko">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<link type="text/css" rel="stylesheet" href="<c:url value='/css/button.css'/>"/>

<script  src="http://code.jquery.com/jquery-latest.min.js"></script>
<script>
	$(document).ready(function() {
		$('#connectMsgBtn').click(function() {
			var form = $("form[name=msgForm]");
			form.attr("action", "<c:url value='/messengerMain.do'/>");
			form.attr("method", "post");
			form.submit();
		});		
	});
</script>
</head>
<body>
<form name="msgForm" id="msgForm" action="<c:url value='  '/>" method="post">
		<input type="text" id="loginId" name="name" value=""/>
		<input type="button" id="connectMsgBtn" name="connectMsgBtn" value="메신저 접속"/>
</form>
</body>
</html>

화면이 바로뜨지 않아서 dispatcher-servelet.xml의 설정 부분을 수정해주었다 

    <bean class="org.springframework.web.servlet.view.UrlBasedViewResolver" p:order="1"
	    p:viewClass="org.springframework.web.servlet.view.JstlView"
	    p:prefix="/WEB-INF/jsp/" p:suffix=".jsp"/>

 

jsp파일의 경로는 아래와 같이 넣어주었다.

실행확인

 

 

 

 

3. 컨트롤러의 /messengerMain.do 에서 name값을 받고 EgovMessengerMain.jsp 화면이 뜨도록 수정해주었다.

 

 

여기서 중요한부분

EgovMessengerMain.jsp 화면에서 웹소켓을연결하는 부분

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html lang="ko">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>messenger</title>
<link type="text/css" rel="stylesheet" href="<c:url value='/css/table.css'/>"/>
<script  src="http://code.jquery.com/jquery-latest.min.js"></script>

<!-- <script src="resource/js/json2.js"></script>-->
<script>
	//chat 팝업창을 여러개 띄우기 위함	
	var webSocket = null;
	$(document).ready(function() {
		var url = 'ws://' + window.location.host + '${pageContext.request.contextPath}/usersServerEndpoint';
		webSocket = connection(url);
		var connectionType;
		
		webSocket.onopen = function(){ processOpen(); };
		webSocket.onmessage = function(message) { processMessage(message); };
		webSocket.onerror = function(message) { processError(message); };
		
	});
	//var webSocket = new WebSocket('ws://' + window.location.host + '/egov-messenger/usersServerEndpoint');
	
	
	function connection(url) {
		var webSocket = null;
		if ('WebSocket' in window) {
			webSocket = new WebSocket(url);
		} else if ('MozWebSocket' in window) {
			webSocket = new MozWebSocket(url);
		} else {
			Console.log('Error: WebSocket is not supported by this browser.');
            return null;
		}
		return webSocket;
	}
	
	function processOpen() {
		connectionType = "firstConnection";
		console.log("{${loginVO}");
		username = "${loginVO.name}";
		webSocket.send(JSON.stringify({ "connectionType" : connectionType, "username" : username }));
	}
		
	//server에서 메시지가 넘어왔을때
	function processMessage(message) {
		var jsonData = JSON.parse(message.data);
		
		if (jsonData.allUsers != null) {
			//다른 사용자 접속 시,
			displayUsers(jsonData.allUsers);
		} 
		
		if (jsonData.disconnectedUser != null) {
			//다른 사용자가 접속을 끊을 때,
			$("#"+jsonData.disconnectedUser).remove();
		}
		
		//다른 사용자와 대화하고자 시도할 때, 채팅창을 팝업
		if (jsonData.enterChatId != null) {
			var roomId = jsonData.enterChatId;
			$("#roomId").val(roomId);
			$("#username").val(jsonData.username);
			openPopup(roomId);
		}
	}
	
	function openPopup(roomId) {
		var popOptions = "width= 500, height= 700, resizable=yes, status= no, scrollbar= yes"; 
		var targetTitle = random(roomId); //두명의 사용자가 다른 팝업으로 뜨기 위해서 targetTitle을 랜덤으로 만들어준다.
		popupPost("<c:url value='/messengerPopup.do'/>", targetTitle, popOptions);
	}
	
	function popupPost(url, target, option) {
		window.open("", target, option);
		
		var form = $("form[name=usersForm]");
		form.attr("target", target);
		form.attr("action", url);
		form.attr("method", "post");
		form.submit();
	}
	
	
	function displayUsers(userList) {
		var username;
		$("#users tr:not(:first)").remove();
		for (var i=0; i<userList.length; i++) {
			if("${loginVO.name}"==userList[i]) {
				username = userList[i]+"(me!)";
			} else{
				username = userList[i];
			}
			$.newTr = $("<tr id="+userList[i]+" onclick='trClick(this)'><td>"+username+"</td></tr>");
			//append
			$("#users").last().append($.newTr);
			
		}
	}
	
	//다른 사용자 선택 시, 선택한 사용자 값을 서버에 전달
	function trClick(selectedTr) {
		if (selectedTr.id != null) {
				connectionType = "chatConnection";
				webSocket.send(JSON.stringify({ "connectionType" : connectionType, "connectingUser" : selectedTr.id }));
			}
	}
	
	function random(roomId) {
		<%
			String rUid = "";
			for(int i=0; i<8; i++) {
				rUid += (char)((Math.random()*26)+97);
			}
		%>
		return roomId+"."+"<%=rUid%>";
	}
	
	function processError(message) {
		/* messagesTextArea.value += "error...\n"; */
	}

	window.onbeforeunload = function() {
		webSocket.close();
	};
</script>
</head>
<body>
	<!-- <textarea id="messagesTextArea" readonly="readonly" rows="10" cols="45"></textarea>
	<textarea id="usersTextArea" readonly="readonly" rows="10" cols="10"></textarea>
	<br />
	<br />
	<input id="textMessage" type="text" size="50" />
	<select id="locationSelect">
		<option value="US">US</option>
		<option value="Canada">Canada</option>
		<option value="Other">Other</option>
	</select> -->
	<form name="usersForm">
		<input type="hidden" id="roomId" name="roomId"/>
		<input type="hidden" id="username" name="username"/>
	<br/>
	<div id="content">Web MESSENGER!!</div>
	본인 외의 대화상태를 선택하면 대화창이 뜹니다. <br/>
	<!-- List -->
	<table id="users" name="users" cellspacing='0'><!-- cellspacing='0' is important, must stay -->
    	<tr><th>Web Messenger Users</th></tr><!-- Table Header -->
    	<tr><td>There is no one to chat</td></tr>
    </table>
	</form>
</body>
</html>

 

설명 :  전체가 아닌 간단설명

생긴것만보면

var url = 'ws://127.0.0.1/usersServerEndpoint'; 부분에서 @ServerEndpoint 가 설정된 웹소켓 서버를

webSocket = connection(url); -> new WebSocket(url); 로 연결하고

아래의 UserServerEndPoint에서 @OnOpen 어노테이션에서 userSession을 추가

 

연결성공해서 웹소켓이 오픈되면webSocket.onopen= 부분을 호출 webSocket.send로 메시지를 보낸다.첫번째 커넥션이니까  if("firstConnection" 을타서session.getBasicRemote().sendText()  로 유저정보를 json데이터로 클라이언트에 전송한다.

 

이후에 webSocket.onmessage 에서 사용자가 들어가고 나가는것을 채팅방형식으로 보여주는 형태다

 

 

 

package egovframework.example.sample.web;

import java.io.IOException;
import java.io.StringReader;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

import javax.json.Json;
import javax.json.JsonArrayBuilder;
import javax.json.JsonObject;
import javax.websocket.EncodeException;
import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @Class Name : UsersServerEndPoint.java
* @Description : 현재 가능한 대화사용자 리스트를 처리하는 WebSocket 서버클래스

*/
@ServerEndpoint(value = "/usersServerEndpoint"/* ,configurator=ServerAppConfig.class*/)
public class UsersServerEndPoint {
	private static final Logger LOGGER = LoggerFactory.getLogger(UsersServerEndPoint.class);
	private static Set<Session> connectedAllUsers = Collections.synchronizedSet(new HashSet<Session>());

	//Spring bean과 연동하기 위해서는 ServerAppConfig를 configurator로 등록해주면 된다.
	/*
	 * @Resource(name="TestService") TestService testService;
	 */

	/**
	 * Handshaking 함수
	 * @param userSession 사용자 session
	 */
	@OnOpen
	public void handleOpen(Session userSession) {
		connectedAllUsers.add(userSession);
	}

	/**
	 * Message전달 함수
	 * @param message 메시지
	 * @param userSession 사용자 session
	 * @throws IOException
	 * @throws EncodeException
	 */
	@OnMessage
	public void handleMessage(String message, Session userSession) throws IOException, EncodeException {
		String username = (String) userSession.getUserProperties().get("username");
		System.out.println("Message userName : " + username);
		System.out.println("Message  : " + message);
		JsonObject jsonObject = Json.createReader(new StringReader(message)).readObject();

		String connectionType = jsonObject.getString("connectionType");

		if ("firstConnection".equals(connectionType) && username == null) {
			// 맨 처음 접속 시,
			// 사용자의 이름을 가져옴
			username = jsonObject.getString("username");

			LOGGER.info(username + " is entered.");

			if (username != null && !isExisted(username)) {
				userSession.getUserProperties().put("username", username);

				for (Session session : connectedAllUsers) {
					session.getBasicRemote().sendText(buildJsonUserData(getUsers()));
				}
			} else {
				// username을 다시 입력하게하는 로직 넣기.
			}

		} else if ("chatConnection".equals(connectionType)) {
			// chatroomId로 또다른 webSocket url에 접근한다.
			// id generation으로 대체가능.
			String chatroomId = genRandom();

			// 다른 사용자와 대화하고자 시도할 때
			// 채팅룸 사용자 저장
			Set<Session> chatroomMembers = new HashSet<Session>();
			chatroomMembers.add(userSession);

			// 선택한 사용자를 사용자들 안에서 찾기.
			String connectingUser = jsonObject.getString("connectingUser");

			if (connectingUser != null && !username.equals(connectingUser)) {
				// 사용자들 중 선택한 유저와 연결
				for (Session session : connectedAllUsers) {
					if (connectingUser.equals(session.getUserProperties().get("username"))) {
						// 선택한 사용자면 chatroomMember로 추가.
						chatroomMembers.add(session);
					}
				}

				// chatroomMembers에게 room입장하라는 신호 보내기
				for (Session session : chatroomMembers) {

					session.getBasicRemote().sendText(
							Json.createObjectBuilder().add("enterChatId", chatroomId).add("username", (String) session.getUserProperties().get("username")).build().toString());
				}
			}
		}
	}

	/**
	 * 연결을 끊기 직전에 호출되는 함수
	 * @param userSession
	 * @throws IOException
	 * @throws EncodeException
	 */
	// 예외처리 필요!
	@OnClose
	public void handleClose(Session userSession) throws IOException, EncodeException {

		String disconnectedUser = (String) userSession.getUserProperties().get("username");
		connectedAllUsers.remove(userSession);

		if (disconnectedUser != null) {
			Json.createObjectBuilder().add("disconnectedUser", disconnectedUser).build().toString();

			for (Session session : connectedAllUsers) {
				session.getBasicRemote().sendText(Json.createObjectBuilder().add("disconnectedUser", disconnectedUser).build().toString());
			}
		}
	}

	/**
	 * 연결되어있는 user정보를 가져오는 함수
	 * @return user set
	 */
	private Set<String> getUsers() {
		HashSet<String> returnSet = new HashSet<String>();

		for (Session session : connectedAllUsers) {
			if (session.getUserProperties().get("username") != null) {
				returnSet.add(session.getUserProperties().get("username").toString());
			};
		}
		return returnSet;
	}

	/**
	 * 유저 정보가 담긴 Set<String>을 json으로 변환해주는 함수
	 * @param set
	 * @return jsondata
	 */
	private String buildJsonUserData(Set<String> set) {

		JsonArrayBuilder jsonArrayBuilder = Json.createArrayBuilder();

		for (String user : set) {
			jsonArrayBuilder.add(user);
		}
		return Json.createObjectBuilder().add("allUsers", jsonArrayBuilder).build().toString();
	}

	/**
	 * 동일한 username을 가진 user session이 있는지 확인하는 함수
	 * @param username 사용자이름
	 * @return 존재여부
	 */
	private boolean isExisted(String username) {
		// 이미 username을 가진 session이 있는지 검사.
		for (Session existedUser : connectedAllUsers) {
			if (username.equals(existedUser.getUserProperties().get("username"))) {
				return true;
			}
		}
		return false;
	}

	/**
	 * chatroomId를 위한 랜덤값을 생성하는 함수
	 * @return chatroomId
	 */
	private String genRandom() {
		String chatroomId = "";
		for (int i = 0; i < 8; i++) {
			chatroomId += (char) ((Math.random() * 26) + 97);
		}
		return chatroomId;
	}

}

 

 

 

ChatServerEndPoint.java

package egovframework.example.sample.web;

import java.io.IOException;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

import javax.websocket.EncodeException;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import egovframework.example.sample.config.ChatServerAppConfig;
import egovframework.example.sample.model.ChatMessage;
import egovframework.example.sample.model.Message;
import egovframework.example.sample.model.MessageDecoder;
import egovframework.example.sample.model.MessageEncoder;
import egovframework.example.sample.model.UsersMessage;

/**
* @Class Name : ChatServerEndPoint.java
* @Description : 두 명의 사용자가 대화할 때 접속 처리및 메시지처리 기능을 하는 WebSocket 서버클래스
* @Modification Information
*
*/
@ServerEndpoint(value = "/chat/{room}", encoders={MessageEncoder.class}, decoders={MessageDecoder.class}, configurator=ChatServerAppConfig.class)
public class ChatServerEndPoint {
	private static final Logger LOGGER = LoggerFactory.getLogger(ChatServerEndPoint.class);
	private Set<Session> chatroomUsers = Collections.synchronizedSet(new HashSet<Session>());

	/**
	 * Handshaking 함수
	 * @param userSession 사용자 session
	 */
	@OnOpen
	public void handleOpen(Session userSession, @PathParam("room") final String room) throws IOException, EncodeException {
		userSession.getUserProperties().put("room", room);
		chatroomUsers.add(userSession);
	}

	/**
	 * 메시지 전달 함수
	 * @param incomingMessage 들어오는 메시지
	 * @param userSession 사용자 session
	 * @param room room Id
	 * @throws IOException
	 * @throws EncodeException
	 */
	@OnMessage
	public void handleMessage(Message incomingMessage, Session userSession, @PathParam("room") final String room) throws IOException, EncodeException {

		ChatMessage incomingChatMessage = (ChatMessage)incomingMessage;
		ChatMessage outgoingChatMessage = new ChatMessage();

		String username = (String) userSession.getUserProperties().get("username");
		if (username == null) {

			username = incomingChatMessage.getMessage();
			if (username != null) {
				userSession.getUserProperties().put("username", username);
			}

			synchronized (chatroomUsers) {
				for (Session session : chatroomUsers){
					session.getBasicRemote().sendObject(new UsersMessage(getUsers()));
				}
			}
		} else {
			outgoingChatMessage.setName(username);
			outgoingChatMessage.setMessage(incomingChatMessage.getMessage());

			for (Session session : chatroomUsers){
				session.getBasicRemote().sendObject(outgoingChatMessage);
			}
		}
	}


	//누군가가 접속 끊을때
	@OnClose
	public void handleClose(Session userSession, @PathParam("room") final String room) throws IOException, EncodeException{
		chatroomUsers.remove(userSession);

		for (Session session : chatroomUsers){
			session.getBasicRemote().sendObject(new UsersMessage(getUsers()));
		}
	}

	/**
	 * 사용자가 접속 끊기 전 호출되는 함수
	 * @param session
	 * @param throwable
	 * @param room
	 */
	@OnError
    public void handleError(Session session, Throwable throwable, @PathParam("room") final String room) {
        // Error handling
		LOGGER.info("ChatServerEndPoint (room: "+room+") occured Exception!");
		LOGGER.info("Exception : "+throwable.getMessage());
    }

	/**
	 * 사용자 정보를 가져오는 함수
	 * @return
	 */
	private Set<String> getUsers() {
		HashSet<String> returnSet = new HashSet<String>();

		for (Session session : chatroomUsers){
			if (session.getUserProperties().get("username") != null) {
				returnSet.add(session.getUserProperties().get("username").toString());
			}
		}
		return returnSet;
	}

}

 

**ChatServerEndPoint.java 같은 경우에는

@ServerEndPoint 가 아래와 같이 특이한부분이 설정되어있는데 이는 

@ServerEndpoint(value = "/chat/{room}", encoders={MessageEncoder.class}, decoders={MessageDecoder.class}, configurator=ChatServerAppConfig.class)

configurator

@Component 어노테이션이 달린 클래스는 스프링 빈에 등록되고 그 인스턴스는 싱글톤으로 스프링에 의해 관리되지만, @SeverEndPoint로 어노테이션이 달린 클래스는 WebSocket이 연결될 때마다 인스턴스가 생성되고 JWA구현에 의해 관리가 되어 내부의 @Autowried가 설정된 멤버가 정상적으로 초기화 되지 않는다.
@Autowried를 사용하기 위해서 SeverEndpointConfig.Configurator를 사용하여 SeverEndPoint의 컨텍스트에 BeanFactory 또는 ApplicationContext를 연결해 주는 작업을 하는 클래스를 생성한다.

 

package egovframework.example.sample.config;

import java.util.HashMap;
import java.util.Map;

import javax.websocket.HandshakeResponse;
import javax.websocket.server.HandshakeRequest;
import javax.websocket.server.ServerEndpointConfig;
import javax.websocket.server.ServerEndpointConfig.Configurator;

import egovframework.example.sample.web.ChatServerEndPoint;

/**
* @Class Name : ChatServerAppConfig.java
* @Description : 사용자리스트에서 다른사용자 선택 시, 사용자와 대화가능한 방(새로운 EndPoint 객체)을 만드는 Configurator
* @Modification Information

*/
public class ChatServerAppConfig extends Configurator{

	//대화창 서버객체(ChatServerEndPoint) 저장하는 Map
	private final static Map<String, ChatServerEndPoint> endpointMap = new HashMap<String, ChatServerEndPoint>();
	private String currentUri;

	@SuppressWarnings("unchecked")
	@Override
     public <T> T getEndpointInstance(Class<T> endpointClass) throws InstantiationException {

		 ChatServerEndPoint endpoint = endpointMap.get(currentUri);

		 if(endpoint == null) {
			 endpoint = new ChatServerEndPoint();
			 endpointMap.put(currentUri, endpoint);
		 }

		 return (T)endpoint;
     }

	@Override
	public void modifyHandshake(ServerEndpointConfig sec,
			HandshakeRequest request, HandshakeResponse response) {
		currentUri = request.getRequestURI().toString();
		super.modifyHandshake(sec, request, response);
	}
}

 

decoders는 들어오는 문자열을 해석하는 클래스

package egovframework.example.sample.model;

import java.io.StringReader;

import javax.json.Json;
import javax.json.JsonObject;
import javax.websocket.DecodeException;
import javax.websocket.Decoder;
import javax.websocket.EndpointConfig;


/**
* @Class Name : MessageDecoder.java
* @Description : 클라이언트에서 서버로 전달되는 메시지를 decoding하는 클래스
* @Modification Information

*/
public class MessageDecoder implements Decoder.Text<Message> {

	@Override
	public void destroy() {}

	@Override
	public void init(EndpointConfig arg0) {}

	/**
	 * 화면에서 넘어오는 데이터를 decoding하는 함수
	 */
	@Override
	public Message decode(String message) throws DecodeException {
		ChatMessage chatMessage = new ChatMessage();

		JsonObject jsonObject = Json
				.createReader(new StringReader(message)).readObject();
		chatMessage.setMessage(jsonObject.getString("message"));
		chatMessage.setRoom(jsonObject.getString("room"));
		return chatMessage;
	}

	@Override
	public boolean willDecode(String message) {
		boolean flag = true;

		try {
			Json.createReader(new StringReader(message)).readObject();
		} catch (Exception ex) {
			flag = false;
		}
		return flag;
	}

}


encoders는 나갈 객체를 문자열화 해준다.

 

package egovframework.example.sample.model;

import java.util.Set;

import javax.json.Json;
import javax.json.JsonArrayBuilder;
import javax.websocket.EncodeException;
import javax.websocket.Encoder;
import javax.websocket.EndpointConfig;


/**
* @Class Name : MessageEncoder.java
* @Description : 서버에서 클라이언트로 전달되는 메시지를 encoding하는 클래스
* @Modification Information
*

*/
public class MessageEncoder implements Encoder.Text<Message>{

	@Override
	public void destroy() {
	}

	@Override
	public void init(EndpointConfig arg0) {
	}

	/**
	 * 서버에서 클라이언트로 전달되는 메시지를 encoding하는 함수
	 */
	@Override
	public String encode(Message message) throws EncodeException {
		String result = null;
		if (message instanceof ChatMessage) {
			 ChatMessage chatMessage = (ChatMessage) message;
			 result = Json.createObjectBuilder().add("messageType", chatMessage.getClass().getSimpleName())
					 .add("name", chatMessage.getName())
					 .add("message", chatMessage.getMessage())
					 .build().toString();
		} else if (message instanceof UsersMessage) {
			UsersMessage userMessage = (UsersMessage) message;
			result = buildJsonUserData(userMessage.getUsers(), userMessage.getClass().getSimpleName());
		}
		return result;
	}

	private String buildJsonUserData(Set<String> set, String messageType) {
		JsonArrayBuilder jsonArrayBuilder = Json.createArrayBuilder();

		for (String user: set) {
			jsonArrayBuilder.add(user);
		}
		return Json.createObjectBuilder().add("messageType", messageType)
										 .add("users", jsonArrayBuilder)
										 .build().toString();
	}

}

 

클라이언트에서 서버로 전송할때는

handleMessage(Message incomingMessage

incomingChatMessage = (ChatMessage)incomingMessage; 이부분에 메시지를 담을수 있게 decode되는것 같다.

인코딩도 반대로 이해하면된다

session.getBasicRemote().sendObject(outgoingChatMessage); 부분

 

 

큰틀만 위와같이 이해하면 나머지는 비슷하니 실제로 적용된 것을 확인해본다.(클릭하면 채팅방 만들고 메시지 주고받는 형식)

 

 

 

 

 

 

 

 

클릭하면 팝업창 두개 오픈

 

반응형