본문 바로가기
Spring Framework/project

Spring 실시간 알림(webSocket)

by bloodFinger 2020. 1. 26.

# 블로그 예제는 단일 서버에서 통신한다는 전제로 구현된 간단한 예제입니다.

조금 더 구체적인 예제와 설계를 참고하고 싶다면 '가상 면접 사례로 배우는 대규모 시스템 설계 기초' 의 12장 채팅시스템 설계 부분을 참고하시면 많은 도움이 될겁니다.



구현 목록

1)로그인 되어 있는 사람의 글에 누군가 로그인 하고있는 회원이  좋아요 , 팔로우 , 스크랩을 했을때 글의 주인에게 

실시간으로 알림이 가는것.

2)만약 로그인이 되어 있지 않다면 그 내용이 저장이 되어 다음에 로그인시 내용을 볼수있게 만들어라!

 

모든페이지에서 알림을 받기위해서 websocket 연결은 전역으로 구현했습니다. (index.jsp)

그리고 서버에서는 로그인 회원의 이메일별로 SocketSession을 관리한다.

 

 

환경설정

pom.xml

		<dependency>
		    <groupId>org.springframework</groupId>
		    <artifactId>spring-websocket</artifactId>
		    <version>5.0.2.RELEASE</version>
		</dependency>

 

handler 생성

servlet-context.xml

	<!-- websocket handler -->
	<bean id="echoHandler" class="mentor.socketHandler.EchoHandler" />

	<websocket:handlers>
		<websocket:mapping handler="echoHandler" path="/echo" />
		<websocket:handshake-interceptors>
	         <bean class="org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor"/>
	      </websocket:handshake-interceptors>

	      <websocket:sockjs/>
	 </websocket:handlers>

handshake-interceptors를 사용하는 이유는 httpSession에 접근하기 위해(로그인된 사람의 아이디나 이메일을 확인)

websocket session에 http session 을 올려줘야 한다.

 

handler

essayboardList.java

package mentor.socketHandler;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.lang.StringUtils;
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;

import member.bean.MemberDTO;
/**
 * 
 * @Title : 웹소켓 핸들러
 * @author : yangjaewoo
 * @date : 2019. 11. 19.
 */
public class EchoHandler extends TextWebSocketHandler {
	//로그인 한 전체
	List<WebSocketSession> sessions = new ArrayList<WebSocketSession>();
	// 1대1
	Map<String, WebSocketSession> userSessionsMap = new HashMap<String, WebSocketSession>();
		
	//서버에 접속이 성공 했을때
	@Override
	public void afterConnectionEstablished(WebSocketSession session) throws Exception {
		sessions.add(session);
		
		String senderEmail = getEmail(session);
		userSessionsMap.put(senderEmail , session);
	}
	
	//소켓에 메세지를 보냈을때
	@Override
	protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
//		String senderEmail = getEmail(session);
		//모든 유저에게 보낸다 - 브로드 캐스팅
//		for (WebSocketSession sess : sessions) {
//			sess.sendMessage(new TextMessage(senderNickname + ": " +  message.getPayload()));
//		}
		
		//protocol : cmd , 댓글작성자, 게시글 작성자 , seq (reply , user2 , user1 , 12)
		String msg = message.getPayload();
		if(StringUtils.isNotEmpty(msg)) {
			String[] strs = msg.split(",");
			
			if(strs != null && strs.length == 5) {
				String cmd = strs[0];
				String caller = strs[1]; 
				String receiver = strs[2];
				String receiverEmail = strs[3];
				String seq = strs[4];
				
				//작성자가 로그인 해서 있다면
				WebSocketSession boardWriterSession = userSessionsMap.get(receiverEmail);
				
				if("reply".equals(cmd) && boardWriterSession != null) {
					TextMessage tmpMsg = new TextMessage(caller + "님이 " + 
										"<a type='external' href='/mentor/menteeboard/menteeboardView?seq="+seq+"&pg=1'>" + seq + "</a> 번 게시글에 댓글을 남겼습니다.");
					boardWriterSession.sendMessage(tmpMsg);
				
				}else if("follow".equals(cmd) && boardWriterSession != null) {
					TextMessage tmpMsg = new TextMessage(caller + "님이 " + receiver +
							 "님을 팔로우를 시작했습니다.");
					boardWriterSession.sendMessage(tmpMsg);
					
				}else if("scrap".equals(cmd) && boardWriterSession != null) {
					TextMessage tmpMsg = new TextMessage(caller + "님이 " +
										//변수를 하나더 보낼수 없어서 receiver 변수에 member_seq를 넣어서 썼다.
										"<a type='external' href='/mentor/essayboard/essayboardView?pg=1&seq="+seq+"&mentors="+ receiver +"'>" + seq + "</a>번 에세이를 스크랩 했습니다.");
					boardWriterSession.sendMessage(tmpMsg);
				}
			}
			// 모임 신청 했을때
			if(strs != null && strs.length == 5) {
				String cmd = strs[0];
				String mentee_name = strs[1];
				String mentor_email = strs[2];
				String meetingboard_seq = strs[3];
				String participation_seq = strs[4];
				
				// 모임 작성한 멘토가 로그인 해있으면
				WebSocketSession mentorSession = userSessionsMap.get(mentor_email);
				if(cmd.equals("apply") && mentorSession != null) {
					TextMessage tmpMsg = new TextMessage(
							mentee_name + "님이 모임을 신청했습니다. " +"<a type='external' href='/mentor/participation/participationView?mseq="+ meetingboard_seq +"&pseq="+ participation_seq +"'>신청서 보기</a>");
					mentorSession.sendMessage(tmpMsg);
				}
			}
		}
	}
	
	//연결 해제될때
	@Override
	public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
		//System.out.println("afterConnectionClosed " + session + ", " + status);
		userSessionsMap.remove(session.getId());
		sessions.remove(session);
	}
	
	//웹소켓 email 가져오기
	private String getEmail(WebSocketSession session) {
		Map<String, Object> httpSession = session.getAttributes();
		MemberDTO loginUser = (MemberDTO)httpSession.get("memDTO");
		
		if(loginUser == null) {
			return session.getId();
		} else {
			return loginUser.getMember_email();
		}
	}
}

# TextWebSocketHandler를 상속받고  아래 3개의 메소드를 오버라이드 해야한다!!

  • afterConnectionEstablished(...) - 클라이언트가 접속에 성공했을때 호출
  • handleTextMessage(...) - socket에 메세지를 보냈을때 호출
  • afterConnectionClosed(...) - 연결이 종료되었을때 호출

 

<흐름>

일단 일대일로 알림을 주고받아야 하는 상황에서 각각의 로그인 되어 있는 회원의 정보를 Map에 저장을 해야 한다.

Map의 키값은 email을 value의 값에는 websocketSession을 저장하고

데이터를 보낼때 map에서 email별로 데이터를 꺼내와서 알림을 보낸다.

 

 

연결 및 전역변수 선언

index.js

   //전역변수 선언-모든 홈페이지에서 사용 할 수 있게 index에 저장
   var socket = null;

   $(document).ready(function (){
	   connectWs();
   });

   function connectWs(){
   	sock = new SockJS( "<c:url value="/echo"/>" );
   	//sock = new SockJS('/replyEcho');
   	socket = sock;

   	sock.onopen = function() {
           console.log('info: connection opened.');
     };

    sock.onmessage = function(evt) {
	 	var data = evt.data;
	   	console.log("ReceivMessage : " + data + "\n");

	   	$.ajax({
			url : '/mentor/member/countAlarm',
			type : 'POST',
			dataType: 'text',
			success : function(data) {
				if(data == '0'){
				}else{
					$('#alarmCountSpan').addClass('bell-badge-danger bell-badge')
					$('#alarmCountSpan').text(data);
				}
			},
			error : function(err){
				alert('err');
			}
	   	});

	   	// 모달 알림
	   	var toastTop = app.toast.create({
            text: "알림 : " + data + "\n",
            position: 'top',
            closeButton: true,
          });
          toastTop.open();
    };

    sock.onclose = function() {
      	console.log('connect close');
      	/* setTimeout(function(){conntectWs();} , 1000); */
    };

    sock.onerror = function (err) {console.log('Errors : ' , err);};

   }

sock.onopen = function()        : 이벤트 리스너(커넥션이 연결되었을때 서버 호출된다.)

sock.onmessage = function()  :  메세지를 보냈을때 호출

sock.onclose = function()        :  서버가 끊겼을때 호출

sock.onerror = function()        :  에러가 발생했을때 호출

 

 

21번째줄의 ajax는 알림창의 갯수를 비동비처리로 알림이 올때마다 숫자를 변경해주는 부분이다. 없어도 알림에는 상관 없습니다~

 

 

 

스크랩버튼을 클릭했을때 socket에 데이터를 보낸다.

	var AlarmData = {
			"myAlarm_receiverEmail" : receiverEmail,
			"myAlarm_callerNickname" : memNickname,
			"myAlarm_title" : "스크랩 알림",
			"myAlarm_content" :  memNickname + "님이 <a type='external' href='/mentor/essayboard/essayboardView?pg=1&seq="+essayboard_seq+"&mentors="+ memberSeq +"'>" + essayboard_seq + "</a>번 에세이를 스크랩 했습니다."
	};
	//스크랩 알림 DB저장
	$.ajax({
		type : 'post',
		url : '/mentor/member/saveAlarm',
		data : JSON.stringify(AlarmData),
		contentType: "application/json; charset=utf-8",
		dataType : 'text',
		success : function(data){
			if(socket){
				let socketMsg = "scrap," + memNickname +","+ memberSeq +","+ receiverEmail +","+ essayboard_seq;
				console.log("msgmsg : " + socketMsg);
				socket.send(socketMsg);
			}

		},
		error : function(err){
			console.log(err);
		}
	});

 

'Spring Framework > project' 카테고리의 다른 글

CSRF 는 무엇인가?  (0) 2020.02.12
Spring Security - LoginFailureHandler  (0) 2020.01.06
Spring Security - LoginSucessHandler  (0) 2020.01.06
Spring Security -인증 및 권한부여  (0) 2020.01.06
spring security 환경설정  (0) 2020.01.06