WebSocket - WebSocket

dragonappear·2022년 2월 1일
2

SpringBoot WebSocket

목록 보기
1/4


출처 및 참고

제목: "[Spring Boot] WebSocket과 채팅 (1)"
작성자: tistory(조용한고라니)
작성자 수정일: 2021년3월30일 
링크: https://dev-gorany.tistory.com/212
작성일: 2022년2월1일


제목: "Spring WebSocket 소개"
작성자: tistory(supawer0728)
작성자 수정일: 2018년3월30일
링크: https://supawer0728.github.io/2018/03/30/spring-websocket
작성일: 2022년2월1일

소켓 통신

소켓통신이란? https://helloworld-88.tistory.com/215
HTTP 통신과 소켓 통신의 차이점

  • 자주 데이터를 주고 받아야 하는 환경에서는 소켓 통신, 아니라면 HTTP 통신을 주로 사용한다.

  • HTTP 통신은 사용자가 서버에 요청을 보내야 응답을 받을 수 있는 단방향 통신인 반면, 소켓 통신은 양방향 통신이다.

  • 소켓 통신은 계속해서 Connection을 들고 있기 때문에 HTTP 통신에 비해 많은 리소스가 소모된다.


WebSocket

  • 기존 단방향의 HTTP 프로토콜과 호환되어 양방향 통신을 제공하기 위해 개발된 프로토콜

  • 일반 Socket 통신 달리 HTTP 포트(80) 을 사용하므로 방화벽에 제약이 없으며, 통상 WebSocket으로 불린다.

  • 접속까지는 HTTP 프로토콜을 이용하고, 그 이후 통신은 자체적인 WebSocket 프로토콜로 통신하게 된다.

  • 웹 소켓은 HTTP(hyper text transfer protocol)를 사용하는 네트워크 데이터 통신의 단점을 보완하는데 그 목적이 있다.

    • HTTP는 HTML이라는 각종 데이터를 운반하기 위한 프로토콜로 다음과 같이 동작한다.

  • 모든 HTTP를 사용한 통신은 클라이언트가 먼저 요청을 보내고, 그 요청에 따라 웹 서버가 응답하는 형태이며 웹 서버는 응답을 보낸 후 브라우저와의 연결을 끊는다.

  • 양쪽이 데이터를 동시에 보내는 것이 아니기 때문에 이러한 통신을 반이중 통신(Half Duplex)라고 한다.

  • 사실 HTTP만으로도 원하는 정보를 송수신할 수 있었지만, 인터넷이 발전함에 따라 더욱 더 원하는 것들이 다양해졌다.

    • 예를 들어, 클라이언트가 먼저 요청을 하지 않아도 서버가 먼저 데이터를 보내거나, 표준 TCP/IP 통신을 사용해 특정 서버와 통신을 하는 등 원하는 것이 늘어가자 그것을 이루고자 많은 플러그인 및 웹 기술이 개발되었다.

Polling,Long Polling,Streaming

웹소켓이 존재하기 전에는 Polling이나 Long Polling,Streaming 등의 방식으로 해결했었다.

Polling

  • 클라이언트가 평범한 HTTP Request를 서버로 계속 요청해 이벤트 내용을 전달 받는 방식

  • 가장 쉬운 방법이지만 클라이언트가 지속적으로 Request를 요청하기 때문에 클라이언트의 수가 많아지면 서버의 부담이 급증한다.

  • HTTP Request Connection을 맺고 끊는 것 자체가 부담이 많은 방식이고, 클라이언트에서 실시간 정도의 빠른 응답을 기대하기 어렵다.

Long Polling

  • 클라이언트에서 서버로 일단 HTTP Request를 요청한다. 이 상태로 계속 기다리다가 서버에서 해당 클라이언트로 전달할 이벤트가 있다면 그 순간 Response 메시지를 전달하여 연결이 종료된다.

  • 곧이어 클라이언트가 다시 HTTP Request를 요청해 서버의 다음 이벤트를 기다리는 방식이다

  • Polling보다 서버의 부담이 줄겠으나, 클라이언트로 보내는 이벤드들의 시간간격이 좁다면 polling과 별차이 없게 되며, 다수의 클라이언트에게 동시에 이벤트가 발생될 경우 서버의 부담이 급증한다.

Streaming

  • Long Polling과 마찬가지로 클라이언트-> 서버로 HTTP Request를 요청한다

  • 서버->클라이언트로 이벤트를 전달할 때 해당 요청을 해제하지 않고 필요한 메세지만 보내기(Flush)를 반복하는 방식

  • Long Polling과 비교하여 서버에 메세지를 보내지 않고도 다시 HTTP Request 연결을 하지 않아도 되어 부담이 경감된다고 한다.

WebSocket

  • 이처럼 HTTP 통신의 특징인 ( 연결 -> 연결 해제) 때문에 효율이 많이 떨어지게 되고, 웹 브라우저 말고 외부 플러그인이 항상 필요하게 되었다.

    • 그래서 이런 상황을 극복하고자 2014년 HTML5에 웹소켓을 포함하게 되었다.
  • 웹소켓은 클라이언트가 접속 요청을 하고 웹 서버가 응답한 후 연결을 끊는 것이 아닌 Connection을 그대로 유지하고 클라이언트의 요청없이도 데이터를 전송할 수 있는 프로토콜이다.

    • 프로토콜의 요청은 [ws://~]로 시작한다.
  • 웹소켓은 HTTP 환경에서 전이중 통신(Full Duplex, 2-way communication)을 지원하기 위한 프로토콜이며 RFC6455에 정의되어 있다.

  • HTTP 프로토콜에서 HandShaking을 완료한 후 ,HTTP로 동작하지만, HTTP와는 다른 방식으로 통신한다.

웹소켓의 장점

  • 기존의 TCP Socket과 다른 점은 최초 접속이 일반 HTTP Request를 통해 HandShaking 과정을 통해 이뤄진다는 점이다.

  • HTTP Request를 그대로 사용하기 때문에 기존의 80,443 포트로 접속을 하므로 추가 방화벽을 열지 않고도 양방향 통신이 가능하고, HTTP 규격인 CORS 적용이나 인증 등 과정을 기존과 동일하게 가져갈 수 있는 것이 장점이다.

웹소켓은 서비스를 동적으로 만들어주지만, Ajax,Streaming,Long polling 기술이 더 효과적일 수 도있다.

  • 예를 들어, 변경 사항의 빈도가 자주 일어나지 않고, 데이터의 크기가 작은 경우 Ajax, Streaming, Long polling 기술이 더 효과적일 수 있다.

실시간성을 보장해야 하고, 변경 사항의 빈도가 잦다면, 또는 짧은 대기 시간,고주파수,대용량의 조합인 경우 WebSocket이 좋은 해결책이 될 수 있다.

뉴스나 메일,SNS 피드는 동적으로 업데이트 하는 것은 맞지만 몇 분마다 업데이트 하는 것이 좋다.

반면 협업,게임,금융 앱은 훨씬 더 실시간에 근접해야 한다.

Browser별 지원 현황

출처: https://github.com/sockjs/sockjs-client#supported-transports-by-browser-html-served-from-http-or-https


WebSocket 접속 과정


출처: https://blog.naver.com/eztcpcom/220070508655

  • 웹소켓을 이용하여 서버와 클라이언트가 통신을 하려면 먼저 웹소켓 접속 과정을 거쳐야 한다.

  • 웹소켓 접속 과정은 TCP/IP 접속, 웹소켓 열기 HandShake 과정으로 나눌 수 있다.

  • 웹소켓도 TCP/IP 위에서 동작하므로, 서버와 클라이언트는 웹소켓을 사용하기 전에 서로 TCP/IP 접속이 되어있어야 한다.

  • TCP/IP 접속이 완료된 후 서버와 클라이언트는 웹 소켓 열기 HandShake 과정을 시작한다.

웹소켓 열기 HandShake

  • 웹소켓 열기 핸드쉐이크는 클라이언트가 먼저 핸드쉐이크 요청을 보내고 이에 대한 응답을 서버가 클라이언트로 보내는 구조이다.

  • 서버와 클라이언트는 HTTP 1.1 프로토콜을 사용하여 요청과 응답을 보낸다.

HandShake Request

GET /chat HTTP/1.1
Host: server.gorany.org
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://localhost:8080
Sec-WebSocket-Protocol: v10.stomp,v11.stomp,my-team-custom
Sec-WebSocket-Version: 13
Header NamedivDesc
GETRequired요청 명령어는 GET을 사용해야 하며, HTTP 버전은 1.1 이상이어야 한다.
HostRequired웹소켓 서버의 주소
UpgradeRequiredWebSokcet이라는 단어를 사용해야 한다. 대소문자는 구분X
ConnectionRequiredUpgrade라는 단어를 사용해야 한다. 대소문자는 구분X
Sec-WebSocket-KeyRequired길이가 16바이트인 임의의 선택된 숫자를 base64 인코딩한 값이다.
OriginRequired클라이언트로 웹 브라우저를 사용하는 경우 필수항목으로, 클라이언트의 주소
Sec-WebSocket-VersionRequired13을 사용한다.
Sec-WebSocket-ProtocolOption클라이언트가 사용하고 싶은 하위 프로토콜 이름을 명시한다.
Sec-WebSocket-ExtensionsOption클라이언트가 사용하고 싶은 추가 옵션을 기술한다.

HandShake Response

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Header NamedivDesc
HTTPRequiredHTTP 버전은 1.1이며, 클라이언트로부터의 요청이 이상 없는 경우 101을 상태 코드로 사용한다.
UpgradeRequiredWebSocket이라는 단어를 사용해야 한다. 대소문자는 구분 X
ConnectionRequiredUpgrade라는 단어를 사용해야 한다. 대소문자는 구분 X
Sec-WebSocket-AcceptRequired클라이언트로부터 받은 Sec-WebSocket-Key를 사용하여 계산된 값이다.
Sec-WebSocket-ProtocolOption서버에서 서비스하는 하위 프로토콜을 명시한다. 클라이언트가 요청하지 않는 하위 프로토콜을 명시하면 HandShake는 실패한다.
Sec-WebSocket-ExtensionsOption서버가 사용하는 추가 옵션을 기술한다. 클라이언트가 요청하지 않는 추가 옵션을 명시하면 HandShake는 실패한다.

주의
위 테이블에 명시된 헤더 중 필수는 반드시 사용해야 하며, 특정한 값이 명시된 헤더는 그 값만 사용해야 한다.


WebSocket 구현 테스트

1. 라이브러리 추가

implementation 'org.springframework.boot:spring-boot-starter-websocket'

2. WebSocket Handler

  • 소켓 통신은 서버와 클라이언트가 1:N의 관계를 맺는다.
    • 즉 하나의 서버에 다수 클라이언트가 접속할 수 있다.
  • 따라서 서버는 다수의 클라이언트가 보낸 메세지를 처리할 핸들러가 필요하다.
    • 텍스트 기반의 채팅을 구현해볼 것이므로TextWebSocketHandler 을 상속받는다
    • 클라이언트로 부터 받은 메시지를 로그 출력하고 클라이언트에게 환영하는 메시지를 보내는 역할을 한다.
@Component
@Slf4j
public class ChatHandler extends TextWebSocketHandler {

    private static List<WebSocketSession> list = new ArrayList<>();

    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        String payload = message.getPayload();
        log.info("payload : {}", payload);

        for (WebSocketSession webSocketSession : list) {
            webSocketSession.sendMessage(message);
        }
    }

    /**
     * 클라이언트가 접속 시 호출되는 메서드
     * @param session
     * @throws Exception
     */
    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        list.add(session);
        log.info("[{}] 클라이언트 접속", session);
    }

    /**
     * 클라이언트가 접속 해제 시 호출되는 메서드
     * @param session
     * @param status
     * @throws Exception
     */
    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        log.info("[{}] 클라이언트 접속 해제", session);
        list.remove(session);
    }
    }
}

payload
페이로드란 전송되는 데이터를 의미한다. 데이터를 전송할때 헤더와 META 데이터, 에러 체크 비트 등과 같은 다양한 요소들을 함께 보내 데이터 전송 효율과 안정성을 높히게 된다. 이 때, 보내고자 하는 데이터 자체를 의미하는 것이 페이로드이다. 예를 들어 택배 배송을 보내고 받을 때 택배 물건이 페이로드이고 송장이나 박스 등은 페이로드가 아니다.

다음 JSON에서 페이로드는 "data"이다. 나머지는 통신을 하는데 있어 용이하게 해주는 부가적 정보들이다.

{
"status":
"from":"localhost",
"to":"http://melonicedlatte.com/chatroom/1",
"method":"GET",
"data":{"message":"There is a cutty dog!"}
}

3. WebSocket Config

  • 핸들러를 이용해 WebSocket을 활성화하기 위한 Config를 작성하자

  • @EnableWebSocket 어노테이션을 사용해 WebSocket을 활성화 해야 한다.

  • WebSocket에 접속하기 위한 EndPoint/chat으로 설정하고, 도메인이 다른 서버에서도 접속 가능하도록 CORS : setAllowedOrigins("*"); 를 추가해준다.

  • 이제 클라이언트가 ws://localhost:8080/chat으로 커넥션을 연결하고 메세지 통신을 할 수 있는 준비를 마쳤다.

@EnableWebSocket
@RequiredArgsConstructor
@Configuration
public class WebSocketConfig implements WebSocketConfigurer {
    private final ChatHandler chatHandler;

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(chatHandler, "ws/chat")
                .setAllowedOriginPatterns("*");
    }
}

CORS
교차 출처 리소스 공유(Cross-Origin Resource Sharing, CORS)는 추가 HTTP 헤더를 사용하여 한 출처에서 실행 중인 웹 어플리케이션이 다른 출처의 선택한 자원에 접근할 수 있는 권한을 부여하도록 브라우저에 알려주는 체제이다. 웹 어플리케이션은 리소스가 자신의 출처(domain,protocol,port)와 다를 때 교차 출처 HTTP 요청을 실행한다. 이에 대한 응답으로 서버는 Access-Control-Allow-Origin 헤더를 다시 보낸다.


출처 : https://developer.mozilla.org/ko/docs/Web/HTTP/CORS

Endpoint
메서드는 같은 URL들에 대해서도 다른 요청을 하게끔 구별하게 해주는 항목, 각각 GET, PUT, DELETE 메서드에 따라 다른 요청을 하는 것을 알 수 있다. Endpoint란 API가 서버에서 자원(resource)에 접근할 수 있도록 하는 URL이다.


출처: https://velog.io/@kho5420/Web-API-%EA%B7%B8%EB%A6%AC%EA%B3%A0-EndPoint

4. ChatController

@Controller
@Slf4j
public class ChatController {

    @GetMapping("/chat")
    public String chatGet(Model model) {

        log.info("me.dragonappear.websocket.chat.ChatController.chatGet");

        model.addAttribute("name", UUID.randomUUID().toString());
        return "chat";
    }
}

5. chat.html 그리고 JS

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">

<head>
    <title>hello</title>
</head>


<body>
<div class="container">

<div class="col-6">
    <label><b>채팅방</b></label>
</div>

<div>
    <div id="msgArea" class="col"></div>

    <div class="col-6">
        <div class="input-group mb-3">
            <input type="text" id="msg" class="form-control" aria-label="Recipient's username" aria-describedby="button-addon2">

            <div class="input-group-append">
                <button class="btn btn-outline-secondary" type="button" id="button-send">전송</button>
            </div>

        </div>
    </div>
</div>
</div>

<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
<script th:inline="javascript">
            $(document).ready(function(){

            const username = [[${name}]];

            $("#disconn").on("click", (e) => {
                disconnect();
            })

            $("#button-send").on("click", (e) => {
                send();
            });

            const websocket = new WebSocket("ws://localhost:8080/ws/chat");

            websocket.onmessage = onMessage;
            websocket.onopen = onOpen;
            websocket.onclose = onClose;

            function send(){

                let msg = document.getElementById("msg");

                console.log(username + ":" + msg.value);
                websocket.send(username + ":" + msg.value);
                msg.value = '';
            }

            //채팅창에서 나갔을 때
            function onClose(evt) {
                var str = username + ": 님이 방을 나가셨습니다.";
                websocket.send(str);
            }

            //채팅창에 들어왔을 때
            function onOpen(evt) {
                var str = username + ": 님이 입장하셨습니다.";
                websocket.send(str);
            }

            function onMessage(msg) {
                var data = msg.data;
                var sessionId = null;
                //데이터를 보낸 사람
                var message = null;
                var arr = data.split(":");

                for(var i=0; i<arr.length; i++){
                    console.log('arr[' + i + ']: ' + arr[i]);
                }

                var cur_session = username;

                //현재 세션에 로그인 한 사람
                console.log("cur_session : " + cur_session);
                sessionId = arr[0];
                message = arr[1];

                console.log("sessionID : " + sessionId);
                console.log("cur_session : " + cur_session);

                //로그인 한 클라이언트와 타 클라이언트를 분류하기 위함
                if(sessionId == cur_session){
                    var str = "<div class='col-6'>";
                    str += "<div class='alert alert-secondary'>";
                    str += "<b>" + sessionId + " : " + message + "</b>";
                    str += "</div></div>";
                    $("#msgArea").append(str);
                }
                else{
                    var str = "<div class='col-6'>";
                    str += "<div class='alert alert-warning'>";
                    str += "<b>" + sessionId + " : " + message + "</b>";
                    str += "</div></div>";
                    $("#msgArea").append(str);
                }
            }
            })
</script>

</body>

</html>

문제점

  • 채팅방이 단 하나이다

  • 웹소켓을 지원하지 않는 브라우저에서는 동작하지 않는다.

  • DB에 저장해야 한다.

  • 메시지 암호화

0개의 댓글