🌿Socket
Socket은 네트워크상에서 통신을 하기 위한 도구(무전기, 전화기)이다.
프로그래밍 언어는 대부분 소켓이라는 규격을 구현해서 소켓 통신을 할 수 있게 만들었다. 그래서 자바에도 소켓이 있고, 자바스크립트에도 소켓이 있다.
인터넷을 통해 데이터를 주고받는 모든 방식은 소켓 방식으로 통신을 한다.
요즘에는 웹으로 넘어가면서 소켓으로 통신을 구현하는 업무가 많이 사라졌다. 별도로 소켓을 만들어서 네트워크를 구현하지 않아도 되게 되었기 때문이다.
WebSocket
WebSocket은 웹 상에서 구현된 소켓을 의미한다.
WebSocket은 Ajax와 유사하지만, Ajax(웹)는 단방향 통신이고, WebSocket은 양방향 통신 정도로 생각하면 된다. 이때 단방향은 무전기이고, 양방향은 전화기이다.
- 브라우저 <-> (통신) <-> 웹 서버
브라우저와 웹 서버와의 통신은 무전기이다.
클라이언트 쪽에서 서버로 데이터를 보내는 걸 Push라고 하는데, 웹은 Push를 못 하기 때문에 새로고침을 한다. 그래서 setInterval을 걸어 놓고 주기적으로 서버에서 다시 데이터를 가져와서 새로고침을 하여 변화하도록 구현을 하는데, 이는 굉장히 불편한 방식이다.
소켓은 쌍방 간의 통신이 가능하므로 서로가 데이터를 보내고 가져올 수 있으므로 기존의 웹과 다른 작업을 할 수 있게 된다.
🌿소켓 구현
프로젝트 설정
pom.xml
<!-- WebSocket -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-websocket</artifactId>
<version>${org.springframework-version}</version>
</dependency>
<dependency>
<groupId>javax.websocket</groupId>
<artifactId>javax.websocket-api</artifactId>
<version>1.1</version>
</dependency>
pom.xml에 WebSocket과 관련된 의존성을 추가하도록 한다.
servlet-context.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/mvc"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<!-- DispatcherServlet Context: defines this servlet's request-processing infrastructure -->
<!-- Enables the Spring MVC @Controller programming model -->
<annotation-driven />
<!-- Handles HTTP GET requests for /resources/** by efficiently serving up static resources in the ${webappRoot}/resources directory -->
<resources mapping="/resources/**" location="/resources/" />
<!-- Resolves views selected for rendering by @Controllers to .jsp resources in the /WEB-INF/views directory -->
<beans:bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<beans:property name="prefix" value="/WEB-INF/views/" />
<beans:property name="suffix" value=".jsp" />
</beans:bean>
<context:component-scan base-package="com.test.socket" />
<context:component-scan base-package="com.test.controller" />
<context:component-scan base-package="com.test.server" />
</beans:beans>
controller와 server를 인식할 수 있도록 추가해 주었다.
파일 생성
com.test.controller
- SocketController.java
com.test.server
- SocketServer.java
views
- test.jsp
🍃소켓 통신
Controller의 역할은 웹 페이지를 돌려주는 것으로 끝이 났고, 서버 측 어딘가에 SocketServer가 있다.
이제부터 발생하는 모든 일은 HTML페이지와 SocketServer의 통신에 관련된 얘기로, Controller는 더 이상 개입하지 않는다.
소켓을 사용하기 위해서는 둘 중에 한쪽에서 처음에 연결을 해야 하는데, 이 구조에서는 웹 페이지가 먼저 SocketServer에 연결을 신청할 수밖에 없다. 그리고 이 통신은 웹과 달리 한쪽에서 끊지 않는 이상 계속해서 연결이 지속된다.
엄밀히 따지면 웹 페이지와 SocketServer 각각 소켓이 생기기 때문에 각각의 소켓이 통신을 한다고 할 수 있다. 양쪽에 전화기를 들고 통화 중인 상태로 이해할 수 있다.
SocketController.java
package com.test.controller;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class SocketController {
@GetMapping(value = "/test.do")
public String test(Model model) {
return "test";
}
}
SocketServer.java
package com.test.server;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.server.ServerEndpoint;
//종단점(Endpoint)
@ServerEndpoint("/testserver.do")
public class SocketServer {
@OnOpen
public void handleOpen() {
System.out.println("클라이언트가 접속했습니다.");
}
@OnClose
public void handleClose() {
System.out.println("클라이언트가 종료했습니다.");
}
/*
@OnMessage
public void handleMessage(String msg) {
System.out.println("클라이언트가 보낸 메시지: " + msg);
}
*/
@OnMessage
public String handleMessage(String msg) {
System.out.println("클라이언트가 보낸 메시지: " + msg);
return "(응답) " + msg;
}
@OnError
public void handleError(Throwable e) {
System.out.println("에러 발생 " + e.getMessage());
}
}
서버가 되기 위해서는 웹 페이지(브라우저)에서 소켓에 연결을 요청한다고 했다.
이때 연결을 요청하기 위한 주소가 필요하다. 소켓 통신에서는 @ServerEndpoint에 주소를 적는다. Endpoint는 네트워크 상에서 상대방을 인식하기 위한 주소로, 전화번호 같은 거라고 생각하면 된다.
test.jsp
소켓 통신 주소
//클라이언트 <-> 서버
const url = 'ws://localhost:8090/socket/testserver.do';
클라이언트가 서버와 통신을 하려면 서버의 주소를 알아야 한다.
Endpoint로 상대의 주소를 알았기 때문에 이를 사용하여 연결을 시도한다. 이게 전에 만든 소켓 서버의 주소가 된다.
소켓 생성 및 연결
//1. 소켓 생성
//2. 서버 접속(연결)
ws = new WebSocket(url);
WebSocket()이라는 생성자 함수를 이용해 소켓을 생성한다.
이때 소켓이 생성될 뿐만 아니라 서버 연결도 동시에 이루어진다.
소켓 연결 확인
//소켓 이벤트
ws.onopen = function(evt) {
log('서버와 연결되었습니다.');
};
function log(msg) {
$('.message').prepend(`
<div>[\${new Date().toLocaleTimeString()}] \${msg}]</div>
`);
}
연결이 제대로 되었는지 알기 위해서는 소켓 이벤트의 콜백 형태로 알 수 있다.
서버 측에서 소켓 연결을 받아들이고 연결이 되는 순간 발생한다.
@OnOpen
package com.test.server;
import javax.websocket.OnOpen;
import javax.websocket.server.ServerEndpoint;
//종단점(Endpoint)
@ServerEndpoint("/testserver.do")
public class SocketServer {
@OnOpen
public void handleOpen() {
System.out.println("클라이언트가 접속했습니다.");
}
}
@OnOpen은 클라이언트가 연결 요청을 했을 때 발생한다.
클라이언트 쪽에서도 접속이 되었다는 사실을 알아낼 수 있다.
시간 순서를 정확히 따지면 @OnOpen이 먼저 발생한다.
이제 서로 쌍방 간에 대화를 할 수 있는 상태가 되었다.
연결 종료
$('#btnDisconnect').click(function() {
ws.close(); //연결 종료 시도
log('연결을 종료합니다');
});
이때 클라이언트 쪽에서 연결을 끊겠다고 말하는 것일 뿐, 끊기는 것에도 절차가 있으므로 아직 끊기지는 않았다.
연결이 끊겼다는 것을 SocketServer에 알려주고, SocketServer에서 확인 메시지를 보낸다. 연결을 할 때와 마찬가지로 서로 쌍방 간의 동의 하에 연결을 끊게 된다.
@OnClose
@OnClose
public void handleClose() {
System.out.println("클라이언트가 종료했습니다.");
}
클라이언트와 연결이 종료되었을 때 발생한다.
데이터 전송
<div>
<input type="text" class="long" id="msg">
<button type="button" id="btnMsg">보내기</button>
</div>
$('#btnMsg').click(function() {
//연결된 서버에게 메시지 전송하기
//ws.send('전달할 메시지');
ws.send($('#msg').val());
log('메시지를 전송했습니다.');
$('#msg').val('');
});
send 메서드를 통해서 데이터를 전송할 수 있다.
@OnMessage
@OnMessage
public void handleMessage(String msg) {
System.out.println("클라이언트가 보낸 메시지: " + msg);
}
클라이언트가 서버에게 메시지를 전송했을 때 발생한다.
자동으로 전달한 메시지가 매개변수로 들어온다는 특징이 있다.
@OnMessage
public String handleMessage(String msg) {
System.out.println("클라이언트가 보낸 메시지: " + msg);
return "(응답) " + msg;
}
반대로 클라이언트에게 돌려주는 메시지를 작성할 수 있다. 만약 "안녕하세요."를 작성하면 한 말을 다시 돌려주는 기능이다.
onmessage는 서버가 나에게 메시지를 보낼 때 발생하는 이벤트이다. 이때 evt.data 프로퍼티에 매개변수로 보낸 데이터가 들어온다.
이렇게 하면 메시지를 보냈을 때 가공한 다음에 돌려주게 된다.
예외 처리
ws.onerror = function(evt) {
log('에러가 발생했습니다.' + evt);
}
@OnError
public void handleError(Throwable e) {
System.out.println("에러 발생 " + e.getMessage());
}
예기치 않은 에러가 발생했을 경우를 대비하여 클라이언트와 서버 측에 각각 예외 처리를 하는 것이 좋다.
전체 코드
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
<link rel="stylesheet" href="https://me2.do/5BvBFJ57">
<style>
</style>
</head>
<body>
<!-- test.jsp -->
<h1>WebSocket <small>연결 테스트</small></h1>
<div>
<button type="button" class="in" id="btnConnect">연결하기</button>
<button type="button" class="out" id="btnDisconnect">종료하기</button>
</div>
<hr>
<div class="message full"></div>
<hr>
<div>
<input type="text" class="long" id="msg">
<button type="button" id="btnMsg">보내기</button>
</div>
<script src="https://code.jquery.com/jquery-1.12.4.js"></script>
<script>
//클라이언트 <-> 서버
const url = 'ws://localhost:8090/socket/testserver.do';
//웹 소켓 참조 변수
let ws;
//1. 소켓 생성
//2. 서버 접속(연결)
//3. 통신
//4. 서버 접속 해제(종료)
$('#btnConnect').click(function() {
//1. 소켓 생성
//2. 서버 접속(연결)
ws = new WebSocket(url);
//소켓 이벤트
ws.onopen = function(evt) {
log('서버와 연결되었습니다.');
};
ws.onclose = function(evt) {
log('서버와 연결이 종료되었습니다.');
}
ws.onmessage = function(evt) {
log(evt.data);
}
ws.onerror = function(evt) {
log('에러가 발생했습니다.' + evt);
}
});
$('#btnDisconnect').click(function() {
ws.close(); //연결 종료 시도
log('연결을 종료합니다');
});
function log(msg) {
$('.message').prepend(`
<div>[\${new Date().toLocaleTimeString()}] \${msg}]</div>
`);
}
$('#btnMsg').click(function() {
//연결된 서버에게 메시지 전송하기
//ws.send('전달할 메시지');
ws.send($('#msg').val());
log('메시지를 전송했습니다.');
$('#msg').val('');
});
</script>
</body>
</html>
전체 코드는 이런 모습이 된다.
🌿채팅 구현
Isaac과 Sopia가 직접적으로 통신하는 Peer-to-Peer 방식으로도 통신할 수 있다.
카카오톡에서는 채팅은 Server를 거치게 하고, 파일의 경우에는 P2P 방식을 사용한다.
pom.xml
<!-- GSON -->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.10.1</version>
</dependency>
구글에서 만든 JSON 처리 라이브러리를 사용한다.
파일 생성
com.test.controller
- ChatController.java
com.test.server
- ChatServer.java
com.test.domain
- Message.java
view
- index.jsp
- chat.jsp
ChatController.java
package com.test.controller;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class ChatController {
@GetMapping(value = "/index.do")
public String index(Model model) {
return "index";
}
@GetMapping(value = "/chat.do")
public String chat(Model model) {
return "chat";
}
}
ChatServer.java
package com.test.server;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;
import com.google.gson.Gson;
import com.test.domain.Message;
@ServerEndpoint("/chatserver.do")
public class ChatServer {
//현재 채팅서버에 접속 중인 클라이언트 목록
private static List<Session> sessionList = new ArrayList<Session>();
//클라이언트 접속
@OnOpen
public void handleOpen(Session session) {
System.out.println("클라이언트가 접속했습니다.");
sessionList.add(session); //현재 접속자의 정보를 배열에 추가
checkSessionList();
clearSessionList();
}
//클라이언트로부터 메시지 전달
@OnMessage
public void handleMessage(String msg, Session session) {
//System.out.println(msg);
//JSON 형식의 문자열 -> 자바 클래스 객체로 변환
Gson gson = new Gson();
Message message = gson.fromJson(msg, Message.class);
//System.out.println(message);
if (message.getCode().equals("1")) {
for (Session s : sessionList) {
//모든 접속자 중에서 방금 메시지를 보낸 세션을 제외하고 검색
if (s != session) {
//본인 이외의 세션(소켓)에게 현재 접속자를 알리는 메시지 전달
try {
s.getBasicRemote().sendText(msg);
} catch (Exception e) {
e.printStackTrace();
}
}
}
} else if (message.getCode().equals("2")) {
//기존 접속자가 대회방을 나갔습니다.
sessionList.remove(session);
for (Session s : sessionList) {
//'누군가가 나갔습니다.'라는 메시지를 남아있는 모든 사람에게 전달
try {
s.getBasicRemote().sendText(msg);
} catch (Exception e) {
e.printStackTrace();
}
}
} else if (message.getCode().equals("3")) {
//대화 메시지
//메시지를 전송한 사람을 제외한 나머지 사람에게 메시지 전달
for (Session s : sessionList) { //메시지를 전송한 사람 제외
if (s != session) {
try {
s.getBasicRemote().sendText(msg);
} catch (Exception e) {
e.printStackTrace();
}
}
}
} else if (message.getCode().equals("4")) {
//이모티콘 메시지
for (Session s : sessionList) {
if (s != session) {
try {
s.getBasicRemote().sendText(msg);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
//접속자 세션 확인
private void checkSessionList() {
System.out.println("Session List");
for (Session session : sessionList) {
System.out.println(session.getId());
}
System.out.println();
}
private void clearSessionList() {
//List 계열의 컬렉션은 향상된 for 내에서 요소를 추가/삭제하는 행동이 불가능하다.
//1. 일반 for문
//2. Iterator
Iterator<Session> iter = sessionList.iterator();
while (iter.hasNext()) {
if (!(iter.next()).isOpen()) {
//혹시 연결이 끊어진 세션이 있으면
iter.remove(); //리스트에서 제거
}
}
}
//클라이언트 종료
@OnClose
public void handleClose() {
System.out.println("클라이언트가 종료했습니다.");
}
}
EndPoint의 앞에는 '/'를 작성해 주어야 한다.
Gson
//클라이언트로부터 메시지 전달
@OnMessage
public void handleMessage(String msg) {
//System.out.println(msg);
//JSON 형식의 문자열 -> 자바 클래스 객체로 변환
Gson gson = new Gson();
Message message = gson.fromJson(msg, Message.class);
System.out.println(message);
}
Gson이 JSON 문자열을 처리해서 자바에 있는 클래스 객체로 만들어준다.
이러면 서로 간에 데이터를 주고받는 게 훨씬 편리해진다.
sessionList
//현재 채팅서버에 접속 중인 클라이언트 목록
private static List<Session> sessionList = new ArrayList<Session>();
//클라이언트 접속
@OnOpen
public void handleOpen(Session session) {
System.out.println("클라이언트가 접속했습니다.");
sessionList.add(session); //현재 접속자의 정보를 배열에 추가
checkSessionList();
}
//접속자 세션 확인
private void checkSessionList() {
System.out.println("Session List");
for (Session session : sessionList) {
System.out.println(session.getId());
}
System.out.println();
}
이때 만드는 세션은 웹 소켓에 있는 세션이다.
한 명 이상이 접속할 것이기 때문에 ArrayList로 관리하도록 한다.
클라이언트가 접속할 때 Session이 자동으로 넘어오고 배열에 추가하면 된다.
Message.java
package com.test.domain;
//클라이언트 <-> (데이터) <-> 서버
public class Message {
private String code; //상태 코드
private String sender; //보내는 사람
private String receiver; //받는 사람
private String content; //대화 내용
private String regdate; //날짜
@Override
public String toString() {
return "Message [code=" + code + ", sender=" + sender + ", receiver=" + receiver + ", content=" + content
+ ", regdate=" + regdate + "]";
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getSender() {
return sender;
}
public void setSender(String sender) {
this.sender = sender;
}
public String getReceiver() {
return receiver;
}
public void setReceiver(String receiver) {
this.receiver = receiver;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public String getRegdate() {
return regdate;
}
public void setRegdate(String regdate) {
this.regdate = regdate;
}
}
code, sender, receiver, content, regdate를 만들었다.
index.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Chat</title>
<link rel="stylesheet" href="https://me2.do/5BvBFJ57">
<style>
</style>
</head>
<body>
<!-- index.jsp -->
<h1>WebSocket <small>chat</small></h1>
<div>
<div class="group">
<label>대화명</label>
<input type="text" name="name" id="name" class="short">
</div>
</div>
<div>
<button type="button" class="in">들어가기</button>
<button type="button" class="in" data-name="Isaac">들어가기(Isaac)</button>
<button type="button" class="in" data-name="Sopia">들어가기(Sopia)</button>
</div>
<script src="https://code.jquery.com/jquery-1.12.4.js"></script>
<script>
$('.in').click(function() {
let name = $('#name').val();
if ($(event.target).data('name') != null && $(event.target).data('name') != '') {
name = $(event.target).data('name');
}
//자식창의 window 객체
let child = window.open('/socket/chat.do', 'chat', 'width=404 height=510');
//child.connect(name); //작동 X
child.addEventListener('load', function(){
//자식창이 다 뜨고 나면 발생
child.connect(name);
});
$('.in').css('opacity', .5)
.prop('disabled', true);
$('#name').prop('readOnly', true);
});
</script>
</body>
</html>
본인이 사용할 대화명을 입력하고 실제 대화는 chat.jsp에서 하도록 한다.
chat.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Chat</title>
<link rel="stylesheet" href="https://me2.do/5BvBFJ57">
<style>
html, body {
padding: 0 !important;
margin: 0 !important;
background-color: #FFF !important;
display: block;
overflow: hidden;
}
body > div {
margin: 0;
padding: 0;
}
#main {
width: 400px;
height: 510px;
margin: 3px;
display: grid;
grid-template-rows: repeat(12, 1fr);
}
#header {
}
#header > h2 {
margin: 0px;
margin-bottom: 10px;
padding: 5px;
}
#list {
border: 1px solid var(--border-color);
box-sizing: content-box;
padding: .5rem;
grid-row-start: 2;
grid-row-end: 12;
font-size: 14px;
overflow: auto;
}
#msg {
margin-top: 3px;
}
#list .item {
font-size: 14px;
margin: 15px 0;
}
#list .item > div:first-child {
display: flex;
}
#list .item.me > div:first-child {
justify-content: flex-end;
}
#list .item.other > div:first-child {
justify-content: flex-end;
flex-direction: row-reverse;
}
#list .item > div:first-child > div:first-child {
font-size: 10px;
color: #777;
margin: 3px 5px;
}
#list .item > div:first-child > div:nth-child(2) {
border: 1px solid var(--border-color);
display: inline-block;
min-width: 100px;
max-width: 250px;
text-align: left;
padding: 3px 7px;
}
#list .state.item > div:first-child > div:nth-child(2) {
background-color: #EEE;
}
#list .item > div:last-child {
font-size: 10px;
color: #777;
margin-top: 5px;
}
#list .me {
text-align: right;
}
#list .other {
text-align: left;
}
#list .msg.me.item > div:first-child > div:nth-child(2) {
background-color: rgba(255, 99, 71, .2);
}
#list .msg.other.item > div:first-child > div:nth-child(2) {
background-color: rgba(100, 149, 237, .2);
}
#list .msg img {
width: 150px;
}
</style>
</head>
<body>
<!-- chat.jsp -->
<div id="main">
<div id="header"><h2>WebSocket <small>대화명</small></h2></div>
<div id="list"></div>
<input type="text" id="msg" placeholder="대화 내용을 입력하세요.">
</div>
<script src="https://code.jquery.com/jquery-1.12.4.js"></script>
<script>
let name;
let ws;
const url = 'ws://localhost:8090/socket/chatserver.do';
function connect(name) {
window.name = name;
$('#header small').text(name);
//연결하기 > 소켓 생성
ws = new WebSocket(url);
//연결 후 작업
ws.onopen = function(evt) {
log('서버 연결 성공');
//서버와 연결 직후 > 본인의 정보를 서버에 전달
//ws.send('무슨 용도의 메시지?');
//메시지 규칙 > 프로토콜
//ws.send('1|Isaac'); //상태코드|본인대화명
//ws.send('2|1|2|점심 뭐 먹어?'); //상태코드|보내는사람|받는사람
//ws.send('3|Isaac'); //상태코드|본인대화명
let message = {
code: '1',
sender: window.name,
receiver: '',
content: '',
regdate: new Date().toLocaleString()
};
ws.send(JSON.stringify(message));
print('', '대화방에 참여했습니다.', 'me', 'state', message.regdate);
$('#msg').focus();
};
//서버에서 클라이언트에게 전달한 메시지
ws.onmessage = function(evt) {
log('메시지 수신');
//console.log(evt.data);
let message = JSON.parse(evt.data); //JSON을 원래 형태로 바꿔 꺼내 쓰기 편리하게 함
//console.log(message);
if (message.code == '1') { //입장
print('', `[\${message.sender}]님이 들어왔습니다.`, 'other', 'state', message.regdate);
} else if (message.code == '2') { //퇴장
print('', `[\${message.sender}]님이 나갔습니다.`, 'other', 'state', message.regdate);
} else if (message.code == '3') { //전송 (메시지)
print(message.sender, message.content, 'other', 'msg', message.regdate);
} else if (message.code == '4') { //전송 (이모티콘)
printEmoticon(message.sender, message.content, 'other', 'msg', message.regdate);
}
}
}//connect
function log(msg) {
console.log(`[\${new Date().toLocaleTimeString()}] \${msg}`);
}
//대화창 출력
function print(name, msg, side, state, time) {
let temp = `
<div class="item \${state} \${side}">
<div>
<div>\${name}</div>
<div>\${msg}</div>
</div>
<div>\${time}</div>
</div>
`;
$('#list').append(temp);
//새로운 내용 추가 + 스크롤을 바닥으로 내림
setTimeout(scrollList, 100);
}
//이모티콘 출력
function printEmoticon(name, msg, side, state, time) {
let temp = `
<div class="item \${state} \${side}">
<div>
<div>\${name}</div>
<div style="background-color:#FFF;border:0;"><img src="/socket/resources/emoticon/\${msg}.png"></div>
</div>
<div>\${time}</div>
</div>
`;
$('#list').append(temp);
//새로운 내용 추가 + 스크롤을 바닥으로 내림
setTimeout(scrollList, 100);
}
//창이 닫히기 바로 직전에 발생
$(window).on('beforeunload', function() {
//alert();
setTimeout(scrollList, 100);
});
function disconnect() {
//대화방에서 나가기 > 다른 사람에게 알리기
let message = {
code: '2',
sender: window.name,
receiver: '',
content: '',
regdate: new Date().toLocaleString()
};
ws.send(JSON.stringify(message));
}
$('#msg').keydown(function(evt) {
if (evt.keyCode == 13) {
//입력한 대화 내용을 서버로 전달
let message = {
code: '3',
sender: window.name,
receiver: '',
content: $('#msg').val(),
regdate: new Date().toLocaleString()
};
if ($('#msg').val().startsWith('/')) {
//대화(X) > 이모티콘(O)
message.code = '4';
//alert(message.content);
}
ws.send(JSON.stringify(message));
$('#msg').val('').focus();
if (message.code == '3') {
print(window.name, message.content, 'me', 'msg', message.regdate);
} else if (message.code == '4') {
printEmoticon(window.name, message.content, 'me', 'msg', message.regdate);
}
}
});
function scrollList() {
$('#list').scrollTop($('#list').outerHeight() + 300);
}
</script>
</body>
</html>
메시지 규칙
//메시지 규칙 > 프로토콜
ws.send('1|Isaac'); //상태코드|본인대화명
ws.send('2|1|2|점심 뭐 먹어?'); //상태코드|보내는사람|받는사람
ws.send('3|Isaac'); //상태코드|본인대화명
상태코드가 1이면 대화방에 들어온 것을 의미하고, 3이면 대화방에 나간 것을 의미한다.
상태코드가 2이면 서로 주고받는 메시지라고 생각한다. 2|1|2|라고 하면 1번 사람이 2번 사람과 주고받는 메시지를 의미한다.
이러한 메시지 규칙을 프로토콜이라고 한다.
code: 상태코드
- 새로운 유저가 들어옴
- 기존 유저가 나감
- 메시지 전달
- 이모티콘 전달
sender: 보내는 유저명
receiver: 받는 유저명
content: 대화 내용
regdate: 날짜/시간
메시지 틀
$('#msg').keydown(function(evt){
if (evt.keyCode == 13) { //Enter
//입력한 대화 내용을 서버로 전달
//ws.send('Hello');
//메시지 틀 (메시지 전송)
let message = {
code: '3',
sender: window.name,
receiver: '',
content: $('#msg').val(),
regdate: new Date().toLocaleString()
};
ws.send(JSON.stringify(message));
$('#msg').val('').focus();
print(window.name, message.content, 'me', 'msg', message.regdate);
}
});
서버 입장에서 Hello라는 사람이 입장한 건지, 퇴장한 건지, 메시지인지 알 수 없다. 그래서 주고받는 메시지를 본론만 주는 게 아니라 최소한의 틀(일종의 DTO)을 작성하여 넘겨야 한다.
채팅 스크롤
function scrollList() {
$('#list').scrollTop($('#list').outerHeight() + 300);
}
스크롤이 생길 정도로 채팅을 하면 내용이 추가될 때마다 위 함수를 발생하여 스크롤바를 자동으로 내리게 한다.
이모티콘
- C:\Class\code\spring\WebSocketTest\src\main\webapp\resources\emoticon
webapp 폴더 아래의 resources 폴더 아래에 emoticon 폴더를 만들어 이미지를 준비했다.
슬래시를 적고 이모티콘에 해당하는 단어를 적으면 해당 이모티콘을 출력하도록 한다.
//이모티콘 출력
function printEmoticon(name, msg, side, state, time) {
let temp = `
<div class="item \${state} \${side}">
<div>
<div>\${name}</div>
<div style="background-color:#FFF;border:0;"><img src="/socket/resources/emoticon/\${msg}.png"></div>
</div>
<div>\${time}</div>
</div>
`;
$('#list').append(temp);
//새로운 내용 추가 + 스크롤을 바닥으로 내림
setTimeout(scrollList, 100);
}
$('#msg').keydown(function(evt) {
if (evt.keyCode == 13) {
//입력한 대화 내용을 서버로 전달
let message = {
code: '3',
sender: window.name,
receiver: '',
content: $('#msg').val(),
regdate: new Date().toLocaleString()
};
if ($('#msg').val().startsWith('/')) {
//대화(X) > 이모티콘(O)
message.code = '4';
//alert(message.content);
}
ws.send(JSON.stringify(message));
$('#msg').val('').focus();
if (message.code == '3') {
print(window.name, message.content, 'me', 'msg', message.regdate);
} else if (message.code == '4') {
printEmoticon(window.name, message.content, 'me', 'msg', message.regdate);
}
}
});
이모티콘 출력은 메시지 출력 대신에 이미지를 출력할 뿐 전달 방식은 크게 다르지 않다.