这个故事探讨了计算机网络中的一些概念,灵感来自迈克尔·尼尔森的发现小说。代码样本也可以在本回购协议中找到。摘录在Debian Linux上使用openbsd风格的netcat;behviour和IPv6支持可能因版本而异。
学生会大楼的角落里有一家咖啡馆,咖啡馆的角落里有两个学生。莉兹在她搬去上大学时哥哥送给她的那本破旧的旧版MacBook的键盘上不停地敲着。在她左边的长椅上,蒂姆在一个线圈装订的笔记本上潦草地写下了方程式。在他们之间是一杯半空的室温咖啡,莉兹偶尔啜饮以保持清醒。
在房间的另一边,咖啡师从他的手机里抬起头来,环顾着商店。他戴着一个耳机,另一个挂着,手机播放电影课指定的观看。这家学生经营的商店有一条不成文的规定,夜班员工可以利用顾客之间的长时间差距来补习功课。除了蒂姆和丽兹,还有两名男学生独自坐着,一连几个小时都在盯着笔记本电脑。否则,商店就空了。
提姆在笔画中间停下来,把笔记本从笔记本上撕下来,揉成一团,放在一堆其他皱巴巴的纸上。
蒂姆打了个哈欠,又开始在新的一页顶端乱涂乱画,但莉兹打断了他。
“什么?”,蒂姆回答说,他夸大了自己在开始写作时被打断的愤怒。
“我必须为网络写这个web服务器的东西”,缩写为Computer Networks 201,这是蒂姆上学期上的一门课。
“80端口”,蒂姆自信地回答,希望通过抢先回答她的问题来缩短对话。
“实际上,我们应该在8080上收听,这样它就可以在没有root的情况下运行,但这不是重点。”
“这意味着其他进程可以在该端口上连接到它。”蒂姆对这个问题看起来很困惑。
“我猜操作系统有一个很大的端口表和监听它们的进程。当你绑定到一个端口时,它会在该表中放置一个指向套接字的指针。”
两人回到各自独立的工作岗位。沉默了一段时间后,蒂姆得意洋洋地喃喃说:“是的!”他屏住呼吸,在一张打印纸上划掉一个数字。他终于找到了一个他为微积分作业苦苦挣扎的证据。
“嘿,蒂姆,看,我在同时运行绑定到同一端口的两个进程。”
然后,她向他展示了这两个程序在各自的终端窗口中运行,通过一个外壳连接到大学的cslab3 Debian服务器。
蒂姆把笔记本电脑转向他自己。他打开第三个终端,停顿片刻,搜索他疲惫的大脑,然后输入netcat 127.0.0.1 8080。
netcat运行并立即退出。在另一个终端窗口中,运行的python server1。py程序退出,打印:
“好的,服务器绑定到一个端口,接受第一个连接到它的套接字,然后退出。我明白了,所以它打印的元组是接受调用的结果,然后它立即退出。但是现在……”将鼠标光标移动到显示server2的编辑器上。py“……这个人在听吗?”
他像以前一样在同一个终端上再次运行netcat 127.0.0.1 8080-v,并打印出:
“看,”他说,“你的代码中有一个bug。server2仍在运行,但你从未调用listen。它实际上对8080端口没有任何作用。”
她在netcat命令的末尾添加了一个-u,并点击回车键。这一次,它不会给出错误或立即退出,而是等待键盘输入。蒂姆很快就认为自己的代码有问题,这让她很恼火,她轻拍蒂米,知道这个绰号让他很恼火。
蒂姆意识到莉兹试图与他作对,但却置之不理,不想让她满足于从他身上得到升职。他朝键盘做手势。Liz朝自己的方向转动笔记本电脑,输入man netcat,拿出netcat的手册,其中将该工具描述为“TCP/IP瑞士军刀”。他向下滚动到-u标志,该标志在文档中被简要描述为“UDP模式”。
“啊,”他回忆道。“我明白了。server1通过TCP进行监听,server2通过UDP进行监听。这一定是SOCK_DGRAM的意思。所以它们是不同的协议。我猜操作系统对每个端口都有一个单独的端口表。直到后来我才认为网络覆盖了UDP。”
“当然。你怎么会有时间提前阅读,却没有时间在任务到期前完成这些任务?”
在莉兹打破沉默之前,他们又沉默了几分钟。
“嘿,蒂姆,看看这个。我可以用两个进程在同一个端口上监听,即使它们都是TCP。”
蒂姆从工作中抬起头来。这次Liz在屏幕上只有一个Python程序,并且在两个终端上运行它:
Liz解释说:“看,这个命令显示了监听端口的进程。”。她输入lsof-i:8080,然后点击return。
>;lsof-i:8080命令PID用户FD类型设备大小/非节点名称python3 174265 liz 3u IPv4 23850797 0t0 TCP本地主机:http alt(侦听)python3 174337 liz 3u IPv4 23853188 0t0 TCP本地主机:http alt(侦听)
“当你连接到它时会发生什么?”,蒂姆问道,这次他的声音里带着一点真正的好奇。
Liz运行netcat localhost 8080一次,其中一个服务器进程退出,而另一个保持运行。然后她再次运行它,另一个进程退出。
蒂姆的注意力转向了代码,他把手指放在屏幕附近阅读代码。莉兹讨厌脏兮兮的屏幕,她说:“别紧张!”把他的手推回。“我不会碰它,”他抗议道。他夸张地表示要将手保持在安全距离后,他指着setsockopt线问道:“嘿,这是什么来源?”
“这是设置一个套接字选项以允许重新使用端口”
“我也没有,”她停下来想一想。“因此,操作系统不能只有一个端口到套接字的表,它必须是一个端口到套接字列表的表。然后是第二个用于UDP的表。对于其他协议,可能更多。”
蒂姆回到作业中,几分钟后,他划掉了另一个问题。他已经接近尾声了,他的姿态稍微放松了一点。莉兹把笔记本电脑向他倾斜,说“看看这个”。她给他看了两个节目。
“哦,那么你是在同一个端口上监听,但有两个不同的IP。这样行吗?”
蒂姆沉思着。“让我们看看。操作系统必须有一个从每个端口和IP组合到一个套接字的表。实际上,有两个:一个用于TCP,另一个用于UDP。”
“是的,”莉兹点点头。“它可以是多个插座,而不是一个插座。但请注意这个。”她将服务器代码中的IP更改为0.0.0.0。
“现在,当我运行绑定到127.0.0.2的服务器时,我明白了,”她继续说。
回溯(最后一次调用):文件";服务器5。py";,第4行,in<;模块>;s、 bind(';127.0.0.2';8080))操作错误:[Errno 99]无法分配请求的地址
“但是,”她总结道,“如果我运行netcat 127.0.0.2 8080,它会连接到0.0.0.0上的服务器”,并显示给他看。
“是的,0.0.0.0意味着‘绑定到所有本地IP’,不是吗?以及以127开头的地址。都是本地环回IP,所以它们被它绑定是有道理的。”
“是的,但它是如何工作的呢?大约有1600万个IP以127开头。这并不是一张大桌子,对吧?”
“我想不是。”他没有答案,于是改变了话题。“不管怎样,HTTP服务器怎么样了?”这是修辞,他知道她没有写一行实际的作业代码。
更多的时间过去了。蒂姆刚完成作业,懒懒地在手机上查时间。他考虑回家睡他那笨重的宿舍床垫。他估计长凳也差不多很舒服,然后把头向后靠在高高的坐垫靠背上。
他盯着天花板,半睡半醒,丽兹戳了戳他说:“蒂姆,看看这个。”
蒂姆打了个哈欠,靠了进去。现在,晨曦开始从他们坐的长凳后面的窗户里透出来。另外两名学生在凌晨悄悄离开,商店当天的第一位顾客已经到了,正在等她去喝咖啡。
“它是IPv6中八个零的缩写,与IPv4中的0.0.0.0具有相同的含义”
“这意味着要监听所有本地IPv6 IP?IPv6就是这样工作的吗?”
她输入netcat";::1" 8080-v,解释:“1是IPv6中的环回地址。它就像‘家’。”
“IPv4。是的,没错。但是看这个。根据lsof,我只在监听IPv6,明白吗?”Liz运行lsof-i:8080,打印一行。
命令PID用户FD类型设备大小/关闭节点名称Python3 455017 liz 3u IPv6 25152485 0t0 TCP*:http alt(侦听)
“但是,”利兹继续说,“我可以通过IPv4 IP连接到它。”
“嗯,”蒂姆低声说。“另一种方式呢?你能从IPv6 IP连接到IPv4服务器吗?”
Tim问:“如果在IPv4服务器仍在运行的情况下,尝试在IPv6上的8080上开始监听,会发生什么?”
回溯(最后一次调用):文件";服务器7。py";,第4行,in<;模块>;s、 bind(';:';,8080))OSError:[Errno 98]地址已在使用中
她指着setsockopt行解释说,“当我添加这个时,我可以在同一端口上从不同进程监听IPv6和IPv4。”
命令PID用户FD类型设备大小/脱离节点名称python3 460409 liz 3u IPv6 25188010 0t0 TCP*:http alt(侦听)python3 460813 liz 3u IPv4 25191765 0t0 TCP*:http alt(侦听)
蒂姆清点了莉兹给他看的东西。“所以当你监听一个端口时,你实际上是在监听一个端口、一个IP、一个协议和一个IP版本的组合?”
“是的,除非你在所有本地IP上监听。如果你在所有IPv6 IP上监听,你也会在所有IPv4 IP上监听,除非你在调用bind之前明确要求不要这样做。”
“是的。因此,对于TCP或UDP、IPv4或IPv6的每种组合,操作系统必须具有从端口和IP对到套接字的哈希映射。”
“到插座列表”,莉兹纠正道。“还记得我怎么能听不止一个吗?”
“但它还必须处理所有‘主’IP上的监听,并能够找到从IPv4 IP在IPv6上监听的套接字。”
“不管怎样,我必须把这个交上来,”蒂姆指着手中松散的文件说。“你要在HTTP服务器到期之前完成它吗?”