무작정 개발.Vlog

[node.js] Socket.io 모듈을 이용한 채팅 프로그램 만들기

by 무작정 개발
반응형
2022.04.11(75일 차)

 

이번에는 Socket.io 모듈을 이용한 채팅 프로그램에 대해 정리할 것이다.

 

https://css-tricks.com/replicating-google-hangouts-chat/

 

Replicating Some Of Google Hangouts Chat Design | CSS-Tricks

I've been using Google Hangouts a bunch. It's really pretty great. One on one or group text chat, audio, or video. Text is archived, maintains history, and is

css-tricks.com

 

node.js
node.js

 

[완성본]

채팅 하기
채팅 하기
콘솔 출력
콘솔 출력

 

Web(웹)은 일반적으로 클라이언트(사용자)에서 서버로 가는 단방향성이다.

하지만 채팅 프로그램은 실시간 양방향성이라 서버에서 클라이언트로 알림을 보내줘야 한다.

 

이번에 2가지 모듈을 사용할 것이다.

 

  • socket.io 모듈

  웹 서버로 소켓을 연결한 후 데이터를 주고받을 수 
  있도록 만든 HTML5 표준으로 웹브라우저가 이 기능을 지원하지 않아도
  Web Socket을 사용할 수 있게 만든 것이 Socket.io 모듈

 

  • cors 모듈

  Ajax의 XMLHttpRequest 는 보안 문제를 이유로  
  웹사이트를 제공하는 서버이외의 다른 서버에는 접속할 수 없는데 
  cors (Cross-Origin Resource Sharing)를 사용하면 제약이 풀림

 

프로젝트 구조
프로젝트 구조

 

모듈 설치 방법은 이전까지 학습하였기에 생략하겠다.

ChatExe프로젝트를 경로로 해서 cmd에서 하단의 2가지 모듈을 설치한다.

npm install socket.io --save
npm install cors --save

 

[핵심]

  • 특정 클라이언트 소켓에서 메세지를 어떻게 보내는지가 핵심

 

Server 쪽 소스 코드
- app.js

 

/*
채팅 서버 만들기

* Web Socket : 웹 서버로 소켓을 연결한후 데이터를 주고받을 수 
  있도록 만든 HTML5 표준으로 웹브라우져가 이 기능을 지원하지 않아도
  Web Socket을 사용할 수 있게 만든것이 Socket.io 모듈
  
  * cors모듈 : Ajax의 XMLHttpRequest 는 보안 문제를 이유로  
  웹사이트를 제공하는 서버이외의 다른서버에는 접속할 수 없는데 
  cors (Cross-Origin Resource Sharing)를 사용하면 제약이 풀림
*/

require("dotenv").config();
var express = require("express");
var http = require("http");
var path = require("path");
//var bodyParser = require("body-parser"); //예전에는 bodyParser를 호출해야 했지만 express에 내장이 되어 안써도 괜춘
var serveStatic = require("serve-static"); //특정 폴더를 패스로 접근 가능하게 하는것.
var expressErrorHandler = require("express-error-handler");
var expressSession = require("express-session");

//
var passport = require("passport");
var flash = require("connect-flash");


var config = require("./config"); 
var database = require("./database/database");
var routerLoader = require("./router/routerLoader");

var cors = require("cors");

//익스프레스 객체 생성
var app = express();

//뷰 엔진 설정
app.set("views",__dirname + "/views");
app.set("view engine","ejs"); //엔진을 ejs로 할꺼얌
console.log("뷰엔진이 ejs로 설정되었습니다.");



app.set("port",process.env.PORT||config.serverPort);

app.use(express.urlencoded({extended:false}));

app.use("/public",serveStatic(path.join(__dirname,"public"))); //public (실제)폴더의 이름을 써준것
//사용자정의

app.use(cors());

app.use(expressSession({

	secret:"my key",
	resave:true,
	saveUninitialized:true

}));

app.use(passport.initialize());
app.use(passport.session());
app.use(flash());



//라우터 객체 생성
var router = express.Router();

routerLoader.init(app,router);


//패스포트 설정
var configPassport = require("./passport/passport");
configPassport(app,passport); //passport.js는 매개변수가 app,passport가 필요하기 때문에 넘겨준다,

//패스포트 라우팅 설정
var userPassport = require("./router/userPassport");
userPassport(router,passport);



var errorHandler = expressErrorHandler({

	static: { //미리 메모리상에 올려둔것
		"404":"./public/404.html" //404에러가 뜨면 public에 404.html로 가라
	}
});

app.use(expressErrorHandler.httpError(404));
app.use(errorHandler); //변수명 담아줌


//이걸 쓰면 에러가 떠도 서버가 죽지않고 살아있다.
process.on("uncaughtException", function(err) {
	console.log("서버 프로세스 종료하지 않고 유지함.");
})


//Express 서버 시작

var host = "localhost";

//httpServer 생성
var server = http.createServer(app).listen(app.get("port"),function(){ 

	console.log("익스프레스 서버를 시작했습니다.:" + app.get("port"));

	//DB연결 함수 호출
	database.init(app,config); 

});


var io = require("socket.io")(server);

var loginID = {}; //빈 객체생성 - 객체생성을 해야 변수를 넣을 수 있음

io.sockets.on("connection",function(socket) {
	
	console.log("Connection Info : ", socket.request.connection._peername);
	
	//ip : socket.request.connection._peername.address
	//port : socket.request.connection._peername.port
	
	//각각의 클라이언트마다 고유한 key값이 있어야 함
	//모바일 : 핸드폰 번호
	//socket.io가 만드는 고유 정보(socket.id : QR_사용자정의)
	
	// 클라이언트로부터 login 이벤트를 받았을 때 처리 - 로그인
	socket.on("login",function(login) {
		
		console.log("login 이벤트 받음");
		
		//매핑정보를 담는 loginID에 login.id를 key값으로 하여, socket.id를 저장
		loginID[login.id] = socket.id;
		
		console.log("접속한 소켓 ID : " + socket.id); // apGAlPSym6djq2sKAAAH -> 이런 복잡한 고유 id번호가 나온다.
		
		// 클라이언트에서 받은 데이터를 socket 객체에 속성으로 추가
		socket.loginId = login.id; // 사용자 ID
		socket.loginAlias = login.alias; //사용자 alias
		
		
		
		//응답 메세지 전송
		sendResponse(socket, "login", "200", 
				socket.loginId + "(" + socket.loginAlias + ") 가 로그인 되었습니다.");
		
		console.log("접속한 클라이언트 ID 갯수: " + Object.keys(loginID).length);
		
	});
	
	
	// 클라이언트로부터 logout 이벤트를 받았을 때 처리 -로그아웃 
	socket.on("logout",function(logout) {
		
		sendResponse(socket, "logout", "444", logout.id + "가 로그아웃 되었습니다.");
		
		//Object key에서 socket.id 삭제
		delete loginID[logout.id];
		// loginID에 담긴 key값중 로그아웃한 사용자id 에 해당 하는 데이터를 삭제
		// loginID의 key값에는 현재 로그인중인 사용자id들이 담겨 있음
		
		console.log("접속한 클라이언트 ID 갯수: " + Object.keys(loginID).length);
		
	});
	
	
	// 클라이언트로부터 message 이벤트를 받았을 때 처리 - 메세지
	socket.on("message",function(message) {
	
		console.log("message 이벤트를 받았습니다.");
		
		//나를 포함한 모든 클라이언트에게 메세지를 전달
		if(message.receiver=='ALL') {
			// emit 메소드를 이용하여 이벤트를 전송하는 작업
			// "message"라는 이벤트 명으로 message를 전송시킴
			console.log("모든 클라이언트에게 message를 전송");
			
			io.sockets.emit("message",message);
			
		}else{ // !ALL
			// 클라이언트가 요구한 특정 대상자에게만 메세지 전달
			if(loginID[message.receiver]) {
				
				// 위와의 차이점은 연결된(connected) 사용자한테만 보낸다는 코드가 들어가있음
				//io.sockets.connected(loginID[message.receiver]).emit("message",message); //3.0
				io.to(loginID[message.receiver]).emit("message",message); //4.0
				
				sendResponse(socket, "message","200",message.receiver + "에게 메세지를 전송했습니다.");
				
			}else{//로그인을 안했을 경우
				sendResponse(socket, "login","404","상대방의 로그인 ID를 찾을 수 없습니다.");
				
			}
			
		}
		
	});
	
});


//응답 함수
function sendResponse(socket,command,code,message) {
	
	var returnMessage = {command:command, code:code, message:message};
	
	
	//서버에서 뭔가 보낼 때는 emit
	socket.emit("response",returnMessage);
	
}

특정 사용자에게만 메시지를 보낼 때는 io.to(loginID [message.receiver]). emit 메서드를 사용한다.

 

io.sockets.on("connection", function(socket) {  여기에서는 클라이언트가 접속하면, 콜백 함수를 실행하면서,

매개변수에 socket을 넣어줌으로 socket 객체를 같이 넘기는데 socket 객체 안에는 접속한 클라이언트의

ip와 port번호가 담겨있다.

 

sendButton을 누르면 서버로 'message'라는 이벤트를 보내도록 작성하였다.

서버 쪽에서는 socket.on("message", function(message) 메서드를 이용하여 이벤트를 처리한다.

들어온 데이터(채팅)는 변수 message를 통해 같이 들어오는데

들어오자마자 message라는 이벤트로 다른 클라이언트들과 자신에게 다시 보낸다.

채팅을 보면 나가 메세지를 보내면 내 자신한테도 보이고, 상대방한테 전송을 하기 때문이다.

io.sockets.emit("message", message);

- 나 자신의 클라이언트에게 이벤트를 보내는 메서드

 

★ 더 자세한 설명은 상단의 소스코드 주석 참고

 

클라이언트 쪽 소스코드
 - client.html

 

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>

<link rel="stylesheet" type="text/css" href="./data/semantic.min.css"/>
<link rel="stylesheet" type="text/css" href="./data/chatClient.css"/>

<script src="https://code.jquery.com/jquery-3.1.1.min.js"></script>
<script type="text/javascript"src="./data/semantic.min.js" ></script>
<script type="text/javascript" src="https://cdn.socket.io/4.0.1/socket.io.js"></script>

<script type="text/javascript">

	var host;
	var port;
	var socket;
	
	$(function(){
		
		$("#connectBtn").bind("click",function(event){
						
			alert("connectBtn 이 클릭 되었습니다.");
			
			
			host = $("#host").val();
			port = $("#port").val();
			
			connectToServer();
			
			
		});
		
		//1.메세지 보내기(emit 메소드)
		
		$("#sendBtn").bind("click",function(event){			
			
			var sender = $("#senderId").val();
			var receiver = $("#receiverId").val();
			var msg = $("#msg").val();
			
			
			var output = {sender:sender,receiver:receiver,command:"chat",type:"text",data:msg};
			
			if(socket==undefined){
				alert("서버가 연결되어 있지 않습니다.");
				return;
			}
			
			socket.emit("message",output);//메세지 보내기
			
			addToDiscussion('self',msg);
			
			$("#msg").val("");
			$("#msg").focus();
			
			
		});
		
		//로그인 버튼
		$("#loginBtn").bind("click",function(event){
			
			var id = $("#id").val();
			var pwd = $("#pwd").val();
			var alias = $("#alias").val();
			var today = $("#today").val();
			
			$("#senderId").val(id);
			
			//서버로 보낼 데이터
			var output = {id:id, pwd:pwd, alias:alias, today:today};
			
			if(socket==undefined){
				alert("서버에 연결되어 있지 않습니다");
				return;
			}
			
			socket.emit("login",output);
			
			
		});
		
		$("#logoutBtn").bind("click",function(event){
			
			if(socket==undefined){
				alert("서버가 연결되어 있지 않습니다.");
				return;
			}
			
			var id = $("#id").val();
			
			var output = {id:id};
			
			socket.emit("logout",output);
			
			$("#id").val("");
			$("#pwd").val("");
			$("#alias").val("");
			$("#today").val("");
			
		});
		
	});


function connectToServer(){
	
	var options = {"forceNew":true};//연결 세션을 만듬
	var url = "http://" + host + ":" + port;
	
	socket = io.connect(url,options);
	
	socket.on("connect",function(){
		
		alert("웹소켓 서버에 연결 되었습니다.: " + url);
		
		//3.서버 메세지 받음
		socket.on("message", function(message){
						
			addToDiscussion('other',message.data);
			
		});
		
		socket.on("response",function(response){
						
			if(response.code==444){
				socket.close();
			}
			
		});
				
		
	});
	
	socket.on("disconnect",function(){
		alert("웹소켓 연결이 종료되었습니다.");
	});	
	
}

function println(data){
	
	$("#result").append("<p>" + data + "</p>");
	
}


function showClock(date){
	
	var year = date.getFullYear();
	
	var month = (date.getMonth() + 1);
	month = month>=10 ? month : '0' + month;
	
	var day = date.getDate();
	day = day>=10 ? day : "0" + day;
	
	var h = date.getHours();
	hh = h>=10 ? h : "0" + h;
	
	var m = date.getMinutes();
	mm = m>10 ? m : "0" + m;
	
	var s = date.getSeconds();
	ss = s>=10 ? s : "0" + s;
	
	
	return year + "-" + month + "-" + day + " " +
			hh + ":" + mm + ":" + ss;
	
}

function addToDiscussion(writer,msg){
	
	var img = "./image/suzi.png";
	
	if(writer == "other"){
		img = "./image/angelina.png";
	}
	
	var contents = "<li class='" + writer + "'>"
				+ "<div class='avatar'>"
				+ "<img src='" + img + "'/>"
				+ "</div>"
				+ "<div class='message'>"
				+ "<p>" + msg + "</p>"
				+ "<time datetime='" + showClock(new Date()) + "'/>"
				+ showClock(new Date()) + "</time>"
				+ "</div></li>";
	
	//출력위치
	$(".discussion").prepend(contents);
	
}



</script>

</head>
<body>

<div class="container">

	<div id="cardbox" class="ui blue fluid card">
	
		 
		<h4 class="ui horizontal divider header">메세지</h4>
		<div class="ui segment" id="result">
			<ol class="discussion">
			
			</ol>		
		</div>	
		
		<br/>
		<div class="content">
			<div class="left floated author">
				<img id="iconImage" class="ui avatar image" src="./image/author.png">
			</div>
			<div>
				<div id="titleText" class="header">일대일 채팅</div>
				<div id="contentsText" class="description">
					연결 및 로그인 후 메세지를 보내세요.
				</div>			
			</div>		
		</div>
		<br/>
		
		<!-- 연결하기 -->
		<div>
			<div class="ui input">
				<input type="text" id="host" value="localhost"/>
			</div>
			<div class="ui input">
				<input type="text" id="port" value="3000"/>
			</div>
			<br/><br/>
			<input type="button" class="ui primary button" id="connectBtn" value="연결하기"/>			
		</div>
		<br/>
		<!-- 로그인/로그아웃 -->
		<div>
			<div class="ui input">
				아이디: <input type="text" id="id"/><br/>
			</div>
			<div class="ui input">
				패스워드: <input type="password" id="pwd"/><br/>
			</div>
			<div class="ui input">
				별명: <input type="text" id="alias"/><br/>
			</div>
			<div class="ui input">
				상태: <input type="text" id="today"/><br/>
			</div>
			<br/><br/>
			<input type="button" class="ui primary button" id="loginBtn" value="로그인"/>
			<input type="button" class="ui primary button" id="logoutBtn" value="로그아웃"/>
		</div>
		<br/>
		<!-- 전송 -->
		<div>
			<div class="description">
				<label>보내는사람 아이디 : </label>
				<div class="ui input">
					<input type="text" id="senderId"/>
				</div>
			</div>
			<div class="description">
				<label>받는사람 아이디 : </label>
				<div class="ui input">
					<input type="text" id="receiverId"/>
				</div>
			</div>
			<div class="description">
				<label>메세지 데이터 : </label>
				<div class="ui input">
					<textarea rows="5" cols="40" id="msg"></textarea>
				</div>
			</div>
			<br/>
			<input type="button" class="ui primary button" id="sendBtn" value="전송"/>
			<input type="button" class="ui primary button" id="clearBtn" value="지우기"/>
		</div>
		<br/>
		
		<!--
		<h4 class="ui horizontal divider header">메세지</h4>
		<div class="ui segment" id="result">
			<ol class="discussion">
			
			<li class="other">
				<div class="avatar">
					<img src="./image/suzi.png"/>
				</div>
				<div class="message">
					<p>어디쯤이야? 다들 기다리고 있어</p>
					<time datetime="2021-06-04 10:39">10시 39분</time>
				</div>
			</li>
			
			<li class="self">
				<div class="avatar">
					<img src="./image/angelina.png"/>
				</div>
				<div class="message">
					<p>차가 막히네 조금 늦을 듯..</p>
					<time datetime="2021-06-04 10:40">10시 40분</time>
				</div>
			</li>
			
			<li class="other">
				<div class="avatar">
					<img src="./image/suzi.png"/>
				</div>
				<div class="message">
					<p>강남역에있는 술집이야..빨랑 와..</p>
					<time datetime="2021-06-04 10:41">10시 41분</time>
				</div>
			</li>
					
			
			</ol>		
		</div>
		-->
	</div>

</div>
</body>
</html>

 

 

자세한 소스 코드를 하단 깃허브 링크 참고

https://github.com/chaehyuenwoo/node.js

 

GitHub - chaehyuenwoo/node.js: node.js & express 관련 공부

node.js & express 관련 공부. Contribute to chaehyuenwoo/node.js development by creating an account on GitHub.

github.com

 

반응형

블로그의 정보

무작정 개발

무작정 개발

활동하기