Node.js 服务器监听 0.0.0.0 和 localhost 上的同一端口,没有错误

标签 node.js express network-programming

我偶然发现了一些有趣的东西,但我无法解释它,谷歌搜索也没有效果。

我有一台 Express 服务器,服务器 1,绑定(bind)到 localhost:

const express = require('express')
const app = express()
app.get('/', (req, res) => res.send('server 1'))
app.listen(4000, 'localhost')
node      37624 user   27u  IPv4 0x681653f502970305      0t0  TCP localhost:4000 (LISTEN)

我有另一个 Express 服务器,服务器 2,绑定(bind)到 0.0.0.0 处的所有接口(interface):

const express = require('express')
const app = express()
app.get('/', (req, res) => res.send('server 2'))
app.listen(4000, '0.0.0.0')
node      37624 user   27u  IPv4 0x681653f502970305      0t0  TCP localhost:4000 (LISTEN)
node      37693 user   25u  IPv4 0x681653f4fdbdc005      0t0  TCP *:4000 (LISTEN)

Curling 0.0.0.0 给出来自服务器 1 的响应,即绑定(bind)到 localhost 的服务器,因此显然这两者是冲突的。

但是,不知何故,这并没有引发人们所期望的错误,EADDRINUSE,怎么可能?

最佳答案

Node 在操作系统中的网络套接字上设置 SO_REUSEADDR 标志导致了此行为。 REUSEADDR 标志与 IPARR_ANY(对于 IPv4,又称为 0.0.0.0)地址有特殊的交互作用。来自socket manual pages (可靠来源):

   SO_REUSEADDR
          Indicates that the rules used in validating addresses supplied
          in a bind(2) call should allow reuse of local addresses.  For
          AF_INET sockets this means that a socket may bind, except when
          there is an active listening socket bound to the address.
          When the listening socket is bound to INADDR_ANY with a spe‐
          cific port then it is not possible to bind to this port for
          any local address.  Argument is an integer boolean flag.

来自 article这就涉及到这个确切的问题:

Some folks don't like SO_REUSEADDR because it has a security stigma attached to it. On some operating systems it allows the same port to be used with a different address on the same machine by different processes at the same time. This is a problem because most servers bind to the port, but they don't bind to a specific address, instead they use INADDR_ANY (this is why things show up in netstat output as *.8080). So if the server is bound to *.8080, another malicious user on the local machine can bind to local-machine.8080, which will intercept all of your connections since it is more specific.

我修改了一些Linux test code明确地证明这一点(底部)。当你运行它时,你会得到以下输出:

Opening 0.0.0.0 with no reuse flag:19999
Opening Loopback with no resuse flag:19999
bind: Address already in use
Correct: could not open lookpback with no reuse 19999
Opening 0.0.0.0 with with reuse flag:19999
Opening Loopback with with resuse flag:19999
Correct: could open lookpback with reuse 19999

第一个测试用例在 IPADDR_ANY 地址上打开一个套接字,而没有设置 REUSEADDR 标志,并且当尝试在环回上打开套接字时,“bind”会抛出 EADDRINUSE 错误(正如您最初预期的那样)。第二个测试用例执行相同的操作,但设置了 REUSEADDR 标志,并且创建第二个套接字时没有错误。

#include <errno.h>
#include <error.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <stdbool.h>
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>

#define PORT 19999

int open_port(int any, int reuse)
{
        int fd = -1;
        int reuseaddr = 1;
        int v6only = 1;
        int addrlen;
        int ret = -1;
        struct sockaddr *addr;
        int family = AF_INET;

        struct sockaddr_in addr4 = {
                .sin_family = AF_INET,
                .sin_port = htons(PORT),
                .sin_addr.s_addr = any ? htonl(INADDR_ANY) : inet_addr("127.0.0.1"),
        };


        addr = (struct sockaddr*)&addr4;
        addrlen = sizeof(addr4);

        if ((fd = socket(family, SOCK_STREAM, IPPROTO_TCP)) < 0) {
                perror("socket");
                goto out;
        }

        if (reuse){
                if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &reuseaddr,
                               sizeof(reuseaddr)) < 0) {
                        perror("setsockopt SO_REUSEADDR");
                        goto out;
                }
        }

        if (bind(fd, addr, addrlen) < 0) {
                perror("bind");
                goto out;
        }

        if (any)
                return fd;

        if (listen(fd, 1) < 0) {
                perror("listen");
                goto out;
        }
        return fd;
out:
        close(fd);
        return ret;
}

int main(void)
{
        int listenfd;
        int fd1, fd2;

        fprintf(stderr, "Opening 0.0.0.0 with no reuse flag:%d\n", PORT);
        listenfd = open_port(1, 0);
        if (listenfd < 0)
                error(1, errno, "Couldn't open listen socket");

        fprintf(stderr, "Opening Loopback with no resuse flag:%d\n", PORT);
        fd1 = open_port(0, 0);
        if (fd1 >= 0)
                error(1, 0, "Was allowed to create an loopback with no reuse");
        fprintf(stderr, "Correct: could not open lookpback with no reuse %d\n", PORT);

        close(listenfd);


        fprintf(stderr, "Opening 0.0.0.0 with with reuse flag:%d\n", PORT);
        listenfd = open_port(1, 1);
        if (listenfd < 0)
                error(1, errno, "Couldn't open listen socket");

        fprintf(stderr, "Opening Loopback with with resuse flag:%d\n", PORT);
        fd1 = open_port(0, 1);
        if (fd1 < 0)
                error(1, 0, "Was not allowed to create an loopback with  reuse");
        fprintf(stderr, "Correct: could open lookpback with reuse %d\n", PORT);
        close(fd1);
        close(listenfd);

        return 0;
}

关于Node.js 服务器监听 0.0.0.0 和 localhost 上的同一端口,没有错误,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/60217071/

相关文章:

javascript - 在浏览器和 node.js 中使用 .js 文件

node.js - Model.json 中的属性

node.js - Node + Express : How do the req and res variables have global scope without causing collisions?

javascript - Nodejs/mongodb 代码中的异步问题

linux - 多路径 TCP : Multiple connections Not Showing

node.js - 如何为 react 前端和 Node 后端集成oauth?

c++ - 在 ubuntu 上安装 npm 期间,使用 C++ 17 编译 native Node 插件失败

javascript - 从单独的路由文件中访问 express 'app' 对象的最佳方法是什么?

C# https 请求总是隧道到服务器,但 Java 只有一次

c - 通过 TCP 传输数据总是停止在 251 传输