C语言中的套接字是如何工作的?

21

我对C语言中的socket编程有些困惑。

你创建一个socket,将其绑定到接口和IP地址并让其监听。我在一些网站上找到了一些相关资源,理解得很好。特别是,我发现一篇文章《Unix系统下的网络编程》非常有用。

让我困惑的是套接字中数据到达的时间。

如何确定数据包何时到达以及数据包的大小?需要自己完成所有繁重的工作吗?

我的基本假设是,数据包的长度可以变化,因此一旦二进制数据开始出现在socket中,你如何开始从中构建数据包呢?

4个回答

17

简短回答是你需要自己承担所有的重活。你可以被通知数据可读,但你不会知道有多少字节数可用。在大部分使用可变长度包的IP协议中,将会在包前面添加一个已知固定长度的头部。这个头部将包含包的长度。你先读取头部,获得包的长度,然后再读取整个包。你会一直重复这个模式(先读头部,再读包)直到通信完成。

当从套接字读取数据时,你请求特定数量的字节。读取调用可能会阻塞直至请求的字节数已被读取,但它可能会返回少于所请求的字节数。如果发生这种情况,你只需重试读取并请求剩余的字节即可。

下面是一个典型的 C 函数用于从套接字中读取指定数量的字节:

/* buffer points to memory block that is bigger than the number of bytes to be read */
/* socket is open socket that is connected to a sender */
/* bytesToRead is the number of bytes expected from the sender */
/* bytesRead is a pointer to a integer variable that will hold the number of bytes */
/*           actually received from the sender. */
/* The function returns either the number of bytes read, */
/*                             0 if the socket was closed by the sender, and */
/*                            -1 if an error occurred while reading from the socket */
int readBytes(int socket, char *buffer, int bytesToRead, int *bytesRead)
{
    *bytesRead = 0;
    while(*bytesRead < bytesToRead)
    {
        int ret = read(socket, buffer + *bytesRead, bytesToRead - *bytesRead);
        if(ret <= 0)
        {
           /* either connection was closed or an error occurred */
           return ret;
        }
        else
        {
           *bytesRead += ret;
        }
    }
    return *bytesRead;
}

在关闭连接后,早期返回应该是“return bytesRead;”而不是“return ret;”,因为我假定readBytes函数应返回实际读取的字节数。如果出现错误,你可以定义它返回非正整数,但你也可以通过检查readBytes返回的字节数是否与请求的不同来检测是否发生了错误。 - A. Levy
在C编程中,使用负值来标记错误状态是常见的做法,应尽可能遵循此规则,以防止API用户混淆。 - Guss
我修改了我的示例函数,即使在读取期间出现错误,也会返回读取的字节数。需要注意的一点是,在套接字中,返回零的读取表示连接已关闭,可能并不一定是错误。 - dfjacobs

13

因此,你的问题的答案很大程度上取决于你使用UDP还是TCP作为传输协议。

对于UDP,情况变得简单了很多,你可以调用recv/recvfrom/recvmsg并指定你需要的数据包大小(你可能会从源头发送固定长度的数据包),并假设如果有可用数据,则它以数据包长度的倍数存在。(也就是说,你使用发送端数据包的大小调用recv*,然后就完成了)

对于TCP,情况变得更加有趣——为了说明这一点,我将假设你已经知道如何使用socket()、bind()、listen()和accept(),后者是获取新建连接的文件描述符(FD)的方法。

有两种方式可以对套接字进行I/O操作:阻塞和非阻塞。在阻塞模式下,你调用read(fd, buf, N),并且read函数等待直到你将N个字节读入buf中。而在非阻塞模式下,你必须使用select()或poll()检查FD是否可读,并且只有在FD可读时才能进行read()操作。

当处理基于TCP的连接时,操作系统不关心数据包的大小,因为它被视为一个连续的数据流,而不是独立的数据包大小块。

如果你的应用程序使用"packets"(打包或解包的数据结构),你应该能够使用适当的大小参数调用read(),并一次从套接字中读取整个数据结构。唯一需要注意的限制是要记得正确地处理发送数据的字节顺序,以防源系统和目标系统具有不同的字节顺序。这适用于UDP和TCP。

就*NIX套接字编程而言,我强烈推荐W. Richard Stevens的《Unix网络编程,第1卷》(UNPv1)和《Unix环境高级编程》(APUE)。前者是一本关于基于网络的编程的巨著,无论传输方式如何,后者是一本涵盖*NIX编程的好书。此外,还可以查找《TCP/IP Illustrated》第1卷和第2卷。


3
当您在套接字上执行读取操作时,您会告诉它最多要读取多少字节,但如果没有那么多字节,它会给您它所拥有的任意数量。因此,您需要设计协议以便知道是否存在部分数据包。例如,在之前发送可变长度二进制数据时,我会在开头放置一个整数,表示期望收到的字节数。我会执行读取操作,请求比协议中最大可能的数据包更大的字节数,然后将第一个整数与我收到的字节数进行比较,根据情况处理或尝试更多读取,直到获取完整的数据包。

1
套接字在比原始数据包更高的级别上运行 - 就像您可以从中读取/写入的文件一样。此外,当您尝试从套接字读取时,操作系统将阻止(暂停)您的进程,直到它有数据来满足请求。

1
只有在套接字未设置为非阻塞时才是真实的。 - Dominic Eidson

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接