websocket 채팅 - 진행중

2dean·2023년 8월 23일
0

study

목록 보기
1/1
post-thumbnail

WebSocket ?

WebSocket은 서버와 클라이언트 간에 양방향 통신을 가능하게 하는 프로토콜이다.

HTTP 프로토콜에서는 클라이언트에서 요청을 보내면 서버에서 응답을 하고 연결이 끊어졌지만, WebSocket을 사용하면 클라이언트와 서버 간에 계속해서 연결을 유지하면서 양방향으로 데이터를 주고받을 수 있다.

클라이언트에서는 WebSocket 객체를 만들고 서버와의 연결을 설정한 다음 데이터를 전송할 수 있다.

서버에서는 클라이언트와의 연결을 수신하고 연결을 유지하면서 데이터를 전송할 수 있다.

요구사항

  • 웹소켓을 활용해 채팅방
  • ID를 입력받아서 채팅방입장
  • 방은 1개
  • 입,퇴장 안내
  • 사진 전송
  • 현재 접속자 명단 보임
  • DB에 데이터 저장 안 함

구조

라이브러리 추가

  • Spring web
  • Thymeleaf
  • Websocket
  • Lombok
  • json-simple

java

java
└─ WEBSOCKET
	│  WebsocketApplication.java
    │
    ├─config
    │      WebSocketConfig.java
    │
    ├─controller
    │      ChatController.java
    │
    └─handler
    		ChatHandler.java

resources

resources
	└─ static
    │    ├─ css
    │    │    └─ styles.css
    │    └─ js
    │         └─ main.js
    └─ templates
    		├─ websocket_index.html
   			└─ websocket_chatroom.html

websocket 통신 흐름

  • 인풋 태그에 메시지를 입력하고 send 버튼을 누름
  • send() 에서 타입, 메시지, 사용자 이름으로 구성된 객체를 서버로 보냄
  • 서버의 websocketHandler에서 해당 객체를 받음
  • 서버에서 클라이언트로 보낼 객체를 만듦 - 타입, 메시지, 사용자 수 등 필요한 데이터를 담은 후
  • 현재 연결된 session들에 해당 객체를 보내줌
  • 클라이언트에서는 서버에서 받은 객체의 타입에 따라 처리해줌

구현하기

서버

웹소켓 핸들러 만들기 - ChatHandler.java

웹소켓 프로토콜은 기본적으로 Text, Binary 타입을 지원하기 때문에 필요에 따라 * TextWebSocketHandler, BinaryWebSocketHandler 를 상속하여 구현해주면 된다.

import lombok.extern.slf4j.Slf4j;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.BinaryMessage;
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 java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;


@Slf4j
@Component
public class ChatHandler extends TextWebSocketHandler {
    Map<String, WebSocketSession> sessionMap = new HashMap<>(); //웹소켓 세션을 담아둘 맵
    Map<String, String> userMap = new HashMap<>();	//사용자

    /* 클라이언트가 소켓 연결시 동작 */
    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        log.info("SESSION > {} 연결되었습니다.", session.getId());
        log.info("SESSION.toString() > {}",session.toString());

        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 handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {

        String msg = message.getPayload();
        log.info("===============Message=================");
        log.info("{}", msg);
        log.info("===============Message=================");
        JSONObject obj = jsonToObjectParser(msg);


        for(String key : sessionMap.keySet()) {
            // 세션을 찾아서
            WebSocketSession wss = sessionMap.get(key);

            if(userMap.get(wss.getId()) == null) {
                userMap.put(wss.getId(), (String)obj.get("userName")); // 세션 ID - userName 저장
            }

            JSONArray usersArray = new JSONArray();
            usersArray.addAll(userMap.values());
            obj.put("users", usersArray);
            obj.put("userCount", usersArray.size());

            //클라이언트(모든접속자) 에게 메시지 전달
            wss.sendMessage(new TextMessage(obj.toJSONString()));
        }

        log.info("클라이언트로 보낼 obj : {}" , obj);
    }


    /* 바이너리 메시지 발송 */
    @Override
    public void handleBinaryMessage(WebSocketSession session, BinaryMessage message) {
        //ByteBuffer 객체는 메시지의 실제 데이터를 담고 있음
        ByteBuffer byteBuffer = message.getPayload();

		// 데이터 확인
        log.info("===============handleBinaryMessage=================");
        log.info("SESSION {}", session);
        log.info("BYTE BUFFER {}", byteBuffer);

		//1. 클라이언트에 이미지만 전달 할 경우
        /*
        for (String key : sessionMap.keySet()) {
              WebSocketSession wss = sessionMap.get(key);
              try {
                  wss.sendMessage(new BinaryMessage(byteBuffer)); //버퍼 발송
              } catch (IOException e) {
                  e.printStackTrace();
              }
          } 
          */

		// 2. 클라이언트에 이미지와 그 외의 정보를 함께 넘겨줄 경우
        JSONObject obj = new JSONObject();
        obj.put("type", "file");

        obj.put("userCount", sessionMap.size()); // 사용자 수
        obj.put("userName", userMap.get(session.getId())); // 사용자 정보
        obj.put("sessionId", session.getId()); // 세션 ID 등의 추가 정보 추가
        obj.put("imageData", Base64.getEncoder().encodeToString(byteBuffer.array())); // 이미지 데이터를 Base64로 인코딩하여 추가

        for (String key : sessionMap.keySet()) {
            WebSocketSession wss = sessionMap.get(key);
            try {
                wss.sendMessage(new TextMessage(obj.toString())); // JSON 객체를 문자열로 변환하여 메시지 전송
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        
        log.info("클라이언트로 보낼 obj : {}" , obj);
    }


    /* 클라이언트가 소켓 종료시 동작 */
    @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);
            obj.put("userCount", sessionMap.size());

            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;
    }



}

handleBinaryMessage()

원래 이미지만 전달 하려면 아래와 같이하면 되는데
이미지

@Override
	public void handleBinaryMessage(WebSocketSession session, BinaryMessage message) {
		//바이너리 메시지 발송
	    ByteBuffer byteBuffer = message.getPayload();
	
		//이미지를 발송한다.
        for (String key : sessionMap.keySet()) {
            WebSocketSession wss = sessionMap.get(key);
            try {
                wss.sendMessage(new BinaryMessage(byteBuffer)); //버퍼 발송
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        
        
	} // method

핸들러 등록 - WebSocketConfig.java

  • 핸들러 등록하는 class
  • 클라이언트에서 ws://localhost:8080/chat으로 요청이 들어오면 websocket 통신을 진행
@Slf4j
@Configuration
@EnableWebSocket // 웹소켓 활성화
@RequiredArgsConstructor
public class WebSocketConfig implements WebSocketConfigurer { 
	//WebSocket 요청 처리를 구성하는 콜백 메서드를 정의

    private final ChatHandler chatHandler; // 

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        // WebSocketHandler 추가
        registry.addHandler(chatHandler, "/chat")
        					.setAllowedOriginPatterns("*"); //도메인이 다른 서버에서도 접속 가능하도록 CORS : setAllowedOprigins(" * ");를 추가해줍니다.
    }
}

클라이언트

웹소켓 연결

  • WebSocketConfig 에서 설정한 것 처럼 ws://localhost:8080/chat 으로 요청하면
    웹소켓이 연결된다.
const socket = new WebSocket('ws://localhost:8080/chat');

// 웹 소켓 연결 이벤트
socket.onopen = function (event) {
    console.log("웹소켓 서버와 연결에 성공했습니다.");
};
profile
냅다 써보는 공부의 흔적😇

0개의 댓글