文章目录
1. 前言
The GNU C Library Reference Manual for version 2.35
2. 套接字
Sockets
本章描述了使用套接字进行进程间通信的 GNU 工具。
套接字是一种通用的进程间通信通道。像管道一样,套接字表示为文件描述符。与管道不同,套接字支持不相关进程之间的通信,甚至支持运行在通过网络通信的不同机器上的进程之间的通信。套接字是与其他机器通信的主要方式;telnet、rlogin、ftp、talk 和其他熟悉的网络程序都使用套接字。
并非所有操作系统都支持套接字。在 GNU C 库中,无论操作系统如何,头文件 sys/socket.h 都存在,并且套接字函数始终存在,但如果系统不真正支持套接字,这些函数总是会失败。
不完整:我们目前没有记录广播消息或配置 Internet 接口的设施。到目前为止,也没有记录与 IPv6 相关的可重入函数和一些较新的函数。
2.1. 套接字概念
Socket Concepts
创建套接字时,必须指定要使用的通信样式以及应实现它的协议类型。套接字的通信方式定义了在套接字上发送和接收数据的用户级语义。选择一种沟通方式指定了对以下问题的答案:
-
数据传输的单位是什么?一些通信方式将数据视为没有更大结构的字节序列;其他人将字节分组为记录(在此上下文中称为数据包)。
-
正常运行时数据会丢失吗?一些通信方式保证所有发送的数据都按照发送的顺序到达(除非系统或网络崩溃);其他样式偶尔会丢失数据作为正常操作的一部分,并且有时可能会多次发送数据包或以错误的顺序发送数据包。
设计程序以使用不可靠的通信方式通常涉及采取预防措施以检测丢失或错误排序的数据包并根据需要重新传输数据。
-
是否完全与一个合作伙伴进行交流?一些通信方式就像电话一样——你用一个远程套接字建立连接,然后自由地交换数据。其他样式就像邮寄信件——您为发送的每封邮件指定一个目标地址。
您还必须选择一个命名空间来命名套接字。套接字名称(“地址”)仅在特定命名空间的上下文中才有意义。事实上,即使是用于套接字名称的数据类型也可能取决于命名空间。命名空间也称为“域”,但我们避免使用该词,因为它可能与同一术语的其他用法混淆。每个命名空间都有一个以“PF_”开头的符号名称。以“AF_”开头的相应符号名称指定该命名空间的地址格式。
最后,您必须选择执行通信的协议。协议决定了使用什么底层机制来传输和接收数据。每个协议都适用于特定的命名空间和通信方式;因此,命名空间有时被称为协议族,这就是命名空间名称以“PF_”开头的原因。
协议的规则适用于两个程序之间的数据传递,可能在不同的计算机上;这些规则大部分由操作系统处理,您无需了解它们。关于协议,您需要了解的是:
- 为了在两个套接字之间进行通信,它们必须指定相同的协议。
- 每个协议对于特定的样式/命名空间组合都是有意义的,并且不能与不适当的组合一起使用。例如,TCP 协议仅适用于通信的字节流样式和 Internet 名称空间。
- 对于样式和命名空间的每种组合,都有一个默认协议,您可以通过将 0 指定为协议号来请求该协议。这就是你通常应该做的——使用默认值。
在整个以下描述中,需要在不同位置使用变量/参数来表示尺寸。麻烦就从这里开始了。在第一个实现中,这些变量的类型只是 int。在当时的大多数机器上,int 是 32 位宽的,这创建了一个需要 32 位变量的事实上的标准。这很重要,因为对这种类型的变量的引用被传递给内核。
然后 POSIX 人来了,用“所有大小值都是 size_t 类型”的字样统一了接口。在 64 位机器上 size_t 是 64 位宽,因此不再可能指向变量。
Unix98 规范通过引入 socklen_t 类型提供了解决方案。这种类型用于 POSIX 更改为使用 size_t 的所有情况。这种类型的唯一要求是它是至少 32 位的无符号类型。因此,需要传递对 32 位变量的引用的实现可以像使用 64 位值的实现一样快乐。
2.2. 通信方式
Communication Styles
GNU C 库包括对几种不同类型的套接字的支持,每种套接字具有不同的特性。本节介绍支持的套接字类型。此处列出的符号常量在 sys/socket.h 中定义。
宏:int SOCK_STREAM
SOCK_STREAM 样式就像一个管道(请参阅管道和 FIFO)。它通过与特定远程套接字的连接进行操作,并以字节流的形式可靠地传输数据。
使用带有连接的套接字中详细介绍了这种样式的使用。
宏:int SOCK_DGRAM
SOCK_DGRAM 样式用于不可靠地发送单独寻址的数据包。它与 SOCK_STREAM 截然相反。
每次您将数据写入此类套接字时,该数据都会成为一个数据包。由于 SOCK_DGRAM 套接字没有连接,因此您必须为每个数据包指定接收地址。
系统对您传输数据的请求做出的唯一保证是它会尽力交付您发送的每个数据包。第4、5个包失败后,第6个包可能成功;第七包可能在第六包之前到达,也可能在第六包之后第二次到达。
SOCK_DGRAM 的典型用途是,如果在合理的时间内没有看到响应,则可以简单地重新发送数据包。
有关如何使用数据报套接字的详细信息,请参阅数据报套接字操作。
宏:int SOCK_RAW
这种风格提供对低级网络协议和接口的访问。普通用户程序通常不需要使用这种风格。
2.3. 套接字地址
Socket Addresses
套接字的名称通常称为地址。处理套接字地址的函数和符号的命名不一致,有时使用术语“名称”,有时使用“地址”。您可以将这些术语视为涉及套接字的同义词。
使用套接字函数新创建的套接字没有地址。只有给它一个地址,其他进程才能找到它进行通信。我们称之为绑定地址到套接字,方法是使用绑定函数。
如果其他进程要找到它并开始与之通信,您只需要关心套接字的地址。您可以为其他套接字指定一个地址,但这通常是没有意义的;第一次从套接字发送数据或使用它来启动连接时,如果您没有指定地址,系统会自动分配一个地址。
有时客户端需要指定地址,因为服务器根据地址进行区分;例如,rsh 和 rlogin 协议查看客户端的套接字地址,如果它小于 IPPORT_RESERVED(请参阅 Internet 端口),则仅绕过密码检查。
套接字地址的详细信息取决于您使用的命名空间。有关特定信息,请参阅本地命名空间或 Internet 命名空间。
无论命名空间如何,您都使用相同的函数 bind 和 getsockname 来设置和检查套接字的地址。这些函数使用虚假数据类型 struct sockaddr * 来接受地址。在实践中,地址存在于与您正在使用的地址格式相适应的某种其他数据类型的结构中,但是当您将其传递给绑定时,您将其地址转换为 struct sockaddr *。
2.3.1. 地址格式
Address Formats
函数 bind 和 getsockname 使用通用数据类型 struct sockaddr * 来表示指向套接字地址的指针。您不能有效地使用此数据类型来解释地址或构造地址;为此,您必须为套接字的命名空间使用正确的数据类型。
因此,通常的做法是构造一个适当的命名空间特定类型的地址,然后在调用 bind 或 getsockname 时将指针转换为 struct sockaddr *。
您可以从 struct sockaddr 数据类型获得的一条信息是地址格式指示符。这会告诉您使用哪种数据类型来完全理解地址。
本节中的符号在头文件 sys/socket.h 中定义。
数据类型:struct sockaddr
struct sockaddr 类型本身具有以下成员:
short int sa_family
这是这个地址的地址格式的代码。它标识了随后的数据格式。
char sa_data[14]
这是实际的套接字地址数据,它与格式有关。它的长度也取决于格式,很可能超过 14。sa_data 的长度 14 本质上是任意的。
每种地址格式都有一个以“AF_”开头的符号名称。它们中的每一个都对应一个“PF_”符号,该符号指定相应的命名空间。以下是地址格式名称列表:
AF_LOCAL
这指定了与本地命名空间一起使用的地址格式。(PF_LOCAL 是该命名空间的名称。)有关此地址格式的信息,请参阅本地命名空间的详细信息。
AF_UNIX
这是 AF_LOCAL 的同义词。尽管 POSIX.1g 强制要求 AF_LOCAL,但 AF_UNIX 可移植到更多系统。AF_UNIX 是源自 BSD 的传统名称,因此即使大多数 POSIX 系统都支持它。它也是 Unix98 规范中的首选名称。(PF_UNIX 与 PF_LOCAL 也是如此)。
AF_FILE
这是 AF_LOCAL 的另一个同义词,用于兼容性。(PF_FILE 同样是 PF_LOCAL 的同义词。)
AF_INET
这指定了与 Internet 命名空间一起使用的地址格式。(PF_INET 是该命名空间的名称。)请参阅 Internet 套接字地址格式。
AF_INET6
这类似于 AF_INET,但指的是 IPv6 协议。(PF_INET6 是相应命名空间的名称。)
AF_UNSPEC
这指定了没有特定的地址格式。它仅在极少数情况下使用,例如清除“已连接”数据报套接字的默认目标地址。请参阅发送数据报。
为了完整起见,存在相应的命名空间指示符符号 PF_UNSPEC,但没有理由在程序中使用它。
sys/socket.h 为许多不同类型的网络定义了以“AF_”开头的符号,其中大部分或全部并未实际实现。当我们收到有关如何使用它们的信息时,我们将记录那些真正有效的方法。
2.3.2. 设置套接字地址
Setting the Address of a Socket
使用 bind 函数将地址分配给套接字。bind 的原型在头文件 sys/socket.h 中。有关使用示例,请参阅本地命名空间套接字示例,或参阅 Internet 套接字示例。
函数:int bind(int socket, struct sockaddr *addr, socklen_t length)
Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.
bind 函数为套接字套接字分配一个地址。addr 和 length 参数指定地址;地址的详细格式取决于命名空间。地址的第一部分始终是格式指示符,它指定一个命名空间,并表示该地址采用该命名空间的格式。
成功时返回值为 0,失败时返回值为 -1。为此函数定义了以下 errno 错误条件:
EBADF
套接字参数不是有效的文件描述符。
ENOTSOCK
描述符套接字不是套接字。
EADDRNOTAVAIL
指定的地址在本机上不可用。
EADDRINUSE
其他一些套接字已经在使用指定的地址。
EINVAL
套接字套接字已经有一个地址。
EACCES
您无权访问请求的地址。(在 Internet 域中,只允许超级用户在 0 到 IPPORT_RESERVED 减一的范围内指定端口号;请参阅 Internet 端口。)
根据套接字的特定命名空间,可能会出现其他条件。
2.3.3. 读取套接字地址
Reading the Address of a Socket
使用函数 getsockname 来检查 Internet 套接字的地址。这个函数的原型在头文件 sys/socket.h 中。
函数:int getsockname (int socket, struct sockaddr *addr, socklen_t *length-ptr)
Preliminary: | MT-Safe | AS-Safe | AC-Safe mem/hurd | See POSIX Safety Concepts.
getsockname 函数在 addr 和 length-ptr 参数指定的位置返回有关套接字地址的信息。请注意,length-ptr 是一个指针;您应该将其初始化为 addr 的分配大小,并在返回时包含地址数据的实际大小。
地址数据的格式取决于套接字命名空间。对于给定的命名空间,信息的长度通常是固定的,因此通常您可以准确地知道需要多少空间并且可以提供那么多。通常的做法是使用套接字命名空间的正确数据类型为值分配一个位置,然后将其地址转换为 struct sockaddr * 以将其传递给getsockname。
返回值为 0 表示成功,-1 表示错误。为此函数定义了以下 errno 错误条件:
EBADF
套接字参数不是有效的文件描述符。
ENOTSOCK
描述符套接字不是套接字。
ENOBUFS
没有足够的内部缓冲区可用于该操作。
您无法读取文件命名空间中的套接字地址。这与系统的其余部分一致;一般来说,没有办法从文件的描述符中找到文件名。
2.4. 接口命名
Interface Naming
每个网络接口都有一个名称。这通常由几个与接口类型相关的字母组成,如果该类型的接口不止一个,则后面可能跟一个数字。示例可能是 lo(环回接口)和 eth0(第一个以太网接口)。
尽管这样的名称对人类来说很方便,但在程序需要引用接口时必须使用它们会很笨拙。在这种情况下,接口由其索引引用,该索引是一个任意分配的小正整数。
在头文件 net/if.h 中声明了以下函数、常量和数据类型。
常数:size_t IFNAMSIZ
此常量定义了保存接口名称所需的最大缓冲区大小,包括其终止的零字节。
函数:unsigned int if_nametoindex (const char *ifname)
Preliminary: | MT-Safe | AS-Unsafe lock | AC-Unsafe lock fd | See POSIX Safety Concepts.
此函数产生对应于特定名称的接口索引。如果不存在具有给定名称的接口,则返回 0。
函数:char * if_indextoname (unsigned int ifindex, char *ifname)
Preliminary: | MT-Safe | AS-Unsafe lock | AC-Unsafe lock fd | See POSIX Safety Concepts.
此函数将接口索引映射到其对应的名称。返回的名称放置在 ifname 指向的缓冲区中,该缓冲区的长度必须至少为 IFNAMSIZ 字节。如果索引无效,则函数的返回值为空指针,否则为 ifname。
数据类型:struct if_nameindex
此数据类型用于保存有关单个接口的信息。它有以下成员:
unsigned int if_index;
这是接口索引。
char *if_name
这是以 null 结尾的索引名称。
函数:struct if_nameindex * if_nameindex (void)
Preliminary: | MT-Safe | AS-Unsafe heap lock/hurd | AC-Unsafe lock/hurd fd mem | See POSIX Safety Concepts.
此函数返回一个 if_nameindex 结构数组,每个接口对应一个。列表的结尾由接口为 0 和空名称指针的结构指示。如果发生错误,该函数返回一个空指针。
使用后必须使用 if_freenameindex 释放返回的结构。
函数:void if_freenameindex (struct if_nameindex *ptr)
Preliminary: | MT-Safe | AS-Unsafe heap | AC-Unsafe mem | See POSIX Safety Concepts.
此函数释放先前调用 if_nameindex 返回的结构。
2.5. 本地命名空间
The Local Namespace
本节介绍本地命名空间的详细信息,其符号名称(创建套接字时需要)为 PF_LOCAL。本地命名空间也称为“Unix 域套接字”。另一个名称是文件命名空间,因为套接字地址通常实现为文件名。
2.5.1. 本地命名空间概念
Local Namespace Concepts
在本地命名空间中,套接字地址是文件名。您可以指定任何您想要的文件名作为套接字的地址,但您必须对包含它的目录具有写权限。通常将这些文件放在 /tmp 目录中。
本地命名空间的一个特点是该名称仅在打开连接时使用;一旦打开该地址就没有意义并且可能不存在。
另一个特点是你不能从另一台机器连接到这样的套接字——即使另一台机器共享包含套接字名称的文件系统也是如此。您可以在目录列表中看到套接字,但连接到它永远不会成功。一些程序利用了这一点,例如要求客户端发送自己的进程 ID,并使用进程 ID 来区分客户端。但是,我们建议您不要在您设计的协议中使用此方法,因为我们可能有一天会允许来自挂载相同文件系统的其他机器的连接。相反,如果您希望每个新客户都有一个识别号,请给它发送一个识别号。
关闭本地命名空间中的套接字后,应从文件系统中删除文件名。使用 unlink 或 remove 来执行此操作;请参阅删除文件。
本地命名空间只支持一种协议用于任何通信方式;它是协议号 0。
2.5.2. 本地命名空间细节
Details of Local Namespace
要在本地命名空间中创建套接字,请使用常量 PF_LOCAL 作为套接字或套接字对的命名空间参数。该常量在 sys/socket.h 中定义。
宏:int PF_LOCAL
这指定了本地名称空间,其中套接字地址是本地名称,及其相关的协议族。PF_LOCAL 是 POSIX.1g 使用的宏。
宏:int PF_UNIX
为了兼容性,这是 PF_LOCAL 的同义词。
宏:int PF_FILE
为了兼容性,这是 PF_LOCAL 的同义词。
在本地命名空间中指定套接字名称的结构在头文件 sys/un.h 中定义:
数据类型:struct sockaddr_un
此结构用于指定本地命名空间套接字地址。它有以下成员:
short int sun_family
这标识了套接字地址的地址族或格式。您应该存储值 AF_LOCAL 以指定本地命名空间。请参阅套接字地址。
char sun_path[108]
这是要使用的文件名。
不完整:为什么 108 是一个神奇的数字? RMS 建议将其设为零长度数组并调整以下示例以使用 alloca 根据文件名的长度分配适当的存储量。
您应该将本地命名空间中套接字地址的长度参数计算为 sun_family 组件的大小和文件名字符串的字符串长度(不是分配大小!)之和。这可以使用宏 SUN_LEN 来完成:
宏:int SUN_LEN (struct sockaddr_un * ptr)
Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.
该宏计算本地命名空间中套接字地址的长度。
2.5.3. 本地命名空间套接字示例
Example of Local-Namespace Sockets
这是一个示例,展示了如何在本地命名空间中创建和命名套接字。
#include <stddef.h>
#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/un.h>
int
make_named_socket (const char *filename)
{
struct sockaddr_un name;
int sock;
size_t size;
/* Create the socket. */
sock = socket (PF_LOCAL, SOCK_DGRAM, 0);
if (sock < 0)
{
perror ("socket");
exit (EXIT_FAILURE);
}
/* Bind a name to the socket. */
name.sun_family = AF_LOCAL;
strncpy (name.sun_path, filename, sizeof (name.sun_path));
name.sun_path[sizeof (name.sun_path) - 1] = '\0';
/* The size of the address is
the offset of the start of the filename,
plus its length (not including the terminating null byte).
Alternatively you can just do:
size = SUN_LEN (&name);
*/
size = (offsetof (struct sockaddr_un, sun_path)
+ strlen (name.sun_path));
if (bind (sock, (struct sockaddr *) &name, size) < 0)
{
perror ("bind");
exit (EXIT_FAILURE);
}
return sock;
}
2.6. 互联网命名空间
The Internet Namespace
本节描述 Internet 命名空间中使用的协议和套接字命名约定的详细信息。
最初 Internet 命名空间仅使用 IP 版本 4 (IPv4)。随着 Internet 上主机数量的增加,需要一个具有更大地址空间的新协议:IP 版本 6 (IPv6)。IPv6 引入了 128 位地址(IPv4 有 32 位地址)等特性,最终将取代 IPv4。
要在 IPv4 Internet 命名空间中创建套接字,请使用此命名空间的符号名称 PF_INET 作为 socket 或 socketpair 的命名空间参数。对于 IPv6 地址,您需要宏 PF_INET6。这些宏在 sys/socket.h 中定义。
宏:int PF_INET
这指定了 IPv4 Internet 名称空间和相关的协议系列。
宏:int PF_INET6
这指定了 IPv6 Internet 名称空间和相关的协议族。
Internet 命名空间的套接字地址包括以下组件:
- 您要连接的机器的地址。可以通过多种方式指定 Internet 地址;这些在 Internet 套接字地址格式、主机地址和主机名中进行了讨论。
- 该机器的端口号。请参阅 Internet 端口。
您必须确保地址和端口号以称为网络字节顺序的规范格式表示。有关这方面的信息,请参阅字节顺序转换。
2.6.1. Internet 套接字地址格式
Internet Socket Address Formats
在 Internet 命名空间中,对于 IPv4 (AF_INET) 和 IPv6 (AF_INET6),套接字地址由主机地址和该主机上的端口组成。此外,您选择的协议可以有效地作为地址的一部分,因为本地端口号仅在特定协议中才有意义。
在 Internet 命名空间中表示套接字地址的数据类型在头文件 netinet/in.h 中定义。
数据类型:struct sockaddr_in
这是用于表示 Internet 命名空间中的套接字地址的数据类型。它有以下成员:
sa_family_t sin_family
这标识了套接字地址的地址族或格式。您应该将值 AF_INET 存储在此成员中。地址族按主机字节顺序存储。请参阅套接字地址。
struct in_addr sin_addr
这是 IPv4 地址。请参阅主机地址和主机名,了解如何在此处存储值。IPv4 地址以网络字节顺序存储。
unsigned short int sin_port
这是端口号。请参阅 Internet 端口。端口号按网络字节顺序存储。
当您调用 bind 或 getsockname 时,如果您使用的是 IPv4 Internet 名称空间套接字地址,则应指定 sizeof (struct sockaddr_in) 作为长度参数。
数据类型:struct sockaddr_in6
这是用于表示 IPv6 命名空间中的套接字地址的数据类型。它有以下成员:
sa_family_t sin6_family
这标识了套接字地址的地址族或格式。您应该将 AF_INET6 的值存储在此成员中。请参阅套接字地址。地址族按主机字节顺序存储。
struct in6_addr sin6_addr
这是主机的 IPv6 地址。请参阅主机地址和主机名,了解如何在此处存储值。地址以网络字节顺序存储。
uint32_t sin6_flowinfo
这结合了 IPv6 流量类别和流量标签值,如 IPv6 标头中所示。该字段以网络字节顺序存储。仅使用低 28 位(网络字节顺序中的数字);剩余位必须为零。低 20 位是流标签,第 20 到 27 位是流量类别。通常,该字段为零。
uint32_t sin6_scope_id
对于链路本地地址,这标识了该地址有效的接口。范围 ID 以主机字节顺序存储。通常,该字段为零。
uint16_t sin6_port
这是端口号。请参阅 Internet 端口。端口号按网络字节顺序存储。
2.6.2. 主机地址
Host Addresses
Internet 上的每台计算机都有一个或多个 Internet 地址,这些数字在 Internet 上的所有计算机中标识该计算机。用户通常将 IPv4 数字主机地址写成四个数字的序列,用句点分隔,如“128.52.46.32”,而 IPv6 数字主机地址写成最多八个数字的序列,用冒号分隔,如’5f03:1200:836f:c100::1’。
每台计算机也有一个或多个主机名,它们是由句点分隔的字符串,如“www.gnu.org”。
让用户指定主机的程序通常接受数字地址和主机名。要打开连接,程序需要一个数字地址,因此必须将主机名转换为它所代表的数字地址。
2.6.2.1. Internet 主机地址
Internet Host Addresses
IPv4 Internet 主机地址是一个包含四个字节数据的数字。从历史上看,这些被分为两部分,一个网络号和一个该网络内的本地网络地址号。1990 年代中期引入了无类别地址,改变了这种行为。由于一些函数隐含地期望旧定义,我们首先描述基于类的网络,然后描述无类地址。IPv6 仅使用无类别地址,因此以下段落不适用。
基于类的 IPv4 网络号由前一个、两个或三个字节组成;其余字节是本地地址。
IPv4 网络号在网络信息中心 (NIC) 注册,并分为三类 - A、B 和 C。各个机器的本地网络地址号在特定网络的管理员处注册。
A 类网络有 0 到 127 范围内的单字节数字。A 类网络数量很少,但它们每个都可以支持非常多的主机。中型 B 类网络有两个字节的网络编号,第一个字节的范围是 128 到 191。C 类网络是最小的;它们有三个字节的网络号,第一个字节在 192-255 范围内。因此,Internet 地址的前 1、2 或 3 个字节指定了一个网络。Internet 地址的其余字节指定该网络内的地址。
A 类网络 0 保留用于向所有网络广播。此外,每个网络中的主机号 0 保留用于向该网络中的所有主机广播。这些用途现在已过时,但出于兼容性原因,您不应使用网络 0 和主机号 0。
A类网络127保留用于环回;您始终可以使用 Internet 地址“127.0.0.1”来指代主机。
由于一台机器可以是多个网络的成员,它可以有多个 Internet 主机地址。但是,永远不应该有超过一台具有相同主机地址的机器。
Internet 地址的标准数字和点表示法有四种形式:
A B C D
这分别指定了地址的所有四个字节,并且是常用的表示。
a.b.c
地址的最后一部分 c 被解释为 2 字节的数量。这对于在具有网络地址编号 a.b 的 B 类网络中指定主机地址很有用。
a.b
地址的最后一部分 b 被解释为一个 3 字节的数量。这对于在网络地址编号为 a 的 A 类网络中指定主机地址很有用。
a
如果只给出一部分,则直接对应主机地址号。
在地址的每个部分中,适用于指定基数的常用 C 约定。换句话说,前导“0x”或“0X”表示十六进制基数;前导“0”表示八进制;否则假定为十进制基数。
无类别地址
IPv4 地址(以及 IPv6 地址)现在被认为是无类别的;A、B 和 C 类之间的区别可以忽略不计。相反,IPv4 主机地址由 32 位地址和 32 位掩码组成。掩码包含网络部分的设置位和主机部分的清除位。网络部分从左边开始是连续的,其余位代表主机。因此,网络掩码可以简单地指定为设置位数。A、B 和 C 类只是这个一般规则的特例。例如,A 类地址的网络掩码为“255.0.0.0”或前缀长度为 8。
无类别 IPv4 网络地址以数字和点表示法编写,附加前缀长度和斜杠作为分隔符。例如,A 类网络 10 写为“10.0.0.0/8”。
IPv6 地址
IPv6 地址包含 128 位(IPv4 有 32 位)数据。主机地址通常写为 8 个 16 位十六进制数,用冒号分隔。两个冒号用于缩写连续零的字符串。例如,IPv6 环回地址‘0:0:0:0:0:0:0:1’可以写成‘::1’。
2.6.2.2. 主机地址数据类型
Host Address Data Type
IPv4 Internet 主机地址在某些上下文中表示为整数(类型 uint32_t)。在其他情况下,整数被封装在 struct in_addr 类型的结构中。如果用法一致会更好,但是从结构中提取整数或将整数放入结构中并不难。
您会发现旧代码使用 unsigned long int 表示 IPv4 Internet 主机地址,而不是 uint32_t 或 struct in_addr。历史上 unsigned long int 是一个 32 位数字,但对于 64 位机器,这已经改变了。如果在这种类型没有 32 位的机器上使用 unsigned long int 可能会破坏代码。uint32_t 由 Unix98 指定,保证为 32 位。
IPv6 Internet 主机地址有 128 位,并封装在 struct in6_addr 类型的结构中。
Internet 地址的以下基本定义在头文件 netinet/in.h 中声明:
数据类型:struct in_addr
此数据类型在某些上下文中用于包含 IPv4 Internet 主机地址。它只有一个字段,名为 s_addr,将主机地址编号记录为 uint32_t。
宏:uint32_t INADDR_LOOPBACK
你可以用这个常数来代表“这台机器的地址”,而不是找到它的实际地址。它是 IPv4 Internet 地址“127.0.0.1”,通常称为“localhost”。这个特殊的常量省去了查找自己机器地址的麻烦。此外,系统通常会专门实现 INADDR_LOOPBACK,避免在一台机器与自己对话的情况下产生任何网络流量。
宏:uint32_t INADDR_ANY
绑定到地址时,您可以使用此常量代表“任何传入地址”。请参阅设置套接字的地址。当您想要接受 Internet 连接时,这是在 struct sockaddr_in 的 sin_addr 成员中提供的常用地址。
宏:uint32_t INADDR_BROADCAST
这个常量是你用来发送广播消息的地址。
宏:uint32_t INADDR_NONE
某些函数返回此常量以指示错误。
数据类型:struct in6_addr
此数据类型用于存储 IPv6 地址。它存储 128 位数据,可以通过多种方式访问(通过联合)。
常量:struct in6_addr in6addr_loopback
此常量是 IPv6 地址“::1”,即环回地址。有关这意味着什么的描述,请参见上文。提供宏 IN6ADDR_LOOPBACK_INIT 以允许您将自己的变量初始化为此值。
常量:struct in6_addr in6addr_any
这个常量是 IPv6 地址‘::’,未指定的地址。有关这意味着什么的描述,请参见上文。提供宏 IN6ADDR_ANY_INIT 以允许您将自己的变量初始化为该值。
2.6.2.3. 主机地址函数
Host Address Functions
这些用于操作 Internet 地址的附加函数在头文件 arpa/inet.h 中声明。它们以网络字节顺序表示 Internet 地址,以主机字节顺序表示网络编号和网络内本地地址编号。有关网络和主机字节顺序的说明,请参见字节顺序转换。
函数:int inet_aton (const char *name, struct in_addr *addr)
Preliminary: | MT-Safe locale | AS-Safe | AC-Safe | See POSIX Safety Concepts.
此函数将 IPv4 Internet 主机地址名称从标准数字和点表示法转换为二进制数据,并将其存储在 addr 指向的结构 in_addr 中。如果地址有效,inet_aton 返回非零,否则返回零。
函数:uint32_t inet_addr (const char *name)
Preliminary: | MT-Safe locale | AS-Safe | AC-Safe | See POSIX Safety Concepts.
此函数将 IPv4 Internet 主机地址名称从标准数字和点表示法转换为二进制数据。如果输入无效,inet_addr 返回 INADDR_NONE。这是一个过时的 inet_aton 接口,如前所述。它已过时,因为 INADDR_NONE 是一个有效地址 (255.255.255.255),而 inet_aton 提供了一种更简洁的方式来指示错误返回。
函数:uint32_t inet_network (const char *name)
Preliminary: | MT-Safe locale | AS-Safe | AC-Safe | See POSIX Safety Concepts.
此函数从地址名称中提取网络号,以标准数字和点表示法给出。返回的地址按主机顺序排列。如果输入无效,inet_network 返回 -1。
该功能仅适用于传统的 IPv4 A、B 和 C 类网络类型。它不适用于无类别地址,不应再使用。
函数:char * inet_ntoa (struct in_addr addr)
Preliminary: | MT-Safe locale | AS-Unsafe race | AC-Safe | See POSIX Safety Concepts.
此函数将 IPv4 Internet 主机地址 addr 转换为标准数字和点表示法的字符串。返回值是指向静态分配缓冲区的指针。后续调用将覆盖相同的缓冲区,因此如果需要保存字符串,则应复制该字符串。
在多线程程序中,每个线程都有自己的静态分配缓冲区。但是在同一个线程中对 inet_ntoa 的后续调用仍然会覆盖最后一次调用的结果。
应该使用下面描述的较新的函数 inet_ntop 而不是 inet_ntoa,因为它同时处理 IPv4 和 IPv6 地址。
函数:struct in_addr inet_makeaddr (uint32_t net, uint32_t local)
Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.
此函数通过将网络号 net 与本地网络内的本地地址号组合在一起来生成 IPv4 互联网主机地址。
函数:uint32_t inet_lnaof (struct in_addr addr)
Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.
此函数返回 Internet 主机地址 addr 的 local-address-within-network 部分。
该功能仅适用于传统的 IPv4 A、B 和 C 类网络类型。它不适用于无类别地址,不应再使用。
函数:uint32_t inet_netof (struct in_addr addr)
Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.
此函数返回 Internet 主机地址 addr 的网络号部分。
该功能仅适用于传统的 IPv4 A、B 和 C 类网络类型。它不适用于无类别地址,不应再使用。
函数:int inet_pton (int af, const char *cp, void *buf)
Preliminary: | MT-Safe locale | AS-Safe | AC-Safe | See POSIX Safety Concepts.
此函数将 Internet 地址(IPv4 或 IPv6)从表示(文本)格式转换为网络(二进制)格式。af 应该是 AF_INET 或 AF_INET6,以适合被转换的地址类型。cp 是指向输入字符串的指针,而 buf 是指向结果缓冲区的指针。调用者有责任确保缓冲区足够大。
函数:const char * inet_ntop (int af, const void *cp, char *buf, socklen_t len)
Preliminary: | MT-Safe locale | AS-Safe | AC-Safe | See POSIX Safety Concepts.
此函数将 Internet 地址(IPv4 或 IPv6)从网络(二进制)转换为表示(文本)形式。af 应该是 AF_INET 或 AF_INET6,视情况而定。cp 是指向要转换的地址的指针。buf 应该是指向保存结果的缓冲区的指针,len 是该缓冲区的长度。该函数的返回值将是这个缓冲区地址。
2.6.2.4. 主机名
Host Names
除了 Internet 地址的标准数字和点符号外,您还可以通过符号名称来引用主机。符号名称的优点是通常更容易记住。例如,互联网地址为“158.121.106.19”的机器也被称为“alpha.gnu.org”;“gnu.org”域中的其他机器可以简称为“alpha”。
在内部,系统使用数据库来跟踪主机名和主机号之间的映射。该数据库通常是文件 /etc/hosts 或名称服务器提供的等效文件。用于访问该数据库的函数和其他符号在 netdb.h 中声明。它们是 BSD 功能,如果包含 netdb.h,则无条件定义。
数据类型:struct hostent
此数据类型用于表示 hosts 数据库中的条目。它有以下成员:
char *h_name
这是主机的“官方”名称。
char **h_aliases
这些是主机的替代名称,表示为以空字符结尾的字符串向量。
int h_addrtype
这是主机地址类型;实际上,它的值始终是 AF_INET 或 AF_INET6,后者用于 IPv6 主机。原则上,数据库中可以表示其他类型的地址以及互联网地址;如果这样做了,您可能会在此字段中找到除 AF_INET 或 AF_INET6 之外的值。请参阅套接字地址。
int h_length
这是每个地址的长度(以字节为单位)。
char **h_addr_list
这是主机的地址向量。(回想一下,主机可能连接到多个网络并在每个网络上具有不同的地址。)向量由空指针终止。
char *h_addr
这是 h_addr_list[0] 的同义词;换句话说,它是第一个主机地址。
就主机数据库而言,每个地址只是一个长度为 h_length 个字节的内存块。但在其他情况下,有一个隐含的假设,即您可以将 IPv4 地址转换为结构 in_addr 或 uint32_t。struct hostent 结构中的主机地址始终以网络字节顺序给出;请参阅字节顺序转换。
您可以使用 gethostbyname、gethostbyname2 或 gethostbyaddr 来搜索主机数据库以获取有关特定主机的信息。信息以静态分配的结构返回;如果您需要跨调用保存信息,则必须复制该信息。您还可以使用 getaddrinfo 和 getnameinfo 来获取此信息。
函数:struct hostent * gethostbyname (const char *name)
Preliminary: | MT-Unsafe race:hostbyname env locale | AS-Unsafe dlopen plugin corrupt heap lock | AC-Unsafe lock corrupt mem fd | See POSIX Safety Concepts.
gethostbyname 函数返回有关名为 name 的主机的信息。如果查找失败,则返回一个空指针。
函数:struct hostent * gethostbyname2 (const char *name, int af)
Preliminary: | MT-Unsafe race:hostbyname2 env locale | AS-Unsafe dlopen plugin corrupt heap lock | AC-Unsafe lock corrupt mem fd | See POSIX Safety Concepts.
gethostbyname2 函数类似于 gethostbyname,但允许调用者指定结果的所需地址族(例如 AF_INET 或 AF_INET6)。
函数:struct hostent * gethostbyaddr (const void *addr, socklen_t length, int format)
Preliminary: | MT-Unsafe race:hostbyaddr env locale | AS-Unsafe dlopen plugin corrupt heap lock | AC-Unsafe lock corrupt mem fd | See POSIX Safety Concepts.
gethostbyaddr 函数返回有关具有 Internet 地址 addr 的主机的信息。参数 addr 实际上并不是指向 char 的指针——它可以是指向 IPv4 或 IPv6 地址的指针。length 参数是 addr 处地址的大小(以字节为单位)。format 指定地址格式;对于 IPv4 Internet 地址,指定值 AF_INET;对于 IPv6 Internet 地址,请使用 AF_INET6。
如果查找失败,gethostbyaddr 返回一个空指针。
如果通过 gethostbyname 或 gethostbyaddr 查找名称失败,可以通过查看变量 h_errno 的值找出原因。(这些函数设置 errno 会更简洁,但 h_errno 的使用与其他系统兼容。)
以下是您可能在 h_errno 中找到的错误代码:
HOST_NOT_FOUND
数据库中不知道这样的主机。
TRY_AGAIN
当无法联系名称服务器时会发生这种情况。如果你稍后再试一次,你可能会成功。
NO_RECOVERY
发生不可恢复的错误。
NO_ADDRESS
主机数据库包含名称条目,但没有关联的 Internet 地址。
上述查找函数有一个共同点:它们不可重入,因此无法在多线程应用程序中使用。因此,为 GNU C 库提供了一组可以在此上下文中使用的新函数。
函数:int gethostbyname_r (const char *restrict name, struct hostent *restrict result_buf, char *restrict buf, size_t buflen, struct hostent **restrict result, int *restrict h_errnop)
Preliminary: | MT-Safe env locale | AS-Unsafe dlopen plugin corrupt heap lock | AC-Unsafe lock corrupt mem fd | See POSIX Safety Concepts.
gethostbyname_r 函数返回有关名为 name 的主机的信息。调用者必须在 result_buf 参数中传递一个指向 struct hostent 类型对象的指针。此外,该函数可能需要额外的缓冲区空间,调用者必须在 buf 和 buflen 参数中传递一个指针和缓冲区的大小。
函数调用成功返回后,*result 中提供了一个指向存储结果的缓冲区的指针。作为 buf 参数传递的缓冲区只有在调用者完成结果 hostent 结构后才能被释放,或者已经复制了它,包括它指向的所有其他内存。如果发生错误或未找到条目,则指针 *result 为空指针。成功由零返回值表示。如果函数失败,则返回值为错误号。除了为 gethostbyname 定义的错误之外,它还可以是 ERANGE。在这种情况下,应该使用更大的缓冲区重复调用。附加错误信息不存储在全局变量 h_errno 中,而是存储在 h_errnop 指向的对象中。
这是一个小例子:
struct hostent *
gethostname (char *host)
{
struct hostent *hostbuf, *hp;
size_t hstbuflen;
char *tmphstbuf;
int res;
int herr;
hostbuf = malloc (sizeof (struct hostent));
hstbuflen = 1024;
tmphstbuf = malloc (hstbuflen);
while ((res = gethostbyname_r (host, hostbuf, tmphstbuf, hstbuflen,
&hp, &herr)) == ERANGE)
{
/* Enlarge the buffer. */
tmphstbuf = reallocarray (tmphstbuf, hstbuflen, 2);
hstbuflen *= 2;
}
free (tmphstbuf);
/* Check for errors. */
if (res || hp == NULL)
return NULL;
return hp;
}
函数:int gethostbyname2_r (const char *name, int af, struct hostent *restrict result_buf, char *restrict buf, size_t buflen, struct hostent **restrict result, int *restrict h_errnop)
Preliminary: | MT-Safe env locale | AS-Unsafe dlopen plugin corrupt heap lock | AC-Unsafe lock corrupt mem fd | See POSIX Safety Concepts.
gethostbyname2_r 函数类似于 gethostbyname_r,但允许调用者为结果指定所需的地址族(例如 AF_INET 或 AF_INET6)。
函数:int gethostbyaddr_r (const void *addr, socklen_t length, int format, struct hostent *restrict result_buf, char *restrict buf, size_t buflen, struct hostent **restrict result, int *restrict h_errnop)
Preliminary: | MT-Safe env locale | AS-Unsafe dlopen plugin corrupt heap lock | AC-Unsafe lock corrupt mem fd | See POSIX Safety Concepts.
gethostbyaddr_r 函数返回有关具有 Internet 地址 addr 的主机的信息。参数 addr 实际上并不是指向 char 的指针——它可以是指向 IPv4 或 IPv6 地址的指针。length 参数是 addr 处地址的大小(以字节为单位)。format 指定地址格式;对于 IPv4 Internet 地址,指定值 AF_INET;对于 IPv6 Internet 地址,请使用 AF_INET6。
与 gethostbyname_r 函数类似,调用者必须为内部使用的结果和内存提供缓冲区。如果成功,该函数将返回零。否则,该值是一个错误号,其中 ERANGE 具有调用者提供的缓冲区太小的特殊含义。
您还可以使用 sethostent、gethostent 和 endhostent 一次扫描整个主机数据库一个条目。使用这些函数时要小心,因为它们不可重入。
函数:void sethostent (int stayopen)
Preliminary: | MT-Unsafe race:hostent env locale | AS-Unsafe dlopen plugin heap lock | AC-Unsafe corrupt lock fd mem | See POSIX Safety Concepts.
此函数打开主机数据库以开始扫描它。然后,您可以调用 gethostent 来读取条目。
如果 stayopen 参数不为零,这将设置一个标志,以便随后对 gethostbyname 或 gethostbyaddr 的调用不会关闭数据库(如通常那样)。如果您多次调用这些函数,这样可以提高效率,避免每次调用都重新打开数据库。
函数:struct hostent * gethostent (void)
Preliminary: | MT-Unsafe race:hostent race:hostentbuf env locale | AS-Unsafe dlopen plugin heap lock | AC-Unsafe corrupt lock fd mem | See POSIX Safety Concepts.
此函数返回 hosts 数据库中的下一个条目。如果没有更多条目,它将返回一个空指针。
函数:void endhostent (void)
Preliminary: | MT-Unsafe race:hostent env locale | AS-Unsafe dlopen plugin heap lock | AC-Unsafe corrupt lock fd mem | See POSIX Safety Concepts.
此函数关闭主机数据库。
2.6.3. 互联网端口
Internet Ports
Internet 命名空间中的套接字地址由机器的 Internet 地址加上一个端口号组成,该端口号区分给定机器上的套接字(对于给定协议)。端口号范围从 0 到 65,535。
小于 IPPORT_RESERVED 的端口号保留给标准服务器,例如 finger 和 telnet。有一个跟踪这些的数据库,您可以使用 getservbyname 函数将服务名称映射到端口号;请参阅服务数据库。
如果您编写的服务器不是数据库中定义的标准服务器之一,则必须为其选择一个端口号。使用大于 IPPORT_USERRESERVED 的数字;这些数字是为服务器保留的,不会由系统自动生成。避免与其他用户运行的服务器发生冲突取决于您。
当您使用套接字而不指定其地址时,系统会为其生成一个端口号。此数字介于 IPPORT_RESERVED 和 IPPORT_USERRESERVED 之间。
在 Internet 上,拥有两个具有相同端口号的不同套接字实际上是合法的,只要它们从不尝试使用相同的套接字地址(主机地址加端口号)进行通信。您不应该复制端口号,除非在更高级别协议需要它的特殊情况下。通常,系统不会让你这样做;bind 通常坚持使用不同的端口号。要重用端口号,您必须设置套接字选项 SO_REUSEADDR。请参阅套接字级选项。
这些宏在头文件 netinet/in.h 中定义。
宏:int IPPORT_RESERVED
小于 IPPORT_RESERVED 的端口号保留给超级用户使用。
宏:int IPPORT_USERRESERVED
大于或等于 IPPORT_USERRESERVED 的端口号保留用于显式使用;它们永远不会被自动分配。
2.6.4. 服务数据库
The Services Database
跟踪“知名”服务的数据库通常是文件 /etc/services 或来自名称服务器的等效文件。您可以使用这些在 netdb.h 中声明的实用程序来访问服务数据库。
数据类型:struct servent
此数据类型包含有关来自服务数据库的条目的信息。它有以下成员:
char *s_name
这是服务的“官方”名称。
char **s_aliases
这些是服务的备用名称,表示为字符串数组。空指针终止数组。
int s_port
这是服务的端口号。端口号以网络字节顺序给出;请参阅字节顺序转换。
char *s_proto
这是与此服务一起使用的协议的名称。请参阅协议数据库。
要获取有关特定服务的信息,请使用 getservbyname 或 getservbyport 函数。信息以静态分配的结构返回;如果您需要跨调用保存信息,则必须复制该信息。
函数:struct servent * getservbyname (const char *name, const char *proto)
Preliminary: | MT-Unsafe race:servbyname locale | AS-Unsafe dlopen plugin heap lock | AC-Unsafe corrupt lock fd mem | See POSIX Safety Concepts.
getservbyname 函数使用协议 proto 返回有关名为 name 的服务的信息。如果找不到这样的服务,则返回一个空指针。
这个函数对服务器和客户端都很有用;服务器使用它来确定它们应该侦听的端口(请参阅侦听连接)。
函数:struct servent * getservbyport (int port, const char *proto)
Preliminary: | MT-Unsafe race:servbyport locale | AS-Unsafe dlopen plugin heap lock | AC-Unsafe corrupt lock fd mem | See POSIX Safety Concepts.
getservbyport 函数使用协议 proto 在端口端口返回有关服务的信息。如果找不到这样的服务,则返回一个空指针。
您还可以使用 setservent、getservent 和 endservent 扫描服务数据库。使用这些函数时要小心,因为它们不可重入。
函数:void setservent (int stayopen)
Preliminary: | MT-Unsafe race:servent locale | AS-Unsafe dlopen plugin heap lock | AC-Unsafe corrupt lock fd mem | See POSIX Safety Concepts.
此函数打开服务数据库以开始扫描它。
如果 stayopen 参数不为零,这将设置一个标志,以便随后对 getservbyname 或 getservbyport 的调用不会关闭数据库(如通常那样)。如果您多次调用这些函数,这样可以提高效率,避免每次调用都重新打开数据库。
函数:struct servent * getservent (void)
Preliminary: | MT-Unsafe race:servent race:serventbuf locale | AS-Unsafe dlopen plugin heap lock | AC-Unsafe corrupt lock fd mem | See POSIX Safety Concepts.
此函数返回服务数据库中的下一个条目。如果没有更多条目,则返回一个空指针。
函数:void endservent (void)
Preliminary: | MT-Unsafe race:servent locale | AS-Unsafe dlopen plugin heap lock | AC-Unsafe corrupt lock fd mem | See POSIX Safety Concepts.
此函数关闭服务数据库。
2.6.5. 字节顺序转换
Byte Order Conversion
不同种类的计算机使用不同的约定来对字中的字节进行排序。一些计算机将一个字中的最高有效字节放在最前面(这称为“大端”顺序),而另一些计算机则把它放在最后(“小端”顺序)。
为了使具有不同字节顺序约定的机器可以进行通信,Internet 协议为通过网络传输的数据指定了规范的字节顺序约定。这称为网络字节顺序。
建立 Internet 套接字连接时,必须确保 sockaddr_in 结构的 sin_port 和 sin_addr 成员中的数据以网络字节顺序表示。如果您在通过套接字发送的消息中编码整数数据,您也应该将其转换为网络字节顺序。如果您不这样做,您的程序在其他类型的机器上运行或与其他类型的机器通信时可能会失败。
如果使用 getservbyname 和 gethostbyname 或 inet_addr 来获取端口号和主机地址,则这些值已经是网络字节顺序,您可以将它们直接复制到 sockaddr_in 结构中。
否则,您必须显式转换值。使用 htons 和 ntohs 转换 sin_port 成员的值。使用 htonl 和 ntohl 为 sin_addr 成员转换 IPv4 地址。(记住,struct in_addr 等价于 uint32_t。)这些函数在 netinet/in.h 中声明。
函数:uint16_t htons (uint16_t hostshort)
Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.
此函数将 uint16_t 整数 hostshort 从主机字节顺序转换为网络字节顺序。
函数:uint16_t ntohs (uint16_t netshort)
Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.
此函数将 uint16_t 整数 netshort 从网络字节顺序转换为主机字节顺序。
函数:uint32_t htonl (uint32_t hostlong)
Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.
此函数将 uint32_t 整数 hostlong 从主机字节顺序转换为网络字节顺序。
这用于 IPv4 Internet 地址。
函数:uint32_t ntohl (uint32_t netlong)
Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.
此函数将 uint32_t 整数 netlong 从网络字节顺序转换为主机字节顺序。
这用于 IPv4 Internet 地址。
2.6.6. 协议数据库
Protocols Database
与套接字一起使用的通信协议控制数据交换方式的低级细节。例如,该协议实现了诸如校验和之类的东西来检测传输中的错误,以及消息的路由指令。普通用户程序没有理由直接处理这些细节。
Internet 命名空间的默认通信协议取决于通信方式。对于流通信,默认为 TCP(“传输控制协议”)。对于数据报通信,默认为 UDP(“用户数据报协议”)。对于可靠的数据报通信,默认为 RDP(“可靠数据报协议”)。您应该几乎总是使用默认值。
Internet 协议通常由名称而不是数字指定。主机知道的网络协议存储在数据库中。这通常来自文件 /etc/protocols,或者它可能是名称服务器提供的等价物。您可以使用 getprotobyname 函数在数据库中查找与命名协议关联的协议号。
以下是用于访问协议数据库的实用程序的详细说明。这些在 netdb.h 中声明。
数据类型:struct protoent
此数据类型用于表示网络协议数据库中的条目。它有以下成员:
char *p_name
这是协议的正式名称。
char **p_aliases
这些是协议的备用名称,指定为字符串数组。数组的最后一个元素是空指针。
int p_proto
这是协议号(按主机字节顺序);将此成员用作套接字的协议参数。
您可以使用 getprotobyname 和 getprotobynumber 在协议数据库中搜索特定协议。信息以静态分配的结构返回;如果您需要跨调用保存信息,则必须复制该信息。
函数:struct protoent * getprotobyname (const char *name)
Preliminary: | MT-Unsafe race:protobyname locale | AS-Unsafe dlopen plugin heap lock | AC-Unsafe corrupt lock fd mem | See POSIX Safety Concepts.
getprotobyname 函数返回有关名为 name 的网络协议的信息。如果没有这样的协议,它返回一个空指针。
函数:struct protoent * getprotobynumber (int protocol)
Preliminary: | MT-Unsafe race:protobynumber locale | AS-Unsafe dlopen plugin heap lock | AC-Unsafe corrupt lock fd mem | See POSIX Safety Concepts.
getprotobynumber 函数使用 number 协议返回有关网络协议的信息。如果没有这样的协议,它返回一个空指针。
您还可以使用 setprotoent、getprotoent 和 endprotoent 一次扫描一个协议的整个协议数据库。使用这些函数时要小心,因为它们不可重入。
函数:void setprotoent (int stayopen)
Preliminary: | MT-Unsafe race:protoent locale | AS-Unsafe dlopen plugin heap lock | AC-Unsafe corrupt lock fd mem | See POSIX Safety Concepts.
此函数打开协议数据库以开始扫描它。
如果 stayopen 参数不为零,这将设置一个标志,以便后续调用 getprotobyname 或 getprotobynumber 不会关闭数据库(如通常那样)。如果您多次调用这些函数,这样可以提高效率,避免每次调用都重新打开数据库。
函数:struct protoent * getprotoent (void)
Preliminary: | MT-Unsafe race:protoent race:protoentbuf locale | AS-Unsafe dlopen plugin heap lock | AC-Unsafe corrupt lock fd mem | See POSIX Safety Concepts.
此函数返回协议数据库中的下一个条目。如果没有更多条目,它将返回一个空指针。
函数:void endprotoent (void)
Preliminary: | MT-Unsafe race:protoent locale | AS-Unsafe dlopen plugin heap lock | AC-Unsafe corrupt lock fd mem | See POSIX Safety Concepts.
此函数关闭协议数据库。
2.6.7. Internet 套接字示例
Internet Socket Example
这是一个示例,展示了如何在 Internet 命名空间中创建和命名套接字。新创建的套接字存在于运行程序的机器上。此示例不查找和使用机器的 Internet 地址,而是指定 INADDR_ANY 作为主机地址;系统将其替换为机器的实际地址。
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
int
make_socket (uint16_t port)
{
int sock;
struct sockaddr_in name;
/* Create the socket. */
sock = socket (PF_INET, SOCK_STREAM, 0);
if (sock < 0)
{
perror ("socket");
exit (EXIT_FAILURE);
}
/* Give the socket a name. */
name.sin_family = AF_INET;
name.sin_port = htons (port);
name.sin_addr.s_addr = htonl (INADDR_ANY);
if (bind (sock, (struct sockaddr *) &name, sizeof (name)) < 0)
{
perror ("bind");
exit (EXIT_FAILURE);
}
return sock;
}
下面是另一个例子,展示了如何在给定主机名字符串和端口号的情况下填写 sockaddr_in 结构:
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
void
init_sockaddr (struct sockaddr_in *name,
const char *hostname,
uint16_t port)
{
struct hostent *hostinfo;
name->sin_family = AF_INET;
name->sin_port = htons (port);
hostinfo = gethostbyname (hostname);
if (hostinfo == NULL)
{
fprintf (stderr, "Unknown host %s.\n", hostname);
exit (EXIT_FAILURE);
}
name->sin_addr = *(struct in_addr *) hostinfo->h_addr;
}
2.7. 其他命名空间
Other Namespaces
支持某些其他名称空间和相关协议系列,但尚未记录,因为它们不经常使用。PF_NS 是指施乐网络软件协议。PF_ISO 代表开放系统互连。PF_CCITT 是指来自 CCITT 的协议。socket.h 定义了这些符号和其他未实际实现的命名协议。
PF_IMPLINK 用于主机和 Internet 消息处理器之间的通信。有关此和 PF_ROUTE(一种偶尔使用的局域路由协议)的信息,请参阅 GNU Hurd 手册(将来会出现)。
2.8. 打开和关闭套接字
Opening and Closing Sockets
本节介绍用于打开和关闭套接字的实际库函数。相同的功能适用于所有命名空间和连接样式。
2.8.1. 创建套接字
Creating a Socket
创建套接字的原语是套接字函数,在 sys/socket.h 中声明。
函数:int socket(int namespace, int style, int protocol)
Preliminary: | MT-Safe | AS-Safe | AC-Safe fd | See POSIX Safety Concepts.
此函数创建一个套接字并指定通信样式 style,该样式应为通信样式中列出的套接字样式之一。命名空间参数指定命名空间;它必须是 PF_LOCAL(请参阅本地命名空间)或 PF_INET(请参阅 Internet 命名空间)。protocol 指定特定的协议(参见 Socket Concepts);零通常适用于协议。
socket 的返回值是新套接字的文件描述符,如果出错则为 -1。为此函数定义了以下 errno 错误条件:
EPROTONOSUPPORT
指定的命名空间不支持协议或样式。
EMFILE
该进程已经打开了太多的文件描述符。
ENFILE
系统已经打开了太多的文件描述符。
EACCES
该进程无权创建指定样式或协议的套接字。
ENOBUFS
系统内部缓冲区空间不足。
套接字函数返回的文件描述符支持读写操作。但是,像管道一样,套接字不支持文件定位操作。
有关如何调用套接字函数的示例,请参阅本地命名空间套接字示例或 Internet 套接字示例。
2.8.2. 关闭套接字
Closing a Socket
当你使用完一个套接字后,你可以简单地用 close 关闭它的文件描述符;请参阅打开和关闭文件。如果仍有数据等待通过连接传输,则正常关闭尝试完成此传输。您可以使用 SO_LINGER 套接字选项来控制此行为以指定超时时间;请参阅套接字选项。
您还可以通过调用 sys/socket.h 中声明的 shutdown 来仅关闭连接上的接收或传输。
函数:int shutdown(int socket, int how)
Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.
shutdown函数关闭socket socket的连接。参数 how 指定要执行的操作:
0
停止接收此套接字的数据。如果有更多数据到达,则拒绝它。
1
停止尝试从此套接字传输数据。丢弃任何等待发送的数据。停止寻找已发送数据的确认;如果丢失,请勿重新传输。
2
停止接收和发送。
成功时返回值为 0,失败时返回值为 -1。为此函数定义了以下 errno 错误条件:
EBADF
套接字不是有效的文件描述符。
ENOTSOCK
套接字不是套接字。
ENOTCONN
套接字未连接。
2.8.3. 套接字对
Socket Pairs
套接字对由一对已连接(但未命名)的套接字组成。它与管道非常相似,并且以几乎相同的方式使用。套接字对是使用 sys/socket.h 中声明的 socketpair 函数创建的。套接字对很像管道。主要区别在于套接字对是双向的,而管道有一个仅输入端和一个仅输出端(请参阅管道和 FIFO)。
函数:int socketpair (int namespace, int style, int protocol, int filedes[2])
Preliminary: | MT-Safe | AS-Safe | AC-Safe fd | See POSIX Safety Concepts.
此函数创建一个套接字对,返回filedes[0] 和filedes[1] 中的文件描述符。套接字对是全双工通信通道,因此可以在任一端执行读取和写入。
命名空间、样式和协议参数被解释为用于套接字函数。style 应该是通信样式中列出的通信样式之一。命名空间参数指定命名空间,它必须是 AF_LOCAL(参见本地命名空间);协议指定通信协议,但零是唯一有意义的值。
如果style指定了无连接的通信方式,那么你得到的两个socket严格来说是没有连接的,但是它们每个都知道对方是默认的目的地址,所以它们可以互相发送数据包。
socketpair 函数在成功时返回 0,在失败时返回 -1。为此函数定义了以下 errno 错误条件:
EMFILE
进程打开的文件描述符过多。
EAFNOSUPPORT
不支持指定的命名空间。
EPROTONOSUPPORT
不支持指定的协议。
EOPNOTSUPP
指定的协议不支持创建套接字对。
2.9. 使用带有连接的套接字
Using Sockets with Connections
最常见的通信方式包括与特定的其他套接字建立连接,然后与该套接字反复交换数据。建立连接是不对称的;一侧(客户端)请求连接,而另一侧(服务器)创建一个套接字并等待连接请求。
2.9.1. 建立连接
Making a Connection
在建立连接时,客户端在服务器等待并接受连接时建立连接。这里我们讨论客户端程序必须使用 sys/socket.h 中声明的 connect 函数做什么。
函数:int connect (int socket, struct sockaddr *addr, socklen_t length)
Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.
connect 函数启动从带有文件描述符套接字的套接字到地址由 addr 和 length 参数指定的套接字的连接。(此套接字通常在另一台机器上,并且它必须已经设置为服务器。)有关如何解释这些参数的信息,请参阅套接字地址。
通常,connect 会等到服务器响应请求后才返回。您可以在套接字套接字上设置非阻塞模式以使连接立即返回,而无需等待响应。有关非阻塞模式的信息,请参阅文件状态标志。
connect 的正常返回值为 0。如果发生错误,connect 返回 -1。为此函数定义了以下 errno 错误条件:
EBADF
套接字套接字不是有效的文件描述符。
ENOTSOCK
文件描述符套接字不是套接字。
EADDRNOTAVAIL
指定的地址在远程机器上不可用。
EAFNOSUPPORT
此套接字不支持 addr 的命名空间。
EISCONN
套接字套接字已经连接。
ETIMEDOUT
建立连接的尝试超时。
ECONNREFUSED
服务器主动拒绝建立连接。
ENETUNREACH
无法从此主机访问给定地址的网络。
EADDRINUSE
给定地址的套接字地址已被使用。
EINPROGRESS
套接字套接字是非阻塞的,无法立即建立连接。您可以通过select确定连接何时完全建立;请参阅等待输入或输出。在连接完全建立之前,在同一个套接字上的另一个连接调用将失败并显示 EALREADY。
EALREADY
套接字套接字是非阻塞的,并且已经有一个正在进行的连接(参见上面的 EINPROGRESS)。
该函数被定义为多线程程序中的取消点,因此必须为此做好准备,并确保即使线程被取消,分配的资源(如内存、文件描述符、信号量或其他)也被释放。
2.9.2. 监听连接
Listening for Connections
现在让我们考虑一下服务器进程必须做什么才能接受套接字上的连接。首先它必须使用listen函数在socket上启用连接请求,然后通过调用accept来接受每个传入的连接(参见Accepting Connections)。在服务器套接字上启用连接请求后,选择函数会报告套接字何时准备好接受连接(请参阅等待输入或输出)。
使用无连接通信方式的套接字不允许监听函数。
您可以编写一个网络服务器,它甚至在请求连接之前才开始运行。请参阅 inetd 服务器。
在 Internet 命名空间中,没有特殊的保护机制来控制对端口的访问;任何机器上的任何进程都可以连接到您的服务器。如果你想限制对你的服务器的访问,让它检查与连接请求相关的地址或实现一些其他的握手或识别协议。
在本地命名空间中,普通文件保护位控制谁有权连接到套接字。
函数:int listen (int socket, int n)
Preliminary: | MT-Safe | AS-Safe | AC-Safe fd | See POSIX Safety Concepts.
侦听功能使套接字套接字能够接受连接,从而使其成为服务器套接字。
参数 n 指定待处理连接的队列长度。当队列填满时,尝试连接的新客户端将失败并显示 ECONNREFUSED,直到服务器调用 accept 以接受来自队列的连接。
listen 函数在成功时返回 0,在失败时返回 -1。为此函数定义了以下 errno 错误条件:
EBADF
参数套接字不是有效的文件描述符。
ENOTSOCK
参数套接字不是套接字。
EOPNOTSUPP
套接字套接字不支持此操作。
2.9.3. 接受连接
Accepting Connections
当服务器接收到连接请求时,它可以通过接受请求来完成连接。使用函数接受来做到这一点。
已建立为服务器的套接字可以接受来自多个客户端的连接请求。服务器的原始套接字不会成为连接的一部分;相反,accept 会创建一个参与连接的新套接字。accept 返回此套接字的描述符。服务器的原始套接字仍然可用于侦听进一步的连接请求。
服务器套接字上挂起的连接请求数是有限的。如果来自客户端的连接请求到达的速度快于服务器对它们的处理速度,则队列可能会填满,并且其他请求会因 ECONNREFUSED 错误而被拒绝。您可以将此队列的最大长度指定为监听函数的参数,尽管系统也可能对该队列的长度施加其自己的内部限制。
函数:int accept (int socket, struct sockaddr *addr, socklen_t *length_ptr)
Preliminary: | MT-Safe | AS-Safe | AC-Safe fd | See POSIX Safety Concepts.
此函数用于接受服务器套接字套接字上的连接请求。
如果没有连接挂起,accept 函数会等待,除非套接字套接字设置了非阻塞模式。(您可以使用 select 等待挂起的连接,使用非阻塞套接字。)有关非阻塞模式的信息,请参见文件状态标志。
addr 和 length-ptr 参数用于返回有关发起连接的客户端套接字名称的信息。有关信息格式的信息,请参阅套接字地址。
接受连接不会使套接字成为连接的一部分。相反,它会创建一个连接的新套接字。accept 的正常返回值是新套接字的文件描述符。
接受后,原始套接字套接字保持打开和未连接状态,并继续侦听,直到您将其关闭。您可以通过再次调用 accept 来接受与套接字的进一步连接。
如果发生错误,accept 返回 -1。为此函数定义了以下 errno 错误条件:
EBADF
套接字参数不是有效的文件描述符。
ENOTSOCK
描述符套接字参数不是套接字。
EOPNOTSUPP
描述符套接字不支持此操作。
EWOULBLOCK
套接字设置了非阻塞模式,并且没有立即可用的挂起连接。
该函数被定义为多线程程序中的取消点,因此必须为此做好准备,并确保即使线程被取消,分配的资源(如内存、文件描述符、信号量或其他)也被释放。
使用无连接通信方式的套接字不允许接受函数。
2.9.4. 谁与我有关?
Who is Connected to Me?
函数:int getpeername (int socket, struct sockaddr *addr, socklen_t *length-ptr)
Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.
getpeername函数返回socket连接的socket地址;它将地址存储在 addr 和 length-ptr 指定的内存空间中。它将地址的长度存储在 *length-ptr 中。
有关地址格式的信息,请参阅套接字地址。在某些操作系统中,getpeername 仅适用于 Internet 域中的套接字。
返回值为 0 表示成功,-1 表示错误。为此函数定义了以下 errno 错误条件:
EBADF
参数套接字不是有效的文件描述符。
ENOTSOCK
描述符套接字不是套接字。
ENOTCONN
插座插座未连接。
ENOBUFS
没有足够的内部缓冲区可用。
2.9.5. 传输数据
Transferring Data
一旦套接字连接到对等点,您就可以使用普通的读写操作(请参阅输入和输出原语)来传输数据。套接字是双向通信通道,因此可以在任一端执行读取和写入操作。
还有一些特定于套接字操作的 I/O 模式。为了指定这些模式,您必须使用 recv 和 send 函数而不是更通用的 read 和 write 函数。recv 和 send 函数带有一个额外的参数,您可以使用它来指定各种标志来控制特殊的 I/O 模式。例如,您可以指定 MSG_OOB 标志来读取或写入带外数据,指定 MSG_PEEK 标志来查看输入,或指定 MSG_DONTROUTE 标志来控制在输出中包含路由信息。
2.9.5.1. 发送数据
Sending Data
send 函数在头文件 sys/socket.h 中声明。如果您的 flags 参数为零,您也可以使用 write 而不是 send;请参阅输入和输出原语。如果套接字已连接但连接已断开,您会收到一个 SIGPIPE 信号,用于发送或写入的任何使用(请参阅杂项信号)。
函数:ssize_t send (int socket, const void *buffer, size_t size, int flags)
Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.
send 函数类似于 write,但带有额外的 flags 标志。标志的可能值在套接字数据选项中描述。
此函数返回传输的字节数,如果失败则返回 -1。如果套接字是非阻塞的,则发送(如写入)可以在仅发送部分数据后返回。有关非阻塞模式的信息,请参阅文件状态标志。
但是请注意,成功的返回值仅表示消息已发送且没有错误,不一定表示已接收且没有错误。
为此函数定义了以下 errno 错误条件:
EBADF
套接字参数不是有效的文件描述符。
EINTR
在发送任何数据之前,操作被信号中断。请参阅被信号中断的原语。
ENOTSOCK
描述符套接字不是套接字。
EMSGSIZE
套接字类型要求消息以原子方式发送,但消息太大而无法实现。
EWOULBLOCK
套接字上设置了非阻塞模式,写操作将阻塞。(通常发送块,直到操作可以完成。)
ENOBUFS
没有足够的内部缓冲区可用空间。
ENOTCONN
你从来没有连接过这个插座。
EPIPE
此套接字已连接,但连接现在已断开。在这种情况下,send 先生成一个 SIGPIPE 信号;如果该信号被忽略或阻塞,或者如果其处理程序返回,则发送失败并显示 EPIPE。
该函数被定义为多线程程序中的取消点,因此必须为此做好准备,并确保即使线程被取消,分配的资源(如内存、文件描述符、信号量或其他)也被释放。
2.9.5.2. 接收数据
Receiving Data
recv 函数在头文件 sys/socket.h 中声明。如果您的 flags 参数为零,您也可以使用 read 而不是 recv;请参阅输入和输出原语。
函数:ssize_t recv (int socket, void *buffer, size_t size, int flags)
Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.
recv 函数与 read 类似,但带有额外的 flags 标志。标志的可能值在套接字数据选项中描述。
如果 socket 设置了非阻塞模式,并且没有数据可以读取,recv 会立即失败而不是等待。有关非阻塞模式的信息,请参阅文件状态标志。
此函数返回接收到的字节数,如果失败则返回 -1。为此函数定义了以下 errno 错误条件:
EBADF
套接字参数不是有效的文件描述符。
ENOTSOCK
描述符套接字不是套接字。
EWOULBLOCK
套接字上已设置非阻塞模式,读取操作将阻塞。(通常,recv 会阻塞,直到有可供读取的输入。)
EINTR
在读取任何数据之前,操作被信号中断。请参阅被信号中断的原语。
ENOTCONN
你从来没有连接过这个插座。
该函数被定义为多线程程序中的取消点,因此必须为此做好准备,并确保即使线程被取消,分配的资源(如内存、文件描述符、信号量或其他)也被释放。
2.9.5.3. 套接字数据选项
Socket Data Options
send 和 recv 的 flags 参数是位掩码。您可以将以下宏的值按位或一起获得此参数的值。所有都在头文件 sys/socket.h 中定义。
宏:int MSG_OOB
发送或接收带外数据。请参阅带外数据。
宏:int MSG_PEEK
查看数据,但不要将其从输入队列中删除。这仅对诸如 recv 之类的输入函数有意义,对 send 无效。
宏:int MSG_DONTROUTE
不要在消息中包含路由信息。这仅对输出操作有意义,并且通常仅对诊断或路由程序感兴趣。我们不试图在这里解释它。
2.9.6. 字节流套接字示例
Byte Stream Socket Example
这是一个示例客户端程序,它为 Internet 命名空间中的字节流套接字建立连接。一旦连接到服务器,它就不会做任何特别有趣的事情;它只是向服务器发送一个文本字符串并退出。
本程序使用 init_sockaddr 设置套接字地址;请参阅 Internet 套接字示例。
#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#define PORT 5555
#define MESSAGE "Yow!!! Are we having fun yet?!?"
#define SERVERHOST "www.gnu.org"
void
write_to_server (int filedes)
{
int nbytes;
nbytes = write (filedes, MESSAGE, strlen (MESSAGE) + 1);
if (nbytes < 0)
{
perror ("write");
exit (EXIT_FAILURE);
}
}
int
main (void)
{
extern void init_sockaddr (struct sockaddr_in *name,
const char *hostname,
uint16_t port);
int sock;
struct sockaddr_in servername;
/* Create the socket. */
sock = socket (PF_INET, SOCK_STREAM, 0);
if (sock < 0)
{
perror ("socket (client)");
exit (EXIT_FAILURE);
}
/* Connect to the server. */
init_sockaddr (&servername, SERVERHOST, PORT);
if (0 > connect (sock,
(struct sockaddr *) &servername,
sizeof (servername)))
{
perror ("connect (client)");
exit (EXIT_FAILURE);
}
/* Send data to the server. */
write_to_server (sock);
close (sock);
exit (EXIT_SUCCESS);
}
2.9.7. 字节流连接服务器示例
Byte Stream Connection Server Example
服务器端要复杂得多。由于我们希望允许多个客户端同时连接到服务器,因此仅通过调用 read 或 recv 来等待来自单个客户端的输入是不正确的。相反,正确的做法是使用 select(请参阅等待输入或输出)等待所有打开的套接字上的输入。这也允许服务器处理额外的连接请求。
一旦从客户端收到消息,这个特定的服务器就不会做任何有趣的事情。当它检测到文件结束条件(由于客户端关闭其连接端)时,它会关闭该客户端的套接字。
本程序使用make_socket设置socket地址;请参阅 Internet 套接字示例。
#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#define PORT 5555
#define MAXMSG 512
int
read_from_client (int filedes)
{
char buffer[MAXMSG];
int nbytes;
nbytes = read (filedes, buffer, MAXMSG);
if (nbytes < 0)
{
/* Read error. */
perror ("read");
exit (EXIT_FAILURE);
}
else if (nbytes == 0)
/* End-of-file. */
return -1;
else
{
/* Data read. */
fprintf (stderr, "Server: got message: `%s'\n", buffer);
return 0;
}
}
int
main (void)
{
extern int make_socket (uint16_t port);
int sock;
fd_set active_fd_set, read_fd_set;
int i;
struct sockaddr_in clientname;
size_t size;
/* Create the socket and set it up to accept connections. */
sock = make_socket (PORT);
if (listen (sock, 1) < 0)
{
perror ("listen");
exit (EXIT_FAILURE);
}
/* Initialize the set of active sockets. */
FD_ZERO (&active_fd_set);
FD_SET (sock, &active_fd_set);
while (1)
{
/* Block until input arrives on one or more active sockets. */
read_fd_set = active_fd_set;
if (select (FD_SETSIZE, &read_fd_set, NULL, NULL, NULL) < 0)
{
perror ("select");
exit (EXIT_FAILURE);
}
/* Service all the sockets with input pending. */
for (i = 0; i < FD_SETSIZE; ++i)
if (FD_ISSET (i, &read_fd_set))
{
if (i == sock)
{
/* Connection request on original socket. */
int new;
size = sizeof (clientname);
new = accept (sock,
(struct sockaddr *) &clientname,
&size);
if (new < 0)
{
perror ("accept");
exit (EXIT_FAILURE);
}
fprintf (stderr,
"Server: connect from host %s, port %hd.\n",
inet_ntoa (clientname.sin_addr),
ntohs (clientname.sin_port));
FD_SET (new, &active_fd_set);
}
else
{
/* Data arriving on an already-connected socket. */
if (read_from_client (i) < 0)
{
close (i);
FD_CLR (i, &active_fd_set);
}
}
}
}
}
2.9.8. 带外数据
Out-of-Band Data
具有连接的流允许以比普通数据更高的优先级交付的带外数据。通常,发送带外数据的原因是发送异常情况通知。要发送带外数据,请使用 send,指定标志 MSG_OOB(请参阅发送数据)。
带外数据接收优先级较高,因为接收进程不需要按顺序读取;要读取下一个可用的带外数据,请使用带有 MSG_OOB 标志的 recv(请参阅接收数据)。普通读操作不读取带外数据;他们只读取普通数据。
当一个套接字发现带外数据正在传输中时,它会向套接字的所有者进程或进程组发送一个 SIGURG 信号。您可以使用 fcntl 函数的 F_SETOWN 命令指定所有者;请参阅中断驱动输入。您还必须为此信号建立一个处理程序,如信号处理中所述,以便采取适当的操作,例如读取带外数据。
或者,您可以使用 select 函数测试待处理的带外数据,或等到有带外数据;它可以等待套接字上的异常情况。有关选择的更多信息,请参阅等待输入或输出。
带外数据的通知(无论是使用 SIGURG 还是使用 select)表明带外数据正在发送中;数据可能要到稍后才会真正到达。如果您尝试在带外数据到达之前读取它,recv 将失败并出现 EWOULDBLOCK 错误。
发送带外数据会自动在普通数据流中放置一个“标记”,显示序列中带外数据“本来应该在哪里”。当带外数据的含义是“取消到目前为止发送的所有内容”时,这很有用。以下是如何在接收过程中测试标记之前是否发送了任何普通数据:
success = ioctl (socket, SIOCATMARK, &atmark);
如果套接字的读指针已到达“标记”,则整数变量 atmark 设置为非零值。
这是一个丢弃带外标记之前的任何普通数据的函数:
int
discard_until_mark (int socket)
{
while (1)
{
/* This is not an arbitrary limit; any size will do. */
char buffer[1024];
int atmark, success;
/* If we have reached the mark, return. */
success = ioctl (socket, SIOCATMARK, &atmark);
if (success < 0)
perror ("ioctl");
if (result)
return;
/* Otherwise, read a bunch of ordinary data and discard it.
This is guaranteed not to read past the mark
if it starts before the mark. */
success = read (socket, buffer, sizeof buffer);
if (success < 0)
perror ("read");
}
}
如果您不想丢弃标记之前的普通数据,则可能无论如何都需要读取其中的一些数据,以便在内部系统缓冲区中为带外数据腾出空间。如果您尝试读取带外数据并收到 EWOULDBLOCK 错误,请尝试读取一些普通数据(保存它以便在需要时使用它)并查看是否腾出空间。这是一个例子:
struct buffer
{
char *buf;
int size;
struct buffer *next;
};
/* Read the out-of-band data from SOCKET and return it
as a ‘struct buffer’, which records the address of the data
and its size.
It may be necessary to read some ordinary data
in order to make room for the out-of-band data.
If so, the ordinary data are saved as a chain of buffers
found in the ‘next’ field of the value. */
struct buffer *
read_oob (int socket)
{
struct buffer *tail = 0;
struct buffer *list = 0;
while (1)
{
/* This is an arbitrary limit.
Does anyone know how to do this without a limit? */
#define BUF_SZ 1024
char *buf = (char *) xmalloc (BUF_SZ);
int success;
int atmark;
/* Try again to read the out-of-band data. */
success = recv (socket, buf, BUF_SZ, MSG_OOB);
if (success >= 0)
{
/* We got it, so return it. */
struct buffer *link
= (struct buffer *) xmalloc (sizeof (struct buffer));
link->buf = buf;
link->size = success;
link->next = list;
return link;
}
/* If we fail, see if we are at the mark. */
success = ioctl (socket, SIOCATMARK, &atmark);
if (success < 0)
perror ("ioctl");
if (atmark)
{
/* At the mark; skipping past more ordinary data cannot help.
So just wait a while. */
sleep (1);
continue;
}
/* Otherwise, read a bunch of ordinary data and save it.
This is guaranteed not to read past the mark
if it starts before the mark. */
success = read (socket, buf, BUF_SZ);
if (success < 0)
perror ("read");
/* Save this data in the buffer list. */
{
struct buffer *link
= (struct buffer *) xmalloc (sizeof (struct buffer));
link->buf = buf;
link->size = success;
/* Add the new link to the end of the list. */
if (tail)
tail->next = link;
else
list = link;
tail = link;
}
}
}
2.10. 数据报套接字操作
Datagram Socket Operations
本节介绍如何使用不使用连接的通信样式(样式 SOCK_DGRAM 和 SOCK_RDM)。使用这些样式,您可以将数据分组为数据包,每个数据包都是一个独立的通信。您单独指定每个数据包的目的地。
数据报包就像字母:您单独发送每个带有自己的目标地址的包,它们可能以错误的顺序到达或根本不到达。
使用无连接通信方式的套接字不允许监听和接受函数。
2.10.1. 发送数据报
Sending Datagrams
在数据报套接字上发送数据的常规方法是使用在 sys/socket.h 中声明的 sendto 函数。
您可以在数据报套接字上调用 connect,但这仅指定套接字上进一步数据传输的默认目标。当套接字具有默认目标时,您可以使用 send(请参阅发送数据)甚至 write(请参阅输入和输出原语)在此处发送数据包。您可以通过在 addr 参数中使用 AF_UNSPEC 的地址格式调用 connect 来取消默认目标。有关连接功能的更多信息,请参阅建立连接。
函数:ssize_t sendto (int socket, const void *buffer, size_t size, int flags, struct sockaddr *addr, socklen_t length)
Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.
sendto 函数通过套接字套接字将缓冲区中的数据传输到由 addr 和 length 参数指定的目标地址。size 参数指定要传输的字节数。
标志的解释方式与发送相同;请参阅套接字数据选项。
返回值和错误条件也和send一样,但是不能依赖系统检测错误并上报;最常见的错误是数据包丢失或指定地址没有人接收它,而您机器上的操作系统通常不知道这一点。
由于与先前调用有关的问题,一次调用 sendto 也可能报告错误。
该函数被定义为多线程程序中的取消点,因此必须为此做好准备,并确保即使线程被取消,分配的资源(如内存、文件描述符、信号量或其他)也被释放。
2.10.2. 接收数据报
Receiving Datagrams
recvfrom 函数从数据报套接字读取数据包,并告诉您它是从哪里发送的。该函数在 sys/socket.h 中声明。
函数:ssize_t recvfrom (int socket, void *buffer, size_t size, int flags, struct sockaddr *addr, socklen_t *length-ptr)
Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.
recvfrom 函数从套接字套接字读取一个数据包到缓冲区缓冲区。size 参数指定要读取的最大字节数。
如果数据包长于 size 字节,则您将获得数据包的第一个 size 字节,而其余的数据包将丢失。没有办法读取数据包的其余部分。因此,当您使用数据包协议时,您必须始终知道数据包的预期时间。
addr 和 length-ptr 参数用于返回数据包的来源地址。请参阅套接字地址。对于本地域中的套接字,地址信息将没有意义,因为您无法读取此类套接字的地址(请参阅本地命名空间)。如果您对此信息不感兴趣,可以指定空指针作为 addr 参数。
这些标志的解释方式与 recv 相同(请参阅套接字数据选项)。返回值和错误条件也与recv相同。
该函数被定义为多线程程序中的取消点,因此必须为此做好准备,并确保即使线程被取消,分配的资源(如内存、文件描述符、信号量或其他)也被释放。
如果您不需要找出谁发送了数据包(因为您知道它应该来自哪里,或者因为您对所有可能的发送者都一视同仁),您可以使用普通的 recv(请参阅接收数据)而不是 recvfrom。如果您不想指定标志,甚至可以使用 read(请参阅输入和输出原语)。
2.10.3. 数据报套接字示例
Datagram Socket Example
这是一组通过本地命名空间中的数据报流发送消息的示例程序。客户端和服务器程序都使用本地命名空间套接字示例中介绍的 make_named_socket 函数来创建和命名它们的套接字。
首先,这是服务器程序。它处于等待消息到达的循环中,将每条消息弹回给发送者。显然这不是一个特别有用的程序,但它确实显示了所涉及的一般思想。
#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/un.h>
#define SERVER "/tmp/serversocket"
#define MAXMSG 512
int
main (void)
{
int sock;
char message[MAXMSG];
struct sockaddr_un name;
size_t size;
int nbytes;
/* Remove the filename first, it’s ok if the call fails */
unlink (SERVER);
/* Make the socket, then loop endlessly. */
sock = make_named_socket (SERVER);
while (1)
{
/* Wait for a datagram. */
size = sizeof (name);
nbytes = recvfrom (sock, message, MAXMSG, 0,
(struct sockaddr *) & name, &size);
if (nbytes < 0)
{
perror ("recfrom (server)");
exit (EXIT_FAILURE);
}
/* Give a diagnostic message. */
fprintf (stderr, "Server: got message: %s\n", message);
/* Bounce the message back to the sender. */
nbytes = sendto (sock, message, nbytes, 0,
(struct sockaddr *) & name, size);
if (nbytes < 0)
{
perror ("sendto (server)");
exit (EXIT_FAILURE);
}
}
}
2.10.4. 读取数据报示例
Example of Reading Datagrams
这里是上面服务器对应的客户端程序。
它向服务器发送一个数据报,然后等待回复。请注意,此示例中客户端(以及服务器)的套接字必须指定一个名称。这样服务器就可以将消息引导回客户端。由于套接字没有关联的连接状态,服务器可以做到这一点的唯一方法是引用客户端的名称。
#include <stdio.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/un.h>
#define SERVER "/tmp/serversocket"
#define CLIENT "/tmp/mysocket"
#define MAXMSG 512
#define MESSAGE "Yow!!! Are we having fun yet?!?"
int
main (void)
{
extern int make_named_socket (const char *name);
int sock;
char message[MAXMSG];
struct sockaddr_un name;
size_t size;
int nbytes;
/* Make the socket. */
sock = make_named_socket (CLIENT);
/* Initialize the server socket address. */
name.sun_family = AF_LOCAL;
strcpy (name.sun_path, SERVER);
size = strlen (name.sun_path) + sizeof (name.sun_family);
/* Send the datagram. */
nbytes = sendto (sock, MESSAGE, strlen (MESSAGE) + 1, 0,
(struct sockaddr *) & name, size);
if (nbytes < 0)
{
perror ("sendto (client)");
exit (EXIT_FAILURE);
}
/* Wait for a reply. */
nbytes = recvfrom (sock, message, MAXMSG, 0, NULL, 0);
if (nbytes < 0)
{
perror ("recfrom (client)");
exit (EXIT_FAILURE);
}
/* Print a diagnostic message. */
fprintf (stderr, "Client: got message: %s\n", message);
/* Clean up. */
remove (CLIENT);
close (sock);
}
请记住,数据报套接字通信是不可靠的。在这个例子中,如果消息永远不会到达服务器或者服务器的响应永远不会返回,客户端程序会无限期地等待。如果需要,由运行程序的用户决定是否终止并重新启动它。更自动的解决方案是使用 select(请参阅等待输入或输出)来为回复建立超时期限,如果超时,则重新发送消息或关闭套接字并退出。
2.11. inetd 守护进程
The inetd Daemon
我们已经在上面解释了如何编写一个自己监听的服务器程序。这样的服务器必须已经在运行,任何人都可以连接到它。
在 Internet 端口上提供服务的另一种方法是让守护程序 inetd 进行侦听。inetd 是一个始终运行并等待(使用 select)指定端口集上的消息的程序。当它接收到消息时,它接受连接(如果套接字样式要求连接),然后分叉一个子进程来运行相应的服务器程序。您可以在文件 /etc/inetd.conf 中指定端口及其程序。
2.11.1. inetd 服务器
inetd Servers
编写一个由inetd 运行的服务器程序非常简单。每次有人请求连接到适当的端口时,都会启动一个新的服务器进程。此时连接已经存在;套接字可用作服务器进程中的标准输入描述符和标准输出描述符(描述符 0 和 1)。因此,服务器程序可以立即开始读取和写入数据。通常程序只需要普通的 I/O 设施;事实上,一个对套接字一无所知的通用过滤程序可以作为由inetd运行的字节流服务器。
您还可以将 inetd 用于使用无连接通信方式的服务器。对于这些服务器,inetd 不会尝试接受连接,因为无法连接。它只是启动服务器程序,它可以从描述符0读取传入的数据报包。服务器程序可以处理一个请求然后退出,或者您可以选择编写它以继续读取更多请求,直到没有更多请求到达,然后退出。当您配置 inetd 时,您必须指定服务器使用这两种技术中的哪一种。
2.11.2. 配置 inetd
Configuring inetd
/etc/inetd.conf 文件告诉inetd 监听哪些端口以及为它们运行哪些服务器程序。通常文件中的每个条目都是一行,但您可以将其拆分为多行,前提是条目的第一行以外的所有内容都以空格开头。以“#”开头的行是注释。
以下是 /etc/inetd.conf 中的两个标准条目:
ftp stream tcp nowait root /libexec/ftpd ftpd
talk dgram udp wait root /libexec/talkd talkd
条目具有以下格式:
service style protocol wait username program arguments
服务字段说明该程序提供的服务。它应该是 /etc/services 中定义的服务的名称。inetd 使用 service 来决定在哪个端口上侦听此条目。
字段样式和协议指定用于侦听套接字的通信样式和协议。样式应该是通信样式的名称,转换为小写并删除“SOCK_”,例如“stream”或“dgram”。协议应该是 /etc/protocols 中列出的协议之一。典型的协议名称是用于字节流连接的“tcp”和用于不可靠数据报的“udp”。
等待字段应该是“wait”或“nowait”。如果 style 是无连接样式,并且服务器一旦启动,就会在多个请求进入时处理它们,使用“等待”。如果 inetd 应该为每个传入的消息或请求启动一个新进程,则使用“nowait”。如果 style 使用连接,那么等待必须是’nowait’。
user 是服务器应该运行的用户名。inetd 以 root 身份运行,因此它可以任意设置其子项的用户 ID。如果可以的话,最好避免对用户使用“root”;但是某些服务器,例如 Telnet 和 FTP,会自行读取用户名和密码。这些服务器最初需要是根用户,以便他们可以按照来自网络的数据的命令登录。
program 与参数一起指定要运行以启动服务器的命令。program 应该是一个绝对文件名,指定要运行的可执行文件。arguments 由任意数量的空格分隔的单词组成,这些单词成为程序的命令行参数。参数中的第一个词是参数零,按照惯例,它应该是程序名称本身(无目录)。
如果您编辑 /etc/inetd.conf,您可以告诉 inetd 重新读取文件并通过向 inetd 进程发送 SIGHUP 信号来遵守其新内容。您必须使用 ps 来确定 inetd 进程的进程 ID,因为它不是固定的。
2.12. 套接字选项
Socket Options
本节介绍如何读取或设置修改套接字行为及其底层通信协议的各种选项。
操作套接字选项时,必须指定选项所属的级别。这描述了该选项是适用于套接字接口,还是适用于较低级别的通信协议接口。
2.12.1. 套接字选项函数
Socket Option Functions
以下是检查和修改套接字选项的函数。它们在 sys/socket.h 中声明。
函数:int getsockopt (int socket, int level, int optname, void *optval, socklen_t *optlen-ptr)
Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.
getsockopt 函数获取有关套接字套接字级别级别选项 optname 的值的信息。
选项值存储在 optval 指向的缓冲区中。在调用之前,您应该在 *optlen-ptr 中提供此缓冲区的大小;返回时,它包含实际存储在缓冲区中的信息字节数。
大多数选项将 optval 缓冲区解释为单个 int 值。
getsockopt 的实际返回值为 0 表示成功,-1 表示失败。定义了以下 errno 错误条件:
EBADF
套接字参数不是有效的文件描述符。
ENOTSOCK
描述符套接字不是套接字。
ENOPROTOOPT
optname 对于给定的级别没有意义。
函数:int setsockopt (int socket, int level, int optname, const void *optval, socklen_t optlen)
Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.
此函数用于设置套接字套接字的级别级别的套接字选项 optname。选项的值在大小为 optlen 的缓冲区 optval 中传递。
2.12.2. 套接字级选项
Socket-Level Options
常量:int SOL_SOCKET
使用此常量作为 getsockopt 或 setsockopt 的级别参数来操作本节中描述的套接字级别选项。
这是一个套接字级选项名称表;都在头文件 sys/socket.h 中定义。
SO_DEBUG
此选项切换底层协议模块中调试信息的记录。该值的类型为 int;非零值表示“是”。
SO_REUSEADDR
此选项控制绑定(请参阅设置套接字地址)是否应允许重用此套接字的本地地址。如果启用此选项,您实际上可以拥有两个具有相同 Internet 端口号的套接字;但是系统不允许您以会混淆 Internet 的方式使用两个同名套接字。此选项的原因是某些更高级别的 Internet 协议(包括 FTP)要求您不断重复使用相同的端口号。
该值的类型为 int;非零值表示“是”。
SO_KEEPALIVE
此选项控制底层协议是否应定期在连接的套接字上传输消息。如果对等方未能响应这些消息,则认为连接已断开。该值的类型为 int;非零值表示“是”。
SO_DONTROUTE
此选项控制传出消息是否绕过正常的消息路由工具。如果设置,消息将直接发送到网络接口。该值的类型为 int;非零值表示“是”。
SO_LINGER
此选项指定当承诺可靠传递的类型的套接字在关闭时仍有未传输的消息时应该发生什么;请参阅关闭套接字。该值的类型为 struct linger。
数据类型:struct linger
此结构类型具有以下成员:
int l_onoff
该字段被解释为布尔值。如果非零,则关闭块,直到数据传输完毕或超时期限到期。
int l_linger
这指定了超时时间,以秒为单位。
SO_BROADCAST
此选项控制是否可以从套接字广播数据报。该值的类型为 int;非零值表示“是”。
SO_OOBINLINE
如果设置了此选项,则在套接字上接收到的带外数据将放置在正常输入队列中。这允许在不指定 MSG_OOB 标志的情况下使用 read 或 recv 读取它。请参阅带外数据。该值的类型为 int;非零值表示“是”。
SO_SNDBUF
此选项获取或设置输出缓冲区的大小。该值是 size_t,它是以字节为单位的大小。
SO_RCVBUF
此选项获取或设置输入缓冲区的大小。该值是 size_t,它是以字节为单位的大小。
SO_STYLE
SO_TYPE
此选项只能与 getsockopt 一起使用。它用于获取套接字的通信方式。SO_TYPE 是历史名称,SO_STYLE 是 GNU 中的首选名称。该值具有 int 类型,其值指定一种通信方式;请参阅通信方式。
SO_ERROR
此选项只能与 getsockopt 一起使用。它用于重置套接字的错误状态。该值是一个 int,表示之前的错误状态。
2.13. 网络数据库
Networks Database
许多系统都带有一个数据库,该数据库记录系统开发人员已知的网络列表。这通常保存在文件 /etc/networks 或名称服务器的等效文件中。该数据库对于路由程序(例如 route)很有用,但对于仅通过网络通信的程序就没有用处。我们提供了访问该数据库的函数,这些函数在 netdb.h 中声明。
数据类型:struct netent
此数据类型用于表示有关网络数据库中条目的信息。它有以下成员:
char *n_name
这是网络的“官方”名称。
char **n_aliases
这些是网络的替代名称,表示为字符串向量。空指针终止数组。
int n_addrtype
这是网络号的类型;对于 Internet 网络,这始终等于 AF_INET。
unsigned long int n_net
这是网络号。网络号按主机字节顺序返回;请参阅字节顺序转换。
使用 getnetbyname 或 getnetbyaddr 函数在网络数据库中搜索有关特定网络的信息。信息以静态分配的结构返回;如果需要保存,您必须复制信息。
函数:struct netent * getnetbyname (const char *name)
Preliminary: | MT-Unsafe race:netbyname env locale | AS-Unsafe dlopen plugin heap lock | AC-Unsafe corrupt lock fd mem | See POSIX Safety Concepts.
getnetbyname 函数返回有关名为 name 的网络的信息。如果没有这样的网络,它会返回一个空指针。
函数:struct netent * getnetbyaddr (uint32_t net, int type)
Preliminary: | MT-Unsafe race:netbyaddr locale | AS-Unsafe dlopen plugin heap lock | AC-Unsafe corrupt lock fd mem | See POSIX Safety Concepts.
getnetbyaddr 函数返回类型为 net 的类型网络的信息。您应该为 Internet 网络的类型参数指定 AF_INET 的值。
如果没有这样的网络,getnetbyaddr 返回一个空指针。
您还可以使用 setnetent、getnetent 和 endnetent 扫描网络数据库。使用这些函数时要小心,因为它们不可重入。
函数:void setnetent (int stayopen)
Preliminary: | MT-Unsafe race:netent env locale | AS-Unsafe dlopen plugin heap lock | AC-Unsafe corrupt lock fd mem | See POSIX Safety Concepts.
此功能打开和倒带网络数据库。
如果 stayopen 参数不为零,这将设置一个标志,以便后续调用 getnetbyname 或 getnetbyaddr 不会关闭数据库(如通常那样)。如果您多次调用这些函数,这样可以提高效率,避免每次调用都重新打开数据库。
函数:struct netent * getnetent (void)
Preliminary: | MT-Unsafe race:netent race:netentbuf env locale | AS-Unsafe dlopen plugin heap lock | AC-Unsafe corrupt lock fd mem | See POSIX Safety Concepts.
此函数返回网络数据库中的下一个条目。如果没有更多条目,它将返回一个空指针。
函数:void endnetent (void)
Preliminary: | MT-Unsafe race:netent env locale | AS-Unsafe dlopen plugin heap lock | AC-Unsafe corrupt lock fd mem | See POSIX Safety Concepts.
该函数关闭网络数据库。