▲ 点击上方“分布式实验室”关注公众号
回复“1”抽取纸质技术书
今天这篇文章,想用一个故事和你讲讲端口监听是怎么回事。耐心往下看。
在学生会大楼的角落里,有一家咖啡店,在咖啡店的角落里有两个学生。利兹敲打着她哥哥在她搬到大学时给她的那台破旧的手摇MacBook的键盘。在她左边的长椅上,蒂姆在一个装订成卷的笔记本上写着公式。他们之间有一杯半空的常温咖啡,莉兹不时地喝上一口以保持清醒。
蒂姆写到一半就停笔了,把这张纸从笔记本上撕下来,揉成一团,放在其他揉成一团的小纸片旁边。
“Shit,现在几点了?”他问到。
利兹看了看她笔记本上的时钟,“刚过两点”。
蒂姆打了个哈欠,又开始在新的一页上面涂鸦,但利兹打断了他。
“蒂姆”
“什么?!”,蒂姆回答说,夸张地表达了他对刚开始写就被打断的恼怒。
“在一个端口上监听是什么意思?”
“呃嗯……”
“我必须为net写这个网络服务器的东西”,net是Computer Networks 201的缩写,这是蒂姆在上学期上的一门课。
“是的,我记得那门课。”
“所以我在一个端口上监听连接。”
“80端口”,蒂姆自信地回答,希望通过抢先回答她的问题来缩短谈话时间。
“实际上,我们应该监听8080,这样它就可以在没有root的情况下运行,但这不是重点。”
“哦,对了。那是什么?”
“好吧,监听一个端口是什么意思?”
“它意味着其他进程可以在该端口上连接到它。”蒂姆对这个问题显得很困惑。
“是的,我知道这一点,但怎么做?”
蒂姆考虑了几秒钟才回答。
“我猜操作系统有一个大的端口表,以及在这些端口上监听的进程。当你绑定到一个端口时,它就会在该表中放一个指向你的套接字的指针。”
“是的,我猜。”利兹说,语气中带着犹豫和不满意。
两人回到了他们各自的工作中。沉默了一段时间后,蒂姆小声嘀咕了一句胜利的 “是的!”,并在一张打印的纸上划掉了一个数字。他终于找到了他在微积分作业中一直纠结的一个证明。
利兹趁机再次引起他的注意。
“嘿,蒂姆,看,我正在同时运行绑定在同一端口的两个进程。”
她调整了两个包含Python代码的窗口的大小。
# server1.py
import socketsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind(('127.0.0.1', 8080))
sock.listen()
print(sock.accept())
然后在它旁边是另一个程序:
# server2.py
import socketsock = 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 socketsock = 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 socketsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind(('127.0.0.2', 8080))
sock.listen()
print(sock.accept())
在它的旁边。
# server5.py
import socketsock = 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 socketsock 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 socketsock = 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 "::1" 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 "::1" 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 socketsock = 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服务器吗?”
利兹耸耸肩:“我有一个空闲的晚间时间可以利用。”
蒂姆摇了摇头,像极了老父亲般的不赞成。
丽兹翻了个白眼,说:“走吧,蒂姆。”
“下周同一时间?”
“是的。”
推荐阅读:《案例:vivo基于Java技术栈的实时监控系统》
点击下方卡片关注分布式实验室,和我们一起
关注分布式最佳实践
▲ 点击上方卡片关注分布式实验室,掌握前沿分布式技术
新的一年,想一起学习K8s、考CKA证书吗?来,这里有最好的学习方案,线下3天封闭式培训,15人小班课,考不过免费复训。Kubernetes实战班上海站3月25日开班,扫描下方二维码了解详情。