监听端口到底是什么意思?


这个故事探讨了计算机网络中的一些概念,灵感来自Michael Nielsen的discovery fiction的想法。代码示例也可以在这个repo中找到。截取的片段是在Debian Linux上使用OpenBSD风格的netcat;行为和IPv6的支持可能因版本而异。

在学生会大楼的角落里,有一家咖啡店,在咖啡店的角落里有两个学生。利兹敲打着她哥哥在她搬到大学时给她的那台破旧的手摇MacBook的键盘。在她左边的长椅上,蒂姆在一个装订成卷的笔记本上写着公式。他们之间有一杯半空的常温咖啡,莉兹不时地喝上一口以保持清醒。

在房间的对面,咖啡师把目光从他的手机上移开,抬起头来,扫视着店内的情况。他一个耳朵戴着耳机,另一个悬空,而他的手机正在播放他的电影课的指定观看内容。在这家学生经营的店里,有一条不成文的规定,上夜班的员工可以利用顾客之间的漫长空隙来补习功课。除了蒂姆和丽兹,还有两名男学生独自坐在那里,紧盯着他们的笔记本电脑,他们已经这样做了几个小时。除此之外,店内空无一人。

蒂姆写到一半就停笔了,把这张纸从笔记本上撕下来,揉成一团,放在其他揉成一团的小纸片旁边。

“Shit,现在几点了?”他问到。

利兹看了看她笔记本上的时钟,“刚过两点”

蒂姆打了个哈欠,又开始在新的一页上面涂鸦,但利兹打断了他。

“蒂姆”

“什么?!”,蒂姆回答说,夸张地表达了他对刚开始写就被打断的恼怒。

“在一个端口上监听是什么意思?”

“呃嗯……”

“我必须为net写这个网络服务器的东西”,net是Computer Networks 201的缩写,这是蒂姆在上学期上的一门课。

“是的,我记得那门课”

“所以我在一个端口上监听连接”

“80端口”,蒂姆自信地回答,希望通过抢先回答她的问题来缩短谈话时间。

“实际上,我们应该监听8080,这样它就可以在没有root的情况下运行,但这不是重点。”

“哦,对了。那是什么?”

“好吧,监听一个端口是什么意思?”

“它意味着其他进程可以在该端口上连接到它。”蒂姆对这个问题显得很困惑。

“是的,我知道这一点,但怎么做?”

蒂姆考虑了几秒钟才回答。

“我猜操作系统有一个大的端口表,以及在这些端口上监听的进程。当你绑定到一个端口时,它就会在该表中放一个指向你的套接字的指针。”

“是的,我猜。”利兹说,语气中带着犹豫和不满意。

两人回到了他们各自的工作中。沉默了一段时间后,蒂姆小声嘀咕了一句胜利的 “是的!”,并在一张打印的纸上划掉了一个数字。他终于找到了他在微积分作业中一直纠结的一个证明。

利兹趁机再次引起他的注意。

“嘿,蒂姆,看,我正在同时运行绑定在同一端口的两个进程。”

她调整了两个包含Python代码的窗口的大小。
# server1.py
import socket

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind(('127.0.0.1', 8080))
sock.listen()
print(sock.accept()) 

然后在它旁边是另一个程序:
# server2.py
import socket

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind(('127.0.0.1', 8080))
print(sock.recv(1024))

然后她向他展示了这两个程序在各自的终端窗口中运行,通过Shell连接到大学的cslab3Debian服务器。

蒂姆将笔记本电脑转向自己。他打开第三个终端,停顿了一会儿,搜索他疲惫的大脑,然后输入netcat 127.0.0.1 8080

netcat 运行后立即退出。在另一个终端窗口中,正在运行的 python server1.py 程序退出,打印。
(<socket.socket fd=4, family=AddressFamily.AF_INET,
type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8080),
raddr=('127.0.0.1', 59558)>, ('127.0.0.1', 59558)) 

他边研究server1.py代码,边自言自语。

“好的,服务器绑定了一个端口,接受了第一个连接它的套接字,然后退出。我明白了,所以它打印的元组是accept调用的结果,然后它立即退出。但是现在......”,将鼠标光标移到显示 server2.py 的编辑器上,“……这一个甚至在听吗?”

他在与之前相同的终端中再次运行netcat 127.0.0.1 8080 -v,结果打印出来如下:
netcat: connect to 127.0.0.1 port 8080 (tcp) failed: Connection refused

“看”,他说,“你的代码中存在一个错误。 server2仍在运行,但你从未调用listen。它实际上没有对8080端口做任何事情。”

“当然是,看”,利兹说,抢回了她的笔记本电脑。

她在“netcat”命令的末尾加了一个-u,然后点击回车。这一次,它没有给出一个错误或立即退出,而是等待键盘输入。她对Tim这么快就认为她的代码有问题感到恼火,她敲出了timmy,知道这个绰号让他很不爽。

netcat会话无声无息地结束了,同时,python server2.py程序退出打印。
b'timmy\n'

蒂姆意识到利兹试图与他作对,但没有理会,不想让她满足于对他的挑衅。他向键盘做了个手势。利兹把笔记本扭向他的方向,他输入man netcat',调出netcat手册,其中描述该工具为“TCP/IP瑞士军刀”。他向下滚动到-u标志,文件将其简单描述为 “UDP模式”。

“啊”,他说,因为他突然想起了什么。“我明白了,server1是通过TCP监听,server2是通过UDP监听。这一定是SOCK_DGRAM的意思。所以它们是不同的协议。我猜操作系统为每个端口都有一个单独的表格。我没想到net涵盖了UDP,直到后来。”

“是的,我提前读了。”

“当然。你怎么会有时间提前阅读,却没有时间在早晨到期前完成这些作业呢?”

“我也可以问你关于Counter Strike的问题”,莉兹反问道。

蒂姆哼了一声。

他们又继续默默地工作了几分钟,然后莉兹打破了沉默。

“嘿,蒂姆,看看这个。我可以在同一个端口上监听两个进程,即使它们都是TCP。”

蒂姆从他的工作中抬起头来。这次利兹在屏幕上只有一个Python程序,而且是在两个终端中运行:
# server3.py
import socket

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
sock.bind(('127.0.0.1', 8080))
sock.listen()
print(sock.accept())

利兹解释说:“看,这个命令显示什么进程正在监听一个端口”。她输入了lsof -i:8080,然后点击回车。

程序打印:
> lsof -i:8080
COMMAND    PID USER   FD   TYPE   DEVICE SIZE/OFF NODE NAME
python3 174265 liz     3u  IPv4 23850797      0t0  TCP localhost:http-alt (LISTEN)
python3 174337 liz     3u  IPv4 23853188      0t0  TCP localhost:http-alt (LISTEN)

“当你连接到它时会发生什么?”,蒂姆问道,这次他的声音中带着一点真正的好奇心。

“看吧。”

利兹运行了一次netcat localhost 8080,其中一个服务器进程退出,而另一个则继续运行。然后她再次运行,另一个进程退出。

蒂姆的注意力转到了代码上,他把手指放在屏幕附近,读了一遍。莉斯讨厌被弄脏的屏幕,她说:“别紧张!”并把他的手推了回去。“我不会碰它”,他抗议道。他做了一个夸张的表演,让自己的手保持一个安全的距离,他指着setsockopt一行,问道:“嘿,这是什么巫术?”

“那是设置一个套接字选项,允许端口被重复使用。”

“哼,这在教科书上有吗?”

“不知道,我在Stack Overflow上找到的。”

“我不知道你可以这样重复使用一个端口。”

“我也不知道 "她停顿了一下,考虑了一下。"所以操作系统不能只是有一个端口到套接字的表格,它必须是一个端口到套接字的列表的表格。然后为UDP建立第二个表格。也许还有其他协议的。”

“是的,这听起来很对”,蒂姆同意。

“嗯”,利兹说,突然听起来不太确定。

“什么?”

“呃,没关系”,她说,她开始认真地敲打。

蒂姆回到他的任务上,几分钟后,他又划掉了一个问题。他快要完成了,他的神情也放松了一些。利兹将她的笔记本电脑向他倾斜,说“看看这个”。她给他看了两个程序:
# server4.py
import socket

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind(('127.0.0.2', 8080))
sock.listen()
print(sock.accept())

在它的旁边,
# server5.py
import socket

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind(('127.0.0.3', 8080))
sock.listen()
print(sock.accept())

“这些不是一样的吗?”蒂姆问道,一边研究它们。

“看一下绑定的IP。”

“哦,所以你是在同一个端口上监听,但有两个不同的IP。这能行吗?”

“似乎是的。而且我可以连接到他们两个。”

利兹运行netcat 127.0.0.2,然后netcat 127.0.0.3,给他看。

蒂姆思考了一下。“所以让我看看。操作系统必须有一个表,从每个端口和IP组合,到一个套接字。实际上,有两个:一个用于TCP,另一个用于UDP。”

“是的”,利兹点点头。“而不是只有一个套接字,可以是多个。但要注意这个。”她把服务器代码中的IP改为 0.0.0.0
# server6.py
import socket

sock socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind(('0.0.0.0', 8080))
sock.listen()
print(sock.accept())

“现在,当我运行绑定到127.0.0.2的服务器时,我得到了这个”,她继续说,
Traceback (most recent call last):
File "server5.py", line 4, in <module>
s.bind(('127.0.0.2', 8080))
OSError: [Errno 99] Cannot assign requested address


“但是”,她总结道,“如果我运行netcat 127.0.0.2 8080,就会连接到0.0.0.0上的服务器”,并给他看。

“对,0.0.0.0意味着'绑定所有本地IP',讲课时没有讲到吗?而以127.开头的地址是本地回环IP,所以它们被它绑定是有道理的。”

“是的,但它是如何工作的?大约有1600万个IP是以127.开头的。它不会用所有的人做一个大表,对吗?”

“我猜不是。”他没有答案,于是改变了话题。“那么无论如何,HTTP服务器的情况如何?”这是个反问句,他知道她没有写过一行实际的任务代码。

“是的,是的”,她回答说,已经潜心于另一个实验。

又过了一段时间。蒂姆刚刚完成他的任务,闲来无事地查看他手机上的时间。他考虑回家去睡他那凹凸不平的宿舍床垫。他评估了一下,觉得长椅也差不多舒服,于是把头向后仰,靠在高高的垫子椅背上。

他正盯着天花板,半梦半醒间,莉兹捅了捅他,说:“蒂姆,看看这个”。

她给他看了另一个程序。
# server7.py
import socket

sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
sock.bind(('::', 8080))
sock.listen()
print(sock.accept())

“看看这个。这是一个IPv6服务器。”

蒂姆打了个哈欠,靠了过来。此时,早晨的阳光已经开始透过他们所坐的长椅后面的窗户出现。另外两个学生在凌晨时分已经悄悄地离开了,店里今天的第一位顾客已经到了,正在等待她的外带咖啡。

“冒号是什么来着?”蒂姆问道。

“这是IPv6中八个零的简称,与IPv4中的 0.0.0.0 含义相同”。

“所以这是说要监听所有本地的IPv6 IP?IPv6是这样工作的吗?”

“是的,基本上是这样。”

她输入netcat &quot;::1&quot; 8080 -v,解释说:“::1是IPv6的回环地址。它就像'家'。”

“所以就像常规IP中的127.0.0.1

“IPv4。是的,没错。但要注意这个。根据lsof,我只在IPv6上收听,看到了吗?”利兹运行lsof -i :8080,打印出一行。
COMMAND    PID USER   FD   TYPE   DEVICE SIZE/OFF NODE NAME
python3 455017 liz     3u  IPv6 25152485      0t0  TCP *:http-alt (LISTEN)

“但是",利兹继续说,“我可以通过一个IPv4的IP连接到它。”
netcat 127.0.0.1 8080 -v

“哼”,蒂姆喃喃道。“那另一种方式呢?你能从一个IPv6 IP连接到一个IPv4服务器吗?”

“不,看这个。”

她运行了python3 server6.py,然后netcat &quot;::1&quot; 8080 -v,打印出了
netcat: connect to ::1 port 8080 (tcp) failed: Connection refused

蒂姆问:“如果你试图在IPv6上开始监听8080,而那个IPv4服务器仍在运行,会发生什么?”

利兹给他看,运行python server7.py
Traceback (most recent call last):
File "server7.py", line 4, in <module>
s.bind(('::', 8080))
OSError: [Errno 98] Address already in use

“但看看这个”,她说,拉出了另一个代码列表。
# server8.py
import socket

sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1)
sock.bind(('::', 8080))
sock.listen()
print(sock.accept())

她指着setsockopt一行,解释说:“当我添加这个时,我可以从不同的进程监听同一端口上的IPv6和IPv4。”

她运行python server8.py,然后lsof -i :8080
COMMAND    PID USER   FD   TYPE   DEVICE SIZE/OFF NODE NAME
python3 460409 liz     3u  IPv6 25188010      0t0  TCP *:http-alt (LISTEN)
python3 460813 liz     3u  IPv4 25191765      0t0  TCP *:http-alt (LISTEN)

蒂姆清点了利兹给他看的东西。“所以当你在一个端口上监听时,你实际上是在监听一个端口、一个IP、一个协议、和一个IP版本的组合?”

“是的,除非你在所有的本地IP上监听。如果你在所有IPv6 IP上监听,你也会在所有IPv4 IP上监听,除非你在调用绑定之前特别要求不要这样做。”

“对。因此,操作系统必须有一个从端口和IP对到套接字的哈希图,用于TCP或UDP、IPv4或IPv6的每个组合。”

“到一个套接字的列表”,利兹纠正说。“还记得我是如何监听不止一个的吗?”

“哦,是的。”

“但它还必须处理对所有'家庭'IP的监听,并且能够从一个IPv4 IP上找到一个监听IPv6的套接字。”

“不管怎么说,我得把这个交上去”,蒂姆说着,指了指他手中松散的文件集。"你打算在交稿前完成那个HTTP服务器吗?”

利兹耸耸肩:“我有一个空闲的晚间时间可以利用。”

蒂姆摇了摇头,像极了老父亲般的不赞成。

丽兹翻了个白眼,说:“走吧,蒂姆。”

“下周同一时间?”

“是的。”

原文链接:What does it mean to listen on a port?(翻译:小灰灰)

0 个评论

要回复文章请先登录注册