防止WebSocket

时间:2016-01-20 12:21:48

标签: javascript websocket

服务器端的代码在打开连接后立即发送消息 (它将初始配置/问候语发送给客户端)。

以下代码在客户端:

var sock = new WebSocket(url);
sock.addEventListener('error', processError);
sock.addEventListener('close', finish);
sock.addEventListener('message', processMessage);

我担心会丢失来自服务器的第一个配置/问候相关消息。从理论上讲,在设置 message事件处理程序之前,没有什么能阻止它被接收。

另一方面,实际上我从来没有想过。 AFAIK JavaScript WebSocket API没有针对这个理论问题的对策:WebSocket constructor既不允许设置message事件处理程序,也不允许在挂起状态下创建WebSocket。

所以:

  1. 要么我错过了某些东西,即使在理论上也不可能用上述代码丢失消息。
  2. 或者它是JavaScript WebSocket API设计中的错误。
  3. 或者每个人都很高兴,因为消息丢失实际上不可能。
  4. 或者这样的行为(在连接时从服务器发送消息)有些被认为是不好的做法,所以没有人讨论在理论上正确实施它的可能性。
  5. P.S。:这些简单但理论上的问题是否更适合Stack OverflowProgrammers@Stack Exchange

4 个答案:

答案 0 :(得分:5)

不要担心。

您的代码在单线程事件循环中运行。

这一行:var sock = new WebSocket(url);根本没有启动websocket连接。 The spec表示它必须在返回Web套接字后执行实际连接,与处理代码运行的事件循环的线程并行:

  
      
  1. 返回一个新的WebSocket对象,但[并行] [2]继续执行这些步骤。
  2.   

仅此一项是不够的,但该套接字的所有后续WebSocket事件都在运行代码的相同单线程事件循环内进行调度。这是规范中关于接收消息的内容:

  

当收到带有类型和数据数据的WebSocket 消息时,用户代理必须排队任务遵循这些步骤

该任务在同一事件循环中排队。这意味着,在您创建WebSocket的任务运行完成之前,处理消息的任务无法运行。因此,在事件循环处理任何与连接相关的消息之前,您的代码将完成运行。

即使您在使用多个线程的浏览器中运行代码,特定代码也将在单个线程事件循环上运行,并且每个事件循环都是独立的。

不同的事件循环可以通过将任务推送到彼此的任务队列中来进行通信。但是这些任务将在接收任务的单线程事件循环中执行,从而使代码保持线程安全。

任务"处理此事件"将由单线程事件循环处理,找到适当的事件处理程序并调用其回调...但这只会在任务处理完毕后才会发生。

更清楚:

我没有声称每个事件循环实际上都处理IO - 但IO调度程序将发送您的代码事件,这些事件将在单个线程中顺序运行(有点,它们确实具有使用的优先级管理不同的"任务队列")。

编辑:客户端代码问题

应该注意的是,Websocket API并不是为DOM的功能addEventListener设计的。

相反,Websocket API遵循HTML4范例,其中事件回调是对象属性(而不是EventListener集合)。即:

// altered DOM API:
sock.addEventListener('message', processMessage);
// original WebSocket API:
sock.onmessage = processMessage;

两个API在我测试的所有浏览器上都能正常工作(包括安全传递第一条消息)。方法的不同可能由HTML4兼容层处理。

然而,关于事件调度的规范是不同的,因此应该避免使用addEventListener

编辑2:测试理论

关于Bronze Man's answer关于失败的消息回复......

即使我使用小型Ruby应用程序和小型Javascript客户端编写测试,我也无法重现声明的问题。

Ruby应用程序启动带有欢迎消息的Websocket echo服务器(我使用plezi.io)。

Javascript客户端包含一个繁忙等待循环,导致Javascript线程挂起(阻塞)指定的时间(在我的测试中为2秒)。

onmessage回调仅在块释放后(2秒后)设置 - 所以来自服务器的欢迎消息将在定义回调之前到达浏览器。

这允许我们测试欢迎消息是否在任何特定浏览器上丢失(这将是浏览器中的错误)。

测试是可靠的,因为服务器是已知数量,并且在upgrade完成后立即将消息发送到套接字(我在C中编写了Iodine服务器后端以及plezi.io框架因为我对内部行为的深入了解,我选择了它们。)

Ruby应用程序:

# run from terminal using `irb`, after `gem install plezi`
require 'plezi'
class WebsocketEcho
    def index
       "Use Websockets"
    end
    def on_message data
       # simple echo
       write data
    end
    def on_open
       # write a welcome message
       # will ths message be lost?
       write "Welcome to the WebSocket echo server."
       puts "New Websocket connection opened, welcome message was sent."
    end
end
# adds mixins to the class and creates route
Plezi.route("/", WebsocketEcho)

# running the server from the terminal
Iodine.threads = 1
Iodine::Rack.app = Plezi.app
Iodine.start

Javascript客户端:

function Client(milli) {
    this.ws = new WebSocket("ws" + window.document.location.href.slice(4, -1));
    this.ws.client = this;
    this.onopen = function (e) { console.log("Websocket opened", e); }
    this.ws.onopen = function (e) { e.target.client.onopen(e); }
    this.onclose = function (e) { console.log("Websocket closed", e); /* reconnect? */ }
    this.ws.onclose = function (e) { e.target.client.onclose(e); }
    if(milli) { // busy wait, blocking the thread.
        var start = new Date();
        var now = null;
        do {
            now = new Date();
        } while(now - start < milli);
    }
    this.onmessage = function (e) { console.log(e.data); }
    // // DOM API alternative for testing:
    // this.ws.addEventListener('message', function (e) { e.target.client.onmessage(e); });
    // // WebSocket API for testing:
    this.ws.onmessage = function (e) { e.target.client.onmessage(e); }    
}
// a 2 second window
cl = new Client(2000);

我的机器上的结果(MacOS):

  • 只有在新客户端创建完成后(在线程完成处理代码之后,如Ruby应用程序的延迟输出所示),Safari 11.01才会启动Websocket连接。一旦建立连接,该消息显然已到达。

  • Chrome 62.0立即启动Websocket连接。一旦2秒窗口结束,消息就会到达。即使消息在设置onmessage处理程序之前到达,消息也不会丢失。

  • FireFox 56.0与Chrome的行为相同,立即启动Websocket连接。一旦2秒窗口结束,消息就会到达。消息没有丢失。

如果有人可以在Windows和Linux上进行测试,那就太棒了......但我不认为浏览器会遇到事件调度的实施问题。我相信规格可以信任。

答案 1 :(得分:2)

你的理论是真实的。

当我的chrome扩展背景页打开到127.0.0.1服务器的websocket连接时,我实际上在ubuntu 1404上的chrome 62上遇到了这种情况。我的服务器首先向应用程序发送serval消息。并且第一个serage消息可能丢失并且可能不会丢失。但是这个bug不会发生在我的mac chrome 62上。我认为这就是数据竞争的样子。它可能永远不会发生,但它可能在理论上发生。所以我们需要防止它发生。

以下是我的客户端代码:

var ws = new WebSocket(url);
var lastConnectTime = new Date();
ws.onerror = processError;
ws.onclose = finish;
ws.onmessage = processMessage;

<强>解决方案

解决方案应该是服务器必须等待客户端第一条消息(即使它没有任何信息)然后向客户端发送消息。

以下是我在代码中的客户端js中的解决方案:

var ws = new WebSocket(url);
var lastConnectTime = new Date();
ws.onerror = processError;
ws.onclose = finish;
ws.onmessage = processMessage;
ws.onopen = function(){
   ws.send("{}");
};

这是我在golang服务器上的解决方案:

func (s *GoServer)ServeHTTP(w http.ResponseWriter, r *http.Request){
    fmt.Println("WebsocketServeHttp recv connect",r.RemoteAddr)
    conn,err:=websocket.Upgrade(w,r,nil,10240,10240)
    if err!=nil{
        panic(err)
    }
    _,_,err=conn.ReadMessage()
    if err!=nil{
        panic(err)
    }
    //... (you can send message to the client now)
}

答案 2 :(得分:1)

确认问题在Ubuntu上的Chrome 62和63上确实存在(作为一种罕见但真实的情况):偶尔会丢失来自服务器的第一条消息。我用tcpdump确认确实存在握手数据包,然后是第一条消息的数据包。在客户端中,第一条消息甚至在Networking选项卡中显示为websocket上的第一帧。然后调用onopen回调,但onmessage不是。

我同意它似乎不可能,并且看着WebKit的WebSocket实现,它似乎不可能,而且我从来没有在Chrome Mac上或在Firefox,所以我唯一的猜测就是Ubuntu上的Chrome引入了竞争条件并进行了一些优化。

答案 3 :(得分:1)

您肯定会丢失消息!公认的答案具有误导性。所有要做的事情是,您进行了一项操作,以放弃打开事件和配置消息侦听器之间的控制线程。

这有可能发生吗?谁知道,这取决于您的应用程序。在这种情况下,我浪费了太多时间在服务器上使用ws库调试此文件(api很烂!):

在服务器上:

ScrollView

看到async handleOpen(socket, request) { const session = await getSession(cookie.parse(request.headers.cookie).id); const user = new User(session.user.id, socket); user.socket.addEventListener('message', this.handleMessage.bind(this, user)); } 吗?放弃控制,使事件丢失。就其价值而言,会话存储在memcached中,因此无法立即使用。