Program Tip

Websocket 전송 안정성 (재 연결 중 Socket.io 데이터 손실)

programtip 2020. 11. 5. 18:53
반응형

Websocket 전송 안정성 (재 연결 중 Socket.io 데이터 손실)


익숙한

NodeJS, Socket.io

문제

Socket.io를 통해 앱에 연결된 두 명의 사용자 U1U2 가 있다고 가정 해보십시오. 알고리즘은 다음과 같습니다.

  1. U1 이 인터넷 연결이 완전히 끊어짐 (예 : 인터넷 끄기)
  2. U2U1에 메시지를 보냅니다 .
  3. U1 은 인터넷이 다운 되었기 때문에 아직 메시지를받지 못합니다.
  4. 서버 가 하트 비트 시간 초과로 U1 연결 끊김을 감지합니다.
  5. U1 이 socket.io에 다시 연결됩니다.
  6. U1U2 로부터 메시지를받지 못합니다 . 4 단계에서 잃어버린 것 같습니다.

가능한 설명

왜 그런지 이해한다고 생각합니다.

  • 4 단계에서 서버 는 소켓 인스턴스와 U1에 대한 메시지 대기열 도 종료합니다.
  • 또한 5 단계에서 U1서버는 새 연결을 생성하므로 (재사용되지 않음) 메시지가 여전히 대기열에 있어도 이전 연결은 손실됩니다.

도움이 필요하다

이러한 종류의 데이터 손실을 어떻게 방지 할 수 있습니까? 사람들이 앱에 영원히 매달리지 않기 때문에 저는 hearbeats를 사용해야합니다. 또한 새 버전의 앱을 배포 할 때 다운 타임이 전혀 발생하지 않기를 원하기 때문에 다시 연결할 수있는 가능성을 제공해야합니다.

추신 : 내가 "메시지"라고 부르는 것은 데이터베이스에 저장할 수있는 단순한 문자 메시지가 아니라 전달이 보장되어야하는 귀중한 시스템 메시지이거나 UI가 망가진 것입니다.

감사!


추가 1

이미 사용자 계정 시스템이 있습니다. 게다가 내 응용 프로그램은 이미 복잡합니다. 오프라인 / 온라인 상태를 추가해도 도움이되지 않습니다. 이미 이런 종류의 항목이 있기 때문입니다. 문제는 다릅니다.

2 단계를 확인하십시오.이 단계에서 U1이 오프라인 상태인지 여부를 기술적으로 말할 수 없습니다 . 그는 인터넷 연결이 끊어졌을 뿐이고 인터넷 상태가 좋지 않아 2 초 동안 말할 수 있습니다. 그래서 U2는 그에게 메시지를 보내지 만 U1은 그에게 아직 인터넷이 다운되어 있기 때문에 메시지를받지 못합니다 (3 단계). 오프라인 사용자를 감지하려면 4 단계가 필요합니다. 예를 들어 시간 제한은 60 초입니다. 결국 10 초 후에 U1에 대한 인터넷 연결이 설정되고 socket.io에 다시 연결됩니다. 그러나 U1 서버에서 시간 초과로 연결이 끊어 졌기 때문에 U2의 메시지는 공간에서 손실됩니다.

그게 문제입니다. 100 % 배송하고 싶지 않습니다.


해결책

  1. 임의의 emitID로 식별되는 {} 사용자에서 방출 (방출 이름 및 데이터)을 수집하십시오. 방출 보내기
  2. 클라이언트 측에서 방출을 확인하십시오 (emitID를 사용하여 서버로 방출을 다시 보냅니다).
  3. 확인 된 경우-emitID로 식별 된 {}에서 오브젝트 삭제
  4. 사용자가 다시 연결 한 경우-이 사용자에 대해 {}을 확인하고 {}의 각 개체에 대해 1 단계를 실행하여 반복합니다.
  5. 연결이 끊어 지거나 연결되었을 때 필요한 경우 사용자를 위해 {} 플러시

    // 서버 const pendingEmits = {};

    socket.on ( 'reconnection', () => resendAllPendingLimits); socket.on ( 'confirm', (emitID) => {delete (pendingEmits [emitID]);});

    // 클라이언트 socket.on ( 'something', () => {socket.emit ( 'confirm', emitID);});


다른 사람들은 다른 답변과 의견에서 이에 대해 암시했지만 근본적인 문제는 Socket.IO가 단지 전달 메커니즘이며 신뢰할 수있는 전달을 위해 단독으로 의존 수 없다는 것입니다. 메시지가 클라이언트에 성공적으로 전달되었는지 확인하는 유일한 사람 은 클라이언트 자체 입니다. 이런 종류의 시스템에 대해 다음과 같은 주장을하는 것이 좋습니다.

  1. 메시지는 클라이언트에게 직접 전송되지 않습니다. 대신 서버로 전송되어 일종의 데이터 저장소에 저장됩니다.
  2. 클라이언트는 다시 연결할 때 "내가 놓친 내용"을 물어볼 책임이 있으며 데이터 저장소에 저장된 메시지를 쿼리하여 상태를 업데이트합니다.
  3. 수신자 클라이언트가 연결되어있는 동안 메시지가 서버로 전송되면 해당 메시지는 클라이언트 에게 실시간으로 전송됩니다.

물론 애플리케이션의 요구 사항에 따라이 부분을 조정할 수 있습니다. 예를 들어 Redis 목록 또는 메시지에 대해 정렬 된 집합을 사용하고 클라이언트가 작동 중이라는 사실을 알고있는 경우이를 지울 수 있습니다. 현재까지.


다음은 몇 가지 예입니다.

행복한 길 :

  • U1과 U2는 모두 시스템에 연결되어 있습니다.
  • U2는 U1이 수신해야하는 서버에 메시지를 보냅니다.
  • 서버는 일종의 영구 저장소에 메시지를 저장하여 일종의 타임 스탬프 또는 순차 ID로 U1에 표시합니다.
  • 서버는 Socket.IO를 통해 U1에 메시지를 보냅니다.
  • U1의 클라이언트는 (아마도 Socket.IO 콜백을 통해) 메시지를 받았음을 확인합니다.
  • 서버는 데이터 저장소에서 지속되는 메시지를 삭제합니다.

오프라인 경로 :

  • U1이 인터넷 연결을 끊습니다.
  • U2는 U1이 수신해야하는 서버에 메시지를 보냅니다.
  • 서버는 일종의 영구 저장소에 메시지를 저장하여 일종의 타임 스탬프 또는 순차 ID로 U1에 표시합니다.
  • 서버는 Socket.IO를 통해 U1에 메시지를 보냅니다.
  • U1의 클라이언트 는 오프라인이기 때문에 영수증을 확인 하지 않습니다 .
  • 아마도 U2는 U1에게 몇 가지 메시지를 더 보냅니다. 모두 동일한 방식으로 데이터 저장소에 저장됩니다.
  • U1이 다시 연결되면 서버에 "내가 본 마지막 메시지는 X / 상태 X가 있습니다. 무엇을 놓쳤습니까?"
  • 서버는 U1의 요청에 따라 데이터 저장소에서 누락 된 모든 메시지를 U1에 보냅니다.
  • U1의 클라이언트는 수신을 확인하고 서버는 데이터 저장소에서 해당 메시지를 제거합니다.

보장 된 전달을 원할 경우 연결이 실제로 중요하지 않은 방식으로 시스템을 설계하는 것이 중요하며 실시간 전달은 단순히 보너스 일뿐입니다 . 이것은 거의 항상 일종의 데이터 저장소를 포함합니다. user568109가 의견에서 언급했듯이 메시지의 저장 및 전달을 추상화하는 메시징 시스템이 있으며 이러한 사전 구축 된 솔루션을 살펴볼 가치가 있습니다. (아직도 Socket.IO 통합을 직접 작성해야합니다.)

데이터베이스에 메시지를 저장하는 데 관심이 없다면 메시지를 로컬 배열에 저장하지 않아도됩니다. 서버는 U1에게 메시지를 보내려고 시도하고 U1의 클라이언트가 메시지를 받았음을 확인할 때까지 "보류중인 메시지"목록에 저장합니다. 클라이언트가 오프라인 상태 인 경우 다시 돌아 왔을 때 서버에 "연결이 끊겼습니다. 놓친 내용을 보내주세요"라고 알리고 서버는 이러한 메시지를 반복 할 수 있습니다.

다행히 Socket.IO는 클라이언트가 네이티브 JS 콜백처럼 보이는 메시지에 "응답"할 수있는 메커니즘을 제공합니다. 다음은 의사 코드입니다.

// server
pendingMessagesForSocket = [];

function sendMessage(message) {
  pendingMessagesForSocket.push(message);
  socket.emit('message', message, function() {
    pendingMessagesForSocket.remove(message);
  }
};

socket.on('reconnection', function(lastKnownMessage) {
  // you may want to make sure you resend them in order, or one at a time, etc.
  for (message in pendingMessagesForSocket since lastKnownMessage) {
    socket.emit('message', message, function() {
      pendingMessagesForSocket.remove(message);
    }
  }
});

// client
socket.on('connection', function() {
  if (previouslyConnected) {
    socket.emit('reconnection', lastKnownMessage);
  } else {
    // first connection; any further connections means we disconnected
    previouslyConnected = true;
  }
});

socket.on('message', function(data, callback) {
  // Do something with `data`
  lastKnownMessage = data;
  callback(); // confirm we received the message
});

이는 영구 데이터 저장소가없는 마지막 제안과 매우 유사합니다.


이벤트 소싱 의 개념에 관심이있을 수도 있습니다 .


이미 사용자 계정 시스템이있는 것 같습니다. 온라인 / 오프라인 계정을 알고 있으면 연결 / 연결 해제 이벤트를 처리 할 수 ​​있습니다.

따라서 해결책은 각 사용자의 데이터베이스에 온라인 / 오프라인 및 오프라인 메시지를 추가하는 것입니다.

chatApp.onLogin(function (user) {
   user.readOfflineMessage(function (msgs) {
       user.sendOfflineMessage(msgs, function (err) {
           if (!err) user.clearOfflineMessage();
       });
   })
});

chatApp.onMessage(function (fromUser, toUser, msg) {
   if (user.isOnline()) {
      toUser.sendMessage(msg, function (err) {
          // alert CAN NOT SEND, RETRY?
      });
   } else {
      toUser.addToOfflineQueue(msg);
   }
})

Michelle의 대답은 거의 정점에 있지만 고려해야 할 몇 가지 중요한 사항이 있습니다. 스스로에게 물어볼 주요 질문은 "내 앱에서 사용자와 소켓간에 차이가 있습니까?"입니다. 또 다른 방법은 "로그인 한 각 사용자가 한 번에 둘 이상의 소켓 연결을 가질 수 있습니까?"입니다.

웹 세계에서는이를 방지하는 무언가를 특별히 배치하지 않는 한 단일 사용자가 다중 소켓 연결을 가질 가능성이 항상있을 것입니다. 가장 간단한 예는 사용자가 동일한 페이지의 두 개의 탭이 열려있는 경우입니다. 이러한 경우에는 인간 사용자에게 메시지 / 이벤트를 한 번만 보내는 데 신경 쓰지 않습니다. 각 탭에서 콜백을 실행하여 UI 상태를 업데이트 할 수 있도록 해당 사용자의 각 소켓 인스턴스에 메시지 / 이벤트를 보내야합니다. 아마도 이것은 특정 응용 프로그램에 대한 우려가 아니지만 내 직감은 그것이 대부분의 경우라고 말합니다. 이것이 걱정된다면 계속 읽어보십시오 ....

이를 해결하려면 (데이터베이스를 영구 저장소로 사용한다고 가정) 3 개의 테이블이 필요합니다.

  1. 사용자-실제 사람들과 1 : 1로
  2. 클라이언트- 소켓 서버에 대한 단일 연결을 가질 있는 "탭"을 나타냅니다 . (모든 '사용자'는 여러 개를 가질 수 있습니다.)
  3. messages-클라이언트에 보내야하는 메시지 (사용자 나 소켓에 보내야하는 메시지가 아님)

사용자 테이블은 앱에 필요하지 않은 경우 선택 사항이지만 OP는 테이블이 있다고 말했습니다.

적절하게 정의해야하는 또 다른 것은 "소켓 연결이란 무엇입니까?", "소켓 연결은 언제 생성됩니까?", "소켓 연결은 언제 재사용됩니까?"입니다. Michelle의 의사 코드는 소켓 연결을 재사용 할 수있는 것처럼 보입니다. Socket.IO를 사용하면 재사용 할 수 없습니다. 나는 많은 혼란의 원인이되는 것을 보았다. Michelle의 예가 의미가있는 실제 시나리오가 있습니다. 그러나 나는 그러한 시나리오가 드물다고 상상해야합니다. 실제로 발생하는 것은 소켓 연결이 끊어지면 해당 연결, ID 등이 재사용되지 않을 때입니다. 따라서 해당 소켓에 대해 특별히 표시된 메시지는 원래 연결 한 클라이언트가 다시 연결될 때 완전히 새로운 연결과 새 ID를 얻으므로 누구에게도 전달되지 않습니다. 이것은

따라서 웹 기반 예제의 경우 여기에 권장하는 단계가 있습니다.

  • 사용자가 소켓 연결을 생성 할 가능성이있는 클라이언트 (일반적으로 단일 웹 페이지)를로드 할 때 사용자 ID에 연결된 클라이언트 데이터베이스에 행을 추가합니다.
  • 사용자가 실제로 소켓 서버에 연결하면 연결 요청과 함께 클라이언트 ID를 서버에 전달합니다.
  • 서버는 사용자의 연결이 허용되고 클라이언트 테이블의 클라이언트 행이 연결에 사용 가능한지 확인하고 그에 따라 허용 / 거부해야합니다.
  • Socket.IO에서 생성 한 소켓 ID로 클라이언트 행을 업데이트합니다.
  • 클라이언트 ID에 연결된 메시지 테이블의 항목을 보냅니다. 초기 연결에는 아무것도 없지만 재 연결을 시도하는 클라이언트의 것이면 일부가있을 수 있습니다.
  • 메시지를 해당 소켓으로 보내야 할 때마다 생성 한 클라이언트 ID (소켓 ID가 아님)에 연결된 메시지 테이블에 행을 추가하십시오.
  • 메시지를 내보내고 승인이있는 클라이언트를 수신합니다.
  • 승인을 받으면 메시지 테이블에서 해당 항목을 삭제하십시오.
  • 일부가 지적했듯이 기술적으로 가능성이 있기 때문에 서버에서 보낸 중복 메시지를 버리는 클라이언트 측에 몇 가지 논리를 만들 수 있습니다.
  • 그런 다음 클라이언트가 소켓 서버에서 연결이 끊어지면 (의도적으로 또는 오류를 통해) 클라이언트 행을 삭제하지 말고 소켓 ID를 최대한 지워야합니다. 동일한 클라이언트가 다시 연결을 시도 할 수 있기 때문입니다.
  • 클라이언트가 다시 연결을 시도 할 때 원래 연결 시도와 함께 보낸 동일한 클라이언트 ID를 보냅니다. 서버는 이것을 초기 연결처럼 볼 것입니다.
  • 클라이언트가 파괴되면 (사용자가 탭을 닫거나 다른 곳으로 이동)이 클라이언트에 대한 클라이언트 행과 모든 메시지를 삭제합니다. 이 단계는 약간 까다로울 수 있습니다.

마지막 단계가 까다롭기 때문에 (적어도 예전에는 그런 일을 한 적이 없습니다) 그리고 클라이언트 행을 정리하지 않고 연결을 끊고 시도하지 않는 정전과 같은 경우가 있기 때문입니다. 동일한 클라이언트 행과 다시 연결하려면-오래된 클라이언트 및 메시지 행을 정리하기 위해 주기적으로 실행되는 무언가를 원할 것입니다. 또는 모든 클라이언트와 메시지를 영구적으로 저장하고 해당 상태를 적절하게 표시 할 수 있습니다.

한 명의 사용자가 두 개의 탭이 열려있는 경우 서버가 각 사용자가 아닌 각 클라이언트가 메시지를 받았는지 알아야하기 때문에 각기 다른 클라이언트에 대해 표시된 메시지 테이블에 두 개의 동일한 메시지를 추가하게됩니다.


여기를보세요 : Handle browser reload socket.io .

내가 생각 해낸 솔루션을 사용할 수 있다고 생각합니다. 올바르게 수정하면 원하는대로 작동합니다.


내가 원하는 것은 각 사용자에 대해 재사용 가능한 소켓을 갖는 것입니다.

고객:

socket.on("msg", function(){
    socket.send("msg-conf");
});

섬기는 사람:

// Add this socket property to all users, with your existing user system
user.socket = {
    messages:[],
    io:null
}
user.send = function(msg){ // Call this method to send a message
    if(this.socket.io){ // this.io will be set to null when dissconnected
        // Wait For Confirmation that message was sent.
        var hasconf = false;
        this.socket.io.on("msg-conf", function(data){
            // Expect the client to emit "msg-conf"
            hasconf = true;
        });
        // send the message
        this.socket.io.send("msg", msg); // if connected, call socket.io's send method
        setTimeout(function(){
            if(!hasconf){
                this.socket = null; // If the client did not respond, mark them as offline.
                this.socket.messages.push(msg); // Add it to the queue
            }
        }, 60 * 1000); // Make sure this is the same as your timeout.

    } else {
        this.socket.messages.push(msg); // Otherwise, it's offline. Add it to the message queue
    }
}
user.flush = function(){ // Call this when user comes back online
    for(var msg in this.socket.messages){ // For every message in the queue, send it.
        this.send(msg);
    }
}
// Make Sure this runs whenever the user gets logged in/comes online
user.onconnect = function(socket){
    this.socket.io = socket; // Set the socket.io socket
    this.flush(); // Send all messages that are waiting
}
// Make sure this is called when the user disconnects/logs out
user.disconnect = function(){
    self.socket.io = null; // Set the socket to null, so any messages are queued not send.
}

그런 다음 연결 해제 사이에 소켓 큐가 보존됩니다.

각 사용자 socket속성을 데이터베이스에 저장 하고 메서드를 사용자 프로토 타입의 일부로 만듭니다. 데이터베이스는 중요하지 않습니다. 사용자를 저장했지만 저장하십시오.

This will avoid the problem mentioned in Additon 1 by requiring a confirmation from the client before marking the message as sent. If you really wanted to, you could give each message an id and have the client send the message id to msg-conf, then check it.

In this example, user is the template user that all users are copied from, or like the user prototype.

Note: This has not been tested.


Been looking at this stuff latterly and think different path might be better.

Try looking at Azure Service bus, ques and topic take care of the off line states. The message wait for user to come back and then they get the message.

Is a cost to run a queue but its like $0.05 per million operations for a basic queue so cost of dev would be more from hours work need to write a queuing system. https://azure.microsoft.com/en-us/pricing/details/service-bus/

And azure bus has libraries and examples for PHP, C#, Xarmin, Anjular, Java Script etc.

So server send message and does not need to worry about tracking them. Client can use message to send back also as means can handle load balancing if needed.


Try this emit chat list

io.on('connect', onConnect);

function onConnect(socket){

  // sending to the client
  socket.emit('hello', 'can you hear me?', 1, 2, 'abc');

  // sending to all clients except sender
  socket.broadcast.emit('broadcast', 'hello friends!');

  // sending to all clients in 'game' room except sender
  socket.to('game').emit('nice game', "let's play a game");

  // sending to all clients in 'game1' and/or in 'game2' room, except sender
  socket.to('game1').to('game2').emit('nice game', "let's play a game (too)");

  // sending to all clients in 'game' room, including sender
  io.in('game').emit('big-announcement', 'the game will start soon');

  // sending to all clients in namespace 'myNamespace', including sender
  io.of('myNamespace').emit('bigger-announcement', 'the tournament will start soon');

  // sending to individual socketid (private message)
  socket.to(<socketid>).emit('hey', 'I just met you');

  // sending with acknowledgement
  socket.emit('question', 'do you think so?', function (answer) {});

  // sending without compression
  socket.compress(false).emit('uncompressed', "that's rough");

  // sending a message that might be dropped if the client is not ready to receive messages
  socket.volatile.emit('maybe', 'do you really need it?');

  // sending to all clients on this node (when using multiple nodes)
  io.local.emit('hi', 'my lovely babies');

};

참고URL : https://stackoverflow.com/questions/20685208/websocket-transport-reliability-socket-io-data-loss-during-reconnection

반응형