ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Socket.io] 웹소켓으로 그림판 채팅 만들기
    Studying/JavaScript 2022. 8. 10. 21:31

    개요

    소켓을 이용하면 양방향 실시간 소통 시스템을 구현할 수 있습니다. 그중에서도 웹 소켓을 사용하여 실시간 그림판 및 채팅 시스템은 간단하게 구현해보았습니다.

     

    Socket 이란

    소켓이란, 네트워크 상에서 돌아가는 두 개의 프로그램 간 양방향 통신을 뜻합니다.

     

    소켓에 대한 특징은 아래와 같습니다.

    • 소켓(Socket)은 프로토콜, IP, 포트 3요로 정의된다.
    • 떨어져 있는 두 호스트를 연결해주는 인터페이스 역할을 한다.
    • 소켓을 통해 데이터 통로가 생성 되면 데이터를 주고받을 수 있다.
    • 요청 없이 응답을 받아올 수 있다.
    • 역할에 따라 서버 소켓, 클라이언트 소켓으로 나뉜다.
    • 통신 URL: ws://www.example.com/

    강조 표시한 부분만 봐도 소켓을 이해하는데 80%는 온 것 같습니다. 그러니까 두 호스트가 데이터 통로로 "요청 없는 응답" 데이터를 주고받나봅니다. 그러니까 A와 B가 카톡으로 대화를 할 때 A가 GET 요청을 하지 않았는데도 B의 답장이 오고, B가 GET 요청 하지도 않았는데 A의 답장이 실시간으로 오는군요.

     

     

    웹 소켓 통신

    웹 통신은 지금까지 HTTP 통신을 사용해왔습니다. HTTP란 프로토콜, 일종의 약속으로 웹에서 클라이언트와 서버가 통신하는 방법을 정의한 규약이죠. 사용자가 URL 요청을 보내면 서버에서 정보를 가져다주는 게 바로 HTTP 통신입니다.

     

    그런데, HTTP 통신은 클라이언트의 요청이 있어야 서버로부터 응답을 받았습니다. 만약 실시간으로 변동하는 데이터(주식, 채팅 등)를 받아오고 싶으면 어떡해야 할까요?

     

    밀리 초마다 주가 변동율을 요청하여 가져온다 → 말도 안 된다

    상대방이 답장을 보냈을 것 같은 타이밍에 데이터를 갱신한다 → 말도 안 된다

     

    이런 실시간 통신을 연결해주는 게 바로 웹 소켓(Web Socket)입니다.

     

    웹 소켓을 이용하면 하나의 HTTP 접속으로 양방향 메세지를 주고 받을 수 있습니다.

     

    여기서 하나의 접속이란, 접속을 확립할 때 HTTP를 사용하고 그 후 통신은 모두 WebSocket 프로토콜로 이루어진다는 뜻입니다.

     

    장시간 접속을 전제로 하기 때문에 접속한 상태라면 자유롭게 클라이언트↔서버 데이터 송신이 가능합니다.

     

    결론적으로, HTTP와 웹 소켓의 가장 큰 차이점은

    클라이언트가 요청을 보내야지만 Server가 응답하는 HTTP 단방향 통신이 아닌,

    웹 소켓은 서버에서도 클라이언트를 인지하는 상태로 양방향 통신이 가능하다는 것입니다.

     

     

     

    소켓 종류

    소켓 종류에는 아래 두 가지가 있습니다.

    스트림(TCP)

    • TCP: Transmission Control Protocol 을 사용하는 양방향 / 연결 지향 방식 소켓.
    • 바이트 스트림을 전송한다.
    • 데이터의 순서를 보장한다.(송신 된 순서대로 중복되지 않게 데이터를 수신. 단, 오버헤드가 발생)
    • 다량의 데이터 전송에 적합하다.

     

    데이터그램(UDP)

    • UDP: User Datagram Protocol 비연결형 소켓.
    • 데이터 크기에 제한이 있다.
    • 확실한 전달이 보장되지 않으며 데이터가 손실 되어도 오류가 발생하지 않는다.
    • 주로 실시간 멀티미디어 정보(전화 등)를 처리하기 위해 사용 된다.

     

     

     

     

    소켓 통신 흐름

    소켓 통신 흐름은 아래 그림과 같습니다. 소켓에는 클라이언트 소켓, 서버 소켓 두 가지가 존재합니다.

     

     

    소켓 통신 흐름

     

    서버

    클라이언트 소켓의 연결 요청을 대기하고, 연결 요청이 오면 소켓을 생성하여 통신합니다.

     

    다음과 같은 메서드를 순서대로 사용하게 됩니다.

    • socket() : 소켓을 생성한다.
    • bind() : IP 및 포트 번호를 설정한다.
    • listen() : 클라이언트 접근 요청에 대해 수신 대기열을 만든다. 몇 개의 클라이언트를 대기할지 결정한다.
    • accept() : 클라이언트와 연결을 기다린다.

     

    클라이언트

    실제로 데이터 송수신이 일어나는 장소입니다.

    • socket() : 소켓을 생성한다.
    • connect() : 통신할 서버의 IP와 포트 번호에 통신을 시도한다.
    • 통신을 시도하면 서버에서 accept() 함수를 이용하여 클라이언트의 socket descriptor를 반환한다.
    • socket descriptor를 통해 클라이언트↔서버 간 read()/write()를 수행한다.

     

     

     

     

    Socket을 사용한 간단한 채팅 프로그램 만들기

    웹 소켓 서버에는 다양한 종류(아파치, php, java, js, ruby, node.js)가 있습니다. 그 중에서도 오늘은 node.js의 Socket.IO 를 활용하여 간단한 채팅 프로그램을 만들어보고자 합니다.

     

    Socket.io는 자바스크립트를 이용하여 거의 모든 웹 브라우저에서 실시간 웹을 구현하도록 만든 기술입니다.

    Socket.io 홈페이지에 들어가면 클라이언트-서버 구현 방법에 대해 친절하게 제시되어 있습니다.

     

     

    Socket 주고받기 테스트

    (js_workspace > js04 > ex07Socket.html)

     

    설치 준비

    터미널을 열어서 Socket.io 라이브러리를 설치합니다.

    npm i -S socket.io

     

    서버 측 (server.js)

    서버에 socket.io 모듈을 불러옵니다.

    const http = require('http');
    const express = require('express');
    const app = express();
    const cors = require('cors');
    const socketio = require('socket.io');

     

    서버를 연결합니다.

    const server = http.createServer(app);
    server.listen(3000, ()=>{
        console.log("run on Server : http://localhost:3000");
    });
    
    const io = socketio.listen(server);
    • socketio.listen(server) : http, express가 같은 3000번 포트를 사용하도록 설정 하듯이, socket 도 같은 포트를 결합시켜야 합니다. 서버의 socket 객체를 반환합니다.

     

     

    socket.io 모듈로 웹 소켓 처리

     

     

    클라이언트에 소켓이 정상적으로 연결 되면 이제 메세지를 작성하고 수신 받아 봅시다.

    io.sockets.on('connection', function(socket) { // socket - 클라이언트 소켓
        console.log(">>>>> 클라이언트 소켓 접속!!!");
    
        socket.emit('news', 'hello');
        socket.on('client-message', function(data) {
            console.log("client message : ", data);
            io.sockets.emit('news', data);
        });
    });
    • socket.on(event_name, handler) : 클라이언트로부터 메세지를 수신합니다.
    • socket.emit(event_name, msg) : 메세지를 전송한 클라이언트에게 메세지를 전송한다. 전송 된 메세지는 클라이언트의 해당 이벤트 리스너에서 처리한다.

     

    클라이언트가 soket.io 서버에 접속하면 connection 이벤트가 발생합니다. 이때 서버에서 실행 할 connection event handler를 두 번째 인자에 작성하게 되고, 위 코드에서는 '>>>>> 클라이언트 소켓 접속!!!' 이라는 문자열을 콘솔에 출력합니다.

     

    이벤트 핸들러 함수의 인자로 전달된 건 클라이언트 socket 객체 입니다. 클라이언트 socket 객체는 해당 클라이언트와 상호작용 하기 위해 필요한 기본 객체입니다.

     

    반대로 서버 socket 객체는 연결 된 전체 클라이언트와 상호작용을 위한 객체입니다.

     

    그 외 모든 메세지 전송 메서드는 아래와 같습니다.

    Method  Description
    io.emit 현재 접속 중인 모든 클라이언트에게 메세지를 전송한다.
    socket.emit 메세지를 전송한 클라이언트에게만 메세지를 전송한다.
    socket.broadcast.emit 메세지를 전송한 클라이언트 제외 모든 클라이언트에게 전송한다.
    io.to(id).emit 특정 클라이언트에게만 전송한다. socket 객체의 id 속성 값을 인자로 넣는다.

     

     

     

    클라이언트  (ex07Socket.html)

    socket.io 를 사용하려면 먼저 클라이언트 측에서 CDN을 추가해야 합니다. 첫 번째/두 번째 방법 중 골라 사용하면 된다네요.

    <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.3.0/socket.io.js"></script>
    <script src="http://localhost:3000/socket.io/socket.io.js"></script>

    두 번째 CDN의 경우 localhost에서 기본으로 지원해주는 socket.io CDN 입니다. 더 정확하다고 보면 된다고 합니다.

     

     

    이제 클라이언트 측에도 소켓을 생성하고, connect() 를 사용하여 서버 소켓과 연결합니다.

    const socket = io.connect('http://localhost:3000');
            socket.on('news', function(data){
                console.log("server data : " + data);
                socket.emit('client-message',"good afternoon" )
            });
    • socket.on(event_name, msg) : 클라이언트가 서버에게 메세지를 보냅니다.

     

    그런 다음 html 파일을 오픈하면 터미널에 아래와 같이 클라이언트 소켓이 잘 실행된 걸 알 수 있습니다.

     

     

     

    클라이언트 콘솔 화면

     

    클라이언트 브라우저의 콘솔 화면을 보면 서버 측에서 보낸 이벤트가 잘 전송 된 것을 확인할 수 있습니다.

     

     

    클라이언트&rarr;서버 메세지 수신

     

    서버&rarr;클라이언트 메세지 수신

     

    서버 또한 클라이언트가 전송한 메세지를 정상적으로 받았다는 걸 확인할 수 있습니다(good afternoon)

    서버는 클라이언트에게 받은 메세지를 다시 emit(data)를 통해 전송했는데, 그 결과로 클라이언트의 브라우저 콘솔에 서버가 보낸 메세지가 출력된 게 보입니다.

     

     

     

     

    실시간 메세지 주고받기

    (js_workspace > js04 > ex08.Socket.html)

    서버는 위 코드에서 변동 없음.

     

    그럼 이제 실시간으로 메세지를 주고 받는 채팅 기능을 구현해 봅시다.

     

    클라이언트 측(ex08.Socket.html)

     

    서버 실행 전

     

     

    UI 생김새는 위와 같습니다. 아직 서버를 실행하기 전입니다.

     

    우선 전송버튼, 메세지 입력란, 메세지 출력 박스에 접근하는 변수를 생성합니다.

    let sendBtn = document.getElementById('sendBtn');
    let messageInput = document.querySelector("#messageInput");
    let messageBox = document.querySelector("#messageBox");

     

    이제 클라이언트 소켓을 연결하겠습니다.

    const socket = io.connect('http://localhost:3000');
    socket.on('connect', function(data){
        console.log("서버 소켓과 연결 됨!");
        messageBox.innerHTML += "<br>서버 소켓과 연결되었습니다."
    });

     

    만약 정상적으로 서버 소켓과 연결 되었다면 메세지 출력 박스에 '서버 소켓과 연결 되었습니다' 문구를 출력해야 합니다.

     

     

    서버 실행 후

     

    서버를 실행하니 위와 같이 메세지가 나타난 걸 볼 수 있네요!

     

    이제 서버-클라이언트가 메세지를 주고받는 부분만 작성하면 됩니다!

     

    아래래 함수는 전송 버튼 클릭 이벤트 리스너입니다.

    sendBtn.onclick = function(e){
        var message = messageInput.value;
        socket.emit('client-message', {sender:'user01',
        message:message});
    }

     

    메세지 입력란에 입력 된 value 값을 message 변수에 저장한 뒤, 연결 된 서버 소켓에게 emit() 으로 전송합니다.

     

    위에서 작성했던 서버를 다시 생각해보면, 서버 소켓은 'client-message' 이벤트를 받았을 때

     

    1. 메세지 내용을 console.log("client-message : ", data); 로 출력하고

    2. io.socket.emit('news', data);모든 클라이언트에게 news라는 이벤트 메세지를 전송했습니다. 보내는 내용은 받은 내용과 동일합니다.

     

     

    이제 클라이언트→서버에게 hello world 를 메세지로 보내면

     

     

     

     

    위와 같이 console.log가 제대로 찍혀 나오네요! 이로서 양방향 소통이 정상적으로 이루어지고 있음을 알 수 있습니다.

     

     

     

    그럼 이제 클라이언트도 서버에게 받은 메세지를 처리하는 핸들러가 필요할 거 같습니다.

    socket.on('news', function(data) {
        messageBox.innerHTML += "<br>"+data['sender']+" : "
        +data['message']
    });

     

    서버가 답장(?)을 보낼 때 이벤트명은 'news' 이므로 해당 이벤트에 대한 핸들러를 만들어줍니다.

    그리고 메세지 출력 박스에 서버에게서 받은 메세지를 출력합니다.

     

    코드 상 클라이언트 본인이 보낸 메세지가 출력되는 것과 같은 거죠.

     

     

     

    전송 결과

     

     

    결론적으로 위와 같이 전송 메세지가 출력되는 걸 볼 수 있습니다. 이제야 채팅 같아졌네요.

     

     

     

     

    실시간 그림판 채팅 만들기

    (js_workspace > js04 > ex09.Socket.html)

     

    이제 마지막으로 그림판 기능만 만들면 됩니다. 제가 원하는 건 상대방과 함께 채팅을 나누면서 자유롭게 캔버스 위에 그림을 그릴 수 있는 기능입니다. 모든 사용자가 한 캔버스를 공유해서 서로의 그림을 지켜볼 수 있죠.

     

    클라이언트(ex09.Socket.html)

    가장 먼저 그림을 그리기 위한 캔버스가 필요합니다. html에는 마침 canvas 태그라는 게 존재하는데요,

    canvas 태그란 자바스크립트를 통해 다양한 그림을 그릴 수 있는 공간을 제공하는 Web API입니다. 자세한 이야기를 여기서 하면 소켓이라는 주제에 벗어나므로 일단 진행하겠습니다.

     

    아래와 같이 원하는 크기의 캔버스를 생성합니다.

    <canvas id="myCanvas" width="540" height="300"></canvas>

     

    그리고 캔버스 요소에 접근 할 변수도 생성해줍니다.

    let canvas = document.getElementById("myCanvas");
    let ctx = canvas.getContext("2d");
    
    let drawing = false;
    • getContext( 2d or 3d ) : 어떤 요소를 그리기 위한 그리기 컨텍스트를 구한다.
      • 기본이 2d
      • 컨텍스트: 캔버스의 그리기 영역이자 그리기 메서드를 가지는 객체.
      • WebGL을 사용하여 3d 그리기까지 가능하다.
    • drawing 변수 : 현재 그림을 그리는 상태면 true, 아니면 false 이다.

     

    이제 아래와 같은 방법으로 그림을 그리면 됩니다!

    ctx.lineStyle = "black";
    ctx.lineWidth = 2;
    ctx.beginPath();

     

    그림을 그린다 라는 건 일단 선을 그리는 거니까 아래와 같은 설정을 해줄 수 있겠네요.

    • 검정색 선
    • 두께는 2
    • beginPath() : 경로를 그리기 시작한다.

     

    그렇다면 우리가 실시간으로 그림을 그리려면 어떻게 해야할까요? 아마 아래와 같은 원리로 작동될 겁니다.

     

    클라이언트 A가 '그림을 그린다'

    → '서버 소켓에게 알린다'

    → 서버 소켓이 모든 클라이언트 소켓에게 'A의 그림을 전송'한다.

    → 메세지를 전달 받은 모든 클라이언트 화면에 그림을 그린다!

     

     

    일단 그림을 그린다 부터 생각해봅시다.

     

    선을 긋는다는 건 연필을 종이에 놓는다 → 꾹 눌러 움직인다 → 연필을 종이에서 뗀다. 입니다. 여기서 펜=마우스로 동일시 하고 코드를 짜면 될 것 같습니다.

     

    아래와 같은 세 가지 마우스 이벤트 리스너를 생성합니다.

    canvas.addEventListener('mousedown', (e)=> {
        drawing = true;
        socket.emit('message', {status:'start', x:e.offsetX, y:e.offsetY});
    });
    
    canvas.addEventListener('mousemove', (e)=> {
        if(drawing) {
            socket.emit('message', {status:'draw', x:e.offsetX, y:e.offsetY});
        }
    });
    
    canvas.addEventListener('mouseup', (e)=> {
        drawing = false;
        socket.emit('message', {status:'end', x:e.offsetX, y:e.offsetY});
    });

     

    위에서부터 사용자가 커서를 내렸을 때 / 커서를 움직일 때 / 커서를 올렸을 때 입니다.

     

    우선 mousedown 이벤트가 발생하면 drawing = true 가 됩니다. 이는 커서가 눌린 채로 이동하는 건지, 그냥 이동하는 건지 구분하기 위해 사용됩니다.

     

    drawing이 true일 때 mousemove 이벤트가 발생하면 그림을 그리는 것으로 판단합니다.

     

    mouseup 이벤트가 발생하면 drawing = false 가 됩니다.

     

     

     

    이제 해야할 일은 서버 소켓에게 알리는 것입니다.

     

    socket.emit() 을 사용하여 서버 소켓에게 현재 상태(시작, 그리는 중, 끝)를 알리고 그 순간의 마우스 좌표를 전송합니다.

     

    해당 데이터를 받은 서버는 이제 모든 클라이언트들의 화면에도 A의 그림이 나타나게 해야겠죠?

    그러기 위해선 A의 그림을 전송,즉 자신이 A에게 받은 좌표 메세지를 다시 모든 클라이언트들에게 돌려주면 됩니다.

     

    서버 측 코드는 아래에 별도로 작성하고, 일단 서버 소켓에서도 io.emit()으로 메세지를 전송했다는 가정 하에 진행 합니다.

     

     

    이제 진짜 마지막으로 남은 일은 메세지를 전달 받은 모든 클라이언트 화면에 그림을 그리는 것입니다.

    var isTrue = false;
    socket.on('start', (data)=> {
        ctx.moveTo(data.x, data.y);
        isTrue = true;
    });
    socket.on('draw', (data)=> {
        if(isTrue) {
            ctx.lineTo(data.x, data.y);
            ctx.stroke();
        }
    });
    socket.on("end", (data)=>{
        isTrue = false;
    });
    • isTrue 변수 : drawing 변수와 같은 역할. 그림을 그리는 상태(커서가 눌린 상태)인지 확인합니다.
    • moveTo(x,y) : 해당 좌표로 경로를 그리지 않고 이동한다.
    • lineTo(x, y) : 경로를 그리며 좌표를 이동한다.
    • stroke() : 경로 선을 긋는다.

     

    위 코드를 풀어서 설명하자면 아래와 같습니다.

     

    • 커서의 상태가 start 라면(커서를 막 내려놓았다면) → 해당 좌표로 경로를 그리지 않고 이동한다. isTrue = true로 할당한다.
    • 커서의 상태가 draw 라면(그림을 그리는 중이라면) → 경로를 그린다.
    • 커서의 상태가 end 라면(커서를 올렸다면) → isTrue = false 할당하여 그림그리기가 끝났음을 알림.

     

    (연속되는 곡선=수많은 점의 집합=작은 경로를 그린 선들의 집합)

     

     

    서버 측(server.js)

    서버가 해야할 일은 아주 간단합니다. 그냥 클라이언트한테 받은 좌표를 재전송 해주는 것 뿐이에요.

    socket.on('message', function(data) {
        console.log("client message : ", data);
        switch(data['status']){
        case 'start':
            io.sockets.emit('start',data); break;
        case 'draw':
            io.sockets.emit('draw',data); break;
        case 'end': io.sockets.emit('end',data); break;
        }
    });

     

     

     

     

    최종 결과

    이제 소켓이 잘 작동하는지 확인해보겠습니다. 두 개의 브라우저를 오픈하여 한 쪽 클라이언트에서 그림을 그리면 다른 클라이언트 화면에도 동시에 실행되는 걸 확인할 수 있습니다. 어릴 때 본, 랜덤한 사람들이 매칭 되어 작은 캔버스 위에 다같이 그림을 그릴 수 있던 외국 홈페이지가 생각나네요.

     

     

     

     

     

    참고 사이트

    🎁 웹 소켓의 정의

    🎁 소켓(Socket) 통신이란?

Designed by Tistory.