記錄

Spring) 웹소켓_2) 멀티채팅 본문

Web/Spring framework

Spring) 웹소켓_2) 멀티채팅

surhommejk 2018. 5. 13. 14:59


chat-ws.jsp(html도 무관)

<%@ page contentType="text/html; charset=UTF-8" trimDirectiveWhitespaces="true"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>채팅</title>
<script type="text/javascript" src="js/jquery-1.11.0.min.js"></script>
<script type="text/javascript">
    
    // 웹소켓으로 쓸 변수 선언
    var wsocket;
    
    // 입장 버튼 클릭시 작동 함수
    function connect() {
        
        // 웹소켓 생성
        // 생성자에 관해서는 이전 포스팅 참고
// 여기서는 이 페이지로 대화 내용을 보내는 것이므로 소켓 경로가 이 페이지(여기)이다
        wsocket = new WebSocket(
                "ws://localhost:8090/spring4-chap09-ws/chat-ws");
        
        // 이렇듯 소켓을 생성하는 단계에서
        // .onopen, onmessage, onclose에 해당하는 함수를 정의
        wsocket.onopen = onOpen;
        wsocket.onmessage = onMessage;
        wsocket.onclose = onClose;
    }
    
    // 나가기 버튼 클릭시 작동 함수
    function disconnect() {
        wsocket.close();
    }
    
    /*
    위 connect()에서 wsocket.onopen을 이 함수로 이미 정의해뒀다는 것을 숙지.
    아래의 onMessage(), onClose()도 마찬가지로 connect()에서 정의해놨기 때문에
    작동되는 것이다.
    
    즉, wsocket.onopen = onOpen; => WebSocket 생성시 발동
        wsocket.onmessage = onMessage;  => 메시지 받으면 발동
        wsocket.onclose = onClose;  => WebSocket.close()시 발동
    
    ※ ※ ※ ※ ※ ※ ※ ※ ※ ※ ※ ※ ※ ※ ※ ※ ※ ※ ※ ※ ※ ※ ※ ※
    작동 시점은 "WebSocket 인터페이스의 연결 상태"가 변화했을 때이고
    리스너가 항상 기다리고 있다는 것을 숙지하자!!
    ※ ※ ※ ※ ※ ※ ※ ※ ※ ※ ※ ※ ※ ※ ※ ※ ※ ※ ※ ※ ※ ※ ※ ※
    */
    
    // 소켓이 연결되면 자동으로 발동
    function onOpen(evt) {
        appendMessage("연결되었습니다.");
    }
    
    // "message" 이름의 MessageEvent 이벤트가 발생하면 처리할 핸들러
    // 이는 서버로부터 메세지가 도착했을 때 호출
    function onMessage(evt) {
        var data = evt.data;
        if (data.substring(0, 4) == "msg:") {
            appendMessage(data.substring(4));
        }
    }
    
    // WebSocket 인터페이스의 연결상태가 readyState 에서 CLOSED 로 바뀌었을 때 호출 이벤트 리스너.
    // 이 이벤트 리스너는 "close"라는 이름의 CloseEvent를 받는다.
    function onClose(evt) {
        appendMessage("연결을 끊었습니다.");
    }
    
    // 전송 버튼 클릭시 발동
    function send() {
        
        var nickname = $("#nickname").val();
        var msg = $("#message").val();
        wsocket.send("msg:"+nickname+":" + msg);
        $("#message").val("");
    }

    // onMessage()에 내장된 함수로 받은 메세지를 채팅 내역에 추가시키는 기능을 한다.
    function appendMessage(msg) {
        
        // 메세지 입력창에 msg를 하고 줄바꿈 처리
        $("#chatMessageArea").append(msg+"<br>");
        
        // 채팅창의 heigth를 할당
        var chatAreaHeight = $("#chatArea").height();
        
        // 쌓인 메세지의 height에서 채팅창의 height를 뺀다
        // 이를 이용해서 바로 밑에서 스크롤바의 상단여백을 설정한다
        var maxScroll = $("#chatMessageArea").height() - chatAreaHeight;
        
        /* .scrollTop(int) : Set the current vertical position of the scroll bar
                     for each of the set of matched elements.*/
// .scrollTop(int) : 파라미터로 들어간 px 만큼 top에 공백을 둔 채
//                   스크롤바를 위치시킨다
        $("#chatArea").scrollTop(maxScroll);
    }

    $(document).ready(function() {
        
        // 메세지 입력창에 keypress 이벤트가 발생했을때 발동 함수
        // 키 하나하나 입력 하면 그때마다 발동된다
        $('#message').keypress(function(event){
            
        // https://www.w3schools.com/jsref/event_key_keycode.asp 참고
// 입력 아스키코드 값을 가져오게 된다

///* In this example, we use a cross-browser solution,
because the keyCode property does not work on the onkeypress event in Firefox.
However, the which property does.

Explanation of the first line in the function below:
if the browser supports event.which, then use event.which,
otherwise use event.keyCode */
var keycode = (event.keyCode ? event.keyCode : event.which);
            
          // enter를 쳤을 때 keycode가 13이다
// https://blog.lael.be/post/75 <<-를 참고(다양한 키 값이 정리되어 있다)
       if(keycode == '13'){
                send(); 
            }
            
            // 만일의 경우를 대비하여 이벤트 발생 범위를 한정
// http://ismydream.tistory.com/98 참고
            event.stopPropagation();
        });
        $('#sendBtn').click(function() { send(); });
        $('#enterBtn').click(function() { connect(); });
        $('#exitBtn').click(function() { disconnect(); });
    });
</script>
<style>
#chatArea {
    width: 200px; height: 100px; overflow-y: auto; border: 1px solid black;
}
</style>
</head>
<body>
    이름:<input type="text" id="nickname">
    <input type="button" id="enterBtn" value="입장">
    <input type="button" id="exitBtn" value="나가기">
<h1>대화 영역</h1>
<div id="chatArea"><div id="chatMessageArea"></div></div>
<br/>
<input type="text" id="message">
<input type="button" id="sendBtn" value="전송">
</body>
</html>








ChatWebSocketHandler.java


public class ChatWebSocketHandler extends TextWebSocketHandler {

    // 접속한 유저들의 목록을 담기 위한 Map 선언
    // ConcurrentHashMap은 Hashtable과 유사하지만 멀티스래드 환경에서 더 안전하다
    /*
        ConcurrentHashMap에 대한 설명(반드시 읽고 숙지)

  https://docs.oracle.com/javase/7/docs/api/java/util/concurrent/ConcurrentHashMap.html
  http://blog.leekyoungil.com/?p=159
  http://limkydev.tistory.com/64
    */
    private Map<String, WebSocketSession> users = new ConcurrentHashMap<>();

    @Override
    public void afterConnectionEstablished(
            WebSocketSession session) throws Exception {

        // session에서 id를 가져와서 로그에 남긴다(없어도 되는 과정)
        log(session.getId() + " 연결 됨");

        // 위에서 선언한 users라는 map에 user를 담는 과정(필수)
        // map에 담는 이유는 메세지를 일괄적으로 뿌려주기 위해서이다
        users.put(session.getId(), session);
    }

    @Override
    public void afterConnectionClosed(
            WebSocketSession session, CloseStatus status) throws Exception {
        log(session.getId() + " 연결 종료됨");

        // map에서 세션에서 연결 종료된 유저를 없애는 이유는
        // 더 이상 메세지를 보낼 필요가 없기 때문에 목록에서 지우는 것이다
        users.remove(session.getId());
    }
    
    @Override
    protected void handleTextMessage(
            WebSocketSession session, TextMessage message) throws Exception {
        log(session.getId() + "로부터 메시지 수신: " + message.getPayload());

        // 클라이언트로부터 메세지를 받으면 동작하는 handleTextMessage 함수!
        // 수신한 하나의 메세지를 users 맵에 있는 모든 유저(세션)들에게
        // 맵을 반복으로 돌면서 일일이 보내주게 되도록 처리
        for (WebSocketSession s : users.values()) { //<-- .values() 로 session들만 가져옴
            
            // 여기서 모든 세션들에게 보내지게 된다
            // 1회전당 현재 회전에 잡힌 session에게 메세지 보낸다
            s.sendMessage(message);

            // 로그에 남기기 위한 것으로 큰 의미가 없음
            log(s.getId() + "에 메시지 발송: " + message.getPayload());
        }
    }

    @Override
    public void handleTransportError(
            WebSocketSession session, Throwable exception) throws Exception {
        log(session.getId() + " 익셉션 발생: " + exception.getMessage());
    }

    private void log(String logmsg) {
        System.out.println(new Date() + " : " + logmsg);
    }

}









ws-config.xml

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:websocket="http://www.springframework.org/schema/websocket"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd
http://www.springframework.org/schema/websocket
http://www.springframework.org/schema/websocket/spring-websocket.xsd">


  <websocket:handlers>
      <websocket:mapping handler="chatHandler" path="/chat-ws" />
  </websocket:handlers>

  <bean id="chatHandler" class="net.madvirus.spring4.chap09.ws.ChatWebSocketHandler" />

</beans>








web.xml

    <servlet>
        <servlet-name>dispatcher</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>

        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>/WEB-INF/ws-config.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>








<flow 정리>



1. 클라이언트에서 '입장' 버튼을 누르면 웹소켓이 생성된다


2. 웹소켓이 생성됨과 동시에 핸들러의 afterConnectionEstablished 메소드가 실행되면서

user 맵에 세션이 담긴다


3. 웹소켓 생성시 wsocket.onopen = onOpen로 할당했기 때문에

 클라이언트의 onOpen 메소드가 클라이언트 단에서 호출된다


4. 클라이언트가 메세지를 입력하고 '전송' 버튼을 누르거나 '엔터'를 친다


5. 핸들러의 handleTextMessage 메소드가 실행되면서 클라이언트가 보낸 메세지를 파라미터로 받는다. 동시에 users 맵에 있는 모든 세션들에 반복문 내에 있는 s.sendMessage(message); 로 인해 메세지가 보내지게 된다


6. 웹소켓 생성시  wsocket.onmessage = onMessage로 할당했기 때문에 메세지가 클라이언트로 전송되면 onMessage함수가 클라이언트 단에서 호출된다


7. 클라리언트가 '나가기' 버튼을 누른다


8. 클라이언트 단에서  wsocket.close()가 실행되면서 클라이언트-서버간의 웹소켓 연결이 끊어진다


9. 핸들러의 afterConnectionClosed가 실행되면서 나간 클라이언트의 정보가 users 맵에서 remove 된다


10. 웹소켓 생성시 wsocket.onclose = onClose로 할당했기 때문에 onClose 함수가 클라이언트 단에서 호출된다



Comments