[Java] WebSocket의 Session 사용 방법(Broadcast)과 웹 채팅 소스 예제


Development note/Java  2020. 2. 20. 21:45

안녕하세요. 명월입니다.


이 글은 Java에서 WebSocket의 Session 사용 방법(Broadcast)과 웹 채팅 소스 예제에 대한 글입니다.


이전에 제가 WebSocket 설정, 브라우저에서 접속, 메시지 전송 등에 대해 설명한 적이 있습니다.

링크 - [Java] Servlet에서 사용하는 웹 소켓 (WebSocket)


CS프로그램의 소켓 통신이면 서버 소켓에서 클라이언트가 접속을 하게 되면 ClientSocket 인스턴스를 받습니다.

그 ClientSocket 인스턴스로 연결을 유지(통신 동기화)하고 서버에서 클라이언트로 메시지를 보내게 됩니다.


WebSocket에서는 Session의 객체가 있습니다. 이 Session의 객체는 브라우저로 부터 Socket 접속을 하면 생성이 되고, 이 객체를 리스트등에 관리하면서 필요하면 꺼내서 메시지를 보낼 수도 있습니다.

이 Session은 웹 Session(서버에 클라이언트 별 정보를 저장하는 오브젝트)과는 의미가 다릅니다. 웹 세션은 브라우저가 Web Request를 할 때 마다, 쿠키의 세션 키로 구분하여 유저의 정보를 가져오는 것을 말합니다.

여기서 소켓 세션은 브라우저가 Websocket을 접속했을 때의 커넥션 정보가 있는 것입니다.CS프로그램이라고 하면 ClientSocket과 비슷한 객체입니다.

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;

// WebSocket 호스트 설정
@ServerEndpoint("/broadsocket")
public class BroadSocket {
  // 접속 된 클라이언트 WebSocket session 관리 리스트
  private static List<Session> sessionUsers = Collections.synchronizedList(new ArrayList<>());
  // 메시지에서 유저 명을 취득하기 위한 정규식 표현
  private static Pattern pattern = Pattern.compile("^\\{\\{.*?\\}\\}");
  // WebSocket으로 브라우저가 접속하면 요청되는 함수	
  @OnOpen
  public void handleOpen(Session userSession) {
    // 클라이언트가 접속하면 WebSocket세션을 리스트에 저장한다.
    sessionUsers.add(userSession);
    // 콘솔에 접속 로그를 출력한다.
    System.out.println("client is now connected...");
  }
  // WebSocket으로 메시지가 오면 요청되는 함수
  @OnMessage
  public void handleMessage(String message, Session userSession) throws IOException {
    // 메시지 내용을 콘솔에 출력한다.
    System.out.println(message);
    // 초기 유저 명
    String name = "anonymous";
    // 메시지로 유저 명을 추출한다.
    Matcher matcher = pattern.matcher(message);
    // 메시지 예: {{유저명}}메시지
    if (matcher.find()) {
      name = matcher.group();
    }
    // 클로져를 위해 변수의 상수화
    final String msg = message.replaceAll(pattern.pattern(), "");
    final String username = name.replaceFirst("^\\{\\{", "").replaceFirst("\\}\\}$", "");
    // session관리 리스트에서 Session을 취득한다.
    sessionUsers.forEach(session -> {
      // 리스트에 있는 세션과 메시지를 보낸 세션이 같으면 메시지 송신할 필요없다.
      if (session == userSession) {
        return;
      }
      try {
        // 리스트에 있는 모든 세션(메시지 보낸 유저 제외)에 메시지를 보낸다. (형식: 유저명 => 메시지)
        session.getBasicRemote().sendText(username + " => " + msg);
      } catch (IOException e) {
        // 에러가 발생하면 콘솔에 표시한다.
        e.printStackTrace();
      }
    });
  }
  // WebSocket과 브라우저가 접속이 끊기면 요청되는 함수
  @OnClose
  public void handleClose(Session userSession) {
    // session 리스트로 접속 끊은 세션을 제거한다.
    sessionUsers.remove(userSession);
    // 콘솔에 접속 끊김 로그를 출력한다.
    System.out.println("client is now disconnected...");
  }
}

예전에 설명한 WebSocket 리스너 구조와 같습니다. 그러나 이번에는 각 리스너에 Session 파라미터를 추가했습니다.

Session 은 WebSocket의 커넥션 정보가 들어있는 인스턴스입니다.


여기서 브라우저로 부터 메시지를 받으면 메시지 안에서 유저명을 추출하여 다른 유저들에게 메시지를 보내는 형식으로 되어 있습니다.

메시지의 형식은 「{{유저명}}메시지」의 형태로 되어있습니다.

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head><title>Web Socket Example</title></head>
<body>
  <form>
    <!-- 유저 명을 입력하는 텍스트 박스(기본 값은 anonymous(익명)) -->
    <input id="user" type="text" value="anonymous">
    <!-- 송신 메시지를 작성하는 텍스트 박스 -->
    <input id="textMessage" type="text">
    <!-- 메세지를 송신하는 버튼 -->
    <input onclick="sendMessage()" value="Send" type="button">
    <!-- WebSocket 접속 종료하는 버튼 -->
    <input onclick="disconnect()" value="Disconnect" type="button">
  </form>
  <br />
  <!-- 콘솔 메시지의 역할을 하는 로그 텍스트 에리어.(수신 메시지도 표시한다.) -->
  <textarea id="messageTextArea" rows="10" cols="50"></textarea>
  <script type="text/javascript">
    // 「WebSocketEx」는 프로젝트 명
    // 「broadsocket」는 호스트 명
    // WebSocket 오브젝트 생성 (자동으로 접속 시작한다. - onopen 함수 호출)
    var webSocket = new WebSocket("ws://localhost:8080/WebSocketEx/broadsocket");
    // 콘솔 텍스트 에리어 오브젝트
    var messageTextArea = document.getElementById("messageTextArea");
    // WebSocket 서버와 접속이 되면 호출되는 함수
    webSocket.onopen = function(message) {
      // 콘솔 텍스트에 메시지를 출력한다.
      messageTextArea.value += "Server connect...\n";
    };
    // WebSocket 서버와 접속이 끊기면 호출되는 함수
    webSocket.onclose = function(message) {
      // 콘솔 텍스트에 메시지를 출력한다.
      messageTextArea.value += "Server Disconnect...\n";
    };
    // WebSocket 서버와 통신 중에 에러가 발생하면 요청되는 함수
    webSocket.onerror = function(message) {
      // 콘솔 텍스트에 메시지를 출력한다.
      messageTextArea.value += "error...\n";
    };
    /// WebSocket 서버로 부터 메시지가 오면 호출되는 함수
    webSocket.onmessage = function(message) {
      // 콘솔 텍스트에 메시지를 출력한다.
      messageTextArea.value += message.data + "\n";
    };
    // Send 버튼을 누르면 호출되는 함수
    function sendMessage() {
      // 유저명 텍스트 박스 오브젝트를 취득
      var user = document.getElementById("user");
      // 송신 메시지를 작성하는 텍스트 박스 오브젝트를 취득
      var message = document.getElementById("textMessage");
      // 콘솔 텍스트에 메시지를 출력한다.
      messageTextArea.value += user.value + "(me) => " + message.value + "\n";
      // WebSocket 서버에 메시지를 전송(형식 「{{유저명}}메시지」)
      webSocket.send("{{" + user.value + "}}" + message.value);
      // 송신 메시지를 작성한 텍스트 박스를 초기화한다.
      message.value = "";
    }
    // Disconnect 버튼을 누르면 호출되는 함수
    function disconnect() {
      // WebSocket 접속 해제
      webSocket.close();
    }
  </script>
</body>
</html>

WebSocket서버에 메시지를 전송할 때의 메시지 형태는 「{{유저 명}}메시지」의 형태로 만들어서 보냅니다. 그것은 서버의 handleMessage 함수에서 정규 표현식으로 유저명과 메시지 내용을 분리하는 것입니다.


그럼 위 소스를 톰켓으로 기동하겠습니다.

기동한 후에 브라우저를 3개 실행하여 접속했습니다. 유저명은 각 「test1」,「test2」,「test3」으로 설정하여 메시지를 보냈습니다.

「test1」에서 보낸 메시지가 「test2」、「test3」의 브라우저에도 보입니다. 이렇게 간단하게 채팅 프로그램이 만들어 졌습니다.

서버 측의 콘솔 로그를 확인했습니다. 클라이언트가 3개 접속해서 각각 메시지를 보낸 것을 확인할 수 있습니다.


-----추가 2020년 6월 3일-----

질문 중에 Websocket이 끊기면 다시 재접속하는 방법에 대한 질문이 있어서 작성합니다.

재 접속을 하는 방법은 onclose이벤트에 다시 new WebSocket을 선언하면 가능합니다. 그런데 다시 선언할 때 주의할 점이 이밴트를 다시 설정해야 합니다.

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<title>Web Socket Example</title>
</head>
<body>
  <form>
    <!-- 유저 명을 입력하는 텍스트 박스(기본 값은 anonymous(익명)) -->
    <input id="user" type="text" value="anonymous">
    <!-- 송신 메시지를 작성하는 텍스트 박스 -->
    <input id="textMessage" type="text">
    <!-- 메세지를 송신하는 버튼 -->
    <input onclick="sendMessage()" value="Send" type="button">
    <!-- WebSocket 접속 종료하는 버튼 -->
    <input onclick="disconnect()" value="Disconnect" type="button">
  </form>
  <br />
  <!-- 콘솔 메시지의 역할을 하는 로그 텍스트 에리어.(수신 메시지도 표시한다.) -->
  <textarea id="messageTextArea" rows="10" cols="50"></textarea>
  <script type="text/javascript">
    // 콘솔 텍스트 에리어 오브젝트
    var messageTextArea = document.getElementById("messageTextArea");
    // 웹 소켓 접속 함수, url 뒤의 파라미터는 callback 함수를 받는다.
    function connectWebSocket(url, message, open, close, error) {
      // WebSocket 오브젝트 생성 (자동으로 접속 시작한다. - onopen 함수 호출)
      let webSocket = new WebSocket(url);
      // 함수 체크하는 함수
      function call(cb, msg) {
        // cb가 함수 타입인지 확인
        if (cb !== undefined && typeof cb === "function") {
          // 함수 호출
          cb.call(null, msg);
        }
      }
      // WebSocket 서버와 접속이 되면 호출되는 함수
      webSocket.onopen = function() {
        // callback 호출
        call(open);
      };
      // WebSocket 서버와 접속이 끊기면 호출되는 함수
      webSocket.onclose = function() {
        // callback 호출
        call(close);
      };
      // WebSocket 서버와 통신 중에 에러가 발생하면 요청되는 함수
      webSocket.onerror = function() {
        // callback 호출
        call(error);
      };
      // WebSocket 서버로 부터 메시지가 오면 호출되는 함수
      webSocket.onmessage = function(msg) {
        // callback 호출
        call(message, msg);
      };
      // 웹 소켓 리턴
      return webSocket;
    }
    // 연결 발생 때 사용할 callback 함수
    var open = function() {
      // 콘솔 텍스트에 메시지를 출력한다
      messageTextArea.value += "Server connect...\n";
    }
    // 종료 발생 때 사용할 callback 함수
    var close = function() {
      // 콘솔 텍스트에 메시지를 출력한다
      messageTextArea.value += "Server Disconnect...\n";
      // 재 접속을 시도한다.
      setTimeout(function() {
        // 재접속
        webSocket = connectWebSocket("ws://localhost:8080/WebSocket/broadsocket", message, open, close, error);
      });
    }
    // 에러 발생 때 사용할 callback 함수
    var error = function() {
      messageTextArea.value += "error...\n";
    }
    // 메세지를 받을 때 사용할 callback 함수
    var message = function(msg) {
      // 콘솔 텍스트에 메시지를 출력한다.  
      messageTextArea.value += msg.data + "\n";
    };
    // 웹 소켓 생성
    var webSocket = connectWebSocket("ws://localhost:8080/WebSocket/broadsocket", message, open, close, error);

    // Send 버튼을 누르면 호출되는 함수  
    function sendMessage() {
      // 유저명 텍스트 박스 오브젝트를 취득  
      var user = document.getElementById("user");
      // 송신 메시지를 작성하는 텍스트 박스 오브젝트를 취득  
      var message = document.getElementById("textMessage");
      // 콘솔 텍스트에 메시지를 출력한다.  
      messageTextArea.value += user.value + "(me) => " + message.value + "\n";
      // WebSocket 서버에 메시지를 전송(형식 「{{유저명}}메시지」)  
      webSocket.send("{{" + user.value + "}}" + message.value);
      // 송신 메시지를 작성한 텍스트 박스를 초기화한다.  
      message.value = "";
    }
    // Disconnect 버튼을 누르면 호출되는 함수  
    function disconnect() {
      // WebSocket 접속 해제  
      webSocket.close();
    }
  </script>
</body>
</html>

접속 종료 버튼을 눌러도 WebSocket이 다시 접속되는 것을 확인할 수 있습니다.


여기까지 Java에서 WebSocket의 Session 사용 방법(Broadcast)과 웹 채팅 소스 예제에 대한 글이었습니다.


궁금한 점이나 잘못된 점이 있으면 댓글 부탁드립니다.