WebSocket은 서버와 클라이언트 간에 양방향 통신을 가능하게 하는 프로토콜이다.
HTTP 프로토콜에서는 클라이언트에서 요청을 보내면 서버에서 응답을 하고 연결이 끊어졌지만, WebSocket을 사용하면 클라이언트와 서버 간에 계속해서 연결을 유지하면서 양방향으로 데이터를 주고받을 수 있다.
WebSocket 프로그래밍은 클라이언트와 서버 간의 WebSocket 연결을 설정하고 유지하는 것을 포함한다.
클라이언트에서는 WebSocket 객체를 만들고 서버와의 연결을 설정한 다음 데이터를 전송할 수 있다.
서버에서는 클라이언트와의 연결을 수신하고 연결을 유지하면서 데이터를 전송할 수 있다.
WebSocket 프로그래밍은 실시간 채팅, 게임, 주식 시세 등 다양한 분야에서 활용된다.
@Component
public class DevLogWebSocketHandler extends TextWebSocketHandler {
/* 클라이언트로부터 메시지 수신시 동작 */
@Override
public void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
...
}
/* 클라이언트가 소켓 연결시 동작 */
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
...
}
/* 클라이언트가 소켓 종료시 동작 */
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
...
}
}
import java.util.HashMap;
import java.util.Map;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Component
public class DevLogWebSocketHandler extends TextWebSocketHandler{
Map<String, WebSocketSession> sessionMap = new HashMap<>(); //웹소켓 세션을 담아둘 맵
Map<String, String> userMap = new HashMap<>(); //사용자
/* 클라이언트로부터 메시지 수신시 동작 */
@Override
public void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
String msg = message.getPayload();
log.info("===============Message=================");
log.info("{}", msg);
log.info("===============Message=================");
JSONObject obj = jsonToObjectParser(msg);
//로그인된 Member (afterConnectionEstablished 메소드에서 session을 저장함)
for(String key : sessionMap.keySet()) {
WebSocketSession wss = sessionMap.get(key);
if(userMap.get(wss.getId()) == null) {
userMap.put(wss.getId(), (String)obj.get("userName"));
}
//클라이언트에게 메시지 전달
wss.sendMessage(new TextMessage(obj.toJSONString()));
}
}
/* 클라이언트가 소켓 연결시 동작 */
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
log.info("{} 연결되었습니다.", session.getId());
super.afterConnectionEstablished(session);
sessionMap.put(session.getId(), session);
JSONObject obj = new JSONObject();
obj.put("type", "getId");
obj.put("sessionId", session.getId());
//클라이언트에게 메세지 전달
session.sendMessage(new TextMessage(obj.toJSONString()));
}
/* 클라이언트가 소켓 종료시 동작 */
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
log.info("{} 연결이 종료되었습니다.", session.getId());
super.afterConnectionClosed(session, status);
sessionMap.remove(session.getId());
String userName = userMap.get(session.getId());
for(String key : sessionMap.keySet()) {
WebSocketSession wss = sessionMap.get(key);
if(wss == session) continue;
JSONObject obj = new JSONObject();
obj.put("type", "close");
obj.put("userName", userName);
wss.sendMessage(new TextMessage(obj.toJSONString()));
}
userMap.remove(session.getId());
}
/**
* JSON 형태의 문자열을 JSONObejct로 파싱
*/
private static JSONObject jsonToObjectParser(String jsonStr) throws Exception{
JSONParser parser = new JSONParser();
JSONObject obj = null;
obj = (JSONObject) parser.parse(jsonStr);
return obj;
}
}
구현체에 등록할 SocketHandler를 정의한다.
웹소켓 프로토콜은 기본적으로 Text, Binary 타입을 지원하기 때문에 필요에 따라 TextWebSocketHandler,
BinaryWebSocketHandler를 상속하여 구현해주면 된다.
WebSocketSession 파라미터는 웹소켓이 연결될 때 생기는 연결정보를 담고 있는 객체이다.
@Configuration
@EnableWebSocket// 웹소켓 활성화
public class WebSocketConfig implements WebSocketConfigurer{
@Autowired
private DevLogWebSocketHandler devLogWebSocketHandler;
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
// WebSocketHandler를 추가
registry.addHandler(devLogWebSocketHandler, "/chating");
}
}
Spring에서 웹소켓을 사용하기 위해서 클라이언트가 보내는 통신을 처리할 핸들러가 필요하다.
위에서 작성한 DevLogWebSocketHandler를 Handshake할 주소와 함께 추가한다.
주소는 PORT 뒤에 endpoint를 입력하면 된다.
ws://127.0.0.1:80/chating
@Controller
public class ChatController {
@RequestMapping("/chat")
public ModelAndView chat() {
ModelAndView mv = new ModelAndView();
mv.setViewName("chat");
return mv;
}
}
#Tomcat Server Setting
server.port=80
#ModelAndView Path Setting
spring.mvc.view.prefix=/WEB-INF/jsp/
spring.mvc.view.suffix=.jsp
#JSP to Modify Not Restart Server
server.servlet.jsp.init-parameters.development=true
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
<meta charset="UTF-8">
<title>DevLog Chating</title>
<style>
*{
margin:0;
padding:0;
}
.container{
width: 500px;
margin: 0 auto;
padding: 25px
}
.container h1{
text-align: left;
padding: 5px 5px 5px 15px;
color: #FFBB00;
border-left: 3px solid #FFBB00;
margin-bottom: 20px;
}
.chating{
background-color: #000;
width: 500px;
height: 500px;
overflow: auto;
}
.chating .me{
color: #F6F6F6;
text-align: right;
}
.chating .others{
color: #FFE400;
text-align: left;
}
.chating .start{
color: #AAAAAA;
text-align: center;
}
.chating .exit{
color: red;
text-align: center;
}
input{
width: 330px;
height: 25px;
}
#yourMsg{
display: none;
}
</style>
</head>
<script type="text/javascript">
var ws;
function wsOpen(){
//websocket을 지정한 URL로 연결
ws = new WebSocket("ws://" + location.host + "/chating");
wsEvt();
}
function wsEvt() {
//소켓이 열리면 동작
ws.onopen = function(e){
}
//서버로부터 데이터 수신 (메세지를 전달 받음)
ws.onmessage = function(e) {
//e 파라미터는 websocket이 보내준 데이터
var msg = e.data; // 전달 받은 데이터
if(msg != null && msg.trim() != ''){
var d = JSON.parse(msg);
//socket 연결시 sessionId 셋팅
if(d.type == "getId"){
var si = d.sessionId != null ? d.sessionId : "";
if(si != ''){
$("#sessionId").val(si);
var obj ={
type: "open",
sessionId : $("#sessionId").val(),
userName : $("#userName").val()
}
//서버에 데이터 전송
ws.send(JSON.stringify(obj))
}
}
//채팅 메시지를 전달받은 경우
else if(d.type == "message"){
if(d.sessionId == $("#sessionId").val()){
$("#chating").append("<p class='me'>" + d.msg + "</p>");
}else{
$("#chating").append("<p class='others'>" + d.userName + " : " + d.msg + "</p>");
}
}
//새로운 유저가 입장하였을 경우
else if(d.type == "open"){
if(d.sessionId == $("#sessionId").val()){
$("#chating").append("<p class='start'>[채팅에 참가하였습니다.]</p>");
}else{
$("#chating").append("<p class='start'>[" + d.userName + "]님이 입장하였습니다." + "</p>");
}
}
//유저가 퇴장하였을 경우
else if(d.type == "close"){
$("#chating").append("<p class='exit'>[" + d.userName + "]님이 퇴장하였습니다." + "</p>");
}
else{
console.warn("unknown type!")
}
}
}
document.addEventListener("keypress", function(e){
if(e.keyCode == 13){ //enter press
send();
}
});
}
function chatName(){
var userName = $("#userName").val();
if(userName == null || userName.trim() == ""){
alert("사용자 이름을 입력해주세요.");
$("#userName").focus();
}else{
wsOpen();
$("#yourName").hide();
$("#yourMsg").show();
}
}
function send() {
var obj ={
type: "message",
sessionId : $("#sessionId").val(),
userName : $("#userName").val(),
msg : $("#chatting").val()
}
//서버에 데이터 전송
ws.send(JSON.stringify(obj))
$('#chatting').val("");
}
</script>
<body>
<div id="container" class="container">
<h1>DevLog Chat</h1>
<input type="hidden" id="sessionId" value="">
<div id="chating" class="chating">
</div>
<div id="yourName">
<table class="inputTable">
<tr>
<th>닉네임</th>
<th><input type="text" name="userName" id="userName"></th>
<th><button onclick="chatName()" id="startBtn">채팅 참가</button></th>
</tr>
</table>
</div>
<div id="yourMsg">
<table class="inputTable">
<tr>
<th>메시지</th>
<th><input id="chatting" placeholder="보내실 메시지를 입력하세요."></th>
<th><button onclick="send()" id="sendBtn">보내기</button></th>
</tr>
</table>
</div>
</div>
</body>
</html>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- View JSP -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
<scope>provided</scope>
</dependency>
<!-- View JSP -->
<!-- json simple -->
<dependency>
<groupId>com.googlecode.json-simple</groupId>
<artifactId>json-simple</artifactId>
<version>1.1.1</version>
</dependency>
<!-- json simple -->
function chatName(){
var userName = $("#userName").val();
if(userName == null || userName.trim() == ""){
alert("사용자 이름을 입력해주세요.");
$("#userName").focus();
}else{
wsOpen();
$("#yourName").hide();
$("#yourMsg").show();
}
}
function wsOpen(){
//websocket을 지정한 URL로 연결
ws = new WebSocket("ws://" + location.host + "/chating");
wsEvt();
}
function wsEvt() {
//소켓이 열리면 동작
ws.onopen = function(e){
...
}
//서버로부터 데이터 수신 (메세지를 전달 받음)
ws.onmessage = function(e) {
...
}
}
WebSocket을 WebSocketConfig.java에 정의한 주소를 사용하여 생성하고 onopen / onmessage 이벤트에 대한 내용을 정의한다.
/* 클라이언트가 소켓 연결시 동작 */
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
log.info("{} 연결되었습니다.", session.getId());
super.afterConnectionEstablished(session);
sessionMap.put(session.getId(), session);
JSONObject obj = new JSONObject();
obj.put("type", "getId");
obj.put("sessionId", session.getId());
//클라이언트에게 메시지 전달
session.sendMessage(new TextMessage(obj.toJSONString()));
}
연결된 세션ID를 sessionMap에 저장하여 관리한다.
type은 화면에서 메세지를 구분하기 위해 사용된다.
sendMessage메소드를 사용하여 클라이언트에게 메세지를 전송한다.
//서버로부터 데이터 수신 (메세지를 전달 받음)
ws.onmessage = function(e) {
//e 파라미터는 websocket이 보내준 데이터
var msg = e.data; // 전달 받은 데이터
if(msg != null && msg.trim() != ''){
var d = JSON.parse(msg);
//socket 연결시 sessionId 셋팅
if(d.type == "getId"){
var si = d.sessionId != null ? d.sessionId : "";
if(si != ''){
$("#sessionId").val(si);
var obj ={
type: "open",
sessionId : $("#sessionId").val(),
userName : $("#userName").val()
}
//서버에 데이터 전송
ws.send(JSON.stringify(obj))
}
}
}
}
서버로부터 전달받은 메세지(type : getId)에는 type, sessionId가 존재한다.
type를 이용하여 메세지를 분기처리하고 sessionId는 화면에 설정한다.
send메소드를 사용하여 서버로 메세지(type : open)를 전송한다.
/* 클라이언트로부터 메시지 수신시 동작 */
@Override
public void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
String msg = message.getPayload();
log.info("===============Message=================");
log.info("{}", msg);
log.info("===============Message=================");
JSONObject obj = jsonToObjectParser(msg);
//로그인된 Member (afterConnectionEstablished 메소드에서 session을 저장함)
for(String key : sessionMap.keySet()) {
WebSocketSession wss = sessionMap.get(key);
if(userMap.get(wss.getId()) == null) {
userMap.put(wss.getId(), (String)obj.get("userName"));
}
//클라이언트에게 메시지 전달
wss.sendMessage(new TextMessage(obj.toJSONString()));
}
}
클라이언트로부터 메세지(type : open)를 전달받으면 handleTextMessage가 실행된다.
sessionMap에는 채팅에 참여한 멤버들의 sessionId가 존재한다.
userMap은 sessionId와 userName을 매핑시키기 위해 생성하였다.
모든 멤버들에게 클라이언트로부터 전달받은 메세지(type : open)를 전송한다.
//서버로부터 데이터 수신 (메세지를 전달 받음)
ws.onmessage = function(e) {
//e 파라미터는 websocket이 보내준 데이터
var msg = e.data; // 전달 받은 데이터
if(msg != null && msg.trim() != ''){
var d = JSON.parse(msg);
...
//새로운 유저가 입장하였을 경우
else if(d.type == "open"){
if(d.sessionId == $("#sessionId").val()){
$("#chating").append("<p class='start'>[채팅에 참가하였습니다.]</p>");
}else{
$("#chating").append("<p class='start'>[" + d.userName + "]님이 입장하였습니다." + "</p>");
}
}
}
}
sessionId값을 통해 자기 자신과 타인을 구분하여 화면에 출력한다.
function send() {
var obj ={
type: "message",
sessionId : $("#sessionId").val(),
userName : $("#userName").val(),
msg : $("#chatting").val()
}
//서버에 데이터 전송
ws.send(JSON.stringify(obj))
$('#chatting').val("");
}
보내기 버튼을 클릭하면 서버로 메세지(type:message)를 전송한다.
/* 클라이언트로부터 메시지 수신시 동작 */
@Override
public void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
String msg = message.getPayload();
log.info("===============Message=================");
log.info("{}", msg);
log.info("===============Message=================");
JSONObject obj = jsonToObjectParser(msg);
//로그인된 Member (afterConnectionEstablished 메소드에서 session을 저장함)
for(String key : sessionMap.keySet()) {
WebSocketSession wss = sessionMap.get(key);
if(userMap.get(wss.getId()) == null) {
userMap.put(wss.getId(), (String)obj.get("userName"));
}
//클라이언트에게 메시지 전달
wss.sendMessage(new TextMessage(obj.toJSONString()));
}
}
채팅에 참가한 모든 멤버에게 메세지(type:message)를 전송한다.
//서버로부터 데이터 수신 (메세지를 전달 받음)
ws.onmessage = function(e) {
//e 파라미터는 websocket이 보내준 데이터
var msg = e.data; // 전달 받은 데이터
if(msg != null && msg.trim() != ''){
var d = JSON.parse(msg);
...
//채팅 메시지를 전달받은 경우
else if(d.type == "message"){
if(d.sessionId == $("#sessionId").val()){
$("#chating").append("<p class='me'>" + d.msg + "</p>");
}else{
$("#chating").append("<p class='others'>" + d.userName + " : " + d.msg + "</p>");
}
}
...
}
}
서버로부터 전달받은 메세지(type:message)를 화면에 출력한다.
sessionId를 확인하여 css를 다르게 적용하여 본인은 우측에 타인은 좌측에 메세지가 출력되도록 하였다.
/* 클라이언트가 소켓 종료시 동작 */
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
log.info("{} 연결이 종료되었습니다.", session.getId());
super.afterConnectionClosed(session, status);
sessionMap.remove(session.getId());
String userName = userMap.get(session.getId());
for(String key : sessionMap.keySet()) {
WebSocketSession wss = sessionMap.get(key);
if(wss == session) continue;
JSONObject obj = new JSONObject();
obj.put("type", "close");
obj.put("userName", userName);
//클라이언트에게 메시지 전달
wss.sendMessage(new TextMessage(obj.toJSONString()));
}
userMap.remove(session.getId());
}
클라이언트가 웹소켓 연결을 종료하면 afterConnectionClosed가 동작한다.
sessionMap에서 해당 session을 제거하고 모든 클라이언트에게 메세지(type:close)를 전달한다.
//서버로부터 데이터 수신 (메세지를 전달 받음)
ws.onmessage = function(e) {
//e 파라미터는 websocket이 보내준 데이터
var msg = e.data; // 전달 받은 데이터
if(msg != null && msg.trim() != ''){
var d = JSON.parse(msg);
...
//유저가 퇴장하였을 경우
else if(d.type == "close"){
$("#chating").append("<p class='exit'>[" + d.userName + "]님이 퇴장하였습니다." + "</p>");
}
}
}
채팅 퇴장 메세지(type:close)를 출력한다.
댓글 영역