정보실

웹학교

정보실

javascript 공유 작업자를 사용하여 WebSocket 연결 확장

본문

이 게시물의 코드는 SharedWorker WebSocket 예제에서 찾을 수 있습니다.


https://ayushgp.github.io/scaling-websockets-using-sharedworkers/ 


Web Sockets 


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


image 


문제점 


그러나 이를 허용하려면 각 클라이언트가 서버와의 연결을 열고 클라이언트가 탭을 닫거나 오프라인이 될 때까지 활성 상태로 유지해야 합니다. 그들은 지속적인 연결을 만듭니다. 이로 인해 클라이언트와 서버 모두 열린 클라이언트 연결마다 WebSocket 서버의 메모리에 최소한 일부 데이터를 저장하게 됩니다.


따라서 클라이언트에 15 개의 탭이 열려 있으면 서버에 15 개의 열린 연결이 있습니다. 이 게시물은 단일 클라이언트에서이로드를 시도하고 줄이기 위한 시도 된 솔루션입니다.


image 


구조에 WebWorkers, SharedWorkers 및 BroadcastChannels


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


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


브로드 캐스트 채널을 사용하면 동일한 출처브라우징 컨텍스트 (창, 탭, 프레임 또는 iframe)간에 간단한 통신이 가능합니다.


위의 모든 정의는 MDN에서 가져온 것입니다. 


SharedWorkers를 사용하여 서버로드 줄이기 

 

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


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


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


기본 웹 소켓 서버 설정 


이제 코드를 살펴 보겠습니다. 이 게시물의 목적을 위해 ws npm 모듈을 사용하여 소켓 연결을 지원하는 매우 간단한 웹 서버를 설정합니다. 다음을 사용하여 npm 프로젝트를 초기화하십시오.


$ npm init

package.json 파일이 있으면 ws 모듈을 추가하고 기본 http 서버를 표현하는 단계를 수행하십시오.


$ npm install --save ws express

이 파일이 있으면 다음 코드로 index.js 파일을 작성하여 포트 3000의 공용 디렉토리에서 파일을 제공하고 포트 3001에서 ws 서버를 실행하는 정적 서버를 설정하십시오.


const  express  =  require("express");
const  path  =  require("path");
const  WebSocket  =  require("ws");
const  app  =  express();

// Use the public directory for static file requests
app.use(express.static("public"));

// Start our WS server at 3001
const wss = new WebSocket.Server({ port: 3001 });

wss.on("connection", ws => {
  console.log('A new client connected!');
  ws.on("message", data => {
    console.log(`Message from client: ${data}`);

    // Modify the input and return the same.
    const  parsed  =  JSON.parse(data);
    ws.send(
      JSON.stringify({
        ...parsed.data,
        // Additional field set from the server using the from field.
        // We'll see how this is set in the next section.
        messageFromServer: `Hello tab id: ${parsed.data.from}`
      })
    );
  });
  ws.on("close", () => {
    console.log("Sad to see you go :(");
  });
});

// Listen for requests for static pages at 3000
const  server  =  app.listen(3000, function() {
  console.log("The server is running on http://localhost:"  +  3000);
});

SharedWorker 만들기 


JavaScript에서 작업자 유형을 만들려면 작업자가 수행 할 작업을 정의하는 별도의 파일을 만들어야 합니다.


작업자 파일 내에서 이 작업자가 초기화 될 때 수행 할 작업을 정의해야 합니다. 이 코드는 SharedWorker가 초기화 될 때 한 번만 호출됩니다. 그런 다음 이 작업자에 연결된 마지막 탭이 닫히지 않거나 이 작업자와의 연결이 종료 될 때까지 이 코드를 다시 실행할 수 없습니다.


이 SharedWorker에 연결하는 각 탭을 처리하기 위해 onconnect 이벤트 핸들러를 정의 할 수 있습니다. worker.js 파일을 보자.


// Open a connection. This is a common
// connection. This will be opened only once.
const ws = new WebSocket("ws://localhost:3001");

// 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 }) => {
  console.log(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 => {
  // Get the MessagePort from the event. This will be the
  // communication channel between SharedWorker and the Tab
  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"});
};

처음부터 명확하지 않을 수 있는 몇 가지 작업이 있습니다. 당신이 게시물을 읽을 때, 우리가 왜 그 일을 했는지에 대해 이것들이 분명해질 것입니다. 아직도 명확히 하고 싶은 몇 가지 사항 :

  • 우리는 Broadcast Channel API를 사용하여 소켓의 상태 변경을 브로드 캐스트 합니다.
  • 연결시 포트에 postMessage를 사용하여 컨텍스트 (탭)의 초기 상태를 설정합니다.
  • 문맥 (탭) 자체에서 보낸 필드를 사용하여 응답을 리디렉션 할 위치를 식별합니다.
  • 서버에서 오는 메시지에서 보낸 사람 필드가 없는 경우 모든 사람에게 브로드 캐스트 합니다.

참고 : 여기의 console.log 문은 탭의 콘솔에서 작동하지 않습니다. 해당 로그를 보려면 SharedWorker 콘솔을 열어야 합니다. SharedWorkers를 위한 개발 도구를 열려면 chrome://inspect로 이동하십시오.


SharedWorker 소비 


SharedWorker를 사용할 스크립트를 저장할 HTML 페이지를 먼저 만들어 보겠습니다.


<!DOCTYPE  html>
<html  lang="en">
<head>
  <meta  charset="UTF-8"  />
  <title>Web Sockets</title>
</head>
<body>
  <script  src="https://cdnjs.cloudflare.com/ajax/libs/node-uuid/1.4.8/uuid.min.js"></script>
  <script  src="main.js"></script>
</body>
</html>

작업자를 worker.js 파일에 정의하고 HTML 페이지를 설정했습니다. 이제 모든 컨텍스트 (탭)에서 이 공유 웹 소켓 연결을 사용하는 방법을 살펴 보겠습니다. 다음 내용으로 main.js 파일을 작성하십시오.


// Create a SharedWorker Instance using the worker.js file. 
// You need this to be present in all JS files that want access to the socket
const worker = new SharedWorker("worker.js");

// Create a unique identifier using the uuid lib. This will help us
// in identifying the tab from which a message was sent. And if a 
// response is sent from server for this tab, we can redirect it using
// this id.
const id = uuid.v4();

// Set initial web socket state to connecting. We'll modify this based
// on events.
let  webSocketState  =  WebSocket.CONNECTING;
console.log(`Initializing the web worker for user: ${id}`);

// Connect to the shared worker
worker.port.start();

// Set an event listener that either sets state of the web socket
// Or handles data coming in for ONLY this tab.
worker.port.onmessage = event => {
  switch (event.data.type) {
    case "WSState":
      webSocketState = event.data.state;
      break;
    case "message":
      handleMessageFromPort(event.data);
      break;
  }
};

// Set up the broadcast channel to listen to web socket events.
// This is also similar to above handler. But the handler here is
// for events being broadcasted to all the tabs.
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);
}

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

// 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:  input
    });
  }
}

// Sent a message to server after approx 2.5 sec. This will 
// give enough time to web socket connection to be created.
setTimeout(() =>  postMessageToWSServer("Initial message"), 2500);```

SharedWorker로 메시지 보내기 


위에서 살펴본 것처럼 worker.port.postMessage()를 사용하여 이 SharedWorker에 메시지를 보낼 수 있습니다. 여기서 JS 객체 / 배열 / 프리미티브 값을 전달할 수 있습니다.


작업자가 그에 따라 조치를 취할 수 있도록 메시지가 어떤 컨텍스트에서 오는지 지정하는 오브젝트를 전달하는 것이 좋습니다. 예를 들어 채팅 애플리케이션이 있고 탭 중 하나가 메시지를 보내려는 경우 다음과 같은 것을 사용할 수 있습니다.


{
    // Define the type and the 
  type: 'message',
  from: 'Tab1'
  value: {
    text: 'Hello',
    createdAt: new Date()
  }
}

파일 공유 응용 프로그램이 있는 경우 파일을 삭제할 때 다른 유형과 값으로 동일한 구조를 사용할 수 있습니다.


{
  type: 'deleteFile',
  from: 'Tab2'
  value: {
    fileName: 'a.txt',
    deletedBy: 'testUser'
  }
}

이를 통해 워커는 무엇을 해야 할지 결정할 수 있습니다.


작업자의 메시지 듣기 


처음에는 다른 탭의 MessagePort를 추적하기 위해 맵을 설정했습니다. 그런 다음 SharedWorker에서 탭으로 직접 들어오는 이벤트를 처리하도록 worker.port.onmessage 이벤트 핸들러를 설정합니다.


서버가 발신자 필드를 설정하지 않은 경우에는 브로드 캐스트 채널을 사용하여 모든 탭에 메시지를 브로드 캐스트 합니다. 모든 탭에는 모든 메시지 브로드 캐스트를 처리하는 WebSocketChannel에 대한 메시지 리스너가 있습니다.


이 유형의 설정은 다음 두 가지 시나리오에서 사용할 수 있습니다.

  • 탭에서 게임을 하고 있다고 가정 해 보겠습니다. 메시지가 이 탭으로 오기를 원합니다. 다른 탭에는 이 정보가 필요하지 않습니다. 여기에서 첫 번째 사례를 사용할 수 있습니다.
  • 이제 페이스 북에서 이 게임을 하고 문자 메시지를 받았습니다. 제목의 알림 수를 업데이트 해야 하므로 이 정보는 모든 탭에서 브로드 캐스트 되어야합니다.

최종 도식 표현 


WebWorks 사용을 최적화 하기 위해 SharedWorkers를 사용했습니다. 이것이 어떻게 사용될 수 있는지에 대한 최종 도표 표현은 다음과 같습니다.


image 


노트 


이것은 여러 브라우징 컨텍스트에서 동일한 소켓 연결을 공유하려고 하는 실험 일 뿐입니다. 이것이 클라이언트 당 필요한 연결 수를 줄이는 데 도움이 될 수 있다고 생각합니다. 이 주변에는 여전히 거친 가장자리가 많이 있습니다. 실시간 애플리케이션의 스케일링 문제에 대한이 솔루션에 대해 어떻게 생각하는지 알려주세요. 코드가 포함 된 저장소 : SharedWorker WebSocket 예제.



페이지 정보

조회 88회 ]  작성일19-11-09 22:42

웹학교