WebSocket连接失败:WebSocket握手期间出错:意外的响应代码:400。

92

我试图将Socket.io与Angular集成,但是在客户端与服务器之间建立连接时遇到了困难。我查看了其他相关问题,但我的问题发生在本地,所以中间没有Web服务器。

这是我的服务器代码:

const app = express();
const server = http.createServer(app);
const io = require('socket.io').listen(server);

io.on('connection', function(socket) {
  socket.emit('greet', { hello: 'Hey, Mr.Client!' });

  socket.on('respond', function(data) {
    console.log(data);
  });
  socket.on('disconnect', function() {
    console.log('Socket disconnected');
  });
});

我正在使用Grunt按照以下顺序加载客户端JavaScript文件:

dist: {
    src: [
        public/bower_components/angular/angular.min.js,
        ...
        public/bower_components/socket.io-client/dist/socket.io.min.js,
        public/bower_components/angular-socket-io/socket.min.js,
        ...
    ]
}

然后在我的控制器中:

function MyController($scope) {

    let socket = io.connect(window.location.href);
    socket.connect('http://localhost:3000');
    socket.on('greet', function(data) {
      console.log(data);
      socket.emit('respond', { message: 'Hello to you too, Mr.Server!' });
    });

    ...
}

在实际使用btford/angular-socket-io库之前,我想确保可以正确连接,但是控制台中出现以下错误:

socket io connection error message

有趣的是,如果重新启动Node.js服务器进程,则可以发送消息,但是使用轮询而不是WebSockets。

after restart

polling message

我尝试了各种不同的选项来调用socket.connect,但没有任何作用。

感激任何帮助。


更新(2016年12月30日):

我刚刚意识到WebSockets部分工作。在Chrome开发者控制台中看到了101切换协议请求。但是,我只能看到那里的engine.io协议数据包(ping,pong)。 但是我的应用程序套接字消息仍然因某种原因回退到轮询...

engine.io packets


5
你是否使用nginx作为代理服务器? - digit
嘿,数字,目前还没有Web服务器。我读到了必须设置升级标头才能使其工作,但我目前只是在本地开发。 - ashe540
我在使用Pusher和Laravel时遇到了问题。我不知道为什么,但是我收到了这个错误:(( - ali Falahati
22个回答

68

问题已解决!我刚刚找到了解决方法,但我仍然想知道这是否是正常行为。

似乎即使Websocket连接正确建立(由101 Switching Protocols请求指示),它仍会默认为长轮询。修复方法就是在Socket.io连接函数中添加此选项:

{transports: ['websocket']}

所以最终代码看起来像这样:

const app = express();
const server = http.createServer(app);
var io = require('socket.io')(server);

io.on('connection', function(socket) {
  console.log('connected socket!');

  socket.on('greet', function(data) {
    console.log(data);
    socket.emit('respond', { hello: 'Hey, Mr.Client!' });
  });
  socket.on('disconnect', function() {
    console.log('Socket disconnected');
  });
});

并且在客户端:

var socket = io('ws://localhost:3000', {transports: ['websocket']});
socket.on('connect', function () {
  console.log('connected!');
  socket.emit('greet', { message: 'Hello Mr.Server!' });
});

socket.on('respond', function (data) {
  console.log(data);
});

现在,消息将显示为帧:

working websockets

这个Github问题指引了我正确的方向。感谢所有提供帮助的人!


你是怎么让 Chrome 显示那样的框架的?当我点击 sockIO 请求时,它不会显示框架选项卡。 - kiwicomb123
当浏览器发起连接时,它会发送一个升级请求,服务器将以101切换协议进行响应。您必须确保在网络选项卡中查找此请求,因为它将显示两个实体之间发送的帧。 - ashe540
是的,我已经导航到那里了。在网络选项卡中有几种情况下,“帧”选项卡不会出现。 - kiwicomb123
1
谢谢,这对我有用,我和 OP 有相同的问题。 - Ryan Rebo
非常感谢,运行得很完美。由于Heroku上的一些端口绑定问题,我已经从Heroku切换到glitch,然后在glitch上遇到了这个错误。 - Punter Bad
4
请注意,添加此选项将有效地移除长轮询回退功能,这是 socket.io 首先如此强大的原因之一。请参见 https://socket.io/docs/using-multiple-nodes/。 - Kathandrax

64

这对我在Nginx、Node服务器和Angular 4中的应用有效

编辑您的Nginx Web服务器配置文件如下:

server {
listen 80;
server_name 52.xx.xxx.xx;

location / {
    proxy_set_header   X-Forwarded-For $remote_addr;
    proxy_set_header   Host $http_host;
    proxy_pass         "http://127.0.0.1:4200";
    proxy_http_version 1.1;
    proxy_set_header   Upgrade $http_upgrade;
    proxy_set_header   Connection "upgrade";
}

8
即使提问者在谈论一个没有涉及到 nginx 反向代理的本地问题,但当其实际运用时,这个回答所提供的 nginx 设置是正确的。 - Scott Stensland
1
为我的配置添加升级标头是我所需要的。 proxy_http_version 1.1; 导致 Nginx 服务无法启动。 - JDavis
3
对于那些想知道为什么需要升级标头的人?https://www.nginx.com/blog/websocket-nginx/ - JoshuaTree
我的代码中缺少了最后两行升级的内容。 - Cybersupernova
这是我的问题,服务器在本地网络上运行顺畅,但当我将其作为公共服务器与nginx一起使用时,会发生随机连接失败的情况。这解决了我的问题。 - Macumbaomuerte
显示剩余2条评论

35

目前被接受的解决方案是误导性的。

根据官方文档,添加transports: ['websocket']选项会有效地删除当无法建立websocket连接时回退到长轮询的能力。这个选项是使socket.io如此强大的原因之一,因为它可以适应许多情况。

在那种希望仅依赖websocket的特殊情况下,建议直接使用WebSocket API

对于其他情况(大多数用户),这很可能是反向代理/服务器配置问题。

官方文档根据您的环境提供以下建议:

NginX配置

http {
  server {
    listen 3000;
    server_name io.yourhost.com;

    location / {
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header Host $host;

      proxy_pass http://nodes;

      # enable WebSockets
      proxy_http_version 1.1;
      proxy_set_header Upgrade $http_upgrade;
      proxy_set_header Connection "upgrade";
    }
  }

  upstream nodes {
    # enable sticky session based on IP
    ip_hash;

    server app01:3000;
    server app02:3000;
    server app03:3000;
  }
}

Apache HTTPD 配置

Header add Set-Cookie "SERVERID=sticky.%{BALANCER_WORKER_ROUTE}e; path=/" env=BALANCER_ROUTE_CHANGED

<Proxy "balancer://nodes_polling">
    BalancerMember "http://app01:3000" route=app01
    BalancerMember "http://app02:3000" route=app02
    BalancerMember "http://app03:3000" route=app03
    ProxySet stickysession=SERVERID
</Proxy>

<Proxy "balancer://nodes_ws">
    BalancerMember "ws://app01:3000" route=app01
    BalancerMember "ws://app02:3000" route=app02
    BalancerMember "ws://app03:3000" route=app03
    ProxySet stickysession=SERVERID
</Proxy>

RewriteEngine On
RewriteCond %{HTTP:Upgrade} =websocket [NC]
RewriteRule /(.*) balancer://nodes_ws/$1 [P,L]
RewriteCond %{HTTP:Upgrade} !=websocket [NC]
RewriteRule /(.*) balancer://nodes_polling/$1 [P,L]

ProxyTimeout 3

HAProxy配置

listen chat
  bind *:80
  default_backend nodes

backend nodes
  option httpchk HEAD /health
  http-check expect status 200
  cookie io prefix indirect nocache # using the `io` cookie set upon handshake
  server app01 app01:3000 check cookie app01
  server app02 app02:3000 check cookie app02
  server app03 app03:3000 check cookie app03

同时值得阅读的是关于在HAProxy中升级连接的这篇文章

更多细节请参考上面的官方文档链接。

编辑:

Varnish (源代码在这里)

sub vcl_recv {
    if (req.http.upgrade ~ "(?i)websocket") {
        return (pipe);
    }
}

sub vcl_pipe {
    if (req.http.upgrade) {
        set bereq.http.upgrade = req.http.upgrade;
        set bereq.http.connection = req.http.connection;
    }
}

我的WebSocket连接升级失败了,不知道为什么。我在nginx中有这个配置:location / { proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwared-For $proxy_add_x_forwarded_for; proxy_set_header Host $http_host; proxy_set_header X-NginX-Proxy true; proxy_pass http://127.0.0.1:3000; proxy_redirect off; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; client_max_body_size 100M; } - invider
抱歉回复晚了 @invider @Norbert。你们解决问题了吗?我看到 proxy_set_header X-Forwared-For $proxy_add_x_forwarded_for; 有一个拼写错误 - 应该是 X-Forwarded-For。另外,你们是否仔细检查过代理目标是否可达(特别是通过端口3000)? - Kathandrax
不记得了。 - invider

12

我通过将传输方式从“websocket”更改为“polling”来解决了这个问题

   var socket = io.connect('xxx.xxx.xxx.xxx:8000', {
      transports: ['polling']
   });

6
这更像是一种权宜之计,而非真正的修复。Socket.io在初始连接时使用轮询,然后“升级”到更可靠的传输方式(如WebSockets)。这个答案强制使socketio只使用轮询(而不是更有效率的传输方式),这可以解决错误,但如果你希望避免在更大的应用程序中进行无休止的轮询,则不是长期解决方案。我相信Socketio也知道如果升级失败会自动切换回轮询,所以你基本上是在避免控制台错误。 - sofly

9
根据您通过 Socket.IO 发送的消息socket.emit('greet', { hello: 'Hey, Mr.Client!' });,似乎您正在使用 hackathon-starter 模板。如果是这样,问题可能因为 express-status-monitor 模块创建了自己的 socket.io 实例而导致,如下所示:https://github.com/RafalWilinski/express-status-monitor#using-module-with-socketio-in-project 您可以选择以下两种方法之一:
  1. Remove that module
  2. Pass in your socket.io instance and port as websocket when you create the expressStatusMonitor instance like below:

    const server = require('http').Server(app);
    const io = require('socket.io')(server);
    ...
    app.use(expressStatusMonitor({ websocket: io, port: app.get('port') })); 
    

太棒了!非常感谢你! - marco burrometo

7

我也遇到了同样的问题,我的应用程序在nginx后面。将这些更改应用到我的Nginx配置文件中解决了错误。

location / {
proxy_pass http://localhost:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
}

6

我曾经遇到过相同的问题,我修改了apache2虚拟主机入口并成功解决了问题。

注意:在服务器上,我已经成功安装并在9001端口上工作而没有任何问题。这个指南仅适用于apache2,与nginx无关,这个答案是给apache2+etherpad爱好者的。

<VirtualHost *:80>
  ServerName pad.tejastank.com
  ServerAlias pad.tejastank.com
  ServerAdmin snippetbucket@gmail.com

  LoadModule  proxy_module         /usr/lib/apache2/modules/mod_proxy.so
  LoadModule  proxy_http_module    /usr/lib/apache2/modules/mod_proxy_http.so
  LoadModule  headers_module       /usr/lib/apache2/modules/mod_headers.so
  LoadModule  deflate_module       /usr/lib/apache2/modules/mod_deflate.so

  ProxyVia On
  ProxyRequests Off
  ProxyPreserveHost on

    <Location />
        ProxyPass http://localhost:9001/ retry=0 timeout=30
        ProxyPassReverse http://localhost:9001/
    </Location>
    <Location /socket.io>
        # This is needed to handle the websocket transport through the proxy, since
        # etherpad does not use a specific sub-folder, such as /ws/ to handle this kind of traffic.
        # Taken from https://github.com/ether/etherpad-lite/issues/2318#issuecomment-63548542
        # Thanks to beaugunderson for the semantics
        RewriteEngine On
        RewriteCond %{QUERY_STRING} transport=websocket    [NC]
        RewriteRule /(.*) ws://localhost:9001/socket.io/$1 [P,L]
        ProxyPass http://localhost:9001/socket.io retry=0 timeout=30
        ProxyPassReverse http://localhost:9001/socket.io
    </Location>


  <Proxy *>
    Options FollowSymLinks MultiViews
    AllowOverride All
    Order allow,deny
    allow from all
  </Proxy>
</VirtualHost>

提高技巧: 请使用a2enmod命令启用apache2的所有模块。

重启apache2以使更改生效。但是要启用站点,需要使用a2ensite命令。


1
我把你所有的 http:// 改成了 https://,把 ws:// 改成了 wss:// -- 一切正常。非常感谢你的分享。 在这种情况下使用 localhost:### 很有趣,我以前从未见过。 - edwardsmarkf
1
是的,这对我起作用了。谢谢。 重写规则可能是我的问题。 - Devender Gupta
1
那对我有用 - <Location /socket.io> 我没有将我的设置包含在其中...但是确实帮了很多。谢谢老兄! - Robert
原因:与远程服务器进行 SSL 握手时出现错误。 - Nevermore

1

在你的控制器中,你正在使用一个 http 方案,但我认为你应该使用一个 ws 方案,因为你正在使用websockets。尝试在你的连接函数中使用 ws://localhost:3000


嘿,马蒂亚,没什么运气。我尝试使用ws而不是http,但问题仍然存在。还有其他想法吗? - ashe540

1

我认为你应该像下面这样为客户端定义你的origins

//server.js
const socket = require('socket.io');
const app = require('express')();
const server = app.listen('port');

const io = socket().attach(server);
io.origins("your_domain:port www.your_domain:port your_IP:port your_domain:*")

io.on('connection', (socket) => {
  console.log('connected a new client');
});

//client.js
var socket = io('ws://:port');


通过添加其他模式,如 http://your_IP:porthttp://your_domain:port,扩展您的起源。 - Heartbit
很遗憾,那似乎没有起作用。还有其他什么想法,为什么会因为400 Bad Request而拒绝使用Websockets?让我困惑的是这都在本地主机上,所以不应该有任何问题。 - ashe540

1
在我的情况下,我刚刚安装了express-status-monitor来解决这个错误。
以下是设置。
install express-status-monitor
npm i express-status-monitor --save
const expressStatusMonitor = require('express-status-monitor');
app.use(expressStatusMonitor({
    websocket: io,
    port: app.get('port')
}));

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接