WebSocket example in Tomcat 8
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);
}
}
}
}