사전 지식

소켓(Socket)이란 

네트워크에서 동작하는 프로그램의 종착점이라고 주로 표현합니다.

IP주소포트 번호로 이루어져 있으며, 서버와 크라이언트가 양방향 통신을 할 수 있게 해주는 소프트웨어 장치입니다. 양방향으로 통신하려면 서로를 알아야 하므로 클라이언트와 서버 둘 다 소켓을 생성하여 연결해줘야 합니다.

 

자바에서는 소켓 기능을 클래스로 만들어 지원합니다

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

 

웹소켓(WebSocket)이란

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

하지만 웹소켓클라이언트의 요청에 응답한 후에도 연결을 그대로 유지하는 연결 지향 방식입니다

따라서 별도의 요청이 없어도 서버는 원하면 언제든 클라이언트로 데이터를 전송할 수 있습니다.

 

앞에서 소켓의 통신 절차가 꽤나 복잡한 것을 알 수 있는데, 웹소켓은 이 복잡한 절차를 간단히 구현할 수 있는 장치가 마련되어있습니다. 웹소캣 서버의 구현은 애너테이션을 이용됩니다

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

위의 애너테이션을 이용해 클라이언트의 요청이 있을 때 적절한 메서드가 실행되어 응답하게 됩니다. 응답은 이벤트 객체를 통해 전달되고, 자바스크립트에서 리스너 메서드를 통해 전달받을 수 있습니다 

 

구현하기

채팅 서버 구현하기

접속한 클라이언트들을 컬렉션으로 관리하면서 한 클라이언트가 메시지를 보내면 다른 모든 클라이언트에 전달하는 간단한 서버입니다. 

 

package websocket;

import java.io.IOException;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;

// 웹소켓 서버의 요청명 지정
@ServerEndpoint("/ChatingServer")
public class ChatServer {
	// 접속한 클라이언트의 세션을 저장할 컬렉션 생성
    private static Set<Session> clients = Collections.synchronizedSet(new HashSet<Session>());

    @OnOpen  // 클라이언트 접속 시 실행
    public void onOpen(Session session) {
        clients.add(session);  // 세션 추가
    }

    @OnMessage  // 메시지를 받으면 실행
    public void onMessage(String message, Session session) throws IOException {
        synchronized (clients) {
            for (Session client : clients) {  // 모든 클라이언트에 메시지 전달
                if (!client.equals(session)) {  // 단, 메시지를 보낸 클라이언트는 제외
                    client.getBasicRemote().sendText(message);
                }
            }
        }
    }

    @OnClose  // 클라이언트와의 연결이 끊기면 실행
    public void onClose(Session session) {
        clients.remove(session); 
    }

    @OnError  // 에러 발생 시 실행
    public void onError(Throwable e) {
        e.printStackTrace();
    }
}

Point

1. @ServerEndpoint 애너테이션으로 해당 요청명으로 접속하는 클라이언트를 이 클래스가 처리하게 합니다. 

웹소캣 접속 URL : ws://호스트:포트번호/컨텍스트루트/ChatingServer

2. Collections 클래스의 synchronizedSet() 메서드는 멀티 스레드 환경에서도 안전한(threed-safe) Set컬렉션을 생성해줍니다.

 

채팅 화면 구현하기

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>    
<html>
<head>
<title>웹소켓 채팅</title>
<script>
// 웹소켓 서버에 접속
var webSocket = new WebSocket("<%= application.getInitParameter("CHAT_ADDR") %>/ChatingServer");
var chatWindow, chatMessage, chatId;

// 채팅창이 열리면 대화창, 메시지 입력창, 대화명 표시란으로 사용할 DOM 객체 저장
window.onload = function() {
    chatWindow = document.getElementById("chatWindow");
    chatMessage = document.getElementById("chatMessage");
    chatId = document.getElementById('chatId').value;    
}

// 메시지 전송
function sendMessage() {
    // 대화창에 표시
    chatWindow.innerHTML += "<div class='myMsg'>" + chatMessage.value + "</div>"
    webSocket.send(chatId + '|' + chatMessage.value);  // 서버로 전송
    chatMessage.value = "";  // 메시지 입력창 내용 지우기
    chatWindow.scrollTop = chatWindow.scrollHeight;  // 대화창 스크롤
}

// 서버와의 연결 종료
function disconnect() {
    webSocket.close();
}

// 엔터 키치면 바로 전송 (편의 기능)
function enterKey() {
    if (window.event.keyCode == 13) {  // 13은 'Enter' 키의 코드값
        sendMessage();
    }
}

// 웹소켓 서버에 연결됐을 때 실행
webSocket.onopen = function(event) {   
    chatWindow.innerHTML += "웹소켓 서버에 연결되었습니다.<br/>";
};

// 웹소켓이 닫혔을 때(서버와의 연결이 끊겼을 때) 실행
webSocket.onclose = function(event) {
    chatWindow.innerHTML += "웹소켓 서버가 종료되었습니다.<br/>";
};

// 에러 발생 시 실행
webSocket.onerror = function(event) { 
    alert(event.data);
    chatWindow.innerHTML += "채팅 중 에러가 발생하였습니다.<br/>";
}; 

// 메시지를 받았을 때 실행
webSocket.onmessage = function(event) { 
    var message = event.data.split("|");  // 대화명과 메시지 분리
    var sender = message[0];   // 보낸 사람의 대화명
    var content = message[1];  // 메시지 내용
    if (content != "") {
        // 대화 출력
        chatWindow.innerHTML += "<div>" + sender + " : " + content + "</div>";
    }
    chatWindow.scrollTop = chatWindow.scrollHeight; 
};
</script>
<style>  
#chatWindow{border:1px solid black; width:270px; height:310px; overflow:scroll; padding:5px;}
#chatMessage{width:236px; height:30px;}
#sendBtn{height:30px; position:relative; top:2px; left:-2px;}
#closeBtn{margin-bottom:3px; position:relative; top:2px; left:-2px;}
#chatId{width:158px; height:24px; border:1px solid #AAAAAA; background-color:#EEEEEE;}
.myMsg{text-align:right;}
</style>
</head>

<body>  <!-- 대화창 UI 구조 정의 --> 
    대화명 : <input type="text" id="chatId" value="${ param.chatId }" readonly />
    <button id="closeBtn" onclick="disconnect();">채팅 종료</button>
    <div id="chatWindow"></div>
    <div>
        <input type="text" id="chatMessage" onkeyup="enterKey();">
        <button id="sendBtn" onclick="sendMessage();">전송</button>
    </div>    
</body>
</html>
더보기
혼자서 두 개의 채팅 화면을 띄우고 대화해보았습니다..

Point

  1. "application.getInitParameter("CHAT_ADDR")" 부분에서 요청명은 서버의 환경에 따라 달라지는 정보이므로 컨텍스트 초기화 매개변수로 관리하는 것이 좋습니다. 실제로 배포할 때 web.xml만 수정하면 되기 때문입니다.

 


참고한 문헌

성낙현의 JSP 웹 프로그래밍 책

+ Recent posts