编程接口指南

高级套接字主题

对于大多数程序员而言,前面介绍的机制足以用来生成分布式应用程序。本节介绍其他功能。

带外数据

流套接字概念包括带外数据。带外数据是一对连接的流套接字之间的逻辑上独立的传输通道。带外数据的传送独立于一般数据。带外数据功能必须支持每次至少可靠传送一条带外消息。此消息至少可以包含一个字节的数据。可以随时暂挂传送至少一条消息。

使用带内信号发送时,紧急数据将与一般数据一同按顺序传送,并从一般数据流中提取消息。提取的消息将单独存储。用户可以在顺序接收紧急数据与无序接收紧急数据之间进行选择,而不必缓冲中间数据。

使用 MSG_PEEK,可以查看带外数据。如果套接字具有进程组,则在通知协议其存在时会生成 SIGURG 信号。进程可以将进程组或进程 ID 设置为使用适当的 fcntl(2) 调用传送 SIGURG,如 SIGIO中断驱动套接字 I/O中所述。如果多个套接字都具有等待传送的带外数据,则用于例外情况的 select(3C) 调用可以确定哪些套接字具有此类数据暂挂。

逻辑标记位于数据流中发送带外数据的位置。远程登录和远程 shell 应用程序使用此功能在客户机进程与服务器进程之间传播信号。接收到信号之后,将废弃数据流中标记之前的所有数据。

要发送带外消息,请将 MSG_OOB 标志应用于 send(3SOCKET)sendto(3SOCKET)。要接收带外数据,请将 MSG_OOB 指定给 recvfrom(3SOCKET)recv(3SOCKET)。如果带外数据是内嵌数据,则不需要 MSG_OOB 标志。SIOCATMARK ioctl(2) 指示读取指针当前是否指向数据流中的标记:

int yes;

ioctl(s, SIOCATMARK, &yes);

如果返回时 yes1,则下次读取将返回标记之后的数据。否则,假设带外数据已经到达,下次读取将提供由客户机在发送带外信号之前发送的数据。以下示例给出了远程登录进程中接收中断或退出信号时刷新输出的例程。此代码读取标记之前的一般数据以废弃这些一般数据,然后读取带外字节。

进程还可以读取或查看带外数据,而无需首先读取标记之前的数据。当底层协议将紧急数据与一般数据一起带内传送,并仅提前发送其存在的通知时,很难访问此数据。此类型协议的示例为 TCP,此协议用于在 Internet 系列中提供套接字流。如果使用此类协议,则使用 MSG_OOB 标志调用 recv(3SOCKET) 时,带外字节可能尚未到达。在这种情况下,此调用会返回 EWOULDBLOCK 错误。此外,输入缓冲区中的带内数据量可能会导致正常流控制阻止对等方发送紧急数据,直到清除缓冲区为止。然后,在对等方可以发送紧急数据之前,此进程必须读取足够的排队数据以清除输入缓冲区。


示例 7–10 接收带外数据时刷新终端 I/O

#include <sys/ioctl.h>

#include <sys/file.h>

...

oob()

{

  int out = FWRITE;

  char waste[BUFSIZ];

  int mark = 0;

 

  /* flush local terminal output */

  ioctl(1, TIOCFLUSH, (char *) &out);

  while(1) {

   if (ioctl(rem, SIOCATMARK, &mark) == -1) {

    perror("ioctl");

    break;

   }

   if (mark)

    break;

   (void) read(rem, waste, sizeof waste);

  }

  if (recv(rem, &mark, 1, MSG_OOB) == -1) {

   perror("recv");

   ...

  }

  ...

}

用于保留套接字流中紧急内嵌数据位置的工具可用作套接字级别选项 SO_OOBINLINE。有关使用情况,请参见 getsockopt(3SOCKET)。使用此套接字级别选项,可以保留紧急数据的位置。但是,将返回一般数据流中标记之后的紧急数据,而不带 MSG_OOB 标志。接收多个紧急指示时将移动标记,但是不会丢失任何带外数据。

非阻止套接字

某些应用程序需要不执行阻止的套接字。例如,服务器可能返回错误代码,不执行无法立即完成的请求。此错误可能会导致进程暂停,等待完成。创建并连接套接字之后,发出 fcntl(2) 调用(如以下示例所示)使此套接字变为非阻止套接字。


示例 7–11 设置非阻止套接字

#include <fcntl.h>

#include <sys/file.h>

...

int fileflags;

int s;

...

s = socket(AF_INET6, SOCK_STREAM, 0);

...

if (fileflags = fcntl(s, F_GETFL, 0) == -1)

  perror("fcntl F_GETFL");

  exit(1);

}

if (fcntl(s, F_SETFL, fileflags | FNDELAY) == -1)

  perror("fcntl F_SETFL, FNDELAY");

  exit(1);

}

...

在非阻塞套接字上执行 I/O 时,请检查errno.h 中的错误 EWOULDBLOCK,此错误通常在操作阻塞时发生。accept(3SOCKET)connect(3SOCKET)send(3SOCKET)recv(3SOCKET)read(2) 以及 write(2) 均可返回 EWOULDBLOCK。如果某操作(如 send(3SOCKET))不能全部完成,但是部分写入有效(如使用流套接字时),则会处理所有可用的数据。返回值是实际发送的数据量。

异步套接字 I/O

在同时处理多个请求的应用程序中,要求在进程之间进行异步通信。异步套接字的类型必须为 SOCK_STREAM。要使套接字异步,请发出 fcntl(2) 调用,如以下示例所示。


示例 7–12 使套接字异步

#include <fcntl.h>

#include <sys/file.h>

...

int fileflags;

int s;

...

s = socket(AF_INET6, SOCK_STREAM, 0);

...

if (fileflags = fcntl(s, F_GETFL ) == -1)

  perror("fcntl F_GETFL");

  exit(1);

}

if (fcntl(s, F_SETFL, fileflags | FNDELAY | FASYNC) == -1)

  perror("fcntl F_SETFL, FNDELAY | FASYNC");

  exit(1);

}

...

在初始化、连接套接字并使其变为非阻止的异步套接字之后,通信类似于异步读写文件。可以使用 send(3SOCKET)write(2)recv(3SOCKET)read(2) 来启动数据传送。信号驱动的 I/O 例程将完成数据传送,如下节中所述。

中断驱动套接字 I/O

SIGIO 信号会在套接字或任何文件描述符完成数据传送时通知进程。使用 SIGIO 的步骤如下所示:

  1. 使用 signal(3C)sigvec(3UCB) 调用设置 SIGIO 信号处理程序。

  2. 使用 fcntl(2) 设置进程 ID 或进程组 ID,以将信号路由到其自己的进程 ID 或进程组 ID。套接字的缺省进程组为组 0

  3. 将套接字转换为异步,如异步套接字 I/O中所示。

使用以下样例代码,给定进程可以在发生套接字请求时接收有关暂挂请求的信息。添加 SIGURG 的处理程序之后,还可以使用此代码来准备接收 SIGURG 信号。


示例 7–13 异步 I/O 请求通知

#include <fcntl.h>

#include <sys/file.h>

 ...

signal(SIGIO, io_handler);

/* Set the process receiving SIGIO/SIGURG signals to us. */

if (fcntl(s, F_SETOWN, getpid()) < 0) {

  perror("fcntl F_SETOWN");

  exit(1);

}

信号和进程组 ID

对于 SIGURGSIGIO,每个套接字都具有一个进程号和进程组 ID。这些值初始化为零,但是可以在稍后使用 F_SETOWN fcntl(2) 命令重新定义,如前面示例中所示。fcntl(2) 采用正值的第三个参数可以设置套接字的进程 ID。fcntl(2) 采用负值的第三个参数可以设置套接字的进程组 ID。SIGURGSIGIO 信号的唯一允许接受者是调用进程。 类似的 fcntl(2)(即 F_GETOWN)将返回套接字的进程号。

还可以通过使用 ioctl(2) 将套接字指定给用户的进程组来接收 SIGURGSIGIO

/* oobdata is the out-of-band data handling routine */

sigset(SIGURG, oobdata);

int pid = -getpid();

if (ioctl(client, SIOCSPGRP, (char *) &pid) < 0) {

  perror("ioctl: SIOCSPGRP");

}

选择特定的协议

如果 socket(3SOCKET) 调用的第三个参数为 0,则 socket(3SOCKET) 会选择缺省协议以用于所请求类型的返回套接字。缺省协议通常是正确的,而备用选项通常不可用。使用原始套接字与较低级别协议或较低级别硬件接口进行直接通信时,请使用协议参数设置解复用 (de-multiplexing)。

使用 Internet 系列中的原始套接字在 IP 上实现新协议,可以确保套接字只接收指定协议的包。要获取特定协议,请确定协议族中定义的协议号。对于 Internet 系列,使用标准例程中介绍的库例程之一,如 getprotobyname(3SOCKET)

#include <sys/types.h>

#include <sys/socket.h>

#include <netinet/in.h>

#include <netdb.h>

 ...

pp = getprotobyname("newtcp");

s = socket(AF_INET6, SOCK_STREAM, pp->p_proto);

借助基于流的连接,使用 getprotobyname 会形成套接字 s,但是协议类型为 newtcp,而不是缺省的 tcp

地址绑定

对于寻址,TCP 和 UDP 使用 4 元组:

TCP 要求这些 4 元组具有唯一性。UDP 则不要求。用户程序并非总是了解用于本地地址和本地端口的正确值,因为主机可能驻留在多个网络中。用户不能直接访问已分配的端口号集合。要避免这些问题,请不要指定地址部分,让系统在需要时适当指定这些部分。这些元组的不同部分可以由套接字 API 的不同部分指定:

bind(3SOCKET)

本地地址和/或本地端口

connect(3SOCKET)

外部地址和外部端口

调用 accept(3SOCKET) 可以从外部客户机检索连接信息。这样,即使 accept(3SOCKET) 调用方并未进行任何指定,也会为系统指定本地地址和端口。将返回外部地址和外部端口。

调用 listen(3SOCKET) 可以导致选择本地端口。 如果尚未完成显式 bind(3SOCKET) 以指定本地信息,则 listen(3SOCKET) 会指定暂时端口号。

驻留在特定端口的服务可以使用 bind(3SOCKET) 绑定到此端口。如果服务不需要本地地址信息,则此类服务可以不指定本地地址。将本地地址设置为 in6addr_any<netinet/in.h> 中具有常量值的一个变量)。如果不需要固定本地端口,则调用 listen(3SOCKET) 可以选择端口。 指定地址 in6addr_any 或端口号 0 的过程称为设置通配符。对于 AF_INET,使用 INADDR_ANY 替代 in6addr_any

通配符地址简化了 Internet 系列中的本地地址绑定。以下样例代码将通过调用 getaddrinfo(3SOCKET) 所返回的特定端口号绑定到套接字,并且未指定本地地址:

#include <sys/types.h>

#include <netinet/in.h>

...

    struct addrinfo  *aip;

...

    if (bind(sock, aip->ai_addr, aip->ai_addrlen) == -1) {

        perror("bind");

        (void) close(sock);

        return (-1);

    }

  

主机上的每个网络接口通常都具有唯一的 IP 地址。带有通配符本地地址的套接字可以接收定向到指定端口号的消息。带有通配符本地地址的套接字还可以接收发送到指定给主机的任何可能地址的消息。要仅允许特定网络中的主机连接到服务器,服务器应绑定相应网络中接口的地址。

同样,也可以不指定本地端口号,此时系统将选择一个端口号。例如,要将特定本地地址绑定到套接字,但是不指定本地端口号,可以按如下方式使用 bind

bzero (&sin, sizeof (sin));

(void) inet_pton (AF_INET6, "::ffff:127.0.0.1", sin.sin6_addr.s6_addr);

sin.sin6_family = AF_INET6;

sin.sin6_port = htons(0);

bind(s, (struct sockaddr *) &sin, sizeof sin);

系统使用两个条件来选择本地端口号:

可以通过 accept(3SOCKET)getpeername(3SOCKET) 查找客户机的端口号和 IP 地址。

在某些情况下,由于关联的创建过程分为两个步骤,因此系统用来选择端口号的算法不适用于应用程序。例如,Internet 文件传输协议会指定数据连接必须始终源于同一本地端口。但是,连接到不同的外部端口可以避免重复关联。在这种情况下,如果先前数据连接的套接字仍然存在,则系统会禁止将相同的本地地址和本地端口号绑定到套接字。

要覆盖缺省的端口选择算法,必须在绑定地址之前执行选项调用。

 ...

int on = 1;

...

setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &on, sizeof on);

bind(s, (struct sockaddr *) &sin, sizeof sin);

通过此调用,可以绑定已处于使用状态的本地地址。此绑定并不违反唯一性要求。在连接时,系统仍会验证任何其他具有相同本地地址和本地端口的套接字不具有相同的外部地址和外部端口。如果关联已经存在,则会返回错误 EADDRINUSE

套接字选项

可以通过 setsockopt(3SOCKET)getsockopt(3SOCKET) 设置和获取多个套接字选项。例如,可以更改发送或接收缓冲区空间。这些调用的一般形式如下所示:

setsockopt(s, level, optname, optval, optlen);

getsockopt(s, level, optname, optval, optlen);

操作系统可以随时相应地调整这些值。

以下是 setsockopt(3SOCKET)getsockopt(3SOCKET) 调用的参数:

s

要应用选项的套接字

level

指定协议级别,例如由 sys/socket.h 中的符号常量 SOL_SOCKET 指示的套接字级别

optname

sys/socket.h 中定义并指定选项的符号常量

optval

指向选项值

optlen

指向选项值的长度

对于 getsockopt(3SOCKET) 而言,optlen 是一个值结果参数。此参数最初设置为 optval 所指向的存储区域的大小。返回时,此参数的值设置为已使用的存储的长度。

当某程序需要确定现有套接字的类型时,此程序应该使用 SO_TYPE 套接字选项和 getsockopt(3SOCKET) 调用来调用 inetd(1M)

#include <sys/types.h>

#include <sys/socket.h>

 

int type, size;

 

size = sizeof (int);

if (getsockopt(s, SOL_SOCKET, SO_TYPE, (char *) &type, &size) <0) {

  ...

}

getsockopt(3SOCKET) 之后,type 将设置为套接字类型的值,如 sys/socket.h 中所定义。对于数据报套接字,type 应该为 SOCK_DGRAM

inetd 守护进程

inetd(1M) 守护进程在启动时调用,并从 /etc/inet/inetd.conf 文件中获取此守护进程侦听的服务。此守护进程针对 /etc/inet/inetd.conf 中列出的每个服务创建一个套接字,将相应的端口号绑定到每个套接字。有关详细信息,请参见 inetd(1M) 手册页。

inetd(1M) 守护进程轮询每个套接字,等待向对应于此套接字的服务发出连接请求。对于 SOCK_STREAM 类型套接字,inetd(1M) 在侦听套接字时接受 (accept(3SOCKET)),派生 (fork(2)),将新套接字复制 (dup(2)) 到文件描述符 01stdinstdout),关闭其他开放式文件描述符,并执行 (exec(2)) 相应服务器。

使用 inetd(1M) 的主要优点就是未使用的服务不占用计算机资源。次要优点是 inetd(1M) 会尽力建立连接。在文件描述符 01 中,由 inetd(1M) 启动的服务器的套接字连接至其客户机。服务器可以立即进行读取、写入、发送或接收。只要服务器在适当的时候使用 fflush(3C),便可以使用 stdio 约定所提供的缓冲 I/O。

getpeername(3SOCKET) 例程将返回连接到套接字的对等方(进程)的地址。此例程在由 inetd(1M) 启动的服务器中非常有用。例如,可以使用此例程记录诸如 fec0::56:a00:20ff:fe7d:3dd2(通常用于表示客户机的 IPv6 地址)的 Internet 地址。inetd(1M) 服务器可以使用以下样例代码:

    struct sockaddr_storage name;

    int namelen = sizeof (name);

    char abuf[INET6_ADDRSTRLEN];

    struct in6_addr addr6;

    struct in_addr addr;



    if (getpeername(fd, (struct sockaddr *)&name, &namelen) == -1) {

        perror("getpeername");

        exit(1);

    } else {

        addr = ((struct sockaddr_in *)&name)->sin_addr;

        addr6 = ((struct sockaddr_in6 *)&name)->sin6_addr;

        if (name.ss_family == AF_INET) {

                (void) inet_ntop(AF_INET, &addr, abuf, sizeof (abuf));

        } else if (name.ss_family == AF_INET6 &&

                   IN6_IS_ADDR_V4MAPPED(&addr6)) {

                /* this is a IPv4-mapped IPv6 address */

                IN6_MAPPED_TO_IN(&addr6, &addr);

                (void) inet_ntop(AF_INET, &addr, abuf, sizeof (abuf));

        } else if (name.ss_family == AF_INET6) {

                (void) inet_ntop(AF_INET6, &addr6, abuf, sizeof (abuf));



        }

        syslog("Connection from %s\n", abuf);

    }

广播及确定网络配置

IPv6 不支持广播,仅 IPv4 支持广播。

数据报套接字发送的消息可以广播到已连接网络中的所有主机。此网络必须支持广播,因为系统不在软件中提供任何广播模拟。广播消息会给网络带来很高的负载,因为广播消息会强制网络中的每台主机都为其服务。通常出于以下两个原因之一使用广播:

要发送广播消息,请创建一个 Internet 数据报套接字:

s = socket(AF_INET, SOCK_DGRAM, 0);

将一个端口号绑定到此套接字:

sin.sin_family = AF_INET;

sin.sin_addr.s_addr = htonl(INADDR_ANY);

sin.sin_port = htons(MYPORT);

bind(s, (struct sockaddr *) &sin, sizeof sin);

通过将数据报发送到网络的广播地址可以仅在此网络中进行广播。 通过将数据报发送到 netinet/in.h 中定义的特殊地址 INADDR_BROADCAST 可以在所有已连接的网络中进行广播。

系统会提供一种机制来确定多条有关系统上网络接口的信息。这些信息包括 IP 地址和广播地址。SIOCGIFCONF ioctl(2) 调用会以单一的 ifconf 结构返回主机的接口配置。此结构包含一个 ifreq 结构数组。主机连接的每个网络接口所支持的所有地址族都具有自己的 ifreq 结构。

以下示例给出了 net/if.h 中定义的 ifreq 结构。


示例 7–14 net/if.h 头文件

struct ifreq {

#define IFNAMSIZ 16

char ifr_name[IFNAMSIZ]; /* if name, e.g., "en0" */

union {

  struct sockaddr ifru_addr;

  struct sockaddr ifru_dstaddr;

  char ifru_oname[IFNAMSIZ]; /* other if name */

  struct sockaddr ifru_broadaddr;

  short ifru_flags;

  int ifru_metric;

  char ifru_data[1]; /* interface dependent data */

  char ifru_enaddr[6];

} ifr_ifru;

#define ifr_addr ifr_ifru.ifru_addr

#define ifr_dstaddr ifr_ifru.ifru_dstaddr

#define ifr_oname ifr_ifru.ifru_oname

#define ifr_broadaddr ifr_ifru.ifru_broadaddr

#define ifr_flags ifr_ifru.ifru_flags

#define ifr_metric ifr_ifru.ifru_metric

#define ifr_data ifr_ifru.ifru_data

#define ifr_enaddr ifr_ifru.ifru_enaddr

};

可以获取接口配置的调用为:

/*

 * Do SIOCGIFNUM ioctl to find the number of interfaces

 *

 * Allocate space for number of interfaces found

 *

 * Do SIOCGIFCONF with allocated buffer

 *

 */

if (ioctl(s, SIOCGIFNUM, (char *)&numifs) == -1) {

        numifs = MAXIFS;

}

bufsize = numifs * sizeof(struct ifreq);

reqbuf = (struct ifreq *)malloc(bufsize);

if (reqbuf == NULL) {

        fprintf(stderr, "out of memory\n");

        exit(1);

}

ifc.ifc_buf = (caddr_t)&reqbuf[0];

ifc.ifc_len = bufsize;

if (ioctl(s, SIOCGIFCONF, (char *)&ifc) == -1) {

        perror("ioctl(SIOCGIFCONF)");

        exit(1);

}

...

}

使用此调用之后,buf 将包含一个 ifreq 结构数组。主机所连接的每个网络都具有一个关联的 ifreq 结构。这些结构的排序顺序有两种:

ifc.ifc_len 的值设置为 ifreq 结构所使用的字节数。

每个结构都有一组指示相应网络为运行或关闭、点对点或广播等等的接口标志。以下示例说明了 ioctl(2) 针对 ifreq 结构所指定的接口返回 SIOCGIFFLAGS 标志。


示例 7–15 获取接口标志

struct ifreq *ifr;

ifr = ifc.ifc_req;

for (n = ifc.ifc_len/sizeof (struct ifreq); --n >= 0; ifr++) {

   /*

    * Be careful not to use an interface devoted to an address

    * family other than those intended.

    */

   if (ifr->ifr_addr.sa_family != AF_INET)

      continue;

   if (ioctl(s, SIOCGIFFLAGS, (char *) ifr) < 0) {

      ...

   }

   /* Skip boring cases */

   if ((ifr->ifr_flags & IFF_UP) == 0 ||

      (ifr->ifr_flags & IFF_LOOPBACK) ||

      (ifr->ifr_flags & (IFF_BROADCAST | IFF_POINTOPOINT)) == 0)

      continue;

}

以下示例使用 SIOGGIFBRDADDR ioctl(2) 命令来获取接口的广播地址。


示例 7–16 接口的广播地址

if (ioctl(s, SIOCGIFBRDADDR, (char *) ifr) < 0) {

  ...

}

memcpy((char *) &dst, (char *) &ifr->ifr_broadaddr,

  sizeof ifr->ifr_broadaddr);

还可以使用 SIOGGIFBRDADDR ioctl(2) 来获取点对点接口的目标地址。

获取接口广播地址之后,使用 sendto(3SOCKET) 传输广播数据报:

sendto(s, buf, buflen, 0, (struct sockaddr *)&dst, sizeof dst);

针对主机所连接的每个接口使用 sendto(3SOCKET),前提是此接口支持广播或点对点寻址。