본문 바로가기
블로그 이미지

방문해 주셔서 감사합니다! 항상 행복하세요!

  
   - 문의사항은 메일 또는 댓글로 언제든 연락주세요.
   - "해줘","답 내놔" 같은 질문은 답변드리지 않습니다.
   - 메일주소 : lts06069@naver.com


Node.js

Node.js 웹소캣 서버 그리고 클라이언트, 채팅방과 함께하는 구성

야근없는 행복한 삶을 위해 ~
by 마샤와 곰 2019. 4. 28.

 

 

Node.js로 웹소캣 서버를 구현하는 방법은, 다른 개발언어에 비해 상당히 빠르고 직관적이며 어렵지가 않다.

websocket과 http 모듈만 설치하면 나머지는 해당 모듈을 통해 구현만 해주면 된다.

먼저 두 모듈을 npm으로 설치한다.

npm install http

npm install websocket

처음으로 할 작업은 웹소캣 서버를 구성하는 일이다.

const WebSocketServer = require('websocket').server;
const http = require('http');
const port = 3000;  //포트
const server = http.createServer(function(request, response) {  //일반 HTTP 요청 처리
    console.log((new Date()) + ' Can not get information reqeust of http  ' + request.url);
    response.writeHead(404);
    response.end();
});
server.listen(port, function() {
    console.log((new Date()) + ' Server is listening on port 3000');
});
 
const wsServer = new WebSocketServer({  //웹소켓 서버 생성
    httpServer: server,
    autoAcceptConnections: false
});

위 코드처럼 일반응답에 대한 처리를 해주고 해당 객체를 웹소캣에 넣어주면 끝이다.

웹소캣은 http 프로토콜을 사용하기 때문에 위 소스코드와 같은 스타일로 작업을 해야 한다.

아래코드를 보자

wsServer.on('request', function(request) {
    if (!originIsAllowed(request.origin)) {
      request.reject();
      console.log((new Date()) + ' Connection from origin ' + request.origin + ' rejected.');
      return;
    }
    
    var connection = request.accept('echo-protocol', request.origin);
    console.log((new Date()) + ' Connection accepted.');
    connection.on('message', function(message) {
        if (message.type === 'utf8') {
            console.log('Received Message: ' + message.utf8Data);
            connection.sendUTF(message.utf8Data);
        }
        else if (message.type === 'binary') {
            console.log('Received Binary Message of ' + message.binaryData.length + ' bytes');
            connection.sendBytes(message.binaryData);
        }
    });
    connection.on('close', function(reasonCode, description) {
        console.log((new Date()) + ' Peer ' + connection.remoteAddress + ' disconnected.');
    });
});

해당 코드는 접속이 되면 해당 접속에 따라서 처리를 하는 부분이다.

위 소스코드는 npm에서 제공하는 예제코드인데..

해당 코드를 그대로 가져다 쓰면 자기자신한테 메시지는 잘 날라오는데..상대방한테는 가지를 않는다.

이유는, 해당 소스코드 자체가 자기자신한테만 보내도록 되어있기 때문이다.

중간에 var connection이라는 부분이 있는데 해당 커넥션은 웹소켓에 들어온 커넥션을 의미한다.

커넥션은 들어온 사용자 별로 고유하게 존재하며 해당 커넥션을 통해 상태 및 메시지를 관리 할 수 있다.

위 소스코드는 자기 자신의 커넥션에 대한 정의만 되어있다.

그러므로 echo처럼 자기자신한테만 메시지가 전달되고 남한테 메시지가 전송되지 않는 것이다.

해당 소스코드를 Collection을 활용해 조금 변경하면 쉽게 해결 가능하다.

이를 위해 사용자를 구분하기 위해 변수 2가지를 설정하였다.

wsServer.on('request', function(request) {
    const user = request.resourceURL.query.user;  //사용자 ID
    const room = request.resourceURL.query.room;  //방번호
});

웹소캣 서버에 접속 할 때 파라미터를 붙여서 접속을 할 수 있다.

여기서 user는 id로 정하고 room은 방번호로 정해서 사용 하겠다.

들어온 사용자id와 방 번호는 map 객체에 넣어서 관리를 하여 방 번호에 따라 전송되도록 수정을 하겠다.

const rooms = new Map();  //채팅방 목록을 담을 객체

wsServer.on('request', function(request) {  //응답을 받는다.

    const user = request.resourceURL.query.user;  //사용자 ID
    const room = request.resourceURL.query.room;  //방번호

    var connection = request.accept();   //들어온 커넥션 객체

    rooms.set(user,{user:user, room:room, con:connection});  //방 목록에 자신 추가
    //즉, 들어온 커넥션 및 정보를 계속 담아줘서 해당 대상에 대해서 발송해야 되는 것이다.

    connection.on('message', function(message) {  //채팅메시지가 도달하면
        for(let target of rooms.entries()) {  //방 목록 객체를 반복문을 활용해 발송
            if(identify.room == target[1].room){  //같은방에 있는 사람이면 전송
                    var res = JSON.stringify({param:message.utf8Data, who:user });  //json형태로 메시지 전달, param은 보낼 메시지 who는 보내는 사람이다.
                    target[1].con.sendUTF(res);
            }
        }
    });

    connection.on('close', function(reasonCode, description) {   //커넥션이 끊기면
        rooms.delete(user);  //방 목록에서 삭제
    });
});

 

위 소스코드대로 하면 메시지 동작은 잘 될 것이다. 기능을 조금만 더 추가하여 보자.

1. 사용자 목록에 대한 정보

2. 최초 접속에 대한 메시지

3. 사용자 퇴장에 대한 메시지 및 정보

중요한 점은, Node.js는 이벤트 루프를 통해 던저버리는(?) 행동을 자주 하므로 무언가 절차식으로 행동을 정의하고 싶다면 Promise를 꼭 사용하여야 한다. 그렇지 않으면 반복문이 열심히 동작하고 있는데 이미 응답이 끝난 상태를 경험 할 수 있다.

아무튼, 3가지 기능을 추가한 최종코드는 아래와 같다.

 

const WebSocketServer = require('websocket').server;
const http = require('http');
const port = 3000;  //포트
const server = http.createServer(function(request, response) {  //일반 HTTP 요청 처리
    console.log((new Date()) + ' Can not get information reqeust of http  ' + request.url);
    response.writeHead(404);
    response.end();
});
server.listen(port, function() {
    console.log((new Date()) + ' Server is listening on port 3000');
});
 
const wsServer = new WebSocketServer({  //웹소켓 서버 생성
    httpServer: server,
    autoAcceptConnections: false
});
const rooms = new Map();  //채팅방 목록을 담을 객체
const requestType = {  //메시지 타입
    A:'welcome',
    B:'send',
    C:'bye',
    D:'beforList'
}

wsServer.on('request', function(request) {
    const user = request.resourceURL.query.user;  //사용자 ID
    const room = request.resourceURL.query.room;  //방번호
    if( NUL(user) || NUL(room)){
        return;
    }

    var connection = request.accept();   //들어온 커넥션 객체
    sendBeforUserList(connection, room); //이미 들어와있는 사용자 목록을 전송

    rooms.set(user,{user:user, room:room, con:connection});  //방 목록에 자신 추가
    msgSender(rooms.get(user), null, requestType.A);  //로그인 타입으로 메시지 전송

    connection.on('message', function(message) {  //채팅메시지가 도달하면
        msgSender(rooms.get(user), message, requestType.B);
    });

    connection.on('close', function(reasonCode, description) {   //커넥션이 끊기면
        msgSender(rooms.get(user), null, requestType.C).then((callbak)=>{  //방에서 나감을 알리고
            rooms.delete(user);  //방 목록에서 삭제
        }).catch((err)=>{
            console.log(err);
        });
    });
});

//파라미터 확인용 함수
function NUL(obj){
    if(obj == undefined || obj == null || obj.length == 0){
        return true;
    }
    return false;
}

//메시지를 보내는 함수
function msgSender(identify, message, type){
    return new Promise((resolve, reject)=>{
        for(let target of rooms.entries()) {  //방 목록 객체를 반복문을 활용해 발송
            if(identify.room == target[1].room){  //같은방에 있는 사람이면 전송
                //타입별 전송 구간(최초접속,메시지전송,방나감)
                if (type == requestType.A ) {  
                    var res = JSON.stringify({param:'room in',fromUser:identify.user, type:type});
                    target[1].con.sendUTF(res);
                } else if (type == requestType.B && message.type === 'utf8') {
                    var res = JSON.stringify({param:message.utf8Data,fromUser:identify.user, type:type});
                    target[1].con.sendUTF(res);
                } else if (type == requestType.C) {
                    var res = JSON.stringify({param:'room out',fromUser:identify.user, type:type});
                    target[1].con.sendUTF(res);
                }
            }
        }
        resolve('succ');        
    });    
}

//방 접속 시 이미 들어와있는 목록을 받기위한 함수
function sendBeforUserList(connection, room){
    new Promise((resolve, reject)=>{
        var beforList = new Array();
        for(let target of rooms.entries()) {  //반복문을 통해 사용자 리스트를 배열에 담아서
            if(room == target[1].room){
                beforList.push(target[1].user);
            }
        }
        resolve(beforList);        
    }).then((list)=>{
        var res = JSON.stringify({param:'',users:list, type:requestType.D});  //막 들어온 사용자한테 해당 대상을 전달.
        connection.sendUTF(res);
    }).catch((err)=>{
        console.log(err);
    });
}

 

클라이언트에서의 요청시 파라미터 정의는 반드시 room과 user라는 파라미터를 전송 하여야 한다.

클라이언트 소스코드에서 해당 room값과 user값을 변경해서 여러개의 창을 띄우면 제법 채팅하는 효과를 볼 수 있다.

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
 <meta charset="utf-8">
 <meta http-equiv="cache-control" content="no-cache">
 <meta http-equiv="pragma" content="no-cache"> 
 <meta http-equiv="expires" content="0">
 <meta http-equiv="X-UA-Compatible" content="IE=10" />
 <meta name="viewport" content="width=device-width, initial-scale=1.0">
 <title>websocket</title>
</head>

<script src="https://code.jquery.com/jquery-2.2.4.js" integrity="sha256-iT6Q9iMJYuQiMWNd9lDyBUStIq/8PuOW33aOqmvFpqI=" crossorigin="anonymous"></script>		
<style type="text/css">
  #console-container {
    border: 1px solid #CCCCCC; overflow-y: scroll;
	height : 250px; width: 60%;
	padding: 13px; border-radius: 3px;
	display:inline-block;
  }
  .myMsg{
	width: 100%;	text-align: left;
	height: 35px;	padding: 3px;
	color: gray;
  }
  .friendsMsg{
	width: 100%;	text-align: right; 	height: 35px;
	padding: 3px;	color: black;
  }
  #users{
    border: 1px solid #CCCCCC;
    overflow-y: scroll;	height : 250px;	width: 30%;
	padding: 13px;	border-radius: 3px;	display:inline-block;
  }
  .members{
	width:100%;	padding: 5px;
  }
</style>



<body>


<!-- 채팅값이 들어가는 테그 -->
<div id="console-container" >

</div>
<div id='users' > </div>

<!-- 인풋 박스 -->
<p>
<div>
  <input type="text" placeholder="type and press enter to chat" id="chat" >
  <input type="button" value='send' id="clicker">
</div><br><br>
</body>
</html>

<script type="application/javascript">
var user = 'admin';   //사용자id, id는 당연히 고유값으로 부여해야 한다.
var room = '1';  //방번호, 해당번호가 틀리면 채팅메시지를 받을 수 없다.
var url = 'ws://127.0.0.1:3000?user='+user+'&room='+room;
var socket = new WebSocket(url);

socket.onopen =function () {
	console.log('connection ok');
};
socket.onclose =function () {
	console.log('connection fail');
};
socket.onmessage = function (response) {
	var msg = JSON.parse(response.data);
	makeMsg(msg);
};

$('#chat').keydown(function(event){
	if(event.keyCode == 13){
		var value = $(this).val();
		socket.send(value);
	}
});

$('#clicker').click(function(){
	var value = $('#chat').val();
	socket.send(value);
});

var stop = true;
//들어온 메시지 그리기 함수
function makeMsg(msg){
	console.log(msg);
	if(msg.type == 'beforList' && stop){//나보다 먼저 들어온 사용자
		msg.users.forEach(function(beforUsr){
			var usr = $('<div/>').attr({id:beforUsr,class:'members'}).text(beforUsr);
			$('#users').append(usr);
		});
		stop = false;
	} else{
		if(msg.type == 'welcome'){  //등록
			var usr = $('<div/>').attr({id:msg.fromUser,class:'members'}).text(msg.fromUser);
			$('#users').append(usr);
		} else if(msg.type == 'bye'){  //나감
			$('#'+msg.fromUser).remove();
		}
		var cls= 'friendsMsg';
		if(msg.fromUser == user){
			cls= 'myMsg';
		}
		var child = $('<div/>').attr('class',cls).append(
			$('<span/>').text(msg.fromUser),
			$('<small/>').text(' ('+yymmhhddss()+ ') '),
			$('<span/>').text(' : '+msg.param)
		);
		$('#console-container').append(child);
		$('#console-container').animate({
			scrollTop: $('#console-container').get(0).scrollHeight
		}, 10);
	}
}

//시간 그리기 함수
function yymmhhddss(){
    var time = new Date();
    var year = time.getFullYear();
    var month = time.getMonth()+1;
    var day = time.getDay();
    var hhmmss = ("0" + time.getHours()).slice(-2) + ":" + ("0" + time.getMinutes()).slice(-2) + ":" + ("0" + time.getSeconds()).slice(-2);    
    if(month < 10){
        month = '0'+month;
    }
    if(day < 10){
        day = '0'+day;
    }    
    param = year + '-' + month + '-' + day + ' ' + hhmmss;
    return param;
}
</script>

이번에는 동작도 잘 하고 제법 채팅방이 존재하는 어플리케이션이 탄생한 것 같다.

반응형
* 위 에니메이션은 Html의 캔버스(canvas)기반으로 동작하는 기능 입니다. Html 캔버스 튜토리얼 도 한번 살펴보세요~ :)
* 직접 만든 Html 캔버스 애니메이션 도 한번 살펴보세요~ :)

댓글