因为涉及到两个进程,所以需要分别编写代码,下面是 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-p
或 C-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 的连接时都有哪些包。
我们可以通过这个链接获取 Wireshark。关于 Wireshark 的基本操作这里我就不介绍了,上手很容易。
打开 Wireshark,然后选择 Adapter for loopback traffic capture:
接着我们就进入了抓取界面,这里给出接下来要使用的 filter:
tcp.port eq 11451
首先我们运行 python 代码,然后在运行 emacs 连接 localhost
代码前开始抓包,完成后停止抓包,并添加 filter。通过以上操作我们可以得到如下结果:
接着,使用 emacs 连接 127.0.0.1
的代码再次尝试,得到如下结果:
可见这次没有上面的那些标红的行,非常顺利地完成了连接。
首先是 localhost,这里我补全了连接断开时的抓包:
接着是 127.0.0.1:
关于 TCP 的三次握手我想我不用讲了。当我们在 emacs 中使用 localhost
来连接 Py 或 JS 进程时,可以看到首先采用的源地址和目标地址是 ::1
,而这是一个 ipv6 地址。与 JS 不同的是,在 emacs 通过 open-network-stream
与 Py 进程建立连接时,我们可以注意到它出现了 SYN 重传:
这可能说明我们的代码不能正确处理 ipv6 的情况,但也可能是环境的问题。考虑到 JS 能够成功而 Py 不行,下面让我们把重点放在 Python 上。
在上面的 SYN 重传中,我们可以注意到重传发送方是 Emacs 进程(Py 进程使用了 11451 端口),而且它收到的不是 SYN, ACK 而是 RST, ACK。我向 Chatgpt 询问什么情况下会出现 RST, ACK,这是它的答案:
什么情况下,创建 TCP 连接时,服务器会向客户端返回 RST,ACK
在 TCP 连接建立过程中,服务器在某些情况下会向客户端发送 RST,ACK(重置连接)报文。以下是几种可能会触发服务器发送 RST,ACK 报文的情况:
- 服务端端口未打开或未监听:如果客户端尝试连接到服务器上未打开或未监听的端口,服务器将无法建立有效的连接,因此会向客户端发送 RST,ACK 报文以重置连接。
- 服务端无法处理连接请求:如果服务器因为某种原因无法处理连接请求,例如由于资源不足、连接队列已满或服务不可用等,服务器可能会发送 RST,ACK 报文以拒绝连接。
- 服务端主动关闭连接:在已经建立的 TCP 连接中,服务器可以主动关闭连接。在这种情况下,服务器会发送 RST,ACK 报文给客户端,表示连接被重置关闭。
- 防火墙或网络设备的干预:防火墙或其他网络设备可能会检测到某些不正常的连接行为,如连接的频率、持续时间或连接规模等超出预定阈值,此时服务器可能会发送 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
)
如果我们在 Emacs 中使用 127.0.0.1
来连接此时的 Py 进程,我们会得到如下结果:
到了这里,问题就真相大白了,Windows 默认将 localhost
解析成 ::1
,这导致使用 Emacs 发起请求时若使用 localhost
则采用 ipv6 格式,不能与配置为 ipv4 的 Python 进程正常连接。而 node 可能内部已经做了处理,从而能够同时处理 ipv4 和 ipv6 两种情况。
在创建连接时,只要我们明确指定了 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 中分别指定 local
和 127.0.0.1
得到的结果,可见 Python 正确地处理了这两种情况:(也包括了连接关闭的抓包)
众所周知, 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 的指向(即默认指向 ::1
和 127.0.0.1
,但 ipv6 优先)。
现在我们再尝试一下以下代码,但不启动服务端:
(setq a (open-network-stream
"hello"
(get-buffer-create "*hel*")
"localhost"
11451))
可见它确实是先尝试 ::1
,再尝试 127.0.0.1
。
通过 google 搜索 localhost is slower than 127.0.0.1
能够找到一些帖子或问答,这个 issue 是个不错的解答:
导致 emacs 通过 localhost 创建连接出现问题的原因是本地将 localhost 首先解析为 ::1
,接着才轮到 127.0.0.1
。在代码中使用 127.0.0.1
是比较保险的选择,服务端也可以考虑提供双栈服务来规避这个问题。