为什么要考虑缓冲区而不是线路?并且可以fgets()一次读取多行



最近我一直在读Unix网络编程第1卷。在第3.9节中,图3.18上面的最后两段说,这里我引用:

。。。但我们的建议是从数据块而不是行的角度来思考,编写代码来读取数据块,如果需要一行,请检查缓冲区,看看它是否包含该行。

在下一段中,作者给出了一个更具体的例子,我在这里引用:

。。。我们将在第6.3节中看到。像select这样的系统函数仍然不知道readline的内部缓冲区,所以一个不小心编写的程序很容易发现自己在select中等待已经接收并存储在readline的butters中的数据。

在第6.5节中,实际问题是";stdio和select()的混合";,这将使程序,在这里我引用这本书,";容易出错";。但是怎么做呢?

我知道作者稍后在同一节中给出了答案,根据我对这本书的理解,这是因为select()隐藏了数据,因此select()无法知道读取的数据是否被消耗。

答案确实存在,但这里的第一个问题是,我真的很难得到它,我无法想象它会对程序造成什么损害,也许我需要一个遭受问题困扰的演示程序来帮助我理解它

仍然在第6.5节中,作者试图通过给出以下内容来进一步解释这个问题:

。。。考虑一下标准输入中有几行输入可用的情况。select将导致第20行的代码使用fgets读取输入,然后将可用行读取到stdio使用的缓冲区中。但是,fgets只返回一行,并将所有剩余数据留在stdio缓冲区中。。。

;第20行";上面提到的是:

if (Fgets(sendline, MAXLINE, fp) == NULL)

其中sendline是一个char数组,fp是指向FILE的指针。我查阅了Fgets的详细实现,它只是用一些额外的错误处理逻辑包装了Fgets(),而没有其他内容。

我的第二个问题来了,fgets是如何做到的,在这里我再次引用,阅读可用的?我的意思是,我查了fgets的手册页,上面说fgets通常停在第一个换行符上。这难道不意味着fgets只能读取一行吗?更具体地说,如果我在终端中键入一行并按下回车键,那么fgets就会读取这一行。我再次这样做,然后fgets读取下一个新行,并且该点一次只读取一行。

感谢您耐心阅读所有描述,并期待您的回答。

考虑缓冲区而不是线路(当涉及到网络编程时)的主要原因之一是因为TCP是一种流协议,其中数据只是以连接开始,以断开连接结束的字节流。

没有消息边界;行";,除了TCP之上的应用级协议已经决定了什么。

这使得阅读";行";从TCP连接,没有这样的基元函数。您必须使用缓冲区进行读取。由于流式传输和缺乏任何类型的边界,一次接收数据的调用可能会给你的应用程序带来比你要求的更少的数据,而且这可能是部分应用程序级别的消息。或者,您可能会收到多条消息,包括结尾的部分消息。

另一个重要的注意事项是,套接字默认情况下是阻塞,因此没有任何数据可供接收的套接字将导致任何读取调用被阻塞,并等待数据。仅select调用告诉读取调用目前是否不会阻止。如果您在一个循环中多次执行读取调用,那么当要接收的数据耗尽时,它可能(并最终)会阻塞。

所有这些都使得使用像fgets这样的高级函数(当然是在fdopen调用之后)从TCP套接字读取数据变得非常困难,因为如果使用阻塞套接字,它可以随时阻塞。或者,如果您使用非阻塞套接字,并且读取调用返回将要阻塞的失败(是,返回为错误),则它可能会返回失败。

如果使用自己的缓冲,则可以在与readrecv相同的循环中使用select,以确保调用不会被阻塞。或者,如果您使用非阻塞套接字,您可以通过单次读取调用收集数据(并附加到缓冲区),并在收到完整消息时添加检测(通过知道消息长度或检测消息终止符或分隔符,如换行符)。


对于CCD_;多行";,它可能会导致底层读取用多行填充缓冲区,但fgets函数本身只会用一行填充您提供的缓冲区。

fgets永远不会给你多行。

select是一个Linux内核调用。它会告诉您Linux内核是否有您的进程尚未收到的数据。

fgets是一个C库调用。为了减少Linux内核调用的数量(通常较慢),C库使用缓冲。它将尝试从Linux内核读取一大块数据(通常大约4096字节),然后只返回您要求的部分。下次调用它时,它会查看它是否已经读取了您要求的部分,然后就不需要从内核中读取了。例如,如果它能够一次从内核读取5行,那么它将返回第一行,另外4行将存储在C库中,并在接下来的4次调用中返回。

fgets读取5行,返回1,并存储4时,Linux内核将看到所有数据都已读取。它不知道您的程序正在使用C库来读取数据。因此,select会说没有数据可读取,并且您的程序将被困在等待下一行,即使已经有了。


那么你如何解决这个问题呢?你基本上有两种选择:根本不做缓冲,或者自己做缓冲,这样你就可以控制它的工作方式。

选项1意味着您每次读取1个字节,直到获得n,然后停止读取。内核准确地知道你读了多少数据,它将能够准确地告诉你是否还有更多的数据。然而,对每个字节进行内核调用相对较慢(测量它),而且连接另一端的计算机可能会因为根本不发送n而导致程序冻结。

我想指出的是,如果你只是在制作一个原型,那么选项1是完全可行的。它确实有效,只是不太好。如果你试图用选项1来解决问题,你会发现解决问题的唯一方法是选择选项2。

选项2意味着自己进行缓冲。每个连接保留一个4096字节的数组。每当select表示有数据时,您都会尝试尽可能多地填充数组,并检查数组中是否有n。如果是,则处理该行,从数组*中删除该行,然后重复。这意味着您可以最大限度地减少内核调用,而且如果另一台计算机不发送n,您也不会冻结,因为未完成的行只会留在数组中。如果所有4096个字节都已使用,但仍然没有n,则您可以选择将其作为一条大线处理(如果这有意义,例如在聊天程序中),也可以断开连接,因为另一台计算机违反了规则。当然,您可以选择使用大于4096的数字。

*专家额外费用:;从阵列中移除该行";如果你实现一个";循环缓冲器";数据结构。

最新更新