Unity3D에서 Node.js로 데이터를 protobuf를 사용해서 Websocket으로 전송하기
요약
Unity3D와 Node.JS의 protobuf를 이용해 Websocket으로 통신하는 예제가 없어 만들어 보았다.
설명하는데 복잡한 감이 있어, 앞으로 하게될 내용을 간략히 추려놓았다.
1. websocket 라이브러리 설치
2. protobuf 라이브러리 설치
3. sample idl작성
4. sample idl을 node.js과 c#용으로 컨버팅
5. 서버와 클라이언트 코드를 작성해 테스트 해보기
준비
Unity3D on Windows
websocket
1) Websocket 라이브러리를 다운로드
2) 다운받은 라이브러리를 release 모드로 컴파일
3) \websocket-sharp\bin\release\websocket-sharp.dll 파일을 유니티 프로젝트에 복사
protobuf
1) .net용 protobuf 라이브러리 다운로드
2) \protobuf-net r668\Full\unity\protobuf-net.dll 파일을 유니티 프로젝트에 복사
Node.js on CentOS
샘플 코드
1. protobuf idl 작성하기
sample idl을 feeds.proto로 저장
package feeds; | |
message Feed { | |
optional string title = 1; | |
message Entry { | |
optional string title = 1; | |
} | |
repeated Entry entry = 2; | |
} |
2. 작성한 idl 컨버팅
node.js
protoc파일은 리눅스용 protobuf 라이브러리가 소스형태라, 컴파일 해야 하며, 컴파일이 성공했다면 생성된다.
$ /usr/local/bin/protoc --descriptor_set_out=feeds.desc --include_imports feeds.proto
생성된 파일을 Node.js 샘플 서버 코드가 있는 경로에 복사
생성된 파일 내용
e | |
feeds.protofeeds"O | |
Feed | |
title ( | |
entry (2.feeds.Feed.Entry | |
Entry | |
title ( |
c#
protogen파일은 라이브러리에 포함되어 있다.
\protobuf-net r668\ProtoGen.exe -i:feeds.proto -o:Feeds.cs
생성된 파일을 유니티 프로젝트에 복사
생성된 파일 내용
//------------------------------------------------------------------------------ | |
// <auto-generated> | |
// This code was generated by a tool. | |
// | |
// Changes to this file may cause incorrect behavior and will be lost if | |
// the code is regenerated. | |
// </auto-generated> | |
//------------------------------------------------------------------------------ | |
// Generated from: feeds.proto | |
namespace feeds | |
{ | |
[global::System.Serializable, global::ProtoBuf.ProtoContract(Name=@"Feed")] | |
public partial class Feed : global::ProtoBuf.IExtensible | |
{ | |
public Feed() {} | |
private string _title = ""; | |
[global::ProtoBuf.ProtoMember(1, IsRequired = false, Name=@"title", DataFormat = global::ProtoBuf.DataFormat.Default)] | |
[global::System.ComponentModel.DefaultValue("")] | |
public string title | |
{ | |
get { return _title; } | |
set { _title = value; } | |
} | |
private readonly global::System.Collections.Generic.List<feeds.Feed.Entry> _entry = new global::System.Collections.Generic.List<feeds.Feed.Entry>(); | |
[global::ProtoBuf.ProtoMember(2, Name=@"entry", DataFormat = global::ProtoBuf.DataFormat.Default)] | |
public global::System.Collections.Generic.List<feeds.Feed.Entry> entry | |
{ | |
get { return _entry; } | |
} | |
[global::System.Serializable, global::ProtoBuf.ProtoContract(Name=@"Entry")] | |
public partial class Entry : global::ProtoBuf.IExtensible | |
{ | |
public Entry() {} | |
private string _title = ""; | |
[global::ProtoBuf.ProtoMember(1, IsRequired = false, Name=@"title", DataFormat = global::ProtoBuf.DataFormat.Default)] | |
[global::System.ComponentModel.DefaultValue("")] | |
public string title | |
{ | |
get { return _title; } | |
set { _title = value; } | |
} | |
private global::ProtoBuf.IExtension extensionObject; | |
global::ProtoBuf.IExtension global::ProtoBuf.IExtensible.GetExtensionObject(bool createIfMissing) | |
{ return global::ProtoBuf.Extensible.GetExtensionObject(ref extensionObject, createIfMissing); } | |
} | |
private global::ProtoBuf.IExtension extensionObject; | |
global::ProtoBuf.IExtension global::ProtoBuf.IExtensible.GetExtensionObject(bool createIfMissing) | |
{ return global::ProtoBuf.Extensible.GetExtensionObject(ref extensionObject, createIfMissing); } | |
} | |
} |
3. 클라이언트와 서버 코드 작성하기
unity sample client script
using UnityEngine; | |
using System.Collections; | |
using WebSocketSharp; | |
using System.IO; | |
public class background : MonoBehaviour { | |
private WebSocket ws; | |
// Use this for initialization | |
void Start () { | |
ws = new WebSocket ("ws://172.20.44.92:8080","echo-protocol" ); | |
ws.OnMessage += (sender, e) =>{ | |
feeds.Feed dfeed = ProtoBuf.Serializer.Deserialize<feeds.Feed>(new MemoryStream(e.RawData)); | |
Debug.Log("Laputa says: " + dfeed.title); | |
}; | |
ws.Connect (); | |
feeds.Feed feed = new feeds.Feed(); | |
feed.title = "hello"; | |
feed.entry.Add(new feeds.Feed.Entry()); | |
MemoryStream mem = new MemoryStream(); // 메모리 스트림 생성 | |
ProtoBuf.Serializer.Serialize(mem, feed); | |
ws.Send (mem.ToArray()); | |
} | |
// Update is called once per frame | |
void Update () { | |
} | |
} |
node.js sample server code
var WebSocketServer = require('websocket').server; | |
var http = require('http'); | |
var server = http.createServer(function(request, response) { | |
console.log((new Date()) + ' Received request for ' + request.url + ' ' + process.pid); | |
response.writeHead(404); | |
response.end(); | |
}); | |
server.listen(8080, function() { | |
console.log((new Date()) + ' Server is listening on port 8080 of process:' + process.pid); | |
}); | |
var wsServer = new WebSocketServer({ | |
httpServer: server, | |
// You should not use autoAcceptConnections for production | |
// applications, as it defeats all standard cross-origin protection | |
// facilities built into the protocol and the browser. You should | |
// *always* verify the connection's origin and decide whether or not | |
// to accept it. | |
autoAcceptConnections: false | |
}); | |
function originIsAllowed(origin) { | |
// put logic here to detect whether the specified origin is allowed. | |
return true; | |
} | |
wsServer.on('request', function(request) { | |
if (!originIsAllowed(request.origin)) { | |
// Make sure we only accept requests from an allowed 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.' + process.pid); | |
connection.on('message', function(message) { | |
if (message.type === 'utf8') { | |
console.log('Server Received Message: ' + message.utf8Data + ' len: ' + message.utf8Data.length); | |
connection.send(message.utf8Data); | |
} | |
else if (message.type === 'binary') { | |
console.log('Server Received Binary Message of ' + message.binaryData.length + ' bytes'); | |
connection.sendBytes(message.binaryData); | |
// 수신한 protobuf data를 복원시키기. | |
var Schema = require('protobuf').Schema; | |
var readFile = require('fs').readFileSync; | |
var schema = new Schema(readFile('feeds.desc')); | |
var FeedMsg = schema['feeds.Feed']; | |
// Convert to protobuf format | |
var outMsg = FeedMsg.parse(message.binaryData); | |
console.log(outMsg.title); | |
} | |
}); | |
connection.on('close', function(reasonCode, description) { | |
console.log((new Date()) + ' Peer ' + connection.remoteAddress + ' disconnected.'); | |
}); | |
}); | |
테스트 해보기
node.js log
Unity3D log