Jump to Table of Contents Pop Out Sidebar

Windows 下在 emacs 中使用 localhost 导致的连接创建卡顿以及解决方法

More details about this document
Create Date:
Publish Date:
Update Date:
2024-04-22 00:22
Creator:
Emacs 29.2 (Org mode 9.6.15)
License:
This work is licensed under CC BY-SA 4.0

在学完了 Python 的 Socket 后,本着练练手的思路,我编写了通过 Socket 连接 emacs 与本地 Python 进程的代码片段,但比较奇怪的是,我的代码每次连接的时候,emacs 会卡顿大约一秒的时间,此时无法执行任何操作,一秒后恢复正常。虽然感觉有些奇怪,但这只出现在连接创建时,所以我也没怎么放在心上。两周前看了这个帖子我才明白了问题所在,既然现在有点时间,不如做个总结。

本文使用的环境如下:

1. 测试代码

因为涉及到两个进程,所以需要分别编写代码,下面是 Python 的代码:

import socket
import select

host = 'localhost'
port = 11451

def start_server():
    server_start = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server_start.bind((host, port))
    server_start.listen(1)

    while True:
        try:
            readable, _, _ = select.select([server_start], [], [], 1.0)
            if server_start in readable:
                client, addr = server_start.accept()
                print('connect start {}'.format(addr))
                while True:
                    data = client.recv(1024)
                    if data == b'stop':
                        client.close()
                        print('connect close')
                        break
                    else:
                        client.sendall(b'hello')
        except KeyboardInterrupt:
            print('Ctrol-c')
            break

start_server()

以下是 elisp 代码:

(setq a (open-network-stream
	 "hello"
	 (get-buffer-create "*hel*")
	 "localhost"
	 11451))

(process-send-string a "1")

首先我们通过 python 启动这个脚本,随后在 emacs 中使用以上代码创建与 python 进程的连接,你很容易会发现在对 setq 表达式求值时有明显的卡顿感,通过 C-pC-n 无法移动光标。如果我们将 elisp 代码中的 "localhost" 换成 "127.0.0.1" 则不会有这个问题:

(setq a (open-network-stream
	 "hello"
	 (get-buffer-create "*hel*")
	 "127.0.0.1"
	 11451))

很明显,"localhost" 导致执行了一些耗时的操作。这里我用 nodejs 排除一下是我的 Python 代码导致的问题的可能性:

const net = require('net')

const server = net.createServer()

server.on('connection', (socket) => {
    console.log('con start')

    socket.on('data', (data) => {
	console.log('get message:', data.toString())
	socket.write('hello\n')
    })

    socket.on('close', () => {
	console.log('con closed')
    })
})

server.listen(11451, () => {
    console.log('start server ...')
})

排除失败(笑)。在对 JS 进程的 open-network-stream 的调用中,不论是使用 localhost 还是 127.0.0.1 都不会在 emacs 中造成卡顿。这说明卡顿的问题并不是与 Python 无关的。

在我文首给出的帖子中,LZ 用 Wireshark 发现了异常,下面让我们用它来分别观察一下 python 和 js 在面对来自 emacs 的连接时都有哪些包。

2. Wireshark 抓包

我们可以通过这个链接获取 Wireshark。关于 Wireshark 的基本操作这里我就不介绍了,上手很容易。

打开 Wireshark,然后选择 Adapter for loopback traffic capture:

1.png

接着我们就进入了抓取界面,这里给出接下来要使用的 filter:

tcp.port eq 11451

2.1. 在使用 Python 时抓包

首先我们运行 python 代码,然后在运行 emacs 连接 localhost 代码前开始抓包,完成后停止抓包,并添加 filter。通过以上操作我们可以得到如下结果:

2.png

接着,使用 emacs 连接 127.0.0.1 的代码再次尝试,得到如下结果:

3.png

可见这次没有上面的那些标红的行,非常顺利地完成了连接。

2.2. 在使用 node 时抓包

首先是 localhost,这里我补全了连接断开时的抓包:

4.png

接着是 127.0.0.1:

5.png

2.3. 现象总结

关于 TCP 的三次握手我想我不用讲了。当我们在 emacs 中使用 localhost 来连接 Py 或 JS 进程时,可以看到首先采用的源地址和目标地址是 ::1 ,而这是一个 ipv6 地址。与 JS 不同的是,在 emacs 通过 open-network-stream 与 Py 进程建立连接时,我们可以注意到它出现了 SYN 重传:

6.png

这可能说明我们的代码不能正确处理 ipv6 的情况,但也可能是环境的问题。考虑到 JS 能够成功而 Py 不行,下面让我们把重点放在 Python 上。

3. 原因分析

在上面的 SYN 重传中,我们可以注意到重传发送方是 Emacs 进程(Py 进程使用了 11451 端口),而且它收到的不是 SYN, ACK 而是 RST, ACK。我向 Chatgpt 询问什么情况下会出现 RST, ACK,这是它的答案:

什么情况下,创建 TCP 连接时,服务器会向客户端返回 RST,ACK

在 TCP 连接建立过程中,服务器在某些情况下会向客户端发送 RST,ACK(重置连接)报文。以下是几种可能会触发服务器发送 RST,ACK 报文的情况:

  1. 服务端端口未打开或未监听:如果客户端尝试连接到服务器上未打开或未监听的端口,服务器将无法建立有效的连接,因此会向客户端发送 RST,ACK 报文以重置连接。
  2. 服务端无法处理连接请求:如果服务器因为某种原因无法处理连接请求,例如由于资源不足、连接队列已满或服务不可用等,服务器可能会发送 RST,ACK 报文以拒绝连接。
  3. 服务端主动关闭连接:在已经建立的 TCP 连接中,服务器可以主动关闭连接。在这种情况下,服务器会发送 RST,ACK 报文给客户端,表示连接被重置关闭。
  4. 防火墙或网络设备的干预:防火墙或其他网络设备可能会检测到某些不正常的连接行为,如连接的频率、持续时间或连接规模等超出预定阈值,此时服务器可能会发送 RST,ACK 报文以阻止连接。

需要注意的是,这只是一些常见的情况,实际上服务器发送 RST,ACK 报文的原因可能有多种多样。具体情况取决于服务器的实现、配置和网络环境。

注意到我们使用 localhost 时 Emacs 发起的是 ipv6 连接,而使用 127.0.0.1 时 Emacs 发起的是 ipv4 连接,在 Chatgpt 的回答中我认为第一点的可能性最大,这就引发了我的思考:难道 ipv4 的端口和 ipv6 的端口不是同一个东西?

如果我的程序监听 ipv6 的 80 端口,那么使用 ipv4 访问会得到响应吗?

如果你的程序在 IPv6 上监听了端口 80,那么使用 IPv4 来访问该端口可能会导致连接失败。

IPv4 和 IPv6 是不同的网络协议,它们使用不同的地址格式和通信方式。IPv6 监听的端口通常只能接受 IPv6 地址的连接请求。

然而,有一种技术称为 IPv6 与 IPv4 的双栈(Dual Stack),它允许系统同时支持 IPv4 和 IPv6。在这种情况下,IPv6 监听的端口也可以接受来自 IPv4 地址的连接。

如果你的程序在 IPv6 上监听了端口 80,并且你的系统启用了 IPv6 与 IPv4 的双栈支持,那么当使用 IPv4 地址来访问该端口时,系统会将 IPv4 地址转换为对应的 IPv6 映射地址,从而允许连接成功。

然而,要确保正确的双栈配置以及适当的地址映射和转换设置,这取决于你的操作系统和网络环境。如果系统没有正确配置双栈或没有适当的 IPv4 到 IPv6 映射,那么使用 IPv4 访问 IPv6 监听的端口可能会导致连接失败。

因此,如果你想要同时支持 IPv4 和 IPv6 访问,建议在程序中监听两个端口,一个是 IPv4 的 80 端口,另一个是 IPv6 的 80 端口。这样,无论是使用 IPv4 还是 IPv6 地址访问,都能够得到正确的响应。

还真是。那么这样一来就能够理解为什么 Python 代码会有问题了,我的代码只使用了 ipv4 端口:

server_start = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_start.bind((host, port))
server_start.listen(1)

参考官方文档,使用 AF_INET 只用于 ipv4。如果我们将代码中的 AF_INET 改为 AF_INET6 就能得到正常结果了:(此时我在 emacs 中使用的是 localhost

7.png

如果我们在 Emacs 中使用 127.0.0.1 来连接此时的 Py 进程,我们会得到如下结果:

8.PNG
9.png

到了这里,问题就真相大白了,Windows 默认将 localhost 解析成 ::1 ,这导致使用 Emacs 发起请求时若使用 localhost 则采用 ipv6 格式,不能与配置为 ipv4 的 Python 进程正常连接。而 node 可能内部已经做了处理,从而能够同时处理 ipv4 和 ipv6 两种情况。

4. 解决方法

在创建连接时,只要我们明确指定了 127.0.0.1 那么 Emacs 就会创建 ipv4 请求,若服务器进程接受 ipv4 请求则可以正确建立连接。由于一般的本地网络连接都不太可能用 ipv6,所以指定 127.0.0.1 应该是最保险的选择,直接省去了域名解析的过程。

当然这是对我们用户来说,如果我们需要编写服务端程序,我们可以考虑一下 ChatGPT 的建议:使用所谓的双栈写法,能够同时处理 ipv4 和 ipv6 的情况,这样不论用户使用的是 localhost 还是 127.0.0.1 都能够正确连接而不出现卡顿,比如下面经过改进的 Python 代码:

import socket
import select

def handle_client(client):
    while True:
        data = client.recv(1024)
        if data == b'stop':
            client.close()
            print('connect close')
            break
        else:
            client.sendall(b'hello')

def start_server():
    server = socket.create_server(('', 11451), family=socket.AF_INET6,
                                  dualstack_ipv6=True)
    print('server start ...')

    while True:
        client, addr = server.accept()
        print('conn start')
        handle_client(client)
        print('conn close')

start_server()

以下抓包是在 Emacs 中分别指定 local127.0.0.1 得到的结果,可见 Python 正确地处理了这两种情况:(也包括了连接关闭的抓包)

10.png

5. 进一步的研究

5.1. localhost 由谁解析

众所周知, localhost 是指向自己发送请求的主机名,一般来说这是由操作系统解析的。在 Windows 下我们可以通过修改 hosts 文件(位于 C:\Windows\System32\drivers\etc\hosts )来添加域名与 IP 的映射,在我的机器中这个文件的部分内容如下:

# Copyright (c) 1993-2009 Microsoft Corp.
#
# This is a sample HOSTS file used by Microsoft TCP/IP for Windows.
#
# This file contains the mappings of IP addresses to host names. Each
# entry should be kept on an individual line. The IP address should
# be placed in the first column followed by the corresponding host name.
# The IP address and the host name should be separated by at least one
# space.
#
# Additionally, comments (such as these) may be inserted on individual
# lines or following the machine name denoted by a '#' symbol.
#
# For example:
#
#      102.54.94.97     rhino.acme.com          # source server
#       38.25.63.10     x.acme.com              # x client host

# localhost name resolution is handled within DNS itself.
#	127.0.0.1       localhost
#	::1             localhost

注意其中的注释 localhost name resolution is handled within DNS itself ,我尝试把最后两行的注释去掉后并 ipconfig /flushdns ,但毫无效果。在 Widnows 10 中我们可能无法通过修改这个文件来改变 localhost 的指向(即默认指向 ::1127.0.0.1 ,但 ipv6 优先)。

现在我们再尝试一下以下代码,但不启动服务端:

(setq a (open-network-stream
	 "hello"
	 (get-buffer-create "*hel*")
	 "localhost"
	 11451))
11.png

可见它确实是先尝试 ::1 ,再尝试 127.0.0.1

5.2. 类似的问题

通过 google 搜索 localhost is slower than 127.0.0.1 能够找到一些帖子或问答,这个 issue 是个不错的解答:

6. 后记

导致 emacs 通过 localhost 创建连接出现问题的原因是本地将 localhost 首先解析为 ::1 ,接着才轮到 127.0.0.1 。在代码中使用 127.0.0.1 是比较保险的选择,服务端也可以考虑提供双栈服务来规避这个问题。