服务器端的代码在连接打开后立即发送消息(它向客户端发送初始配置/问候语)。
以下代码在客户端:
var sock = new WebSocket(url);
sock.addEventListener('error', processError);
sock.addEventListener('close', finish);
sock.addEventListener('message', processMessage);
我担心从服务器丢失与配置/问候语相关的第一条消息。从理论上讲,在设置事件处理程序之前message
没有什么可以阻止它被接收。
另一方面,实际上我从未想过。而AFAIK JavaScript WebSocket API没有针对这个理论问题的对策:WebSocket构造函数既不允许设置message
事件处理程序,也不允许在挂起状态下创建WebSocket。
所以:
- 要么我错过了一些东西,即使在理论上,上述代码的消息丢失也是不可能的。
- 或者它是JavaScript WebSocket API设计中的错误。
- 或者每个人都很高兴,因为消息丢失几乎是不可能的。
- 或者这种行为(在连接时从服务器发送消息)在某种程度上被认为是不好的做法,所以没有人担心在理论上实现它的可能性是正确的。
?
PS:这些简单但理论性的问题更适合Stack Overflow还是Programmers@Stack Exchange?
别担心。
代码在单线程事件循环中运行。
这一行:var sock = new WebSocket(url);
根本不启动 websocket 连接。该规范规定,只有在返回 Web 套接字后,它才能执行实际连接,与处理运行代码的事件循环的线程并行:
- 返回一个新的 WebSocket 对象,但继续执行这些步骤[并行][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:检验理论
关于青铜侠关于消息回复失败的回答......
我无法重现声称的问题,即使我使用小型Ruby应用程序和小型Javascript客户端编写了测试。
Ruby 应用程序启动一个带有欢迎消息的 Websocket echo 服务器(我正在使用 plezi.io)。
Javascript 客户端包含一个 busy-wait 循环,该循环导致 Javascript 线程挂起(块)指定的时间量(在我的测试中为 2 秒)。
onmessage
回调仅在块释放后(2 秒后)设置——因此来自服务器的欢迎消息将在定义回调之前到达浏览器。
这使我们能够测试欢迎消息是否在任何特定浏览器上丢失(这将是浏览器中的错误)。
该测试是可靠的,因为服务器是已知数量,并且一旦upgrade
完成,就会将消息发送到套接字(我用 C 和 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) 上的结果:
Safari 11.01 仅在新客户端创建完成后(线程处理完代码之后,如 Ruby 应用程序的延迟输出所示)启动 Websocket 连接。消息显然是在建立连接后到达的。
Chrome 62.0 会立即启动 Websocket 连接。消息在 2 秒窗口结束后到达。即使消息在设置
onmessage
处理程序之前到达,也不会丢失。FireFox 56.0的行为与Chrome相同,会立即启动Websocket连接。消息在 2 秒窗口结束后到达。消息没有丢失。
如果有人可以在Windows和Linux上进行测试,那就太好了......但我认为浏览器在事件调度方面不会出现实现问题。我相信规格是可以信任的。
你的理论是真实的。
当我的 chrome 扩展程序背景页面打开与 127.0.0.1 服务器的 websocket 连接时,我实际上在 ubuntu 62 上的 chrome 1404 上遇到了这种情况。我的服务器首先向应用程序发送服务消息。第一条薮猫信息可能会丢失,也可能不会丢失。但是这个错误不会发生在我的 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)
}
确认问题确实存在(作为罕见但真实的情况)在 Chrome 62 和 63 在 Ubuntu 上:偶尔丢失来自服务器的第一条消息。我用 tcpdump 确认确实有一个握手数据包,然后是第一条消息的数据包。在客户端中,第一条消息甚至作为 websocket 上的第一个帧显示在"网络"选项卡中。然后调用onopen
回调,但onmessage
不是。
我同意这似乎是不可能的,并且查看WebKit的WebSocket实现,这似乎是不可能的,而且我从未在Chrome Mac或Firefox上看到过它,所以我唯一的猜测是Ubuntu上的Chrome引入了一些优化的竞争条件。
你绝对会丢失消息!接受的答案具有误导性。所要做的就是执行一个操作,该操作放弃打开事件和配置消息侦听器之间的控制线程。
这有可能发生吗?谁知道呢,这取决于您的应用程序。以下是导致我浪费太多时间使用服务器上的 ws 库调试它的情况(api 很糟糕!):
在服务器上:
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));
}
看到await
了吗?这放弃了控制权并允许事件丢失。值得一提的是,会话存储在memcached中,因此无法立即使用。