FMETP Control flow with WebSocket on Express.js
App configuration
- unity 2021.3.6f1
- Test server :
Assets/FMETP_STREAM/FMWebSocket/TestServer_v2.2.4
- express 서버
- FMWebSocket과 통신하기 위해서 Socket.io를 사용하지 않고
ws
모듈 사용함
- Unity app
- FaceCamera 객체 내부의 OnDataByteReadyEvent에서 웹캠 피드를 전송할 방식 선택 가능
Assets/FMETP_STREAM/FMWebSocket/Scripts/WebSocket/FMWebSocketManager
에 전송 방식이 정의되어 있음- SendToAll, SendToServer, SendToOthers, SendToTarget
- SendToAll, SendToServer, SendToOthers, SendToTarget
_meta[]
도 아래의_byteReg[]
와 같이ws.send()
로 보내는 argument임
- FaceCamera 객체 내부의 OnDataByteReadyEvent에서 웹캠 피드를 전송할 방식 선택 가능
WebSocket Base Framing Protocol
- socket.io, FMWebSocket 모두 WebSocket Protocol RFC 6455 프로토콜을 사용함
Express 서버의 index.html
에서의 예시
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
ws.onmessage = function (evt) {
FMHTML_SetElementById("StatusTextConnection", "Status" + (isServer ? "(server)" : "(client)") + ": " + new Date().getHours() + ":" + new Date().getMinutes() + ":" + new Date().getSeconds());
FMHTML_SetElementById("BtnServerText", "Disconnect");
FMHTML_SetElementById("BtnClientText", "Disconnect");
var data = evt.data;
if(typeof evt.data === "string")
{
//console.log('string data!');
}
if(evt.data instanceof ArrayBuffer)
{
var _byteRaw = new Uint8Array(evt.data);
// FMETP: first byte defines the whole data type..., 0 is raw, 1 is string
if(_byteRaw[0] === 0)
{
var _byteData = _byteRaw.slice(4, _byteRaw.length);
// document.getElementById("StatusTextBytes").innerHTML = "(byte)" + _byteData.length;
FMHTML_SetElementById("StatusTextBytes", "(byte)" + _byteData.length);
if(_byteData.length > 18)
{
label_img = FMHTML_GetElementById("LabelVideo", label_img);
label_aud = FMHTML_GetElementById("LabelAudio", label_aud);
var _label = ByteToInt16(_byteData, 0);
// console.log(_byteData.length + ': ' + _label); //Debug label
if (_label == label_img)
{
...
}
}
}
if(_byteRaw[0] === 1)
{
var _byteData = _byteRaw.slice(4, _byteRaw.length);
var stringData = '';
//----conver byte[] to Base64 string----
var len = _byteData.byteLength;
for (var i = 0; i < len; i++)
{
stringData += String.fromCharCode(_byteData[i]);
}
//----conver byte[] to Base64 string----
document.getElementById("StatusTextString").innerHTML = "(string)" + stringData;
}
}
};
- payload 길이에 따라서 보낼 ArrayBuffer의 크기가 정해짐
_byteRaw[0]
0
: raw byte1
: string
_byteRaw[1]
[0, 1, 2, 3]
: To [All, Server, Others, Target]
_byteRaw[3]
3
: Register as server4
: Register as client
Express server에서의 room 구조 구현
- socket.io에는 room이 지원되지만, ws에는 없기 때문에 직접 구현해야 함
FMETP_STREAM에서의 전송 방식 분석
- 처음 접속할 때 접속 방식(server, client)을 설정함
- 특별히 다른 기능을 하진 않고, 전송 방식을 위해서 구별된 것 같음(all/server/others)
- FMWebSocket에서 말하는 server를 room 개념으로 이용할 것임
- 특별히 다른 기능을 하진 않고, 전송 방식을 위해서 구별된 것 같음(all/server/others)
- 각 소켓의 uuid는 아래와 같은 방법으로 생성됨
1 2 3 4 5 6 7 8
function uuidv4() { function s4() { return Math.floor((1 + Math.random()) * 0x10000) .toString(16) .substring(1); } return s4() + s4() + "-" + s4(); }
'-'
을 제외하면 다 합쳐서 12 Bytes임을 알 수 있음
Express 서버에서의 소켓 저장 구조 변경
-
기존
index.js
에서의 구현 방법1 2 3 4 5 6 7 8 9 10 11
ws = new WS_MODULE.Server({ server }); ... const clients = new Map(); ... ws.on("connection", function connection(ws) { const wsid = uuidv4(); const networkType = "undefined"; const metadata = { ws, networkType, wsid };
- server, client 구별 없이 모든 소켓들을 하나의
Map
인clients
에 저장함 - 우선 생성 후
networkType
은 첫 번째 메시지에 따라 나중에 부여함(바꾼 코드에도 그대로 적용됨)
- server, client 구별 없이 모든 소켓들을 하나의
-
저장 방식 수정 후:
1 2 3 4 5 6 7 8 9 10
const servers = new Map(); // (server's uuid, clients' uuid) const allwss = new Map(); // (uuid, metadata) ... ws.on("connection", function connection(ws) { const wsid = uuidv4(); const networkType = "undefined"; const serverid = "undefined"; const metadata = { ws, networkType, wsid, serverid };
servers
: (server uid, client uid의 Array)로 서버-클라이언트 연결 정보를 저장함allwss
: 이전 코드의clients
와 동일하게 wsid와 소켓 정보를 매핑하지만, 소켓 정보에 serverid가 추가됨- server일 경우
"undefined"
가 저장됨
- server일 경우
-
연결 구조에 맞게 아래 이벤트들도 변경함
- server 소켓의 close : client들을 먼저 다 닫은 뒤
ws.close()
실행 - client 소켓의 close :
servers.get(serveruid)
에서 wsid 제거하도록 추가 - server, client 소켓의 message : 전송 방식에 맞게 처리
- server 소켓의 close : client들을 먼저 다 닫은 뒤
Payload 수정
- 홈페이지의 connect as server/client 버튼을 누르면
FMWebSocketConnect
메소드가 실행됨FMWebSocketConnect
가 아래와 같이 EventListener를 추가함1 2 3 4
ws.addEventListener("open", (event) => { RegisterNetworkType(); console.log("**connected to server"); });
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function RegisterNetworkType() {
isServer = _regServer;
var _byteReg = new Uint8Array(16);
_byteReg[2] = 9;
if (_regServer) {
_byteReg[3] = 3;
console.log("Register as Server");
} else {
// client
var roomid = document.getElementById("RoomNumber").value; // string now
_byteReg[3] = 4;
console.log("Register as Client");
for (let i = 4; i < 12; i++) {
_byteReg[i] = parseInt(roomid[i - 4], 16);
}
for (let i = 13; i < 17; i++) {
_byteReg[i - 1] = parseInt(roomid[i - 4], 16);
}
}
ws.send(_byteReg);
}
- 원래는
Uint8Array(4)
로 선언되었지만, server의 uuid도 포함시키기 위해16
으로 수정함- 프로토콜과 관련된 내용은 그대로 두고, client로 연결할 경우 server의 uuid를 사용자로부터 입력받아서
'-'
을 뺀 나머지 id를 payload에 추가함
- 프로토콜과 관련된 내용은 그대로 두고, client로 연결할 경우 server의 uuid를 사용자로부터 입력받아서
수정한 payload 처리
index.js
에서는 소켓으로부터 메시지를 받았을 때 아래와 같이 처리함
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
ws.on("message", function incoming(message) {
if (message.length === 16) {
if (
message[0] === 0 &&
message[1] === 0 &&
message[2] === 9 &&
message[3] === 3 // SERVER
) {
servers.set(wsid, new Array());
serverID = wsid;
serverWS = ws;
console.log("regServer: " + wsid + "[Server] " + serverID);
allwss.get(wsid).networkType = "server";
// create invitation link
ws.send(
"http://" +
ip.address() +
"/join/" +
serverID.slice(0, 8) +
serverID.slice(9)
);
} else if (
message[0] === 0 &&
message[1] === 0 &&
message[2] === 9 &&
message[3] === 4 // CLIENT
) {
var Oserveruid = "";
for (let i = 4; i < 16; i++) {
Oserveruid += message[i].toString(16);
}
Oserveruid = Oserveruid.slice(0, 8) + "-" + Oserveruid.slice(8);
console.log("received server uid : " + Oserveruid);
var serverws = allwss.get(Oserveruid).ws;
console.log("regClient: " + wsid + "[Server] " + Oserveruid);
allwss.get(wsid).networkType = "client";
allwss.get(wsid).serverid = Oserveruid;
servers.get(Oserveruid).push(wsid);
//tell server about the new connected client
serverws.send("OnClientConnectedEvent(" + wsid + ")");
//tell client about the existing server
ws.send("OnFoundServerEvent(" + Oserveruid + ")");
}
}
// if(message.length > 4 && message[0] === 0)
else if (message.length > 4) {
...
- 원래는
message.length==4
였지만, 연결할 때의 payload를 16으로 늘렸기 때문에 바꾸고 적절히 처리해줌 - server일 경우 invitation 링크를 만들어서 string으로 server websocket에 다시 전송함
FMWebSocket.cs
에서의 payload 수정
RegisterNetworkType()
express와 동일하게 수정- 진행 중
발생했던 문제들
- payload type을
Uint8Array
→Uint32Array
로 바꿨을 때 제대로 입력받지 못함
- ws에서 메시지 받을때는 원래 데이터 타입과 상관없이 1B로 쪼개서 받아서 이렇게 배치되는 듯
Uint32Array(4)
대신Uint8Array(16)
으로 바꿈
-
JS의
Array
는 asisgn 연산을 실행하면 shallow copy됨1 2 3 4 5 6 7 8 9 10 11 12
var mmap = new Map(); mmap.set("a", new Array()); mmap.get("a").push(1); var tmp = mmap.get("a"); console.log(tmp); // ['1'] mmap.get("a").push(2); console.log(tmp); // ['1', '2'] mmap.delete("a"); console.log(tmp); // ['1', '2']
- unity, local 둘 다 server로 연결한 상태에서 local의 disconnect 버튼을 누르면 둘 다 종료됨
- chrome 창을 닫거나 유니티를 멈추면 각자의 세션만 종료됨
serverWS.close()
로 웹소켓을 닫아버리기 때문servers
,allwss
로 웹소켓 인스턴스를 관리해서 종료해야 하는 서버 ws만 종료함
Leave a comment