YJ의 새벽

Spring ( Web Socket ( 메시지 ) ) 본문

Spring/Spring

Spring ( Web Socket ( 메시지 ) )

YJDawn 2023. 5. 15. 17:46

 

 

 

  • WebSocket

    - 브라우저와 웹 서버간의 전이중 통신을 지원하는 프로토콜

     전이중 통신(Full Duplex) : 두대의 단말기가 데이터를 동시에 송/수신 하기 위해
      각각 독립된 회선을 사용하는 통신 방식(ex. 전화 )

    - HTML5 부터 지원
    - Java 7 부터 지원 (8 버전 이상 사용 권장)
    - Spring Framework 4 이상 부터 지원

 

WebSocketHandler 인터페이스 : 웹소켓을 위한 메소드를 지원하는 인터페이스        

    -> WebSocketHandler 인터페이스를 상속받은 클래스를 이용해 웹소켓 기능을 구현         

 

       ****   WebSocketHandler 주요 메소드   ***          

void handlerMessage(WebSocketSession session, WebSocketMessage message)   

    - 클라이언트로부터 메세지가 도착하면 실행                 

void afterConnectionEstablished(WebSocketSession session)        

    - 클라이언트와 연결이 완료되고, 통신할 준비가 되면 실행         

void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus)         

     - 클라이언트와 연결이 종료되면 실행        

void handleTransportError(WebSocketSession session, Throwable exception)        

       - 메세지 전송중 에러가 발생하면 실행           

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

 TextWebSocketHandler :  WebSocketHandler 인터페이스를 상속받아 구현한 텍스트 메세지 전용 웹소켓 핸들러 클래스     handlerTextMessage(WebSocketSession session, TextMessage message)   

    - 클라이언트로부터 텍스트 메세지를 받았을때 실행

 

 

 

--- Websocket 요청 시 핸들러 클래스와 연결하기 .  ( servlet-context.xml 에서 bean 등록 )

<!-- Websocket 요청 시 핸들러 클래스와 연결하기.  -->
<beans:bean id="chatHandler" class="edu.kh.comm.chat.model.websocket.ChatWebsocketHandler"></beans:bean>

<websocket:handlers>

    <!-- 웹소켓 요청(주소)을 처리할 bean 지정 -->
    <websocket:mapping handler="chatHandler"
        path="/chat" />

    <websocket:handshake-interceptors>
        <!-- interceptor: http통신에서 req, resp 가로채는 역할 handshake-interceptors: 요청 
            관련 데이터 중 HttpSession(로그인 정보, 채팅방번호) 을 가로채서 WebSocketSession에 넣어주는 역할 -->
        <beans:bean
            class="org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor" />
    </websocket:handshake-interceptors>

    <!-- SockJS 라이브러리를 이용해서 만들어진 웹소켓 객체임을 인식 -->
    <websocket:sockjs />
</websocket:handlers>

 

 

 

 

---pom.xml   에 추가.

 

<!-- Spring WebSocket -->
      <!-- https://mvnrepository.com/artifact/org.springframework/spring-websocket -->
      <dependency>
         <groupId>org.springframework</groupId>
         <artifactId>spring-websocket</artifactId>
         <version>${org.springframework-version}</version>
      </dependency>


      <!-- jackson bind -->
      <!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind -->
      <dependency>
         <groupId>com.fasterxml.jackson.core</groupId>
         <artifactId>jackson-databind</artifactId>
         <version>2.9.8</version>
      </dependency>

 

 

 

 

 

 

 

 

 

 

 

---- jsp 를 참고하자 . 

 

---- js 를 참고하자. 

 

 

 

 

 

 

 

 

----- ChatWebSocketHandler . class 

 

public class ChatWebsocketHandler extends TextWebSocketHandler {
	
	@Autowired
	private ChatService service;
	
	// Set<WebSocketSession> 을 왜 만들었는가 ???
	//  -- WebSocketSession == 웹소켓에 연결된 클라이언트의 세션을 가지고있다. 
	//  --> 세션을 통해서 누가 연결했는지 알수 있다  !!!
	
	//  WebSocketSession 을 모아둔다 ???
	//  -- 현재 웹소켓에 연결되어있는 모든 클라이언트를 알 수 있다.
	//  --> Set 을 분석해서 원하는 클라이언트를 찾아서 메시지(채팅) 을 전달 할 수 있다 !!!
	
	private Set<WebSocketSession> sessions 
		= Collections.synchronizedSet(new HashSet<WebSocketSession>());
	
	// synchronizedSet  :  동기화된 Set을 반환
	//  --> 멀티쓰레드 환경에서 하나의 컬렉션 요소에 여러 스레드가 접근하면 충돌이 발생할수있으므로
	//     동기화(충돌이 안나도록 줄세움 )를 진행
	
	// 클라이언트와 연결이 완료되고, 통신할 준비가 되면 수행 ( == new SockJS()  )
	@Override
	public void afterConnectionEstablished(WebSocketSession session) throws Exception {
		
		// WebSocketSession  :  웹소켓에 접속/요청한 클라이언트의 세션.
		System.out.println(session.getId()+" 연결됨 ");    // 세션 아이디 확인
		
		sessions.add(session);  // WebSocketSession 을 Set에 추가. 
	}

	
	// 클라이언트로부터 텍스트 메시지를 전달 받았을때 수행 .
	@Override
	protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {

		// TextMessage : 웹소켓을 이용해 텍스트로 전달된 메시지가 담겨있는 객체 .
		
		// payload : 전송되는 데이터 
		// message.getPayload()  :  JSON 
		System.out.println("전달된 메시지 : "+ message.getPayload());
		
		// Jackson 라이브러리 : Java에서 JSON 을 다루기위한 라이브러리
		
		// Jackson-databind 라이브러리 : ObjectMapper 객체를 이용해서
		//                              JSON 데이터를 특정 VO 필드에 맞게 자동 매핑
		
		ObjectMapper objectMapper = new ObjectMapper();
		
		ChatMessage chatMessage = objectMapper.readValue(message.getPayload(), ChatMessage.class);
	
		// 시간 세팅
		chatMessage.setCreateDate( new Date( System.currentTimeMillis()));
		System.out.println(chatMessage);
		
		// 채팅 메시지 DB 삽입
		int result = service.insertMessage(chatMessage);
		
		if ( result > 0 ) {
			// 같은 방에 접속중인 클라이언트에게만 메시지 보내기
			//  --> Set<WebSocketSession> 에서 같은 방 클라이언트만 골라내기
			for ( WebSocketSession s : sessions ) {
				
				// WebSocketSession == HttpSession (로그인정보,채팅방정보) 을 가로챈것..
				int chatRoomNo = (Integer) s.getAttributes().get("chatRoomNo");
				
				// WebSocketSession에 담겨있는 채팅장 번호와
				// 메시지에 담겨있는 채팅방 번호가 같은 경우  === 같은방 클라이언트
				if ( chatRoomNo == chatMessage.getChatRoomNo() ) {
					//같은방 클라이언트에게 JSON 형식의 메시지를 보냄 
					s.sendMessage( new TextMessage( new Gson().toJson(chatMessage) ));
				}
			}
		}
	}
	
	// 클라이언트와 연결이 종료되면 수행 .
	@Override
	public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {

		sessions.remove(session); // 웹소켓연결 종료시 종료된 WebSocketSession을 Set에서 제거
		
	}
	
}

 

 

---- ChattingController. class

@Controller
@SessionAttributes({"loginMember", "chatRoomNo"})
public class ChattingController {
	@Autowired
	private ChatService service;

	// 채팅방 목록 조회
	@GetMapping("/chat/roomList")
	public String chattingList(Model model) {
		
		List<ChatRoom> chatRoomList = service.selectChatRoomList();
		
		model.addAttribute("chatRoomList", chatRoomList);
		
		return "chat/chatRoomList";
	}
	
	// 채팅방 만들기
	@PostMapping("/chat/openChatRoom")
	public String openChatRoom(@ModelAttribute("loginMember") Member loginMember, Model model, 
								ChatRoom room, RedirectAttributes ra) {
		
		room.setMemberNo(loginMember.getMemberNo());
		
		int chatRoomNo = service.openChatRoom(room);
		
		String path = "redirect:/chat/";
		
		if(chatRoomNo > 0) {
			path += "room/" + chatRoomNo;
		}else {
			path += "roomList";
			ra.addFlashAttribute("message","채팅방 만들기 실패");
		}
		return path;
	}
	
	// 채팅방 입장
	@GetMapping("/chat/room/{chatRoomNo}")
	public String joinChatRoom(@ModelAttribute("loginMember") Member loginMember, Model model,
								@PathVariable("chatRoomNo") int chatRoomNo, 
								ChatRoomJoin join,
								RedirectAttributes ra) {
		join.setMemberNo(loginMember.getMemberNo());
		List<ChatMessage> list = service.joinChatRoom(join);
		
		if(list != null) {
			model.addAttribute("list", list);
			model.addAttribute("chatRoomNo", chatRoomNo); // session에 올림
			return "chat/chatRoom";
		}else {
			ra.addFlashAttribute("message","채팅방이 존재하지 않습니다.");
			return "redirect:../roomList";
		}
	}
	
	// 채팅방 나가기
	@GetMapping("/chat/exit")
	@ResponseBody
	public int exitChatRoom(@ModelAttribute("loginMember") Member loginMember,
							ChatRoomJoin join) {
		join.setMemberNo(loginMember.getMemberNo());
		
		return service.exitChatRoom(join);
	}
}

 

 

 

---- ChatService . class

 

@Service
public class ChatServiceImpl implements ChatService{

	@Autowired
	private ChatDAO dao;

	// 채팅 목록 조회
	@Override
	public List<ChatRoom> selectChatRoomList() {
		return dao.selectChatRoomList();
	}

	// 채팅방 만들기
	@Override
	public int openChatRoom(ChatRoom room) {
		return dao.openChatRoom(room);
	}

	// 채팅방 입장 + 내용 얻어오기
	@Override
	public List<ChatMessage> joinChatRoom(ChatRoomJoin join) {

		// 현재 회원이 해당 채팅방에 참여하고 있는지 확인
		int result = dao.joinCheck(join);
		
		if(result == 0) { // 참여하고 있지 않은 경우 참여
			dao.joinChatRoom(join);
		}
		// 채팅 메세지 목록 조회
		return dao.selectChatMessage(join.getChatRoomNo());
	}
	
	// 채팅 메세지 삽입
	@Override
	public int insertMessage(ChatMessage cm) {
		
//		cm.setMessage(Util.XSSHandling(cm.getMessage()));
		cm.setMessage(Util.newLineHandling(cm.getMessage()));
		
		return dao.insertMessage(cm);
	}

	// 채팅방 나가기
	@Transactional(rollbackFor = Exception.class)
	@Override
	public int exitChatRoom(ChatRoomJoin join) {
		
		// 채팅방 나가기
		int result = dao.exitChatRoom(join);
		
		if(result > 0) { // 채팅방 나가기 성공 시
			
			// 현재 방에 몇명이 있나 확인
			int cnt = dao.countChatRoomMember(join.getChatRoomNo());
			
			// 0명일 경우 방 닫기
			if(cnt == 0) {
				result = dao.closeChatRoom(join.getChatRoomNo());
			}
		}
		return result;
	}
}

 

 

 

---- ChatDAO . class

 

@Repository
public class ChatDAO {

	@Autowired
	private SqlSessionTemplate sqlSession;

	/** 채팅방 목록 조회
	 * @return  chatRoomList
	 */
	public List<ChatRoom> selectChatRoomList() {
		return sqlSession.selectList("chattingMapper.selectChatRoomList");
	}

	/** 채팅방 만들기
	 * @param room
	 * @return chatRoomNo
	 */
	public int openChatRoom(ChatRoom room) {
		
		int result = sqlSession.insert("chattingMapper.openChatRoom", room);
		
		if(result > 0) return room.getChatRoomNo();
		return 0; 
	}

	
	/** 채팅방 참여 여부 확인
	 * @param join
	 * @return result
	 */
	public int joinCheck(ChatRoomJoin join) {
		return sqlSession.selectOne("chattingMapper.joinCheck", join);
	}
	
	
	/** 채팅방 참여하기
	 * @param join
	 */
	public void joinChatRoom(ChatRoomJoin join) {
		sqlSession.insert("chattingMapper.joinChatRoom", join);
	}

	
	/** 채팅 메세지 목록 조회
	 * @param chatRoomNo
	 * @return list
	 */
	public List<ChatMessage> selectChatMessage(int chatRoomNo) {
		return sqlSession.selectList("chattingMapper.selectChatMessage", chatRoomNo);
	}

	/** 채팅 메세지 삽입
	 * @param cm
	 * @return result
	 */
	public int insertMessage(ChatMessage cm) {
		return sqlSession.insert("chattingMapper.insertMessage", cm);
	}

	/** 채팅방 나가기
	 * @param join
	 * @return result
	 */
	public int exitChatRoom(ChatRoomJoin join) {
		return sqlSession.delete("chattingMapper.exitChatRoom", join);
	}

	/** 채팅방 인원 수 확인
	 * @param chatRoomNo
	 * @return cnt
	 */ 
	public int countChatRoomMember(int chatRoomNo) {
		return sqlSession.selectOne("chattingMapper.countChatRoomMember", chatRoomNo);
	}

	/** 채팅방 닫기
	 * @param chatRoomNo
	 * @return result
	 */
	public int closeChatRoom(int chatRoomNo) {
		return sqlSession.update("chattingMapper.closeChatRoom", chatRoomNo);
	}
}

 

 

--- chatting-mapper .xml

 

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="chattingMapper">

	<resultMap type="chatRoom" id="chatroom_rm">
		<id property="chatRoomNo" column="CHAT_ROOM_NO" />

		<result property="title" column="TITLE" />
		<result property="status" column="STATUS" />
		<result property="memberNo" column="MEMBER_NO" />
		<result property="memberNickname" column="MEMBER_NICK" />
		<result property="cnt" column="CNT" />
	</resultMap>

	<resultMap type="chatMessage" id="chatMessage_rm">
		<id property="cmNo" column="CM_NO" />

		<result property="message" column="MESSAGE" />
		<result property="createDate" column="CREATE_DT" />
		<result property="chatRoomNo" column="CHAT_ROOM_NO" />

		<result property="memberNo" column="MEMBER_NO" />
		<result property="memberNickname" column="MEMBER_NICK" />
	</resultMap>

	<!--=========================================================================================-->

	<!-- 채팅방 목록 조회 -->
	<select id="selectChatRoomList" resultMap="chatroom_rm">
		SELECT CHAT_ROOM_NO, TITLE, MEMBER_NICK,
		(SELECT COUNT(*) FROM CHAT_ROOM_JOIN B WHERE A.CHAT_ROOM_NO = B.CHAT_ROOM_NO) CNT
		FROM CHAT_ROOM A
		JOIN MEMBER_S USING(MEMBER_NO)
		WHERE STATUS = 'Y'
		ORDER BY CHAT_ROOM_NO DESC
	</select>

	
	<!-- 채팅방 만들기 -->
	<insert id="openChatRoom" useGeneratedKeys="true">
	
		<selectKey keyProperty="chatRoomNo" resultType="_int" order="BEFORE">
			SELECT SEQ_CR_NO.NEXTVAL FROM DUAL
		</selectKey>
		
		
		INSERT INTO CHAT_ROOM VALUES
		(#{chatRoomNo}, #{title}, DEFAULT, #{memberNo})
	</insert>


	<!-- 채팅방 참여 여부 확인 -->
	<select id="joinCheck" resultType="_int">
		SELECT COUNT(*) FROM CHAT_ROOM_JOIN
		WHERE CHAT_ROOM_NO = #{chatRoomNo}
		AND MEMBER_NO = #{memberNo}
	</select>

	<!-- 채팅방 참여하기 -->
	<insert id="joinChatRoom">
		INSERT INTO CHAT_ROOM_JOIN
		VALUES(#{memberNo}, #{chatRoomNo})
	</insert>

	<!-- 채팅 메세지 목록 조회 -->
	<select id="selectChatMessage" resultMap="chatMessage_rm">
		SELECT MESSAGE, CREATE_DT, MEMBER_NO, MEMBER_NICK
		FROM CHAT_MESSAGE
		JOIN MEMBER_S USING(MEMBER_NO)
		WHERE CHAT_ROOM_NO = #{chatRoomNo}
		ORDER BY CM_NO
	</select>

	<!-- 채팅 메세지 삽입 -->
	<insert id="insertMessage">
		INSERT INTO CHAT_MESSAGE
		VALUES(SEQ_CM_NO.NEXTVAL, #{message}, DEFAULT, #{chatRoomNo}, #{memberNo})
	</insert>
	
	<!-- 채팅방 나가기 -->
	<delete id="exitChatRoom">
		DELETE FROM CHAT_ROOM_JOIN
		WHERE MEMBER_NO = #{memberNo}
		AND CHAT_ROOM_NO = #{chatRoomNo}
	</delete>

	<!-- 채팅방 인원 수 확인 -->
	<select id="countChatRoomMember" resultType="_int">
		SELECT COUNT(*) FROM CHAT_ROOM_JOIN
		WHERE CHAT_ROOM_NO = #{chatRoomNo}
	</select>

	<!-- 채팅방 닫기 -->
	<update id="closeChatRoom">
		UPDATE CHAT_ROOM SET
		STATUS = 'N'
		WHERE CHAT_ROOM_NO = #{chatRoomNo}
	</update>

</mapper>

 

 

 

 

 

 

 

 

 

 

 

 

 

 

-- 채팅

CREATE TABLE CHAT_ROOM (
	CHAT_ROOM_NO	NUMBER	PRIMARY KEY,
	TITLE	VARCHAR2(200)	NOT NULL,
	STATUS	CHAR(1)	DEFAULT 'Y' CHECK(STATUS IN('Y','N')),
	MEMBER_NO NUMBER REFERENCES MEMBER_S
);


COMMENT ON COLUMN CHAT_ROOM.CHAT_ROOM_NO IS '채팅방번호';
COMMENT ON COLUMN CHAT_ROOM.TITLE IS '채팅방제목';
COMMENT ON COLUMN CHAT_ROOM.STATUS IS '채팅방상태(정상:Y, 삭제:N)';
COMMENT ON COLUMN CHAT_ROOM.MEMBER_NO IS '회원번호(방 개설자)';


CREATE TABLE CHAT_MESSAGE (
	CM_NO	NUMBER	PRIMARY KEY,
	MESSAGE	VARCHAR2(4000)	NOT NULL,
	CREATE_DT	DATE DEFAULT SYSDATE,
	CHAT_ROOM_NO NUMBER	REFERENCES CHAT_ROOM,
	MEMBER_NO	NUMBER REFERENCES MEMBER_S
);

COMMENT ON COLUMN CHAT_MESSAGE.CM_NO IS '채팅메세지번호';
COMMENT ON COLUMN CHAT_MESSAGE.MESSAGE IS '작성한 채팅 메세지';
COMMENT ON COLUMN CHAT_MESSAGE.CREATE_DT IS '메세지 작성 시간';
COMMENT ON COLUMN CHAT_MESSAGE.CHAT_ROOM_NO IS '채팅방번호';
COMMENT ON COLUMN CHAT_MESSAGE.MEMBER_NO IS '회원번호';


CREATE TABLE CHAT_ROOM_JOIN (
	MEMBER_NO NUMBER REFERENCES MEMBER_S,
	CHAT_ROOM_NO NUMBER REFERENCES CHAT_ROOM,
    PRIMARY KEY(MEMBER_NO, CHAT_ROOM_NO)
);


COMMENT ON COLUMN CHAT_ROOM_JOIN.MEMBER_NO IS '회원번호';
COMMENT ON COLUMN CHAT_ROOM_JOIN.CHAT_ROOM_NO IS '채팅방번호';

CREATE SEQUENCE SEQ_CR_NO;
CREATE SEQUENCE SEQ_CM_NO;

 

 

---mybatis-config .xml 

 

		<typeAlias type="edu.kh.comm.chat.model.vo.ChatRoom" alias="chatRoom"/>
		<typeAlias type="edu.kh.comm.chat.model.vo.ChatRoomJoin" alias="chatRoomJoin"/>
		<typeAlias type="edu.kh.comm.chat.model.vo.ChatMessage" alias="chatMessage"/>
        
        		<mapper resource="/mappers/chatting-mapper.xml"/>

 

 

 

 

Comments