http://bcho.tistory.com/889
빠르게 훝어 보는 node.js – #6 MongoDB 연동 (mongo-native)
클라우드 컴퓨팅 & NoSQL/Vert.x & Node.js | 2014/04/03 23:49 | Posted by 조대협
빠르게 훝어보는 node.js
#6- mongo-native 모듈을 이용한 MongoDB 연동
조대협 (http://bcho.tistory.com
Persistence 연동
node.js는 DB나 NoSQL등의 연동을 지원하는데, 이 역시 철저하게 non-blocking io 방식으로 동작한다. 즉 db 연결 socket을 열어서 query를 던져놓고, query 결과가 오면 이벤트를 받아서 callback 함수로 처리하는 순서이다.
그러면 여기서는 몇가지 persistence 연동 방식에 대해서 알아보도록 한다.
MongoDB
먼저 mongodb는 nosql 데이터베이스중에 가장 많이 사용되는 제품중의 하나이다. Json document를 저장하는 스타일의 document db이며, index나 grouping과 같은 RDBMS와 유사한 기능까지 지원하기 때문에 사용이 매우 쉽다.
설치 및 테스트
Mongodb는 mongodb.org에서 다운로드 받을 수 있다. Mongodb는 무료 tutorial이 잘되어 있는 것이 많은데, https://university.mongodb.com/에 가면 언어별 (node.js용도 있다) 튜토리얼이 있으니 참고하기 바란다.
윈도우를 기준으로 다운 받아서 압축을 푼후, db 디렉토리를 만들어 주어야 하는데, 필요한 곳에 디렉토리를 만든다. 여기서C:devmongodb-win32-x86_64-2.4.3 data 에 만들었다.
C:devmongodb-win32-x86_64-2.4.3 에서 다음과 같은 명령어를 이용하여 구동한다.
.binmongod –dbpath C:devmongodb-win32-x86_64-2.4.3data |
인스톨이 끝났으면 간단한 테스트를 해보자, ./bin/mongo.exe를 수행하면 Java Script 기반의 쉘이 수행된다. 이해를 돕기 위해서 하나의 테이블에 Insert , select, delete, update를 수행하는 명령을 SQL 문장과 비교해서 소개한다.
Insert
SQL : insert into users (“name”,”city”) values(“terry”,”seoul”)
Mongo DB : db.users.insert({_id:”terry”,city:”seoul”})
Select
SQL : select * from users where id=”terry”
Mongo DB : db.users.find({_id:”terry”})
Update
SQL : update users set city=”busan” where _id=”terry”
Mongo DB : db.users.update( {_id:”terry”}, {$set :{ city:”Busan” } } )
Delete
SQL : delete from users where _id=”terry”
Mongo DB : db.users.remove({_id:”terry”})
간단하게 나마, mongodb query에 대해서 설명하였다.
개념적을 약간 더 설명하자면, db는 RDBMS에 db와 같은 개념으로 보면 되고, collection은 rdbms의 하나의 테이블로 보면 된다. collection에 들어가는 데이터 필드중에서 _id는 predefined된 필드로 해당 데이터에 대한 primary key로 생각하면 되고, 위와 같이 사용자가 직접 값을 넣어서 입력할 수도 있고 또는 값을 넣지 않으면 mongodb에 의해서 unique한 값으로 자동 지정된다.
조금 더 자세한 쿼리에 대한 설명은 http://docs.mongodb.org/manual/crud/ 를 참고하기 바란다.
GUI 툴로는 robomongo 라는 툴이 있다. http://robomongo.org/
mongodb-native 모듈을 이용하기
node.js에서 mongodb를 연결하는 방법은 여러가지가 있다. 먼저 가장 널리 사용되는 mongo-native 모듈에 대해서 알아보자https://github.com/mongodb/node-mongodb-native
1) mongo native module 설치하기
mongo native module은
% npm install mongo 로 설치가 가능하며, 설치중에 native 모듈을 컴파일 하기 때문에 반드시 컴파일러 (Windows의 경우 Visual C++등)이 깔려 있어야 한다.
2) 간단한 쿼리 수행하기
설치가 끝났으면 간단하게 select를 해서 내용을 json으로 리턴하는 코드를 만들어보자. Select를 할것이기 때문에 미리 mongo에 값을 넣어보자
% mongo.exe 를 실행한 후에 다음 쿼리를 수행한다.
db.users.insert({_id:’terry’,city:’seoul’});
db.users.insert({_id:’cath’,city:’suwon’});
제대로 입력이 되었는지 select를 해본다.
아래는 mongodb 도구인 robomongo를 이용해서 데이터가 들어가 있는 것을 확인한 화면이다.
자아 그러면 이제 코드를 구현해보자
var express = require(‘express’);
var routes = require(‘./routes’);
var http = require(‘http’);
var path = require(‘path’);
var app = express();
var MongoClient = require(‘mongodb’).MongoClient
var Server = require(‘mongodb’).Server;
먼저 위와 같이 MongoClient와 Server 클래스를 생성한다
// all environments
app.set(‘port’, process.env.PORT || 3000);
app.set(‘views’, path.join(__dirname, ‘views’));
app.set(‘view engine’, ‘ejs’);
app.use(express.logger(‘dev’));
app.use(express.json());
app.use(express.urlencoded());
app.use(express.methodOverride());
app.use(app.router);
app.use(express.static(path.join(__dirname, ‘public’)));
var mongoclient = new MongoClient(new Server(‘localhost’,27017,{‘native_parser’:true}));
var db = mongoclient.db(‘test’);
다음으로 mongoclient를 생성한다. 일종의 Connection 설정이라고 보면 되는데, 설정만 여기서 하는 것이지 실제 연결은 open을 호출해야 된다.다음으로 client연결을 이용해서 사용할 db를 선택한다. 여기서는 ‘test’ db를 사용하였다.
app.get(‘/’, function(req,res) {
db.collection(‘users’).findOne({},function(err,doc){
if(err) throw err;
res.send(doc);
});
});
Db 객체를 얻었으면, 쿼리를 수행할 수 있는데, collection(‘collection명.즉 테이블명’)으로 collection을 선택한후, findOne()메서드를 이용하여 하나의 row만 select를 하였다. 비동기 호출이기 때문에 query 수행이 끝나면, function(err,doc) callback 함수를 호출하는데, 위에서는 에러가 났을때는 err를 throw하고 에러가 없을 경우에는 리턴받은 json document를 response에 return하도록 하였다. 여기 까지 끝났으면, 실제mongodb를 연결해보자 연결은 mongoclient.open을 하면 되는데, 연결이 완료되면 open안에 정의된 callback method를 호출한다.
아래코드를 보면 이 callback 메서드 안에서 httpServer를 띄운 것을 볼 수 있는데, http server가 기동되서 mongodb를 사용하기 때문에mongoclient를 먼저 띄우기 위해서 httpserver를 callback안에 넣었다.
mongoclient.open(function(err, mongoclient) {
if(err) throw err;
console.log(‘mongo client connected’);
http.createServer(app).listen(app.get(‘port’), function(){
console.log(‘Express server listening on port ‘ + app.get(‘port’));
});
});
코드 구현이 끝났으면 실행해보자. 다음과 같이 레코드가 JSON으로 리턴됨을 확인할 수 있다.
이 코드에서 보면 mongoclient를 하나만 생성하였다. 그렇다면 내부적으로 물리적인 connection이 하나만 생길까? Mongoclient는 내부적으로connection pooling을 이용한다. 그래서 별도의 설정을 해주지 않아도 내부적으로 여러 개의 connection을 여는데, default가 5개의 connection을 열도록 되어 있고, connection의 수는 open 옵션에서 조정할 수 있다.
var mongoclient = new MongoClient(new Server(‘localhost’,27017,{‘native_parser’:true,’poolSize’:8,’maxPoolSize’:10}));
위의 코드는 max connection을 10개로 하고, 초기에 poolSize를 8개로 지정한 경우이다. 실제로 기동해보면 8개의 connection이 생성되었음을 확인할 수 있다.
3) Insert, update and delete
Select는 해봤는데, 그러면 insert,update,delete는 어떻게 할까? mongodb query와 매우 유사하다.먼저 Insert를 보자
db.collection(‘users’).insert({city:’suji’},function(err,doc){
console.log(‘inserted ‘+doc[0]._id+’:’+doc[0].city);
});
위와 같이 collection을 선택한다음에, insert메서드를 이용해서 json document를 넣으면 된다. 위에서는 callback function을 지정한 예인데, callback function은 생략할 수 도 있다. 위의 코드를 잘 보면 앞에 예제와는 다른게 _id값을 입력하지 않은 것을 볼 수 있다. 이 경우 _id값은 자동으로 mongodb가 생성하여 unique한 값을 넣게 된다.
그리고 insert된 값은 callback의 두번째 인자에서 array 형태로 넘겨 받게 된다.
또는
db.collection(‘users’).insert([{city:’suji’},{city:’busan’}],function(err,doc){
와 같은 배열 형태를 사용하면, 하나의 insert문장으로 여러 개의 document를 동시에 insert할 수 있다.(즉 batch insert가 가능하다)
삭제는 insert와 유사하게 remove 메서드를 이용하면 된다.
db.collection(‘users’).remove({city:’busan’},function(err,doc){});
위의 예제는 city 필드가 busan인 것을 삭제한 것인데, _id 필드 이외에는 index로 지정이 되어 있지 않기 때문에 index를 지정하지 않은 필드로 삭제등을 했을 경우 table full scan이 발생할 수 있으니 주의하도록 해야 한다. (한마디로 느려진다는 이야기)
다음으로 수정은 collection.update 메서드를 이용하면 된다.
db.collection(‘users’).update({_id:’terry’},{$set:{‘sex’:’male’}},function(err,doc){
throw err;
});
_id가 terry인 document에 ‘sex’ 필드를 ‘male’로 변경하는 쿼리이다. 이미 ‘sex’필드가 있으면 그 내용을 ‘male’로 바꾸고, 없으면 새롭게 ‘sex’필드를 만든다.앞에 $set을 줬기 때문에 ‘sex’필드만 내용을 바꾸는데,
db.collection(‘users’).update({_id:’terry’},{‘sex’:’male’},function(err,doc){
throw err;
});
$set을 빼버리게 되면 _id=’terry’인 document의 내용을 {‘sex’:’male’}로 바꿔 버린다. (필드만 추가하는게 아니다.)
4) Search
앞에서는 findOne만 해서 하나의 record만 query 하는 예제 였는데, 여러 개의 record를 받고 싶으면 find 메서드를 사용해서 검색 조건을 주고.toArray를 호출하면 인자로 넘어가는 callback함수의 docs 인자에 쿼리 결과를 배열로 리턴해준다.
db.collection(‘users’).find({city:’suji’}).toArray(function(err,docs) {
if (err) throw err;
res.send(docs);
for (i = 0; i < docs.length; i++) {
console.log(docs[i].city);
}
});
여기서는 아주 기본적인 API만을 다뤘기 때문에 자세한 API들은 http://mongodb.github.io/node-mongodb-native/api-generated 를 참고하기 바란다.
빠르게 훝어보는 node.js
#7- mongoose ODM 을 이용한 MongoDB 연동
조대협 (http://bcho.tistory.com)
Mongoose ODM을 이용한 MongoDB의 연동
Mongoose는 MongoDB 기반의 nodejs용 ODM (Object Data Mapping) 프레임웍이다. 앞에서 mongo-native에 대해서 알아봤는데, 그렇다면mongoose는 무엇인가? 쉽게 생각하면 mongo-native는 JDBC 드라이브러를 이용한 데이타 베이스 프로그래밍이고, mongoose는 자바의JPA/Hibernate/MyBatis와 같은 OR Mapper와 같은 개념이다.
즉 mongodb 내의 데이타를 node.js내의 객체로 정의해준다. ODM 개념을 이용하여, MVC 개념들을 조금더 쉽게 구현할 수 있도록 도와 주며, 특히 javascript가 가지고 있는 한계성중인 하나인 모호성을 보완해준다. mongodb에 json을 저장할때, collection에 들어가는 데이타의 형태(스키마)가 없기 때문에 자유도가 높기는 하지만 반대로 RDBMS에서 정의된 스키마 개념이 없이 때문에 어떤 컬럼이 있는지 코드에서로만은 파악하기가 어려울 수 있다. 이런점을 보완하는 개념이 mongoose의 스키마의 개념인데, 직접 코드를 살펴보자
다음 예제는 HTML에서 이름과 메모를 받아서 DB에 저장하고 조회 하는 예제이다.
<!DOCTYPE html>
<html>
<head>
<title></title>
</head>
<body>
<form name=“memo” method=“post” action=“/insert”>
name <input type=“text” name=“username”/>
<br>
message <input type=“text” name=“memo”/>
<button type=“submit” >Submit</button>
</form>
</body>
</html>
먼저 실행에 앞서서 npm install mongoose를 이용해서 mongoose 모듈을 설치해야 한다.
다음은 express의 /app.js 파일이다.
var express = require(‘express’);
var routes = require(‘./routes’);
var user = require(‘./routes/user’);
var http = require(‘http’);
var path = require(‘path’);
mongoose를 사용하기 위해서 해당 모듈을 import한다.
var mongoose = require(‘mongoose’);
다음으로 Schema를 정의하는데, 이 스키마는 username과 memo라는 필드를 가지고 있으며 각각의 필드는 String 데이타 타입을 갖는다.
var MemoSchema= mongoose.Schema({username:String,memo:String});
이 스키마를 이용해서 아래와 같이 모델을 정의하는데, 첫번째 인자는 이 모델이 mongodb에 저장될 Collection이름(테이블명)이 되고, 두번째 인자는 이 모델을 정의하는데 사용할 스키마(앞에서 정의한)를 지정한다.
참고
mongoose에서는 다양한 데이타 타입을 이용하여 계층화된 스키마를 정의하는 게 가능하다. 아래는http://mongoosejs.com/docs/schematypes.html 에 정의된 예제 스키마 중의 하나이다. var schema = new Schema({ name: String, binary: Buffer, living: Boolean, updated: { type: Date, default: Date.now } age: { type: Number, min: 18, max: 65 } mixed: Schema.Types.Mixed, _someId: Schema.Types.ObjectId, array: [], ofString: [String], ofNumber: [Number], ofDates: [Date], ofBuffer: [Buffer], ofBoolean: [Boolean], ofMixed: [Schema.Types.Mixed], ofObjectId: [Schema.Types.ObjectId], nested: { stuff: { type: String, lowercase: true, trim: true } } })
|
var Memo = mongoose.model(‘MemoModel’,MemoSchema); // MemoModel : mongodb collection name
다음으로 express를 사용하기 위한 기본 설정을 아래와 같이 하고
var app = express();
// all environments
app.set(‘port’, process.env.PORT || 3000);
app.set(‘views’, path.join(__dirname, ‘views’));
app.set(‘view engine’, ‘ejs’);
app.use(express.logger(‘dev’));
app.use(express.json());
app.use(express.urlencoded());
app.use(express.methodOverride());
app.use(app.router);
app.use(express.static(path.join(__dirname, ‘public’)));
HTTP/POST로 들어오는 요청을 mongodb에 저장하는 로직을 구현한다.
app.post(‘/insert’, function(req,res,err){
var memo = new Memo({username:req.body.username,memo:req.body.memo});
memo.save(function(err,silence){
if(err){
console.err(err);
throw err;
}
res.send(‘success’);
});
});
Memo 모델 클래스를 이용해서 memo 객체를 만드는데, username은 앞의 index.html의 폼에서 입력받은 username 값을, memo는 form에서 입력받은 memo 값으로 memo 객체를 생성한다.
저장하는 방법은 간단하다. memo객체의 save라는 메서드를 호출하고, 비동기 IO이기 때문에, callback 함수를 바인딩 하였다. callback함수에서는 데이타 저장 처리가 끝나면, res.send로 success 메세지를 출력한다.
다음으로 mongoose를 mongodb에 연결하고, http server를 기동시켜 보자
mongoose.connect(‘mongodb://localhost/terrydb’,function(err){
if(err){
console.log(‘mongoose connection error :’+err);
throw err;
}
http.createServer(app).listen(app.get(‘port’), function(){
console.log(‘Express server listening on port ‘ + app.get(‘port’));
});
});
mongoose.connect를 이용하여 mongodb에 접속한다. 이때, 접속 URL을 써주면 된다. 여기서는 localhost에 terrydb를 사용하도록 정의하였다.여기서 지정한 옵션 이외에도, 포트 #나, connection pool 설정등 다양한 옵션을 적용할 수 있다. 자세한 내용은 mongoose 문서http://mongoosejs.com/docs/connections.html 를 참고하기 바란다.
connect 메서드는 두번째 인자로 callback 함수를 받는데, 이 예제에서는 callback함수에서 http server를 기동하였다. 이는 mongodb가 연결된 다음에 서비스가 가능하기 때문에, mongodb 연결후에 request를 받기 위해서 callback에서 http server를 기동한 것이다.
그러면 실행을 하고 결과를 보자. http://localhost:3000으로 접속하여 위에서 나타난 index.html 폼에 데이타를 넣고, robomongo를 이용해서 그 결과를 살펴보면 아래와 같이 값이 들어간 것을 확인할 수 있다.
자아, mongoose를 이용해서 데이타를 저장하였다. 그러면 추가로 데이타를 조회하는 기능을 구현해 보자
다음은 http://localhost:3000/users/{username}이 들어오면 {username}값의 데이타를 조회해주는 함수이다.
app.get(‘/users/:username’, function(req,res,err){
var memos = new Memo();
Memo.findOne({‘username’:req.params.username},function(err,memo){
if(err){
console.err(err);
throw err;
}
console.log(memo);
res.send(200,memo);
});
});
app.get(‘/users/:username’ 에서 :username 을 이용하여 URL Parameter로 username을 받고
Memo 모델의 findOne이라는 메서드를 이용해서 데이타를 가져왔다., findOne은 query 조건에 부합하는 데이타중에 하나만 리턴하는 함수이다. 첫번째 인자가 검색 조건인데 여기서는 데이타베이스에서 필드가 username인 필드의 값이 앞에 URL에서 받은 username과 일치하는 레코드를 받도록 하였다.
두 번째 인자는 데이타가 리턴되었을때 수행되는 callback함수 인데, 이 callback 함수 두번째 인자인 memo에 리턴되는 데이타가 저장된다. memo객체로는 JSON 데이타가 리턴되고, 이 JSON을 res.send(200,memo); 을 이용하여, 리턴하였다. 이 코드를 추가한 후에, 실행해보면 다음과 같은 결과를 얻을 수 있다.
이번에는 memomodels 전체 테이블을 쿼리 해보자 (select * from memomodels)
app.get(‘/users’, function(req,res,err){
var memos = new Memo();
Memo.find().select(‘username’).exec(function(err,memos){
if(err){
console.err(err);
throw err;
}
console.log(memos);
res.send(memos);
});
});
Memo.find()를 하면되는데, 예제에서 .select(‘username’)을 추가하였다. 이 메서드는 select username from memomodels 라고 생각하면 된다.즉 쿼리해온 값중에서 특정 필드값만을 리턴하도록 하는 것이다.
이 함수를 추가해서 실행해보면 다음과 같은 결과를 얻을 수 있다.
아래는 http://mongoosejs.com/docs/queries.html 에서 참고한 샘플인데, where문을 이용한 검색 조건 지정에서 부터, select해 오는 개수, Sorting order와, 특정 필드만 가지고 오는 등의 다양한 쿼리 조건을 지정한 예제이다.
Person
.find({ occupation: /host/ })
.where(‘name.last’).equals(‘Ghost’)
.where(‘age’).gt(17).lt(66)
.where(‘likes’).in([‘vaporizing’, ‘talking’])
.limit(10)
.sort(‘-occupation’)
.select(‘name occupation’)
.exec(callback);
이외에도 다양한 쿼리 조건을 지정할 수 있으니 자세한 내용은 http://mongoosejs.com/docs/queries.html 를 참고하기 바란다.
데이타 validation 하기
mongoose의 유용한 기능중의 다른 하나가 Schema에 대한 validation이다. Schema를 정의할 때, 데이타 타입이나 기타 규칙을 정해놓고, 이 규칙을 벗어나면 에러 처리를 해주게 하는 것인데, 웹 개발등에서는 워낙 일반적인 내용이니 구체적인 개념 설명은 생략하겠다.
Validator를 사용하는 방법은 앞에 구현한 코드 부분에서 Schema를 정의 한 부분을 다음과 같이 변경 한다.
// define validator
function NameAlphabeticValidator(val){
return val.match(“^[a-zA-Z()]+$”);
}
function MemoLengthValidator(val){
if(val.length>10) return null;
return val;
}
// schema definition with validation
var MemoSchema= mongoose.Schema({
username:{type:String,validate:NameAlphabeticValidator}
,memo:{type:String,validate:[
{validator:MemoLengthValidator,msg:’memo length should be less than 10′},
{validator:NameAlphabeticValidator,msg:’PATH `{PATH}` should be alphabet only. Current value is `{VALUE}` ‘}
]}
});
var Memo = mongoose.model(‘MemoModel’,MemoSchema); // MemoModel : mongodb collection name
먼저 validation rule을 정의해야 하는데, validation rule을 함수로 구현하면 된다.
이를 validator라고 하는데, NameAlphabeticValidator는 들어오는 인자가 영문일 경우에만 PASS하고, 숫자나 특수 문자가 들어오면 오류 처리를 한다. 다음으로 정의한 MemoLengthValidator의 경우에는 문자열의 길이가 10자 이상인 경우에 에러 처리를 한다.
이렇게 정의한 validator를 스키마 정의시 각 데이타 필드에 지정하면 된다. username에는 위와 같이 validator를 NameAlphabeticValidator를 적용하였다.
다음으로 memo에는 동시에 NameAlphabeticValidator 와 MemoLengthValidator 두 개를 동시에 적용하였는데, 적용할때 msg 인자로validation이 실패했을 때 리턴해주는 메세지도 함께 지정하였다.
이 메세지 부분에서 NameAlphabeticValidator 에서 발생하는 에러 메세지를 주의 깊게 보면 {PATH}와 {VALUE} 가 사용된 것을 볼 수 있는데, {PATH}는 이 에러가 발생하는 JSON 필드명을 그리고 {VALUE}는 실제로 입력된 값을 출력한다. 테스트를 해보면
message부분에 특수문자와 숫자를 넣었다. 실행 결과는 아래와 같이 에러 메세지들이 console로 출력되는데, 아래서 보는 바와 같이 message 부분에 {PATH}와 {VALUE}가 각각 memo와 Memo-1 값으로 대체 된것을 확인할 수 있다
mongoose는 어디에 쓰는게 좋을까?
mongo native가 있고, mongoose가 있는데, 그러면 각각을 어디에 쓰느냐? 이 질문은 JDBC와 JPA를 언제 쓰느냐? 와 같은 질문과 같지 않을까 싶다.
mongoose를만든 커미터에 따르면 mongodb-native모듈이 mongoose보다 빠르다고 한다. 즉 조금 더 유연한 mongodb에 대한 access가 필요하고 높은 성능을 요구할 경우에는 mongodb-native를 사용하고, 정형화 되고 스키마 정의를 통한 명시성 확보가 필요하며 validation등을 효율적으로 하고자 할때 mongoose를 사용하는 것이 좋다. 실제 프로그램에서는 위의 용도에 맞게 두 프레임웍을 섞어 쓰는 것이 좋다.
빠르게 훝어보는 node.js
#8 – MySQL 연동
조대협 (http://bcho.tistory.com)
NoSQL이 근래에 유행이기는 하지만, 데이터간의 관계를 표현하고, 트렌젝션에 일관성을 보장하는 RDBMS는 아직까지는 서버쪽에서는 필수적이다. node.js는 대표적인 오픈소스 RDBMS인 MySQL은 지원한다. 몇가지 MySQL연동 모듈이 있기는 하지만, 여기서는 가장 많이 사용되는 node-mysql 모듈에 대해서 소개하고자 한다.
node-mysql은 오픈소스로 https://github.com/felixge/node-mysql 에 코드가 공개되어 있다.
mysql 모듈을 사용하려면 npm install mysql 명령을 이용하여, node-mysql 모듈을 설치하자.
mysql에 대한 설치와 사용법에 대해서는 워낙 문서들이 많으니 생략하기로 한다.
mysql 연결
간단한 예제 작성을 위해서 express로 프로젝트를 생성한후에,mysql을 어떻게 연결하는지 살펴보도록 하자.
var express = require(‘express’);
var routes = require(‘./routes’);
var user = require(‘./routes/user’);
var http = require(‘http’);
var path = require(‘path’);
먼저 아래와 같이 mysql 모듈을 로딩한다.
var mysql = require(‘mysql’);
다음 로딩된 모듈로부터 Connection 객체를 생성한다. 이 때 실제적인 Connection 연결은 이루어지지 않는다.
var connection = mysql.createConnection({
host :’localhost’,
port : 3306,
user : ‘terry’,
password : ‘asdf1234′,
database:’terry’
});
이렇게 명시적으로 connect 메서드를 이용해서 connection을 연결하거나 또는 첫번째 Query가 실행될 때, 위의 connection 객체의 정보를 이용해서 connection이 생성된다.
connection.connect(function(err) {
if (err) {
console.error(‘mysql connection error’);
console.error(err);
throw err;
}
});
다음으로, express를 위한 환경을 변수들을 설정한후 에
var app = express();
// all environments
app.set(‘port’, process.env.PORT || 3000);
app.set(‘views’, path.join(__dirname, ‘views’));
app.set(‘view engine’, ‘ejs’);
app.use(express.favicon());
app.use(express.logger(‘dev’));
app.use(express.json());
app.use(express.urlencoded());
app.use(express.methodOverride());
app.use(express.cookieParser(‘your secret here’));
app.use(express.session());
app.use(app.router);
app.use(express.static(path.join(__dirname, ‘public’)));
Query 수행
먼저 insert 기능을 구현해보자. 먼저 mysql에 접속하여, 다음 SQL문을 이용해서 userid,name,address 컬럼을 갖는 테이블을 생성하자
CREATE TABLE `terry`.`users` (
`userid` VARCHAR(45) NOT NULL ,
`name` VARCHAR(45) NULL ,
`address` VARCHAR(45) NULL ,
PRIMARY KEY (`userid`) );
그리고 Table에 insert할 값을 받을 HTML 파일을 만들자
다음은 위의 화면을 표현하는 index.html
<html>
<head>
<title></title>
</head>
<body>
<form name=“user” method=“post” action=“/users”>
<b>Add Users</b><p>
id <input type=“text” name=“userid”/>
<br>
name <input type=“text” name=“name”/>
<br>
address <input type=“text” name=“address”/>
<br>
<button type=“submit” >Submit</button>
</form>
</body>
</html>
HTTP/POST로 값을 받아서 index.html에서 입력된 form field로 입력하는 코드를 구현하면 다음과 같다. 간단하게 connection.query에서 sql 문장을 수행하고, 결과를 callback으로 받으면 된다. 인자가 있을 경우에는 {키:값} pair로 만들어서 query의 두번째 인자로 넘기면 된다. 아래는insert into users (userid,name,address) values(폼에서 읽어온 userid,폼에서 읽어온 name,폼에서 읽어온 name)으로 SQL을 생성해서 실행해주는 문장이다. 당연히 비동기 호출이기 때문에, response는 callback 함수에서 res.send(200,xx)으로 보냈다.
// insert
app.post(‘/users’,function(req,res){
var user = {‘userid’:req.body.userid,
‘name’:req.body.name,
‘address’:req.body.address};
var query = connection.query(‘insert into users set ?’,user,function(err,result){
if (err) {
console.error(err);
throw err;
}
console.log(query);
res.send(200,’success’);
});
});
다음으로 select를 구현해보면 아래와 같이 구현이 된다. 리턴값은 callback의 두번째 인자로 전달되는데, res.json을 이용하면 간단하게 json형태로 변환하여 리턴할 수 있다.
//select all
app.get(‘/users’, function(req,res){
var query = connection.query(‘select * from users’,function(err,rows){
console.log(rows);
res.json(rows);
});
console.log(query);
});
위에서 구현한 select를 실행해보면 다음과 같은 결과를 얻을 수 있다.
만약에 where문을 사용하고자 한다면string을 그냥 생성하는 것보다 mysql.escape라는 메서드를 사용하는 것이 좋다. boolean이나 array와 같은 데이터형을 SQL문장으로 적절하게 변환해줄뿐만 아니라 mysql.escape는 SQL injection attack을 방어해준다.
var query = connection.query(‘select * from users where userid=’+mysql.escape(req.params.userid),function(err,rows){
console.log(rows);
자아 이제까지 간단하게 node-mysql 모듈을 이용해서 기본적인 SQL 문장을 수행하는 것에 대해서 알아보았다.
Transaction 처리
node-mysql 모듈을 트렌젝션 처리도 지원해주는데, 아래 예제는 users 테이블과 log 테이블에 각각의 데이터를 하나의 트렌젝션으로 묶어서 수행해주는 예제이다.
// insert with transaction
app.post(‘/userstx’,function(req,res){
var user = {‘userid’:req.body.userid,
‘name’:req.body.name,
‘address’:req.body.address};
connection.beginTransaction(function(err) {
if (err) {
throw err;
}
connection.query(‘insert into users set ?’, user, function (err, result) {
if (err) {
console.error(err);
connection.rollback(function () {
console.error(‘rollback error’);
throw err;
});
}// if err
console.log(‘insert transaction log’);
var log = {‘userid’: req.body.userid};
connection.query(‘insert into log set ?’, log, function (err, result) {
if (err) {
console.error(err);
connection.rollback(function () {
console.error(‘rollback error’);
throw err;
});
}// if err
connection.commit(function (err) {
if (err) {
console.error(err);
connection.rollback(function () {
console.error(‘rollback error’);
throw err;
});
}// if err
res.send(200, ‘success’);
});// commit
});// insert into log
});// inset into users
}); // begin trnsaction
});
먼저 connection.begintransaction을 호출한 후에, transaction이 시작되면 쿼리를 수행하기 위해서, begintrasaction안쪽 부분에서 sql 문장을 수행한다.
insert into users SQL문장을 먼저 수행한후
다음 insert into log SQL 문장을 수행하기 위해서 insert into users Query의 call back에서 insert into log SQL 문장을 수행하였다.
그 다음으로 transaction commit 처리를 insert into logs 의 callback함수에서 connection.commit을 이용해서 수행하였다. 코드를 보면 알겠지만, transaction begin,insert 두개, commit을 순차 수행하기 위해서 callback을 중첩 사용하였다. 코드도 복잡하고 {} 도 많다. 이렇게 node.js의async callback 구조로 인하여, 코드의 복잡도가 증가하는 것을 보통 callback hell(지옥)이라고 하는데, 이를 해결하기 위한 모듈로 Async 와 같은 모듈들이 있다. 이 callback hell에 대해서는 차후에 별도의 글로 따로 소개하도록 하겠다.
Connetion Pooling
node-mysql도 기본적으로 connection pooling을 제공한다. Connection pooling을 사용하는 법은 매우 쉽다.앞의 예제의 경우에는 전체가 하나의 connection만 사용하기 때문에 동시에 많은 요청이 들어올 경우 제대로 처리가 되지 않는다.
Connection을 만드는 부분을 살펴보면 DB Connection을 만드는 방법과 크게 다르지 않다.여기에 몇가지 인자를 추가 지정할 수 있는데, connectionLimit은 pool에 담을 수 있는 최대의 connection 수이다. 여기서는 20개로 설정하였다. (디폴트는 10)
다음으로 사용한 옵션이waitForConnection이라는 옵션인데, true로 되어 있으면 Pool내에 가용한 Connection이 없을 경우에 Connection이 반납되기를 기다리고, false로 되어 있으면, Connection이 없을 경우 바로 에러를 리턴한다.
var pool = mysql.createPool({
host :’localhost’,
port : 3306,
user : ‘terry’,
password : ‘asdf1234′,
database:’terry’,
connectionLimit:20,
waitForConnections:false
});
실제로 테스트를 해보면 재미있는 점이, Pool이 생성되도, 실제로 connection이 만들어지지 않는다. Pool.getConnection시에, 유휴 connection이 없을 경우에만 connectionLimit 범위안에서 connection을 생성한다.
아쉬운 점이 자바 엔터프라이즈에서 사용하는 Connection Pool의 경우 min/max 값을 정해서 자동으로 connection pool의 사이즈를 조정한다거나, 각 Connection을 health 상태를 주기적으로 검사해서 문제가 있는 것은 끊는다거나 등의 기능이 되는데, 거의 기능이 없다. (장애 대응이 쉽지 않을 듯)
Pool을 사용해서 Query를 해보자. 앞에서 구현한 select * from users를 pool로 구현해주면 query 전에 pool.getConnection으로 싸주고, getConnection의 callback 함수안에서 두 번째 인자로 넘어온 connection 객체를 이용해서 쿼리를 수행하면 된다.
//select all
app.get(‘/users’, function(req,res){
pool.getConnection(function(err,connection){
var query = connection.query(‘select * from users’, function (err, rows) {
if(err){
connection.release();
throw err;
}
console.log(rows);
res.json(rows);
connection.release();
});
console.log(query);
});
});
여기서 주의할점은 connection을 사용한 후에, 반드시 connection.release 를 이용해서 pool에 connection을 반납해야 한다.특히 에러가 났을때도 connection을 반납해야 하는 것을 잊지 말아야 한다.이렇게 connection을 반납하지 않아서 유휴 conneciton이 pool에 남아있지 않는 현상을connection leak이라고 하는데, connection pool을 사용할 때 상당히 자주 발생하는 장애이기 때문에 반드시 꼼꼼하게 처리하기 바란다. (나중에leak이 발생하면 찾기도 어렵다.)
PoolCluster
단일 DB를 연결하는 Connection Pool이외에도 여러 개의 데이터 베이스를 연결하는 Conneciton Pool의 개념인 poolCluster를 제공한다. 이poolCluster는 동시에 여러 개의 ConnectionPool을 포함하는 일종의 집합이다. 여러 개의 mysql node를 묶어서 클러스터 구성을 할 수 있는 환경이 없어서, https://github.com/felixge/node-mysql 에 나와있는 예제를 인용하였다.
// create
var poolCluster = mysql.createPoolCluster();
poolCluster.add(config); // anonymous group
poolCluster.add(‘MASTER’, masterConfig);
poolCluster.add(‘SLAVE1’, slave1Config);
poolCluster.add(‘SLAVE2’, slave2Config);
// Target Group : ALL(anonymous, MASTER, SLAVE1-2), Selector : round-robin(default)
poolCluster.getConnection(function (err, connection) {});
// Target Group : MASTER, Selector : round-robin
poolCluster.getConnection(‘MASTER’, function (err, connection) {});
// Target Group : SLAVE1-2, Selector : order
// If can’t connect to SLAVE1, return SLAVE2. (remove SLAVE1 in the cluster)
poolCluster.on(‘remove’, function (nodeId) {
console.log(‘REMOVED NODE : ‘ + nodeId); // nodeId = SLAVE1
});
poolCluster.getConnection(‘SLAVE*’, ‘ORDER’, function (err, connection) {});
// of namespace : of(pattern, selector)
poolCluster.of(‘*’).getConnection(function (err, connection) {});
var pool = poolCluster.of(‘SLAVE*’, ‘RANDOM’);
pool.getConnection(function (err, connection) {});
pool.getConnection(function (err, connection) {});
// destroy
poolCluster.end();
이 코드는 “master- 2개의 slave 복제 구조”로 되어 있는 mysql 에 각 DB에 3개의 connection pool을 연결하여 pool cluster로 묶은 예제이다. poolCluster에서 connection 을 가지고 오는 것은 poolCluster.getConnection(“풀이름”) 을 사용하면 되는데, 위의 SLAVE*와 같이 ‘*’를 사용할 수 있다. 이 경우에는 어떤 정책으로 Pool을 가지고 올지를 정해야 하는데, 3 가지 정책을 지원한다.
RR : Connection을 해당 이름의 풀들에서 번갈아 가면서 한번씩 리턴한다.
ORDER : Connection을 무조건 첫번째 풀에서부터 리턴하는데, 장애가 났을 경우에만 다음 풀의 Connection을 순차적으로 리턴한다. 이 경우에는 HA 구성등에 유리하게 사용할 수 있다.
RANDOM : 순서 없이 무작위로 아무 풀에서나 connection을 리턴한다.
이 외에도 살펴보면, Store Procedure 수행이나, Streaming, Pipe와 같은 기능들도 제공한다. 그러나 아직까지 엔터프라이즈 자바에 비해서는 분산 트렌젝션 관리 능력이나 배치 처리등은 부족한 것이 사실이고, 오픈되어 있는 모듈도 아직 성숙도가 기존의 JDBC/MySQL 모듈에 비해서 높지 않다. 예를 들어서, connection이 끊어지지 않게 주기적으로 Keep alive query를 보내는 기능등. 일반적인 B2C 애플리케이션에서는 충분히 사용가능 하지만, 고가용을 요구하거나 데이타 분실이나 은행과 같은 높은 수준의 트렌젝션 보장이 필요한 곳에는 적절하지 않다. (물론 그런곳에는mysql을 사용하지도 않겠지만)
웹의 발전과 함께, 클라이언트의 요청에 대해서 응답만을 하는 단방향성이 아닌 양방향성의 웹 사이트가 유행하게 되었는데, Socket.IO는 자바스크립트 모듈로, 양방향 통신이 가능한 웹사이트를 구축하기 위해서 HTTP 클라이언트로 푸쉬 메시지를 보내줄수 있는 모듈이다. 넓은 브라우져 지원성과 사용의 편의성 때문에 널리 사용되고 있고, node.js가 인기 있어 지는 이유 중의 하나는socket.io 때문이 아닐까 한다.
배경
Socket.io를 설명하기 전에, 웹에서의 푸쉬 개념에 대해서 이해할 필요가 있다. 웹은 기반적으로 클라이언트에서 서버로 가는 단방향성이지만, 채팅과 같은 실시간 양방향 애플리케이나 쪽지와 같이 서버에서 클라이언트로 알림을 보내줘야 하는 요구 사항이 생겼다. 그래서 여러가지 기법이 생겨났는데, 자바스크립트 기반의 AJAX가 유행하면서 몇가지 기법이 생겨났는데, 그 내용을 살펴보면 다음과 같다.
웹 푸쉬 방식 비교 : Polling vs Long Polling vs Streaming
출처 : https://blogs.oracle.com/theaquarium/entry/slideshow_example_using_comet_dojo
Polling
가장 기본적인 기법으로, 클라이언트가 서버에 주기적으로 폴링(request를 보내는 기법)이다. 주기적으로 클라이언트가 자기가 처리해야할 이벤트가 있는지 없는지를 체크하는 것이다.
서버가 폴링 요청이 들어올때 마다 이를 처리해야 하고, 다음 폴링이 이루어지기 전까지는 어떤 이벤트가 오는지를 모르기 때문에,결정적으로 실시간성이 보장이안된다.
예를 들어 폴링 주기가 10분이라고 할때 폴링 이후에 바로 이벤트가 들어왔을때, 다음 폴링주기 (10분)을 기다려야 된다. 그리고 폴링 주기가 짧을 수 록 서버가 받는 부하가 크다, 예를 클라이언트가 작업할 내용이 있는지 확인하려면, DB드에 작업 내용을 저장해놓고, 폴링 때마다 체크해야 하는데, 이때 매번 DB 쿼리를 해야 한다면, 폴링 때마다 서버가 DB를 쿼리해야하기 때문에, 받아야 하는 트렌젝션이 매우 많고 서버의 부담이 기하 급수적으로 늘어난다. 따라서, 짧은 폴링 주기는 서버에 많은 부하를 주기 때문에, 적절하지 않으며 클라이언트로 보내는 푸쉬 메시지의 실시간성이 필요하지 않은 경우에 적절하며, 서버의 부하가 상대적으로 적고(폴링 주기가 길 경우) 기존의 웹백엔드 인프라 (Tomcat과 같은 미들웨어)를 그대로 활용할 수 있는 장점을 가지고 있다.
Long Polling
Long Polling은 Polling과 비슷하나 즉시성을 갖는다. 방식은 클라이언트가 HTTP request를 보내고, 바로 request를 닫는 것이 아니라, 일정 시간 동안(오랫동안) 열어 놓고 있다가 서버에서 클라이언트로 보내는 메시지가 있으면 메시지를 HTTP response로 실어 보내고, 해당 Connection을 끊는다. 만약에 일정 시간동안 보낼 메시지가 없으면 HTTP 연결을 끊는다.
응답 메시지를 받건 안받건, 끊어진 연결은 다시 연결한다. 기본적으로 클라이언트가 연결을 해서 응답을 요청하는 Polling 형태이고, 응답이 오는지 기다리는 기간이 길기 때문에 이를 Long Polling 이라고 한다.
Long Polling의 경우 서버에 클라이언트들이 거의 항상 연결되어 있는 형태이기 때문에, 동시 서버의 동시에 연결할 수 있는 서버가 지원할 수 있는 동시 연결(Connection)수에 따라 결정된다.
예를 들어 Tomcat과 같은 WAS의 경우에는 HTTP 연결이 열려 있는 경우에는 1개의 Thread가 그 요청을 처리하기 위해서 사용되기 때문에, Tomcat의 Thread가 100개인 경우, 1 Tomcat당 처리할 수 있는 Long Polling 가능한 클라이언트 수는 100개로 한정이 된다. (기존의 HTTP 요청을 처리하는 인프라로 핸들링하기 어렵다.)
서버로부터 푸쉬 메시지를 받으면 재 연결을 해야 하기 때문에, 클라이언트로 푸쉬하는 내용이 적을 경우에 유리하며 실시간 채팅과 같이 푸쉬해야 하는 메시지가 많은 경우에는 적절하지 않다. (채팅 메시지가 하나 왔다갔다 할 때 마다 재 연결을 해야 한다.)
Streaming
마지막으로 Streaming 기법인데, 이 기법은 일반적인 TCP Connection 처럼, 클라이언트가 서버로 연결을 맺은 후에, 그 연결을 통해서 서버가 이벤트를 보내는 방식이다. Long Polling이 이벤트를 받을 때마다 연결을 끊고 재 연결을 한가면, Streaming 방식은 한번 연결되면 계속해서 그 연결을 통해서 이벤트 메시지를 보내는 방식으로 재연결에 대한 부하가 없다.
WebSocket
이러한 푸쉬 로직을 AJAX 자바스크립트로 구현하다가 구현 방식이 브라우져들 마다 각기 상이하기 때문에 나온 표준이 WebSocket이라는 표준이다. http:// 대신 ws:// 로 시작하며 Streaming과 유사한 방식으로 푸쉬를 지원한다.
그러나 문제는 이 WebSocket 기술이 근래에 나왔기 때문에, 예전 브라우져는 지원하지 않는다는 것이다.
<그림: 웹소켓 지원 브라우져 현황>
출처 : http://caniuse.com
Socket.IO
Socket.IO는 웹 클라이언트로의 푸쉬를 지원하는 모듈인데, 이 WebSocket의 한계를 뛰어 넘어주는 모듈이다. 개발자는 Socket.IO로 개발을 하고클라이언트로 푸쉬 메시지를 보내기만 하면, WebSocket을 지원하지 않는 브라우져의 경우, 브라우져 모델과 버전에 따라서 AJAX Long Polling, MultiPart Streaming, Iframe을 이용한 푸쉬, JSONP Polling, Flash Socket 등 다양한 방법으로 내부적으로 푸쉬 메시지를 보내준다.
즉 WebSocket을 지원하지 않는 어느 브라우져라도 푸쉬 메시지를 일관된 모듈로 보낼 수 있다.
간단한 예제를 살펴보자
간단한 채팅 프로그램
간단한 채팅 프로그램을 하나 살펴보자. 아래 프로그램은 브라우져에 접속하면 메시지를 입력 받을 수 있는 Input Box를 띄워주고, 여기에 메시지를 입력하면 현재 연결되어 있는 모든 웹브라우져에 메시지를 보내주는 프로그램이다.
먼저 서버쪽 코드를 보자
/**
* Module dependencies. */
var express = require(‘express’); var routes = require(‘./routes’); var http = require(‘http’); var path = require(‘path’);
var app = express(); app.use(express.static(path.join(__dirname, ‘public’)));
var httpServer =http.createServer(app).listen(8080, function(req,res){ console.log(‘Socket IO server has been started’); }); // upgrade http server to socket.io server var io = require(‘socket.io’).listen(httpServer);
io.sockets.on(‘connection’,function(socket){ socket.emit(‘toclient’,{msg:’Welcome !’}); socket.on(‘fromclient’,function(data){ socket.broadcast.emit(‘toclient’,data); // 자신을 제외하고 다른 클라이언트에게 보냄 socket.emit(‘toclient’,data); // 해당 클라이언트에게만 보냄. 다른 클라이언트에 보낼려면? console.log(‘Message from client :’+data.msg); }) });
|
<코드. App.js>
Express에서 Socket IO를 사용한 예제인데, 기존에 Express에서 사용한것과 같은 방식으로 httpServer를 생성한다. 다음에, 이httpServer를 socketIO를 지원하는 서버로 다음과 같이 업그레이드를 한다.
var io = require(‘socket.io’).listen(httpServer);
다음으로, 클라이언트가 socket.io 채널로 접속이 되었을때에 대한 이벤트를 정의한다.
io.sockets.on(‘connection’,function(socket){
와 같이 클라이언트가 접속이 되면, callback을 수행하는데, 이때, 연결된 클라이언트의 socket 객체를 같이 넘긴다. 이 socket 객체를 받아서, 이 코드에서는 연결된 클라이언트에게 “Welcome !”이라는 메시지를 보냈다.
socket.emit(‘toclient’,{msg:’Welcome !’});
일반적인 이벤트 처리 방식과 같게, 해당 클라이언트 소켓에 emit 메서드를 이용하여, 이벤트를 전송하면 된다. 여기서는“toclient”라는 이벤트 명으로 msg라는 키를 갖고, value는 ‘Welcome !’ 이라는 값을 가지는 메시지를 전송하였다.
다음으로, 클라이언트로부터 오는 메시지를 처리하는 루틴인데, 채팅창에서 글을 쓰고 엔터를 누르면 서버로 “fromclient” 라는 이벤트를 보내도록 작성해놓았다. 그러면 서버쪽에서는 다음과 같이 socket.on(‘fromclient’ 라는 메서드를 이용하여 해당 이벤트에따른 처리를 한다. 이때 들어오는 데이터는 채팅 문자열이 {msg:”문자열”} 형식으로 data라는 변수를 통해서 아래와 같이 들어오는데,
socket.on(‘fromclient’,function(data){
채팅에서 이메세지를 다른 클라이언트들과 자신에게 다시 보낸다.
socket.broadcast.emit(‘toclient’,data);
socket.emit(‘toclient’,data);
socket.broadcast.emit은 자신을 제외한 다른 모든 클라이언트에게 이벤트를 보내는 메서드이고, socket.emit은 자신의 클라이언트(웹)에게 이벤트를 보내는 메서드이다.
그러면 이제 클라이언트(웹)쪽의 코드를 보자
<html>
<head>
<title></title> <script src=“/socket.io/socket.io.js”></script> <script src=“//code.jquery.com/jquery-1.11.0.min.js”></script>
</head> <body> <b>Send message</b><p> Message <input type=“text” id=“msgbox”/> <br> <span id=“msgs”></span>
<script type=“text/javascript”> var socket = io.connect(‘http://localhost’); $(“#msgbox”).keyup(function(event) { if (event.which == 13) { socket.emit(‘fromclient’,{msg:$(‘#msgbox’).val()}); $(‘#msgbox’).val(”); } }); socket.on(‘toclient’,function(data){ console.log(data.msg); $(‘#msgs’).append(data.msg+'<BR>’); }); </script> </body> </html> |
<코드. index.html>
먼저 socket.io를 사용하기 위해서 script src를 아래와 같이 정의하고
<script src=“/socket.io/socket.io.js”></script>
다음으로, 자바 스크립트가 실행되면, socket.io 서버로 연결을 한다.
var socket = io.connect(‘http://localhost’);
그리고, input box에서 엔터를 누르면, input box의 메시지를 읽어서, ‘fromclient’라는 이벤트를 서버에 전송한다.
socket.emit(‘fromclient’,{msg:$(‘#msgbox’).val()});
그리고 반대로, 서버로부터, ‘toclient’라는 이벤트가 들어오면, 들어온 문자열을 msgs라는 id를 갖는 <span> 영역에 append 한다.
socket.on(‘toclient’,function(data){
console.log(data.msg);
$(‘#msgs’).append(data.msg+'<BR>’);
클라이언트와 서버쪽 코드가 다 완성되었으면, 이를 배포하고 node.js를 실행해서 테스트를 해보자.
<그림. socket.io를 이용한 채팅 프로그램 화면>
다음에는 Socket.IO API들에 대한 소개와 1:1 귓속말, 그리고 그룹의 개념을 가지는 채팅방 예제에 대해서 설명하도록 한다.
Socket.IO APIs
Socket.IO는 이밖에도 다양한 이벤트를 전달할 수 있는 API를 제공하는데, 이에 대해서 살펴보자.
여기서 사용하는 socket이라는 객체는
io.sockets.on(‘connection’,function(socket){
에 의해서 callback function에 의해서 전달된 인자임을 미리 명시해둔다.
- 이벤트보내기 받기
먼저 소켓으로 또는부터 이벤트를 보내고 받는 방법부터 알아보자.앞에 예제에서도 봤지만 가장 간단한 방법은
* 이벤트 보내기 socket.emit(‘이벤트명’,{메세지});
현재 연결되어 있는 클라이언트 소켓에 “이벤트명”으로 “{메시지}” 데이터로 이벤트를 보낸다.
* 이벤트 받기 socket.on(‘이벤트명’,function(data){ });
현재 연결되어 있는 클라이언트 소켓으로부터 들어오는 “이벤트명”이벤트에 대해서 두번째 인자로 정의된 callback function에 의해서 이벤트에 대한 처리를 한다. 이때 이벤트 메시지는 callback function의인자인 “data”를 통해서 전달된다.
하나의 클라이언트가 아니라 다수의 다른 클라이언트나 또는 다른 클라이언트에 이벤트를 어떻게 보내는지 알아보자
* 나를 제외한 다른 클라이언트들에게 이벤트 보내기 socket.broadcast.emit(‘이벤트명’,{메세지});
socket에 대해서 broadcast를 하면, 나를 제외한 다른 소켓 클라이언트들에게 이벤트를 보낼 수 있다.
* 나를 포함한 모든 클라이언트들에게 이벤트 보내기io.sockets.emit(‘이벤트명’,function(data){ });
개별 클라이언트 소켓을 대표하는 객체가 socket이라면, 전체 연결된 socket들을 대표하는 객체는io.sockets이다. 여기서는 io.sockets.emit를 사용했는데, 이는 전체 연결된 클라이언트 소켓에 대해서이벤트를 보내도록 한 것이다.
내 소켓이 아닌 다른 특정 소켓에게 이벤트를 보내는 방법이 있는데,
* io.sockets(socket_id).emit(‘이벤트명’,function(data){ });
를 사용하면 된다. 이때 socket_id는 socket.id 값으로 각 클라이언트 소켓은 id라는 property를 가지고있고, 이는 각 소켓을 구별해주는 식별자가 된다. 그나중에 예제에서 설명하겠지만, 채팅 귓속말과 같이특정 소켓으로 메시지를 보내려면, 메시지를 전달하는 대상이 되는 소켓의 id값을 알아서 위의io.socket(socket_id)로 이벤트를 전달해야 한다.
- 소켓에 데이터 바인딩
각 소켓에는 소켓에 연관된 데이터를 set 메서드를 이용해서 binding하고, get을 이용해서 binding 된 데이터를꺼낼 수 있다.
* socket.set(‘key’, ‘value’,function() {});
* socket.get(‘key’, function(err,value) {});
* socket.del(‘key’, function(err,value) {});
값으 저장할때는 set을 이용하여 socket에 key값을 키로 사용하여 value라는 데이터를 저장한다. 값을 읽어올 때는 get을 이용하여 socket에 저장된 key이름으로 저장된 값을 value를 통해서 리턴한다.그리고 해당값을 삭제하고자 할때는 del을 이용하여 socket에 key이름으로 저장된 값을 삭제한다.
socket도 객체이기 때문에 Object의 property를 사용해도 되는데, 예를 들어 socket.key = value 왜 굳이set/get을 사용할까? socket.io의 set/get 내부 구현을 뜯어보면 실제로는 Object의 property를 사용한다. 단socket.io의 store를 Redis로 지정하게 되면, 이 set/get은 값을 내부 Object property에 저장하지 않고, Redis에 저장하게 되서, 이 값들은 클러스터 노드 (다른 노드간)에서도 접근이 가능해진다.
- Room 처리 (그룹)
socket.io는 소켓들을 그룹핑하는 채널과 같은 개념인 ‘room’이라는 개념을 지원한다. 채팅 프로그램의 대화방과 같은 개념이다. Room을 사용하게 되면 broadcast를 하더라도 같은 room안에 있는 클라이언트에게만 이벤트가 전송된다.
* socket.join(‘roon name’)
* socket.leave(‘roon name’)
소켓을 특정 room에 binding하는 방법은 join을 이용하면 해당 소켓은 그 room에 binding이 되고, leave를 하면 그 room에서부터 나오게 된다.
특정 room에 있는 socket에게 이벤트를 보내는 것은 앞에 설명한 똑같이 socket.emit을 사용하면 되는데, broadcast를 하거나 room안에 있는 전체 클라이언트 소켓에게 이벤트를 보낼때는 아래와 같이 room을 명시해주면 된다
* io.sockets.in(‘roon name’).emit(‘event’,message)
‘room name’ room안에 있는 모든 클라이언트들에게 이벤트 보내기
* socket.broadcast.to(‘roon name’).emit(‘event’,message)
‘room name’의 room 안에 있는 나를 제외한 다른 클라이언트들에게 이벤트 보내기
또한 현재 생성되어 있는 room에 대한 정보를 읽어오는 방법이 몇가지가 있는데,
* io.sockets.manager.rooms
현재 생성되어 있는 모든 room의 목록을 리턴한다.
* io.sockets.clients(‘roon name’)
‘room name’의 room 안에 있는 모든 클라이언트 소켓 목록을 리턴한다.
몇가지 주요 메서드들을 성명하였지만, 여기서 설명한 것은 일부에 불과하다. 상세한 내용들은 아래 내용을 참고하기 바란다.
상세한 Configuration 처리 https://github.com/LearnBoost/Socket.IO/wiki/Configuring-Socket.IO
이벤트 종류(disconnect 등) https://github.com/LearnBoost/socket.io/wiki/Exposed-events
자아 그러면 소개된 메서드들을 가지고, 앞에서 만든 채팅 프로그램의 기능을 더해보도록 하자.
작성한 대화방 http://bcho.tistory.com/896 에 이어서 이번에는 1:1 귓속말 대화가 가능한 기능을 추가해보자
귓속말이 가능한 대화방
이번에는 특정 사용자가 다른 사용자에게 귓속말을 보내는 기능을 가지는 대화방을 만들어보자. 이를 통해서 특정 클라이언트 소켓에 메세지를 어떻게 보내는지를 배울 수 있다.
이 대화방의 기능은 다음과 같다.
- 대화방에 입장하는 손님들은 자동으로 대화명을 부여 받는다.
- 사용자는 대화명을 바꿀 수 있다.
- 사용자는 대화 수신자의 대화명을 선택하여 특정 사용자에게 귓속말을 보낼 수 있다.
코드를 보면서 설명하도록 하자.
app.js
var express = require(‘express’);
var routes = require(‘./routes’); var http = require(‘http’); var path = require(‘path’);
var app = express(); app.use(express.static(path.join(__dirname, ‘public’)));
var httpServer =http.createServer(app).listen(3000, function(req,res){ console.log(‘Socket IO server has been started’); }); // upgrade http server to socket.io server var io = require(‘socket.io’).listen(httpServer);
var socket_ids = []; var count = 0;
function registerUser(socket,nickname){ // socket_id와 nickname 테이블을 셋업 socket.get(‘nickname’,function(err,pre_nick){ if(pre_nick != undefined ) delete socket_ids[pre_nick]; socket_ids[nickname] = socket.id socket.set(‘nickname’,nickname,function(){ io.sockets.emit(‘userlist’,{users:Object.keys(socket_ids)}); });
}); }
io.sockets.on(‘connection’,function(socket){ socket.emit(‘new’,{nickname:’GUEST-‘+count}); registerUser(socket,’GUEST-‘+count); count++;
socket.on(‘changename’,function(data){ registerUser(socket,data.nickname); }); socket.on(‘disconnect’,function(data){ socket.get(‘nickname’,function(err,nickname){ if(nickname != undefined){ delete socket_ids[nickname]; io.sockets.emit(‘userlist’,{users:Object.keys(socket_ids)});
}// if }); }); socket.on(‘send_msg’,function(data){ socket.get(‘nickname’,function(err,nickname){
data.msg = nickname + ‘ : ‘+data.msg; if(data.to ==’ALL’) socket.broadcast.emit(‘broadcast_msg’,data); // 자신을 제외하고 다른 클라이언트에게 보냄 else{ socket_id = socket_ids[data.to]; if(socket_id != undefined){ io.sockets.socket(socket_id).emit(‘broadcast_msg’,data); }// if } socket.emit(‘broadcast_msg’,data); }); }); }); |
먼저 Express에서 socket.io를 사용할 준비를 하고
특정 사용자 즉 클라이언트에게만 메세지를 보내려면
io.sockets.socket(socket_id).emit
메세드를 사용해야 한다. 해당 클라이언트의 socket_id를 알아야 하며, 우리는 대화명(이하 nickname)을 통해서 특정 사용자에게 메세지를 보낼 것이기 때문에 nickname 에서 socket_id로의 맵핑 테이블이 필요하다.
var socket_ids = []
에서 socket_ids는 nickname to socket.id에 대한 맵핑 정보를 저장한다.
io.sockets.on(‘connection’,function(socket){
socket.emit(‘new’,{nickname:’GUEST-‘+count});
registerUser(socket,’GUEST-‘+count);
count++;
클라이언트가 접속되면 new 라는 이벤트를 통해서 nickname을 생성해서 보낸다. nickname은 사용자가 접속한 순서대로 GUEST-0,GUEST-1,… 식으로 순차적으로 이름을 배정한다.
다음으로, 새로운 사용자가 추가되었음을 알리고, 현재 사용자 리스트들을 보내야 하는데, 이를 registerUser 메서드에서 수행한다.
function registerUser(socket,nickname){
// socket_id와 nickname 테이블을 셋업
socket.get(‘nickname’,function(err,pre_nick){
if(pre_nick != undefined ) delete socket_ids[pre_nick];
socket_ids[nickname] = socket.id
socket.set(‘nickname’,nickname,function(){
io.sockets.emit(‘userlist’,{users:Object.keys(socket_ids)});
});
});
}
먼저 register user에서는 socket.get을 이용하여 현재의 클라이언트 소켓의 nickname을 pre_nick이라는 변수로 읽어온다. 대화명이 바뀔 경우 기존에 socket_ids에 기존의 대화명으로 저장되어 있는 socket.id를 삭제하기 위함이다. 기존의 데이타를 삭제 하였으면, socket_ids에 nickname을 Key 값으로 하여, socket.id를 저장한다. 다음. nickname을 해당 socket에 set명령을 이용해서 저장한후에, userlist라는 이벤트를 통해서, 현재 접속된 사용자의 nickname들을 보낸다. 이 nickname들은 socket_ids의 property의 키들로 저장이 되었기 때문에, Object.keys(socket_ids)를 이용하여 nickname 리스트를 추출할 수 있다.
마찬가지로, 대화명이 변경되었을 때에도 registerUser 함수를 이용하여 전체 사용자의 리스트를 업데이트 하여 클라이언트에게 보내준다.
socket.on(‘changename’,function(data){
registerUser(socket,data.nickname);
});
만약에 해당 클라이언트가 브라우져를 닫았을 경우에는 대화방을 떠난 것으로 간주하여, ‘disconnect’이벤트에 대해서,
socket.on(‘disconnect’,function(data){
socket.get(‘nickname’,function(err,nickname){
if(nickname != undefined){
delete socket_ids[nickname];
현재 socket의 nickname을 읽어서 socket_ids에서 해당 nickname에 해당 하는 데이타를 삭제한 후에,
io.sockets.emit(‘userlist’,{users:Object.keys(socket_ids)});
를 이용하여, 업데이트된 사용자 목록을 다시 클라이언트들이 업데이트 하도록 이벤트를 보낸다.
마지막으로 클라이언트로 부터 대화메세지를 다른 사용자에게 대화 메세지를 보내는 “send_msg” 이벤트가 들어왔을때
socket.on(‘send_msg’,function(data){
socket.get(‘nickname’,function(err,nickname){
data.msg = nickname + ‘ : ‘+data.msg;
에서 처리하는데, 현재 클라이언트의 nickname을 socket.get을 이용해서 읽어와서 보낼 메세지문자열을 “대화명”+”대화내용”으로 만들어서 저장한다.
만약에 전체 메세지인 경우 data.to 가 ‘ALL’로 들어오는데, 이경우 broadcast를 하고
if(data.to ==’ALL’) socket.broadcast.emit(‘broadcast_msg’,data);
특정 nickname이 data.to에 들어오는 경우 귓속말로 간주하여, nickname을 이용해서 socket_ids로 부터 해당 nickname을 사용하는 클라이언트의 socket.id를 가져온후
socket_id = socket_ids[data.to];
if(socket_id != undefined){
다음으로, 해당 socket_id로 메세지를 보낸다.
io.sockets.socket(socket_id).emit(‘broadcast_msg’,data);
다음은 위의 서버를 사용하기 위한 HTML 클라이언트 폼이다.
index.html
<html>
<head>
<title></title> <script src=“/socket.io/socket.io.js”></script> <script src=“//code.jquery.com/jquery-1.11.0.min.js”></script>
</head> <body> <b>Send message</b><p> Name <input type=“text” id=“nickname” /> <input type=“button” id=“changename”value=“Change name”/><br> To <select id=“to”> <option value=“ALL”>ALL</option> </select> Message <input type=“text” id=“msgbox”/> <br> <span id=“msgs”></span>
<script type=“text/javascript”> var socket = io.connect(‘http://localhost’); $(‘#changename’).click(function(){ socket.emit(‘changename’,{nickname:$(‘#nickname’).val()}); }); $(“#msgbox”).keyup(function(event) { if (event.which == 13) { socket.emit(‘send_msg’,{to:$(‘#to’).val(),msg:$(‘#msgbox’).val()}); $(‘#msgbox’).val(”); } }); socket.on(‘new’,function(data){ console.log(data.nickname); $(‘#nickname’).val(data.nickname); });
// 새로운 사용자가 들어오거나, 사용자가 이름을 바꿨을때 “To” 리스트를 변경함 socket.on(‘userlist’,function(data){ var users = data.users; console.log(users); console.log(data.users.length); $(‘#to’).empty().append(‘<option value=”ALL”>ALL</option>’); for(var i=0;i<data.users.length;i++){ $(‘#to’).append(‘<option value=”‘+users[i]+'”>’+users[i]+”</option>”); } });
socket.on(‘broadcast_msg’,function(data){ console.log(data.msg); $(‘#msgs’).append(data.msg+'<BR>’); }); </script> </body> </html> |
앞에서 작성한 채팅 프로그램과 크게 다를바는 없으나, new 이벤트와 userlist 이벤트 핸들러가 추가되었다.
‘new’ 이벤트는 채팅방에 들어왔을때, 서버로 부터 대화명과 함께 보내지며, 클라이언트에서는 이 대화명을 받아서, “대화명 창”부분에 세팅 한다.
socket.on(‘new’,function(data){
console.log(data.nickname);
$(‘#nickname’).val(data.nickname);
});
‘userlist’ 이벤트는 현재 대화방에 있는 사용자들의 nickname을 모두 받은 후에, 대화 상대를 지정하는 HTML <select> box 부분에 대화명들을 넣어준다.
socket.on(‘userlist’,function(data){
var users = data.users;
console.log(users);
console.log(data.users.length);
$(‘#to’).empty().append(‘<option value=”ALL”>ALL</option>’);
for(var i=0;i<data.users.length;i++){
$(‘#to’).append(‘<option value=”‘+users[i]+'”>’+users[i]+”</option>”);
}
});
다음은 위의 예제를 실행한 화면이다. 아래와 같이 TO 부분에서 사용자를 지정하고 메세지를 보내면 특정 사용자에게만 메세지가 전달됨을 확인할 수 있다.
다음에는 대화방의 기능을 추가하여, socket.io의 room 개념에 대해서 알아보도록 한다.
채팅 프로그램에 방(room/그룹)의 기능을 추가하기
다음은 앞에서 만든 1:1 귓속말이 가능한 채팅에 “채팅방” 기능을 추가한 버전이다.
var express = require(‘express’);
var routes = require(‘./routes’); var http = require(‘http’); var path = require(‘path’);
var app = express(); app.use(express.bodyParser()); app.use(express.cookieParser(‘your secret here’)); app.use(express.session()); app.use(express.static(path.join(__dirname, ‘public’))); app.set(‘views’, path.join(__dirname, ‘views’)); app.set(‘view engine’, ‘ejs’); app.use(express.favicon()); app.use(express.logger(‘dev’)); app.use(express.json()); app.use(express.urlencoded()); app.use(express.methodOverride()); app.use(app.router);
var httpServer =http.createServer(app).listen(3000, function(req,res){ console.log(‘Socket IO server has been started’); }); // upgrade http server to socket.io server var io = require(‘socket.io’).listen(httpServer);
var count = 0; var rooms = [];
app.get(‘/:room’,function(req,res){ console.log(‘room name is :’+req.params.room); res.render(‘index’,{room:req.params.room}); });
io.sockets.on(‘connection’,function(socket){
socket.on(‘joinroom’,function(data){ socket.join(data.room);
socket.set(‘room’, data.room,function() { var room = data.room; var nickname = ‘손님-‘+count; socket.set(‘nickname’,nickname,function(){ socket.emit(‘changename’, {nickname: nickname});
// Create Room if (rooms[room] == undefined) { console.log(‘room create :’ + room); rooms[room] = new Object(); rooms[room].socket_ids = new Object(); } // Store current user’s nickname and socket.id to MAP rooms[room].socket_ids[nickname] = socket.id
// broad cast join message data = {msg: nickname + ‘ 님이 입장하셨습니다.’}; io.sockets.in(room).emit(‘broadcast_msg’, data);
// broadcast changed user list in the room io.sockets.in(room).emit(‘userlist’, {users: Object.keys(rooms[room].socket_ids)}); count++; }); });
});
socket.on(‘changename’,function(data){ socket.get(‘room’,function(err,room){ socket.get(‘nickname’,function(err,pre_nick) { var nickname = data.nickname; // if user changes name get previous nickname from nicknames MAP if (pre_nick != undefined) { delete rooms[room].socket_ids[pre_nick]; } rooms[room].socket_ids[nickname] = socket.id socket.set(‘nickname’,nickname,function() { data = {msg: pre_nick + ‘ 님이 ‘ + nickname + ‘으로 대화명을 변경하셨습니다.’}; io.sockets.in(room).emit(‘broadcast_msg’, data);
// send changed user nickname lists to clients io.sockets.in(room).emit(‘userlist’, {users: Object.keys(rooms[room].socket_ids)}); }); });
}); });
socket.on(‘disconnect’,function(data){ socket.get(‘room’,function(err,room) { if(err) throw err; if(room != undefined && rooms[room] != undefined){
socket.get(‘nickname’,function(err,nickname) { console.log(‘nickname ‘ + nickname + ‘ has been disconnected’); // 여기에 방을 나갔다는 메세지를 broad cast 하기 if (nickname != undefined) { if (rooms[room].socket_ids != undefined && rooms[room].socket_ids[nickname] != undefined) delete rooms[room].socket_ids[nickname]; }// if data = {msg: nickname + ‘ 님이 나가셨습니다.’};
io.sockets.in(room).emit(‘broadcast_msg’, data); io.sockets.in(room).emit(‘userlist’, {users: Object.keys(rooms[room].socket_ids)}); }); } }); //get });
socket.on(‘send_msg’,function(data){ socket.get(‘room’,function(err,room) { socket.get(‘nickname’,function(err,nickname) { console.log(‘in send msg room is ‘ + room); data.msg = nickname + ‘ : ‘ + data.msg; if (data.to == ‘ALL’) socket.broadcast.to(room).emit(‘broadcast_msg’, data); // 자신을 제외하고 다른 클라이언트에게 보냄 else { // 귓속말 socket_id = rooms[room].socket_ids[data.to]; if (socket_id != undefined) {
data.msg = ‘귓속말 :’ + data.msg; io.sockets.socket(socket_id).emit(‘broadcast_msg’, data); }// if } socket.emit(‘broadcast_msg’, data); }); }); }) }); |
코드를 살펴보자
처음에 입장은 http://localhos:3000/{방이름} 으로 하게 된다.
app.get(‘/:room’,function(req,res){
console.log(‘room name is :’+req.params.room);
res.render(‘index’,{room:req.params.room});
});
그러면 URL에 있는 방이름을 받아서, index.ejs에 있는 UI로 채팅창을 띄워주고 방이름을 parameter로 index.ejs에 넘겨준다.
socket.on(‘joinroom’,function(data){
socket.join(data.room);
클라이언트가 서버에 접속되면 맨 먼저 클라이언트가 join 이벤트를 보내는데, 이 join 이벤트를 받으면, 이때 같이 온 room 이름으로 현재 소켓을room 이름의 room에 join한다.
socket.set(‘room’, data.room,function() {
다음으로, 해당 소켓이 어느 룸에 있는지 를 set 명령을 이용하여 socket에 저장해놓는다.
var room = data.room;
var nickname = ‘손님-‘+count;
socket.set(‘nickname’,nickname,function(){
socket.emit(‘changename’, {nickname: nickname});
// Create Room
if (rooms[room] == undefined) {
console.log(‘room create :’ + room);
rooms[room] = new Object();
rooms[room].socket_ids = new Object();
}
윗부분이 room 데이터 객체를 생성하는 것인데, 앞의 예제와는 달리, 현재 연결된 클라이언트의 socket.id를 이제는 room 단위로 관리를 해야 한다. 그래서 rooms라는 객체를 이용하여, 해당 room에 대해서 rooms.room이라는 객체로 만들고, 그리고, 이 room에 현재 연결된 클라이언트 의socket.id를 저장하는 socket_ids 객체를 생성한다.
// Store current user’s nickname and socket.id to MAP
rooms[room].socket_ids[nickname] = socket.id
그리고 나서, socket_ids에 귓속말 채팅방 예제와 같이 nickname to socket.id 에 대한 맵핑 정보를 저장한다.
// broad cast join message
data = {msg: nickname + ‘ 님이 입장하셨습니다.’};
io.sockets.in(room).emit(‘broadcast_msg’, data);
// broadcast changed user list in the room
io.sockets.in(room).emit(‘userlist’, {users:
Object.keys(rooms[room].socket_ids)});
count++;
});
그리고 위와 같이 현재 room에 들어 있는 클라이언트들에게만 , 새로운 사용자가 입장했음을 알리고, 사용자 리스트를 업데이트하는 이벤트를 보낸다.
disconnect에 대한 부분도 크게 달라진 것이 없다. Socket_ids 객체가 rooms 아래로 들어갔고, 메시지를 보낼때, 귓속말 채팅방 예제가io.sockets.emit 대신에, room의 범위를 지정하는 in() 메서드를 써서 io.sockets.in(room).emit와 같이 보내게 된다.
Sendmsg 이벤트 부분도, broadcast하는 부분에서 to를 이용하여 다음과 같이socket.broadcast.to(room).emit 특정 room에 있는 클라이언트에게만 메시지를 보내는 것으로만 변경되었다
아래는 클라이언트쪽의 코드이다. 앞의 예제에서 나온 귓속말이 가능한 대화방과 코드가 거의 동일하다. 단
<script type=“text/javascript”>
var socket = io.connect(‘http://localhost’);
socket.emit(‘joinroom’,{room:'<%=room%>’});
처음에 접속하였을 때, 서버 코드에서 방이름을 URL로부터 읽어서, 그 방이름으로 join 하는 이벤트를 보낸다.
/vies/index.ejs
<html>
<head>
<title></title> <script src=“/socket.io/socket.io.js”></script> <script src=“//code.jquery.com/jquery-1.11.0.min.js”></script>
</head> <body>
<b>Welcome ROOM : <%= room%></b><p> Name <input type=“text” id=“nickname” /> <input type=“button” id=“changename”value=“Change name”/><br> To <select id=“to”> <option value=“ALL”>ALL</option> </select> Message <input type=“text” id=“msgbox”/> <br> <span id=“msgs”></span>
<script type=“text/javascript”> var socket = io.connect(‘http://localhost’); socket.emit(‘joinroom’,{room:'<%=room%>’});
$(‘#changename’).click(function(){ socket.emit(‘changename’,{nickname:$(‘#nickname’).val()}); }); $(“#msgbox”).keyup(function(event) { if (event.which == 13) { socket.emit(‘send_msg’,{to:$(‘#to’).val(),msg:$(‘#msgbox’).val()}); $(‘#msgbox’).val(”); } }); socket.on(‘new’,function(data){ console.log(data.nickname); $(‘#nickname’).val(data.nickname); });
// 새로운 사용자가 들어오거나, 사용자가 이름을 바꿨을때 “To” 리스트를 변경함 socket.on(‘userlist’,function(data){ var users = data.users; console.log(users); console.log(data.users.length); $(‘#to’).empty().append(‘<option value=”ALL”>ALL</option>’); for(var i=0;i<data.users.length;i++){ $(‘#to’).append(‘<option value=”‘+users[i]+'”>’+users[i]+”</option>”); } });
socket.on(‘broadcast_msg’,function(data){ console.log(data.msg); $(‘#msgs’).append(data.msg+'<BR>’); }); </script> </body> </html> |
다음은 실제 실행 화면이다.
node.js 노드가 하나가 아니라 여러개의 프로세스를 이용해서 운영할 때,socket.io를 어떻게 사용해야 할까? 이런 멀티 프로세스를 지원하기 위해서, node.js는 내부적으로 redis store를 지원한다. redis에는 publish/subscribe라는 기능이 있는데, 마치 메세지 큐처럼 메세지를 subscriber로 보낼 수 있는 기능이다.
아래 그림을 보자,하나의 node프로세스에서 메세지를 보내면, 다른 프로세스로 redis를 통해서 메세지를 전달한다. 이때 메세지를 보내는 프로세스는 redis에 메세지를 “publish”하고 나머지 프로세스들은 “subscribe”를 이용하여 메세지를 읽어드린다. 이때, 메세지를 전달하는 채널은“dispatch”라는 이름의 채널을 이용한다.
그러면 실제로, socket.io에서 redis store를 사용하려면 어떻게 해야 할까? 간단한 설정만으로 가능하다. 아래와 같이 redis client를 생성한 후에, socket.io에 set 명령을 이용하여 store를 redis client로만 지정해주면 된다.
var httpServer =http.createServer(app).listen(process.argv[2], function(req,res){
console.log(‘Socket IO server has been started listen:’+process.argv[2]); }); // upgrade http server to socket.io server var io = socketio.listen(httpServer); var pub = redis.createClient(6379,’127.0.0.1′); var sub = redis.createClient(6379,’127.0.0.1′); var store = redis.createClient(6379,’127.0.0.1′);
io.set(‘store’,new socketio.RedisStore({ redis: redis ,redisPub : pub ,redisSub : sub ,redisClient : store }));
|
그리고, cluster 모듈을 이용하거나, 앞단에 nginx(http:// http://nginx.org/ ) haproxy (http://haproxy.1wt.eu/) 로드밸런서를 이용하여 여러개의 node.js 프로세스에 대한 end point를 하나로 묶으면, 대규모 분산 서비스를 할 수 있는 socket.io 클러스터를 구성할 수 있다.