프로그래밍/Js

Shared Workers 이용하여 WebSocket 연결 방법 (웹소켓을 연결한 상태에서 새로고침 또는 다른 페이지로 이동시 웹소켓 연결을 유지하는게 목적)

소행성왕자 2021. 12. 17. 17:35

이 문서에서 하려고 하는 목적

사용자가 페이지를 새로 고칠 때 소켓이 닫힙니다.
 
그것은 원래 그렇습니다.
 
그러나 하고싶은것은 페이지 새로 고침 전반에 걸쳐 지속적인 웹 소켓 연결을 갖는 것입니다.
 

해결방법

사용자가 귀하의 페이지에 없는 경우에도 연결되도록 하는 것은 일종의 보안 위반이기 때문에 이것이 작동하는지 확실하지 않습니다. 

(새로 고침을 하다 보면 페이지를 빠져나왔다가 다시 들어가게 되기 때문입니다.)

가장 좋은 방법백그라운드에서 실행할 수 있는 서비스 워커를 사용하는 것입니다

웹 소켓을 사용하고 있으므로 SharedWorker를 사용하여 웹 소켓에 대한 새 스레드를 만드는 것이 좋습니다.

 

즉 웹소켓을 연결한 상태에서 새로고침 또는 다른 페이지로 이동시 웹소켓 연결을 유지하는데 목적이 있습니다.

 

해결방법 결과

위와 같은 목적을 달성하기 위해서 아래 3가지에 대해서 알아보도록 하겠습니다.

  • WebWorkers
  • SharedWorkers
  • BroadcastChannels

WebWorkers : 페이지를 로드할 때 각 탭 또는 브라우저에서 새 세션을 생성

SharedWorkers : 각 탭에서 동일한 세션을 사용한다는 것입니다.

따라서 모든 탭이나 창에는 작업할 동일한 작업자와 동일한 웹 소켓 연결이 있습니다.

웹 소켓

웹 소켓을 사용하면 클라이언트 브라우저와 서버 간의 실시간 통신이 가능합니다.
 
그들은 클라이언트가 서버에서 데이터를 요청할 수 있도록 허용할 뿐만 아니라 서버가 서버에서 데이터를 푸시하도록 허용하기 때문에
 
HTTP와 다릅니다.

 

문제

 
그러나 이를 허용하기 위해 각 클라이언트는 서버와의 연결을 열고 클라이언트가 탭을 닫거나 오프라인이 될 때까지 연결을 유지해야 합니다.
 
그들은 지속적인 연결을 만듭니다.
 
이것은 상호 작용을 상태 저장으로 만들어 클라이언트와 서버가 열려 있는
 
각 클라이언트 연결에 대해 WebSocket 서버의 메모리에 최소한 일부 데이터를 저장하도록 합니다.
 
따라서 클라이언트에 15개의 탭이 열려 있으면 서버에 대한 15개의 열린 연결이 있습니다.
 
이 문서는 단일 클라이언트에서 이 부하를 줄이려고 시도한 솔루션입니다.


WebWorkers, SharedWorkers and BroadcastChannels


WebWorkers 는 웹 콘텐츠가 백그라운드 스레드에서 스크립트를 실행하는 간단한 수단입니다.
 
작업자 스레드는 사용자 인터페이스를 방해하지 않고 작업을 수행할 수 있습니다.
 
일단 생성되면 작업자는 해당 코드에 의해 지정된 이벤트 핸들러에 메시지를 게시하여 이를 생성한 JavaScript 코드에 메시지를 보낼 수 있니다 (반대의 경우도 마찬가지).

SharedWorkers 는 여러 창, iframe 또는 작업자와 같은 여러 탐색 컨텍스트에서 액세스할 수 있는 웹 작업자 유형입니다.

BroadcastChannels을 사용하면 동일한 출처를 가진 탐색 컨텍스트
 
(즉, 창, 탭, 프레임 또는 iframe) 간에 간단한 통신이 가능합니다.

SharedWorkers 를 사용하여 서버 부하 줄이기

 
SharedWorker 동일한 브라우저에서 여러 연결이 열려 있는 단일 클라이언트의 이 문제를 해결하는 데 사용할 수 있습니다.
 
각 탭/브라우저, 창 SharedWorker 에서 연결을 여는 대신 서버에 대한 연결을 열 수 있습니다.

이 연결은 웹사이트의 모든 탭이 닫힐 때까지 열려 있습니다.
 
그리고 단일 연결은 열려 있는 모든 탭에서 서버와 통신하고 서버에서 메시지를 받는 데 사용할 수 있습니다.

브로드캐스트 채널 API를 사용하여 웹 소켓의 상태 변경을 모든 컨텍스트(탭)에 브로드캐스트합니다.

기본 웹 소켓 서버 설정

이제 코드를 실행해 보겠습니다.
 
다음을 사용하여 npm 프로젝트를 초기화합니다.

$ git clone https://github.com/ayushgp/shared-worker-socket-example.git
$ npm install websocket

우리는 서버 포트를 9101 를 사용할 것입니다.

server.js
var https = require('https');
var fs   = require('fs');
var WebSocketServer = require('websocket').server;


var https_options = {
			ca: fs.readFileSync("/home/workermymq/html/worker2/fullchain.pem"),
	                 key: fs.readFileSync("/home/workermymq/html/worker2/privkey.pem"),
	                 cert: fs.readFileSync("/home/workermymq/html/worker2/cert.pem")

};


var httpsServer = https.createServer( https_options, function(request, response) {
	    console.log((new Date()) + ' Received request for ' + request.url);
	    response.writeHead(404);
	    response.end();
})

httpsServer.listen(9101);
var WebSocketServer = require('ws').Server;

var wss = new WebSocketServer({
	    server: httpsServer,
	    autoAcceptConnections: false
});

//https://stackoverflow.com/questions/13364243/websocketserver-node-js-how-to-differentiate-clients
let CLIENTS=[];

wss.getUniqueID = function () {
    function s4() {
        return Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1);
    }
    return s4() + s4() + '-' + s4();
};


wss.on('connection', function connection(ws,request) {
	console.log("A new client connected!");
	
    ws.id = wss.getUniqueID();
	CLIENTS.push(ws);

	const ip = request.headers['x-forwarded-for'] || request.connection.remoteAddress;
	

	if(ws.readyState === ws.OPEN){
	    //let sendData = {command: 'message', msg: ' 클라이언트'+ip+' 접속을 환영합니다 from 서버', sid:ws.id};
        //let sendData = '';
		//ws.send(JSON.stringify(sendData));
	    //sendAll(sendData);
	}

	/*
	ws.on('message', function incoming(message) {
		message = JSON.parse(message);
		console.log('received: %s', message);
		//ws.send(JSON.stringify(message));
		sendAll(message);
	});
	*/

	ws.on("message", data => {
		console.log(`Message from client: ${data}`);
		const parsed = JSON.parse(data);
		sendAll(data);

		ws.send(
		  JSON.stringify({
			...parsed.data,
			messageFromServer: `Hello tab id: ${parsed.data.from}`
		  })
		);
	});

	ws.on("close", () => {
		CLIENTS.pop(ws);
		console.log("Sad to see you go :(");
	});

	/*
	setTimeout(
		() =>
		  ws.send(JSON.stringify({ broadcast: "A broadcast for all clients!" })),
		15000
	);
	*/


});


function sendAll (message) {
    for (var i=0; i<CLIENTS.length; i++) {
	 	console.log(">>>>:"+i);
        CLIENTS[i].send(JSON.stringify(message));
	//CLIENTS[i].send(message);
    }
}



SharedWorker 만들기

WorkerJavaScript에서 모든 유형을 생성하려면 작업자가 수행할 작업을 정의하는 별도의 파일을 생성해야 합니다.

작업자 파일 내에서 이 작업자가 초기화될 때 수행할 작업을 정의해야 합니다.

이 코드 SharedWorker는 초기화 될 때 한 번만 호출됩니다.

그 후 이 워커에 연결된 마지막 탭이 닫히지 않거나 이 워커와의 연결이 종료될 때까지 이 코드를 다시 실행할 수 없습니다.

worker.js
// Open a connection. This is a common
// connection. This will be opened only once.
const ws = new WebSocket("wss://workermymq.co.com:9101");

// Create a broadcast channel to notify about state changes
const broadcastChannel = new BroadcastChannel("WebSocketChannel");

// Mapping to keep track of ports. You can think of ports as
// mediums through we can communicate to and from tabs.
// This is a map from a uuid assigned to each context(tab)
// to its Port. This is needed because Port API does not have
// any identifier we can use to identify messages coming from it.
const idToPortMap = {};

// Let all connected contexts(tabs) know about state cahnges
ws.onopen = () =>
  broadcastChannel.postMessage({ type: "WSState", state: ws.readyState });
ws.onclose = () =>
  broadcastChannel.postMessage({ type: "WSState", state: ws.readyState });

// When we receive data from the server.
ws.onmessage = ({ data }) => {
  // Construct object to be passed to handlers
  const parsedData = { data: JSON.parse(data), type: "message" };
  if (!parsedData.data.from) {
    // Broadcast to all contexts(tabs). This is because
    // no particular id was set on the from field here.
    // We're using this field to identify which tab sent
    // the message
    broadcastChannel.postMessage(parsedData);
  } else {
    // Get the port to post to using the uuid, ie send to
    // expected tab only.
    idToPortMap[parsedData.data.from].postMessage(parsedData);
  }
};

// Event handler called when a tab tries to connect to this worker.
onconnect = e => {
  const port = e.ports[0];
  port.onmessage = msg => {
    // Collect port information in the map
    idToPortMap[msg.data.from] = port;

    // Forward this message to the ws connection.
    ws.send(JSON.stringify({ data: msg.data }));
  };

  // We need this to notify the newly connected context to know
  // the current state of WS connection.
  port.postMessage({ state: ws.readyState, type: "WSState" });
};​


index.html 만들기

<!DOCTYPE html>
<html lang="ko">
  <head>
    <meta charset="UTF-8" />
    <title>Web Sockets</title>
    
  </head>
  <body>
    <input type="text" id="msg" name="msg" class="form-control">&nbsp;&nbsp;
    <button id="btnMsg" type="button" class="btn btn-sm btn-primary">보내기</button>
    <p></p>
    <div id="message">메세지 출력 자리 </div>

    <!-- jQuery -->
    <script src="../../worker2/libs/jquery-3.2.1.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/node-uuid/1.4.8/uuid.min.js"></script>
    <script src="main.js"></script>
  </body>
</html>



main.js 만들기

const worker = new SharedWorker("worker.js");
const id = uuid.v4();

// Set initial state
let webSocketState = WebSocket.CONNECTING;

console.log(`Initializing the web worker for user: ${id}`);
worker.port.start();
worker.port.onmessage = event => {
  switch (event.data.type) {
    case "WSState":
      webSocketState = event.data.state;
      break;
    case "message":
      handleMessageFromPort(event.data);
      break;
  }
};

const broadcastChannel = new BroadcastChannel("WebSocketChannel");
broadcastChannel.addEventListener("message", event => {
  switch (event.data.type) {
    case "WSState":
      webSocketState = event.data.state;
      break;
    case "message":
      handleBroadcast(event.data);
      break;
  }
});

// Listen to broadcasts from server
function handleBroadcast(data) {
  console.log("This message is meant for everyone!!!!!");
  
  console.log(data);

  if(typeof data.data == "string") {
    const parsed = JSON.parse(data.data);
    
    console.log(parsed.data.data_message);
    $("#message").html(parsed.data.data_message);
  }
}

function handleMessageFromPort(data) {
  console.log(`This message is meant only for user with id: ${id}`);

}

// Use this method to send data to the server.
function postMessageToWSServer(input) {
  if (webSocketState === WebSocket.CONNECTING) {
    console.log("Still connecting to the server, try again later!");
  } else if (
    webSocketState === WebSocket.CLOSING ||
    webSocketState === WebSocket.CLOSED
  ) {
    console.log("Connection Closed!");
  } else {
    worker.port.postMessage({
      // Include the sender information as a uuid to get back the response
      from: id,
      data_message: input
    });
  }
}

setTimeout(() => postMessageToWSServer(" 새로운 분 들어오셨습니다."), 2500);


const sendItemrequest = msg=>{
  /*
  let itemrequestMsg = {
      'ID': id,
      'Key': {
          'Name': 'itemName',
          'msg' : msg
      }
  };
  */
  
  let itemrequestMsg = {
    'ID': id,
    'data_message': msg
  }

  /*
  let itemrequestObj = {
      'commandObj': itemrequestMsg,
      'command': 'requestitem'
  }
  */

  //Send Item Request message to Web Workers
  worker.port.postMessage(itemrequestMsg);
};


$(document).ready(function () {
  $("#btnMsg").on("click", _=>{
      let msg = $("input[name=msg]").val();
      sendItemrequest(msg);
     
  });
  
});​




참고
 

Scaling WebSocket Connections using Shared Workers

You can find the code for this post on SharedWorker WebSocket example.

ayushgp.xyz

 

Run websocket in web worker or service worker - javascript

I have 9 websocket connections from different sites working to update the DOM with data. Currently I am connecting to all and listening to all websockets and updating the data with a function call....

stackoverflow.com