NodeJS와 UDP소켓 통신 연결
HoloLens에서 UDP 소켓 통신 연결을 구현하였다.
2019.11.27
이번에 회사에서 진행하는 HoloLens to Driver Controller부분 구현과, 친한 동아리 선배와 함께 진행하는 프로젝트의 서버 부분 구현을 하게 되었는데, 이때 두 파트 동시에 Socket통신을 하도록 요구받았다.
HoloLens의 경우 딱히 서버 플랫폼이 정해져 있지 않았는데, 선배와 같이 진행하는 프로젝트에서는 백엔드 플랫폼이 Node JS이므로 Node JS를 응용한 Socket 통신 서버를 구현하기로 하였다.
Node JS에서의 Socket 통신
1 . TCP
사용 모듈
tcp socket 통신은 net
모듈을 사용한다.
TCP SERVER
var net = require('net');
var server = net.createServer(function(socket) {
// connection event
console.log('클라이언트 접속');
socket.write('Welcome to Socket Server');
socket.on('data', function(chunk) {
console.log('클라이언트가 보냄 : ',
chunk.toString());
});
socket.on('end', function() {
console.log('클라이언트 접속 종료');
});
});
server.on('listening', function() {
console.log('Server is listening');
});
server.on('close', function() {
console.log('Server closed');
});
server.listen(3000);
TCP CLIENT
var net = require('net');
var ip = '127.0.0.1';
var port = 3000;
var socket = new net.Socket();
socket.connect({host:ip, port:port}, function() {
console.log('서버와 연결 성공');
socket.write('Hello Socket Server\n');
socket.end();
socket.on('data', function(chunk) {
console.log('서버가 보냄 : ',
chunk.toString());
});
socket.on('end', function() {
console.log('서버 연결 종료');
});
});
2 . UDP
사용 모듈
udp socket 통신은 dgram
모듈을 사용한다.
UDP SERVER
var dgram = require('dgram');
var socket = dgram.createSocket('udp4');
socket.bind(3000);
socket.on('listening', function() {
console.log('listening event');
});
socket.on('message', function(msg, rinfo) {
console.log('메세지 도착', rinfo.address, msg.toString());
});
socket.on('close', function() {
console.log('close event');
});
UDP CLIENT
var dgram = require('dgram');
var socket = dgram.createSocket('udp4');
var msg = new Buffer('Hello UDP Receiver');
socket.send(msg, 0, msg.length, 3000, '127.0.0.1',
function(err) {
console.log(err);
if ( err ) {
console.log('UDP message send error', err);
return;
}
console.log('메세지 전송 성공');
socket.close();
}
);
HoloLens 적용 방안
이번 MR 프로젝트에서는 HoloLens MR HMD 사용자가 산업용 Application 내에서
드라이버 동기화 모듈을 조작 할수 있고, 해당 모듈의 Master와 Slave간의
Time Offset(ns)를 알 수 있어야 하는 기능이 필요했다.
이때, 모듈의 Master는 드라이버를 조작할 수 있는 명령이나 오프셋을
UDP 소켓 인터페이스를 통해 외부와 소통하기 때문에
해당 모듈이 HoloLens사용자와 같은 AP에 속해있다면,
HoloLens와 모듈간의 소켓통신으로 해당 기능을 구현할 수 있다.
하지만, 2018년 8월 쯤 실시간 화상통화를 위해 HoloLens의 소켓 통신에 대한 자료를 찾아보던 중
Unity 3D 엔진 기반 UDP 핸들러를 만들어보았지만 패킷만 받으면 해당 App이 멈춰버렸다.
구글, Windows HoloLens 포럼등 다양한 곳에 검색해본 결과,
HoloLens 자체 이슈로 많은 사람들이 공통적인 이슈를 가지고 있었다.
물론 시간이 흐른 지금 다시 조사해 보니 여러 솔루션이 발견된 것 같지만,
과제 종료까지 얼마 남지 않은 상황에서 해당 기능을 최대한 빠르게, 버그 없이 구현하기 위해선
번거롭지만 AP 사이에 존재하는 다른 존재를 응용하기로 하였다.
화상채팅에서 서버역할을 담당하는 컴퓨터를 기반으로해서,
홀로렌즈와 PC 사이는 HTTP 프로토콜로 정보를 공유하고,
PC와 동기화 모듈 사이는 소켓 통신으로 진행하도록 한다.
상세 기능 구현
기능 1 . 마스터 모듈 제어
마스터 모듈은 두 드라이버를 동시에 제어하는데, 제어는 2가지가 있다.
제어 | 메세지 길이 | 설명 |
---|---|---|
Sync Msg | 9Byte | 예약 메세지. 정해진 시간에 정해진 동작을 수행한다. |
Async Msg | 3Byte | 즉발 메세지. 메시지를 수신하는 즉시 동작을 수행한다. |
Sync Msg
Sync Msg는 총 9Byte의 메세지로, 다음과 같이 구성된다.
Offset | Byte | Description |
---|---|---|
0 | type | 항상 s. 해당 메세지는 Sync Msg임을 나타낸다. |
1 | func | 드라이버의 동작. f : 정방향, b : 역방향, s : 정지 |
2 | speed | 드라이버의 속도 1~9 정수 func가 s일경우 0 |
3 | hour | 해당 기능이 동작할 시각. 시(Hour)의 10의자리. 24시간제 사용. |
4 | hour | 해당 기능이 동작할 시각. 시(Hour)의 1의자리. 24시간제 사용. |
5 | min | 해당 기능이 동작할 시각. 분(Min)의 10의자리. |
6 | min | 해당 기능이 동작할 시각. 분(Min)의 1의자리. |
7 | sec | 해당 기능이 동작할 시각. 초(Sec)의 10의자리. |
8 | sec | 해당 기능이 동작할 시각. 초(Sec)의 1의자리. |
예를들어, 드라이버를 18시 26분 21초에 Forward방향으로 7속도로 돌리고 싶다면,
보내는 메시지는 sf7182621
이다.
Async Msg
Async Msg는 총 3Byte의 메세지로, 다음과 같이 구성된다.
즉발 메세지이므로 Sync Msg에서 시각 부분이 없는 형태이다.
Offset | Byte | Description |
---|---|---|
0 | type | 항상 a. 해당 메세지는 Async Msg임을 나타낸다. |
1 | func | 드라이버의 동작. f : 정방향, b : 역방향, s : 정지 |
2 | speed | 드라이버의 속도 1~9 정수 func가 s일경우 0 |
예를들어, 드라이버를 지금 즉시 멈추고 싶다면,
보내는 메시지는 as0
이다.
마스터 모듈 제어기능 구현
해당 기능을 HoloLens에서 바로 UDP 패킷으로 마스터 모듈에게 전송하면 좋겠지만,
패킷을 전송하는것은 PC가 하는 방향으로 설계하기로 했으므로
Node JS에서 이를 받아주는 방향으로 설계한다.
//=============================================================================
// Node JS Express 모듈의 페이지 라우팅 부분
//=============================================================================
module.exports = (app) => {
let dgram = require('dgram') // UDP 통신을 위해 사용하는 모듈
let ip = require('ip') // IP를 알아내기 위해 사용하는 모듈
//===============================================================
// Sync 메세지를 보내고 싶은 경우
//===============================================================
app.get('/sync' , (req,res) => {
let port = 50000 // 메세지를 보낼 포트
let subnetMask = '255.255.255.0' // 서브넷 마스크
let broadcastIP = ip.or(ip.address() , ip.not(subnetMask) ) // 브로드캐스트 아이피
let rotate = req.query.rotate // GET 방식 파라미터 가져옴
let speed = req.query.speed // GET 방식 파라미터 가져옴
let time = req.query.time // GET 방식 파라미터 가져옴
let socket = dgram.createSocket('udp4'); // UDP 패킷으로 소켓 생성
let msg = Buffer.from(`s${rotate}${speed}${time}`); //9 Byte 패킷 저장
// msg라는 9Byte 메세지를 broadcastIP:port 라는 주소로 전체 송신
socket.send(msg, 0, msg.length, port, broadcastIP,
(err) => {
if ( err ) console.log(err)
else { socket.close();
console.log('Sync Message sending !');
}
}
)
//그 후, 해당 결과를 HTTP 접속자에게 리턴해주기 위해 ejs를 사용하여 res 렌더링
res.render(`${__dirname}/HTML/syncResultPage.ejs`, {
broadcastIP : broadcastIP, type : 'sync', rotate : rotate,
port : port, speed:speed, time:time
})
})
//===============================================================
// Async 메세지를 보내고 싶은 경우
//===============================================================
app.get('/async' , (req,res) => {
let port = 50000 // 메세지를 보낼 포트
let subnetMask = '255.255.255.0' // 서브넷 마스크
let broadcastIP = ip.or(ip.address() , ip.not(subnetMask) ) // 브로드캐스트 아이피
let rotate = req.query.rotate // GET 방식 파라미터 가져옴
let speed = req.query.speed // GET 방식 파라미터 가져옴
let socket = dgram.createSocket('udp4'); // UDP 패킷으로 소켓 생성
let msg = Buffer.from(`a${rotate}${speed}`); //3 Byte 패킷 저장
// msg라는 3Byte 메세지를 broadcastIP:port 라는 주소로 전체 송신
socket.send(msg, 0, msg.length, port, broadcastIP,
(err) => {
if ( err ) console.log(err)
else { socket.close();
console.log('Async Message sending !');
}
}
)
//그 후, 해당 결과를 HTTP 접속자에게 리턴해주기 위해 ejs를 사용하여 res 렌더링
res.render(`${__dirname}/HTML/asyncResultPage.ejs`, {
broadcastIP : broadcastIP,type : 'async', rotate : rotate,
port : port, speed:speed,
})
})
// ... 중략 ...
}
기능 2 . Master - Slave Offset 패킷 수신
동기화 모듈은 1초에 한번씪 Master와 Slave 사이의 Offset (ns)를 PC에게 전송한다.
따라서 Node JS 서버는 이를 저장해서 핸들링을 해주어야 한다.
수신되는 offset은 9자리 숫자로 총 9 Byte이다.
구현할 Node JS서버는 해당 패킷을 파일로서 저장한다.
let dgram = require('dgram') // UDP 용 모듈
let express = require('express') // Web Server 모듈
let path = require('path') // 파일의 경로를 쉽게 찾아주는 모듈
let socket = dgram.createSocket('udp4') // UDP 패킷 설정
let app = express() // Web Server Init
let fs = require('fs') // 파일 시스템 모듈
require('./routes.js')(app) // 페이지 라우팅 모듈 임포트
let portExpress = process.env.PORT || 5000 // Web Page는 5000번 포트
let portUDP = process.env.PORT || 5001 // 마스터 모듈로부터 offset을 받아오는 포트는 5001번
socket.bind(portUDP) // UDP 포트와 바인딩
socket.on('listening', () => { console.log('listening event') }) // 5001번 포트 리스닝
// 만약, 5001번 포트로 메세지가 들어왔다면?
socket.on('message', (msg, rinfo) => {
console.log('Message received : ', rinfo.address, msg.toString()) // 로그 출력
fs.writeFileSync(`${__dirname}/HTML/offset.txt`,msg.toString(),'utf8')
// File System의 write File을 이용하여, ./HTML/offset.txt로 msg를 (9자리 숫자)를 저장함
})
socket.on('close', () => { console.log('close event') })
app.use(express.static(path.join(__dirname,'./HTML'))) // Web서버 Root설정
app.listen(portExpress, () => {console.log(`Server on port : ${portExpress}`)})
// 5000번 포트에 웹서버 가동
이로써 동기화 모듈이 일정 기간마다 패킷을 5001번 포트로 보내주면, Node JS 서버는
해당 패킷을 파일로서 저장하게 된다.
기능 3 . HoloLens에서 Offset 값 확인
HoloLens에서는 HTTP로써 확인이 해당 값 확인이 가능하므로,
Express 모듈을 사용해서 특정 페이지에 접근할 시
기능2에서 저장한 Offset.txt를 보여주는 기능이 필요하다.
해당 기능은 Express와 FS로 구현한다.
module.exports = (app) => {
// ... 중략 ...
app.get('/offset', (req,res) => {
let fs = require('fs')
fs.readFile(`${__dirname}/HTML/offset.txt`,'utf8', (err,data) => {
if( !err)
res.send(data)
})
})
// ... 중략 ...
}
이로써 동기화 모듈 - HoloLens 연동에 필요한 기능 구현을 마쳤다.
이제 남은 것은 Unity Editor상에서 드라이버 연동 인터페이스를 만드는 것 뿐!!
UI 작업후 실제 동작 부분 스크립트를 코딩중이다.
해당 내용은 거의 그대로 기술 보고서에 기술할 것이다!
부록 | Node JS 전체 소스코드
server.js
let dgram = require('dgram')
let express = require('express')
let path = require('path')
let socket = dgram.createSocket('udp4')
let app = express()
let fs = require('fs')
require('./routes.js')(app)
let portExpress = process.env.PORT || 5000
let portUDP = process.env.PORT || 5001
socket.bind(portUDP)
socket.on('listening', () => { console.log('listening event') })
socket.on('message', (msg, rinfo) => {
console.log('Message received : ', rinfo.address, msg.toString())
fs.writeFileSync(`${__dirname}/HTML/offset.txt`,msg.toString(),'utf8')
})
socket.on('close', () => { console.log('close event') })
app.use(express.static(path.join(__dirname,'./HTML')))
app.listen(portExpress, () => {console.log(`Server on port : ${portExpress}`)})
routes.js
module.exports = (app) => {
let dgram = require('dgram')
let ip = require('ip')
app.get('/sync' , (req,res) => {
let port = 50000
let subnetMask = '255.255.255.0'
let broadcastIP = ip.or(ip.address() , ip.not(subnetMask) )
let rotate = req.query.rotate
let speed = req.query.speed
let time = req.query.time
let socket = dgram.createSocket('udp4');
let msg = Buffer.from(`s${rotate}${speed}${time}`);
socket.send(msg, 0, msg.length, port, broadcastIP,
(err) => {
if ( err ) console.log(err)
else { socket.close();
console.log('Sync Message sending !');
}
}
)
res.render(`${__dirname}/HTML/syncResultPage.ejs`, {
broadcastIP : broadcastIP, type : 'sync', rotate : rotate,
port : port, speed:speed, time:time
})
})
app.get('/async' , (req,res) => {
let port = 50000
let subnetMask = '255.255.255.0'
let broadcastIP = ip.or(ip.address() , ip.not(subnetMask) )
let rotate = req.query.rotate
let speed = req.query.speed
let socket = dgram.createSocket('udp4');
let msg = Buffer.from(`a${rotate}${speed}`);
socket.send(msg, 0, msg.length, port, broadcastIP,
(err) => {
if ( err ) console.log(err)
else { socket.close();
console.log('Async Message sending !');
}
}
)
res.render(`${__dirname}/HTML/asyncResultPage.ejs`, {
broadcastIP : broadcastIP,type : 'async', rotate : rotate,
port : port, speed:speed,
})
})
app.get('/offset', (req,res) => {
let fs = require('fs')
fs.readFile(`${__dirname}/HTML/offset.txt`,'utf8', (err,data) => {
if( !err)
res.send(data)
})
})
app.get('/currentTime', (req,res) => {
let date = new Date()
let hour = date.getHours()
let min = date.getMinutes()
let sec = date.getSeconds()
console.log(`CurrentTime ${hour}:${min}:${sec}`)
res.send(`${hour}:${min}:${sec}`)
})
}