WebSocket/WebSocket in Tomcat8

WebSocket example in Tomcat 8

Soul-Learner 2014. 8. 4. 18:21

Tomcat 8 WebSocket Chat example


원문참조: http://tomcat.apache.org/tomcat-8.0-doc/web-socket-howto.html

W3C WebSocket API : https://www.w3.org/TR/2011/WD-websockets-20110419/


테스트 환경

Windows 7, JDK 1.7, Tomcat 8, Chrome



client.jsp ( 웹브라우저에서 요청할 때는 'http://localhost~' 으로 하지말고 'http://192.168.8.32:8080/MyWeb/websocket/chat' 처럼 반드시 IP 주소를 사용해야 정상적으로 서버소켓 측에 접속된다 )

<%@ page contentType="text/html; charset=utf-8" pageEncoding="utf-8"%>

<!DOCTYPE html>

<html>

<head>

    <meta charset="utf-8">

   <title>Apache Tomcat WebSocket Examples: Chat</title>

   <style type="text/css">

       input#chat {

           width: 410px

       }

       #console-container {

           width: 400px;

       }

       #console {

           border: 1px solid #CCCCCC;

           border-right-color: #999999;

           border-bottom-color: #999999;

           height: 170px;

           overflow-y: scroll;

           padding: 5px;

           width: 100%;

       }

       #console p {

           padding: 0;

           margin: 0;

       }

   </style>

   

   <script type="application/javascript">


       var Chat = {};

       Chat.socket = null;

       // connect() 함수 정의

        // 서버에 연결하고 onopen(), onclose(), onmessage() 콜백함수를 등록함

       Chat.connect = (function(host) {

            // 서버에 접속시도

           if ('WebSocket' in window) {

               Chat.socket = new WebSocket(host);

           } else if ('MozWebSocket' in window) {

               Chat.socket = new MozWebSocket(host);

           } else {

               Console.log('Error: WebSocket is not supported by this browser.');

               return;

           }

             // 서버에 접속이 되면 호출되는 콜백함수

           Chat.socket.onopen = function () {

               Console.log('Info: WebSocket connection opened.');

               // 채팅입력창에 메시지를 입력하기 위해 키를 누르면 호출되는 콜백함수

               document.getElementById('chat').onkeydown = function(event) {

                    // 엔터키가 눌린 경우, 서버로 메시지를 전송함

                   if (event.keyCode == 13) {

                       Chat.sendMessage();

                   }

               };

           };

           // 연결이 끊어진 경우에 호출되는 콜백함수

           Chat.socket.onclose = function () {

            // 채팅 입력창 이벤트를 제거함

               document.getElementById('chat').onkeydown = null;

               Console.log('Info: WebSocket closed.');

           };

           // 서버로부터 메시지를 받은 경우에 호출되는 콜백함수

           Chat.socket.onmessage = function (message) {

            // 수신된 메시지를 화면에 출력함

               Console.log(message.data); 

           };

       });

    // connect() 함수 정의 끝

   

    // 위에서 정의한 connect() 함수를 호출하여 접속을 시도함

       Chat.initialize = function() {

           if (window.location.protocol == 'http:') {

               //Chat.connect('ws://' + window.location.host + '/websocket/chat');

            Chat.connect('ws://localhost:8080/WebApp/websocket/chat');

           } else {

               Chat.connect('wss://' + window.location.host + '/websocket/chat');

           }

       };

       // 서버로 메시지를 전송하고 입력창에서 메시지를 제거함

       Chat.sendMessage = (function() {

           var message = document.getElementById('chat').value;

           if (message != '') {

               Chat.socket.send(message);

               document.getElementById('chat').value = '';

           }

       });

       var Console = {}; // 화면에 메시지를 출력하기 위한 객체 생성

       // log() 함수 정의

       Console.log = (function(message) {

           var console = document.getElementById('console');

           var p = document.createElement('p');

           p.style.wordWrap = 'break-word';

           p.innerHTML = message;

           console.appendChild(p); // 전달된 메시지를 하단에 추가함

           // 추가된 메시지가 25개를 초과하면 가장 먼저 추가된 메시지를 한개 삭제함

           while (console.childNodes.length > 25) {

               console.removeChild(console.firstChild);

           }

           // 스크롤을 최상단에 있도록 설정함

           console.scrollTop = console.scrollHeight;

       });

       // 위에 정의된 함수(접속시도)를 호출함

       Chat.initialize();


   </script>

</head>

<body>


<div>

   <p>

       <input type="text" placeholder="type and press enter to chat" id="chat" />

   </p>

   <div id="console-container">

       <div id="console"/>

   </div>

</div>

</body>

</html>




ChatAnnotation.java

package org.kdea.java.websocket;


import java.io.IOException;

import java.util.Iterator;

import java.util.Set;

import java.util.concurrent.CopyOnWriteArraySet;

import java.util.concurrent.atomic.AtomicInteger;


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(value = "/websocket/chat") //클라이언트가 접속할 때 사용될 URI

public class ChatAnnotation {


    private static final String GUEST_PREFIX = "Guest";

    // AtomicInteger 클래스는 getAndIncrement()를 호출할 때마다 카운터를 1씩 증가하는 기능을 가지고 있다

    private static final AtomicInteger connectionIds = new AtomicInteger(0);

    // CopyOnWriteArraySet 을 사용하면 컬렉션에 저장된 객체를 좀더 간편하게 추출할 수 있다

    // 예를 들어, toArray()메소드를 통해 쉽게 Object[] 형의 데이터를 추출할 수 있다.

    private static final Set<ChatAnnotation> connections =

            new CopyOnWriteArraySet<ChatAnnotation>();


    private final String nickname;

    // 클라이언트가 새로 접속할 때마다 한개의 Session 객체가 생성된다.

    // Session 객체를 컬렉션에 보관하여 두고 해당 클라이언트에게 데이터를 전송할 때마다 사용한다

    private Session session;


    public ChatAnnotation() { // 클라이언트가 새로 접속할 때마다 이 클래스의 인스턴스가 새로 생성됨

    // 클라이언트가 접속할 때마다 서버측에서는 Thread 가 새로 생성되는 것을 확인할 수 있다

    String threadName = "Thread-Name:"+Thread.currentThread().getName();

    // getAndIncrement()은 카운트를 1 증가하고 증가된 숫자를 리턴한다

        nickname = GUEST_PREFIX + connectionIds.getAndIncrement();

        System.out.println(threadName+", "+nickname);

    }



    @OnOpen

    public void start(Session session) {

        System.out.println("클라이언트 접속됨 "+session);

        //Session:접속자마다 한개의 세션이 생성되어 데이터 통신수단으로 사용됨

        //한개의 브라우저에서 여러개의 탭을 사용해서 접속하면 Session은 서로 다르지만 HttpSession 은 동일함

        this.session = session;

        connections.add(this);

        String message = String.format("* %s %s", nickname, "has joined.");

        broadcast(message);

    }



    @OnClose

    public void end() {

        connections.remove(this);

        String message = String.format("* %s %s", nickname, "has disconnected.");

        broadcast(message);

    }


    // 현재 세션과 연결된 클라이언트로부터 메시지가 도착할 때마다 새로운 쓰레드가 실행되어 incoming()을 호출함

    @OnMessage

    public void incoming(String message) {

   

    String threadName = "Thread-Name:"+Thread.currentThread().getName();

    System.out.println(threadName+", "+nickname);

        if(message==null || message.trim().equals("")) return;

        String filteredMessage = String.format("%s: %s", nickname, message);

        broadcast(filteredMessage);

    }


    

    @OnError

    public void onError(Throwable t) throws Throwable {

        System.err.println("Chat Error: " + t.toString());

    }


    // 현재 세션으로부터 도착한 메시지를 모든 접속자에게 전송한다

   private void broadcast(String msg) {

    Iterator<ChatAnnotation> ss = connections.iterator();

        for (int i=0;i<connections.size();i++) {

        ChatAnnotation client = ss.next();

            try {

                synchronized (client) {

                    // 서버에 접속 중인 모든 이용자에게 메지지를 전송한다

                    client.session.getBasicRemote().sendText(msg);

                }

            } catch (IllegalStateException ise){

            // 특정 클라이언트에게 현재 메시지 보내기 작업 중인 경우에 동시에 쓰기작업을 요청하면 오류 발생함

            if(ise.getMessage().indexOf("[TEXT_FULL_WRITING]")!=-1) {

            new Thread() {

            @Override

            public void run() {

            while(true) {

            try{

            client.session.getBasicRemote().sendText(msg);

            break;

            }catch(IllegalStateException _ise){

            try {

Thread.sleep(100); // 메시지 보내기 작업을 마치도록 기다려준다

} catch (InterruptedException e) {}

            }

            catch(IOException ioe){

            ioe.printStackTrace();

            }

            }

            }

            }.start();

            }

            } catch (Exception e) {

            // 메시지 전송 중에 오류가 발생(클라이언트 퇴장을 의미함)하면 해당 클라이언트를 서버에서 제거한다

                System.err.println("Chat Error: Failed to send message to client:"+e);

                connections.remove(client);

                try {

                // 접속 종료

                    client.session.close();

                } catch (IOException e1) {

                    // Ignore

                }

                // 한 클라이언트의 퇴장을 모든 이용자에게 알린다

                String message = String.format("* %s %s",

                        client.nickname, "has been disconnected.");

                broadcast(message);

            }

        }

    }

}



특정 수신자에게만 메시지를 전달하는 예

package org.kdea.ws;


import java.io.IOException;

import java.util.*;


import java.util.concurrent.atomic.AtomicInteger;


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(value = "/websocket/chat") //클라이언트가 접속할 때 사용될 URI

public class ChatAnnotation {


    private static final String GUEST_PREFIX = "Guest";

    // AtomicInteger 클래스는 getAndIncrement()를 호출할 때마다 카운터를 1씩 증가하는 기능을 가지고 있다

    private static final AtomicInteger connectionIds = new AtomicInteger(0);


    private static final Map<String,Session> sessionMap = new HashMap<String,Session>();

    

    private final String nickname;

    // 클라이언트가 새로 접속할 때마다 한개의 Session 객체가 생성된다.

    // Session 객체를 컬렉션에 보관하여 두고 해당 클라이언트에게 데이터를 전송할 때마다 사용한다

    private Session session;


    public ChatAnnotation() {

    // 클라이언트가 접속할 때마다 서버측에서는 Thread 가 새로 생성되는 것을 확인할 수 있다

    String threadName = "Thread-Name:"+Thread.currentThread().getName();

    // getAndIncrement()은 카운트를 1 증가하고 증가되기 전의 숫자를 리턴한다

        nickname = GUEST_PREFIX + connectionIds.getAndIncrement();

        System.out.println("생성자:"+threadName+", "+nickname);

    }


    @OnOpen

    public void start(Session session) {

    System.out.println("클라이언트 접속됨 "+session);

    // 접속자마다 한개의 세션이 생성되어 데이터 통신수단으로 사용됨

        this.session = session;

        sessionMap.put(nickname, session);

        String message = String.format("* %s %s", nickname, "has joined.");

        broadcast(message);

    }


    @OnClose

    public void end() {

        sessionMap.remove(this.session);

        String message = String.format("* %s %s", nickname, "has disconnected.");

        broadcast(message);

    }


    // 현재 세션과 연결된 클라이언트로부터 메시지가 도착할 때마다 새로운 쓰레드가 실행되어 incoming()을 호출함

    @OnMessage

    public void incoming(String message) {

   

    String threadName = "Thread-Name:"+Thread.currentThread().getName();

    System.out.println("메시지 도착:"+threadName+", "+nickname);

        if(message==null || message.trim().equals("")) return;

        String filteredMessage = String.format("%s: %s", nickname, message);

        

        //Guest0의 메시지는 특정 클라이언트(Guest2)에게만 전달하는 경우

        if(this.nickname.equals("Guest0")) {

        sendToOne(filteredMessage, sessionMap.get("Guest2"));

        }

        else //현재 접속된 모든 클라이언트에게 메시지를 전달하는 경우

        {

        broadcast(filteredMessage);

        }

    }


    @OnError

    public void onError(Throwable t) throws Throwable {

        System.err.println("오류/세션제거("+nickname+"):Chat Error: " + t.toString());

        sessionMap.remove(this.nickname);

    }


    // 클라이언트로부터 도착한 메시지를 특정 수신자(Session)에게만 전달한다

    private void sendToOne(String msg, Session ses) {

    try {

ses.getBasicRemote().sendText(msg);

} catch (IOException e) {

e.printStackTrace();

}

    }

    

    // 클라이언트로부터 도착한 메시지를 모든 접속자에게 전송한다

    private void broadcast(String msg) {

   

    Set<String> keys = sessionMap.keySet();

    Iterator<String> it = keys.iterator();

    while(it.hasNext()){

    String key = it.next();

    Session s = sessionMap.get(key);

    try{

    s.getBasicRemote().sendText(msg);

    }catch(IOException e) {

    sessionMap.remove(key);

    try {

s.close();

} catch (IOException e1) {

e1.printStackTrace();

}

    String message = String.format("* %s %s",

                        key, "has been disconnected.");

                broadcast(message);

    }

    }

    }

}