网络编程的常见误区
常见误区 - 不要当个受害者
多年来,我们注意到许多问题是由于对 TCP 协议的普遍困惑而引起的。用知识武装自己,这样您将来就不会浪费时间。
TCP 是一个流
TCP 的模型是单个无限长度的连续流。这个概念非常重要必须理解,并且这个没有正确理解的话会产生一堆的误解。
这到底是什么意思,对开发人员有什么影响?
想象一下,您正在尝试通过 socket 发送一些消息。因此,您可以执行以下操作(使用伪代码):
socket.write("Hi Sandy.");
socket.write("Are you busy tonight?");
数据如何显示在另一端?如果您认为另一端将会分两次单独收到两个独立的句子,那么您就成了一个常见陷阱的受害者!喘气!继续阅读。
TCP 不会将写入视为单独的数据。 TCP 将所有写入视为单个连续流的一部分。因此,当您发出上述写操作时,TCP 会将数据简单地复制到其缓冲区中:
TCP_Buffer = "Hi Sandy.Are you busy tonight?"
然后尽可能快的把数据发出去。为了通过网络发送数据,TCP 和其他网络协议将会把数据分成一小块一小块的,这样就可以通过媒介传输(WiFi,以太网等等)了。为了这么做,TCP 会以它任何最合适的方式来分解数据。以下是一些有关如何分解和发送数据的示例:
- "Hi San" , "dy.Ar" , "e you " , "busy to" , "night?"
- "Hi Sandy.Are you busy" , " tonight?"
- "Hi Sandy.Are you busy tonight?"
上面的示例还演示了数据将如何到达另一端。让我们思考下第一例子。
Sandy 发出了socket.read()
命令,然后等待接收数据。因此她读取到的第一条数据可能是 "Hi San”。Sandy 可能正准备开始处理这个数据。在应用程序处理数据的同时,TCP 流继续接收第二个和第三个数据包。然后 Sandy 发出另一个 socket.read()
命令。并且这次她收到了 "dy.Are you”。
这突出了TCP的连续流性质。在使用 TCP 协议的 API 时绝没有数据包和数据分离的这些概念。
但这不是主要缺点吗?所有其他使用 TCP 的协议如何工作的?
HTTP 是一个很好的例子,因为它非常简单,而且因为大多数人以前都看过它。当客户端连接服务器并发送请求时,它以非常特定的方式进行。它发送一个 HTTP header,并且标头的每一行都以 CRLF(回车,换行)终止。所以像这样:
GET /page.html HTTP/1.1
Host: google.com
此外,HTTP header 的结尾由两个连续的 CRLF 作为标记。由于协议指定了终止符,因此很容易从 TCP socket 读取数据,直到到达终结符为止。
HTTP/1.1 200 OK
Content-Length: 216
{ Exactly 216 bytes of data go here }
HTTP 协议让 TCP 使用起来方便了不少。读取数据,直到获得连续的 CRLF。这就是你的 header。然后从 header 里解析出 content-length
,现在你就可以直接读取这个长度的字节数据了。
回到我们之前的例子,我们可以简单地为我们的消息使用指定的终止符:
socket.write("Hi Sandy.\n");
socket.write("Are you busy tonight?\n");
如果 Sandy 用的是 AsyncSocket 那他会非常幸运。因为 AsyncSocket 提供了非常简单方便的读取方法来指定终止符。AsyncSocket 为你做了剩下的事情,你会收到两个单独的语句。
写入数据
当你给 TCP 写入数据的时候发生了什么? 写完数据后,是否意味着另一方已收到该数据?我们至少可以假设计算机已经发送了数据吗?答案是否定的。
回想一下两件事:
- 必须将所有发送和接收的数据分解成小段,以便通过网络发送。
- TCP 处理许多复杂的问题,例如重新发送丢失的数据包,提供有序的传输,以便信息按正确的顺序到达。
因此,当你发出写操作时,数据仅被复制到 OS 网络堆栈中的基础缓冲区中。这时候 TCP 开始施展它的魔法了,处理之前提到的那一堆酷酷的东西:
- 将数据分解成小块,以便可以通过网络发送
- 保证丢失的数据得到正确的重发
- 保证数据按正确的顺序送达
- 监控网络总的拥堵情况
- 采用各种各样的算法来尽快处理这些任务
因此当你发出命令“写入数据”时,操作系统会反馈说“我已经拿到你的数据了,我将会尽我最大的能力把数据发到目的地”。
但是……我怎么知道对方何时接收到我的数据?
这正是大多数人遇到问题的地方。可以这样来理解这个问题:
假设您想发送一封信给朋友。不是电子邮件,而是传统的邮政邮件。你知道,通过邮局寄送。因此,您写了封信并将其放在邮箱中。邮递员随后过来收走了邮件。此时,您可以放心,邮局将尽一切努力将这封信发送给您的朋友。但是,您如何确定您的朋友是否收到了这封信?我想,如果这封信回来时贴有“退回发件人”字样,则可以确定您的朋友没有收到。但是,如果不退回怎么办?能知道它已经进入您朋友的邮箱了吗?(假设这是一封非常非常重要的信。)答案是不知道。也许它永远不会离开邮箱。也许是他的室友捡起它,然后不小心把它扔了。如果室友负责点并将这封信留在您的朋友的桌子上?够了吗?如果您的朋友正在度假,而您的信件却被一堆垃圾邮件掩埋弄丢了怎么办?因此,真正知道您的朋友是否收到这封信的唯一方法是您何时收到他们的回复。
这是 stocket 的一个很好的隐喻。当你将数据写入 stocket 时,就像将信件放入邮箱一样。操作系统就像是来收信的本地邮递员。将信件路由到目的地的巨型邮局系统就像网络一样。邮递员将你的信件投到您的朋友邮箱中,就像你朋友计算机上的操作系统一样。然后由你朋友计算机上的应用程序从OS读取数据并进行处理(从邮箱中取出信件,然后进行实际读取)。
那么我怎么知道远程目的地何时接收到我的数据?这不是TCP可以告诉您的。充其量只能告诉您这封信已经寄到他们的邮箱了。它无法告诉你的应用程序是否已读取并处理了该数据。也许远程应用程序崩溃了。或者,也许远程用户在有机会读取数据之前退出了该应用程序。也许远程用户遇到了断电。长话短说,如果需要的话应该从应用层来回答这个问题。