本章介绍套接字接口,并提供说明要点的样例程序。本章讨论以下主题:
SunOS 4 二进制兼容性介绍与 SunOS 4 环境的二进制兼容性。
套接字基础知识介绍套接字的创建、连接以及关闭。
客户机/服务器程序介绍客户机/服务器体系结构。
高级套接字主题介绍高级主题,如多点传送和异步套接字。
流控制传输协议介绍用于实现流控制传输协议 (Stream Control Transmission Protocol, SCTP) 的接口。
本章介绍的接口是多线程安全的。在多线程应用程序中可以自由调用包含套接字接口调用的应用程序。但是,请注意,并未指定应用程序可获得的并发度。
在 SunOS 4 环境中所做的两个主要更改对 SunOS 5.10 发行版同样有效。使用二进制兼容性软件包,基于 SunOS 4 的动态链接的套接字应用程序可以在 SunOS 5.10 上运行。
必须在编译行中显式指定套接字库(-lsocket 或 libsocket)。
还可能需要使用 -lsocket -lnsl(而非 -lnsl -lsocket)与 libnsl 进行链接。
必须使用套接字库重新编译所有基于 SunOS 4 套接字的应用程序,以便在 SunOS 5.10 环境中运行。
自 1981 年以来,套接字已经成为 SunOS 发行版不可缺少的组成部分。套接字是可以绑定名称的通信端点。套接字具有类型和关联的进程。 套接字是为实现用于进程间通信的客户机/服务器模型而设计的,其中:
网络协议的接口需要支持多个通信协议,如 TCP/IP、Xerox Internet 协议 (XNS) 以及 UNIX 系列。
网络协议的接口需要支持等待连接的服务器代码以及启动连接的客户机代码。
根据通信是面向连接的通信还是无连接通信,操作也不相同。
应用程序可能需要指定要传送的数据报的目标地址,而不是使用 open(2) 调用绑定此地址。
套接字使网络协议可用,并使其行为类似于 UNIX 文件。应用程序将根据需要创建套接字。套接字适用于 close(2)、read(2)、write(2)、ioctl(2) 以及 fcntl(2) 接口。操作系统可区分文件的文件描述符和套接字的文件描述符。
套接字接口例程位于必须与应用程序链接的库中。/usr/lib 中包含库 libsocket.so 以及其余系统服务库。libsocket.so 用于动态链接。
套接字类型定义对于用户可见的通信属性。Internet 系列套接字提供对 TCP/IP 传输协议的访问。对于可以通过 IPv6 和 IPv4 进行通信的套接字,由值 AF_INET6 标识 Internet 系列。 此外,还支持值 AF_INET,目的是为了与旧应用程序的源代码兼容并提供对 IPv4 的原始访问。
SunOS 环境支持四种套接字类型:
流套接字。使用该套接字,进程可以使用 TCP 进行通信。流套接字提供没有记录边界的双向、可靠、有序且不重复的数据流。建立连接之后,可以将数据作为字节流从这些套接字中读取或向其中写入。套接字类型为 SOCK_STREAM
。
数据报套接字。使用该套接字,进程可以使用 UDP 进行通信。数据报套接字支持双向消息流。数据报套接字上的进程接收消息的顺序可能不同于发送消息的顺序。数据报套接字上的进程可能会接收重复消息。通过数据报套接字发送的消息可能会被丢弃。但数据中的记录边界会被保留。套接字类型为 SOCK_DGRAM
。
原始套接字。该套接字提供对 ICMP 的访问。原始套接字还提供对网络栈不直接支持的其他基于 IP 的协议的访问。虽然这些套接字的确切特征取决于协议提供的接口,但是它们通常是面向数据报的套接字。原始套接字并不适用于大多数应用程序。提供原始套接字是为了支持开发新的通信协议,或者为了访问现有协议的更加深奥的功能。只有超级用户进程才能使用原始套接字。套接字类型为 SOCK_RAW
。
SEQ 套接字。该套接字支持 1–N 个流控制传输协议 (Stream Control Transfer Protocol, SCTP) 连接。有关 SCTP 的更多详细信息将在流控制传输协议中介绍。
有关详细信息,请参见选择特定的协议。
SunOS 5.10 平台提供两组套接字接口。提供了 BSD 套接字接口,并且从 SunOS 版本 5.7 开始,还提供了 XNS 5 (UNIX03) 套接字接口。XNS 5 接口与 BSD 接口稍有不同。
以下手册页介绍了 XNS 5 套接字接口:
相应的 3N 手册页介绍了传统的 BSD 套接字行为。此外,3N 节中还添加了以下新接口:
有关生成使用 XNS 5 (UNIX03) 套接字接口的应用程序的信息,请参见 standards(5) 手册页。
本节介绍基本套接字接口的用法。
socket(3SOCKET) 调用创建指定系列和指定类型的套接字。
s = socket(family, type, protocol);
如果未指定协议,则系统将选择支持所需套接字类型的协议。将返回套接字句柄。套接字句柄即为文件描述符。
family 由 sys/socket.h 中定义的一个常量指定。名为 AF_suite 的常量指定要在解释名称时使用的地址格式:
Apple Computer Inc. Appletalk 网络
Internet IPv6 和 IPv4 系列
仅 Internet IPv4 系列
Xerox Corporation PUP internet
UNIX 文件系统
套接字类型在 sys/socket.h 中定义。AF_INET6、AF_INET 和 AF_UNIX 支持 SOCK_STREAM、SOCK_DGRAM 或 SOCK_RAW 这些类型。以下示例创建 Internet 系列的流套接字:
s = socket(AF_INET6, SOCK_STREAM, 0);
此调用生成流套接字。TCP 协议提供底层通信。在大多数情况下,将 protocol 参数设置为缺省值 0。可以指定缺省协议之外的其他协议,如高级套接字主题中所述。
创建套接字时不带名称。只有在套接字绑定到地址之后,远程进程才能引用此套接字。用于通信的进程通过地址连接。在 Internet 系列中,连接由本地和远程地址以及本地和远程端口组成。 不能存在重复排序组,如 protocol、local address、local port、foreign address、foreign port。 在大多数系列中,连接必须唯一。
使用 bind(3SOCKET) 接口,进程可以指定套接字的本地地址。此接口组成 local address、local port 组。connect(3SOCKET) 和 accept(3SOCKET) 通过添加地址元组的远程部分来完成套接字的关联。bind(3SOCKET) 调用的用法如下:
bind (s, name, namelen);
套接字句柄为 s。绑定名称是由支持协议解释的字节字符串。Internet 系列名称包含 Internet 地址和端口号。
#include <sys/types.h> #include <netinet/in.h> ... struct sockaddr_in6 sin6; ... s = socket(AF_INET6, SOCK_STREAM, 0); bzero (&sin6, sizeof (sin6)); sin6.sin6_family = AF_INET6; sin6.sin6_addr.s6_addr = in6addr_arg; sin6.sin6_port = htons(MYPORT); bind(s, (struct sockaddr *) &sin6, sizeof sin6);
地址 sin6 的内容在讨论 Internet 地址绑定的地址绑定中介绍。
通常以非对称形式建立连接,一个进程用作客户机,而另一个进程则用作服务器。服务器将套接字绑定到与服务关联的已知地址,并阻塞在套接字上等待连接请求。然后,不相关的进程便可连接到此服务器。客户机通过启动到服务器套接字的连接,向服务器请求服务。在客户端,connect(3SOCKET) 调用启动连接。在 Internet 系列中,此连接可能如下所示:
struct sockaddr_in6 server; ... connect(s, (struct sockaddr *)&server, sizeof server);
如果在连接调用期间未绑定客户机的套接字,则系统会自动选择一个名称并将其绑定到套接字。有关更多信息,请参见地址绑定。这种自动选择是将本地地址绑定到客户端上套接字的常规方法。
要接收客户机的连接,服务器必须在绑定其套接字之后执行两个步骤。第一步是说明可以排队多少连接请求。第二步接受连接。
struct sockaddr_in6 from; ... listen(s, 5); /* Allow queue of 5 connections */ fromlen = sizeof(from); newsock = accept(s, (struct sockaddr *) &from, &fromlen);
套接字句柄 s 是绑定到连接请求要发送到的地址的套接字。listen(3SOCKET) 的第二个参数指定可以对其进行排队的最大未完成连接数。from 结构使用客户机的地址进行填充。可能传递 NULL 指针。fromlen 为此结构的长度。
通常,accept(3SOCKET) 例程阻塞进程。accept(3SOCKET) 返回一个连接到请求客户机的新套接字描述符。fromlen 的值将更改为此地址的实际大小。
服务器无法指示其只接受来自特定地址的连接。服务器可以检查由 accept(3SOCKET) 返回的 from 地址并关闭与不可接受的客户机之间的连接。服务器可以接受多个套接字上的连接,或者避免在调用 accept(3SOCKET) 时阻塞。高级套接字主题中介绍了这些技术。
如果连接失败,则会返回错误,但是由系统绑定的地址保持不变。如果连接成功,则套接字与服务器关联,并且可以开始数据传送。
下表列出在连接尝试失败时返回的比较常见的错误。
表 7–1 套接字连接错误
套接字错误 |
错误说明 |
---|---|
ENOBUFS |
支持调用的可用内存不足。 |
EPROTONOSUPPORT |
请求未知协议。 |
EPROTOTYPE |
请求不支持的套接字类型。 |
ETIMEDOUT |
未在指定时间内建立连接。当目标主机关闭或由于丢失传输而导致网络问题时,会发生此错误。 |
ECONNREFUSED |
主机拒绝服务。当服务器进程未在请求地址中显示时,会发生此错误。 |
ENETDOWN 或 EHOSTDOWN |
这些错误是由底层通信接口传送的状态信息造成的。 |
ENETUNREACH 或 EHOSTUNREACH |
由于不存在到网络或主机的路由,因此可能会发生这些操作错误。这些错误还可能是由于中间网关或切换节点所返回的状态信息造成的。返回的状态信息并不总是足以区分网络故障和主机故障。 |
本节介绍用于发送和接收数据的接口。可以使用常规 read(2) 和 write(2) 接口来发送或接收消息。
write(s, buf, sizeof buf); read(s, buf, sizeof buf);
还可以使用 send(3SOCKET) 和 recv(3SOCKET):
send(s, buf, sizeof buf, flags); recv(s, buf, sizeof buf, flags);
send(3SOCKET) 和 recv(3SOCKET) 非常类似于 read(2) 和 write(2),但是 flags 参数至关重要。如果需要一个或多个以下项,则可以将 sys/socket.h 中定义的 flags 参数指定为非零值:
带外数据特定于流套接字。使用 recv(3SOCKET) 调用指定 MSG_PEEK 之后,所有显示的数据均返回到用户,但是仍视为不可读取。套接字上的下一个 read(2) 或 recv(3SOCKET) 调用将返回相同数据。当前只有路由表管理进程使用发送数据而不路由包的选项(应用于传出包)。
可以通过 close(2) 接口调用废弃 SOCK_STREAM 套接字。如果在 close(2) 接口调用之后数据排队到保证可靠传送的套接字,则协议会继续尝试传送数据。如果数据在任意时间之后还不能传送,则会将其废弃。
shutdown(3SOCKET) 可正常关闭 SOCK_STREAM
套接字。这两个进程均可确认不再发送。此调用的形式为:
shutdown(s, how);
其中 how 定义为
禁止进一步接收数据
禁止进一步传输数据
禁止进一步传输和接收
以下两个示例说明启动和接受 Internet 系列流连接。
以下是针对服务器的示例程序。服务器创建套接字并将名称绑定到此套接字,然后显示端口号。此程序调用 listen(3SOCKET) 将套接字标记为可以接受连接请求并初始化请求队列。此程序的其余部分为一个死循环。每次循环都接受一个新的连接并将其从队列中删除,从而创建一个新的套接字。服务器读取并显示此套接字中的消息,然后关闭此套接字。地址绑定中介绍了 in6addr_any 用法。
#include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <netdb.h> #include <stdio.h> #define TRUE 1 /* * This program creates a socket and then begins an infinite loop. * Each time through the loop it accepts a connection and prints * data from it. When the connection breaks, or the client closes * the connection, the program accepts a new connection. */ main() { int sock, length; struct sockaddr_in6 server; int msgsock; char buf[1024]; int rval; /* Create socket. */ sock = socket(AF_INET6, SOCK_STREAM, 0); if (sock == -1) { perror("opening stream socket"); exit(1); } /* Bind socket using wildcards.*/ bzero (&server, sizeof(server)); server.sin6_family = AF_INET6; server.sin6_addr = in6addr_any; server.sin6_port = 0; if (bind(sock, (struct sockaddr *) &server, sizeof server) == -1) { perror("binding stream socket"); exit(1); } /* Find out assigned port number and print it out. */ length = sizeof server; if (getsockname(sock,(struct sockaddr *) &server, &length) == -1) { perror("getting socket name"); exit(1); } printf("Socket port #%d\n", ntohs(server.sin6_port)); /* Start accepting connections. */ listen(sock, 5); do { msgsock = accept(sock,(struct sockaddr *) 0,(int *) 0); if (msgsock == -1) perror("accept"); else do { memset(buf, 0, sizeof buf); if ((rval = read(msgsock,buf, sizeof(buf))) == -1) perror("reading stream message"); if (rval == 0) printf("Ending connection\n"); else /* assumes the data is printable */ printf("-->%s\n", buf); } while (rval > 0); close(msgsock); } while(TRUE); /* * Since this program has an infinite loop, the socket "sock" is * never explicitly closed. However, all sockets are closed * automatically when a process is killed or terminates normally. */ exit(0); }
要启动连接,示例 7–2 中的客户机程序会创建一个流套接字,然后调用 connect(3SOCKET),指定用于连接的套接字地址。如果存在目标套接字,并且接受了请求,则连接会完成。现在,程序可以发送数据。数据按顺序传送,并且没有消息边界。此连接会在任何一个套接字关闭时销毁。有关此程序中的数据表示例程(如 ntohl(3SOCKET)、ntohs(3SOCKET)、htons(3SOCKET) 以及 htonl(3XNET))的更多信息,请参见 byteorder(3SOCKET) 手册页。
#include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <netdb.h> #include <stdio.h> #define DATA "Half a league, half a league . . ." /* * This program creates a socket and initiates a connection with * the socket given in the command line. Some data are sent over the * connection and then the socket is closed, ending the connection. * The form of the command line is: streamwrite hostname portnumber * Usage: pgm host port */ main(int argc, char *argv[]) { int sock, errnum, h_addr_index; struct sockaddr_in6 server; struct hostent *hp; char buf[1024]; /* Create socket. */ sock = socket( AF_INET6, SOCK_STREAM, 0); if (sock == -1) { perror("opening stream socket"); exit(1); } /* Connect socket using name specified by command line. */ bzero (&server, sizeof (server)); server.sin6_family = AF_INET6; hp = getipnodebyname(argv[1], AF_INET6, AI_DEFAULT, &errnum); /* * getipnodebyname returns a structure including the network address * of the specified host. */ if (hp == (struct hostent *) 0) { fprintf(stderr, "%s: unknown host\n", argv[1]); exit(2); } h_addr_index = 0; while (hp->h_addr_list[h_addr_index] != NULL) { bcopy(hp->h_addr_list[h_addr_index], &server.sin6_addr, hp->h_length); server.sin6_port = htons(atoi(argv[2])); if (connect(sock, (struct sockaddr *) &server, sizeof (server)) == -1) { if (hp->h_addr_list[++h_addr_index] != NULL) { /* Try next address */ continue; } perror("connecting stream socket"); freehostent(hp); exit(1); } break; } freehostent(hp); if (write( sock, DATA, sizeof DATA) == -1) perror("writing on stream socket"); close(sock); freehostent (hp); exit(0); }
可以将一对一 SCTP 连接支持添加到流套接字。以下示例代码将 -p 添加到现有程序,使此程序可以指定要使用的协议。
#include <stdio.h> #include <netdb.h> #include <string.h> #include <errno.h> int main(int argc, char *argv[]) { struct protoent *proto = NULL; int c; int s; int protocol; while ((c = getopt(argc, argv, "p:")) != -1) { switch (c) { case 'p': proto = getprotobyname(optarg); if (proto == NULL) { fprintf(stderr, "Unknown protocol: %s\n", optarg); return (-1); } break; default: fprintf(stderr, "Unknown option: %c\n", c); return (-1); } } /* Use the default protocol, which is TCP, if not specified. */ if (proto == NULL) protocol = 0; else protocol = proto->p_proto; /* Create a IPv6 SOCK_STREAM socket of the protocol. */ if ((s = socket(AF_INET6, SOCK_STREAM, protocol)) == -1) { fprintf(stderr, "Cannot create SOCK_STREAM socket of type %s: " "%s\n", proto != NULL ? proto->p_name : "tcp", strerror(errno)); return (-1); } printf("Success\n"); return (0); }
请求可以在多个套接字或多个文件之间多路复用。使用 select(3C) 进行多路复用:
#include <sys/time.h> #include <sys/types.h> #include <sys/select.h> ... fd_set readmask, writemask, exceptmask; struct timeval timeout; ... select(nfds, &readmask, &writemask, &exceptmask, &timeout);
select(3C) 的第一个参数是列表中由接下来的三个参数指向的文件描述符数。
select(3C) 的第二、第三以及第四个参数指向三组文件描述符:一组描述符用于读取,一组用于写入,一组用于接受例外情况。带外数据是唯一的例外情况。可以将其中任一指针指定为空指针。每一组都是包含长整数位掩码数组的结构。可以使用 select.h 中定义的 FD_SETSIZE 来设置数组的大小。此数组的长度足以为每个 FD_SETSIZE 文件描述符存储一个比特位。
宏 FD_SET (fd, &mask) 和 FD_CLR (fd, &mask) 分别添加和删除组 mask 中的文件描述符 fd。此组应该在使用之前设置为零,并且宏 FD_ZERO (&mask) 将清除组 mask。
select(3C) 的第五个参数用于指定超时值。如果 timeout 指针为 NULL,则会阻止 select(3C),直到可以选择描述符或收到信号为止。如果 timeout 中的字段均设置为 0,则 select(3C) 会进行轮询并立即返回。
select(3C) 例程通常返回所选的文件描述符数,或者在已超时的情况下返回零。对于错误或中断,select(3C) 例程返回 -1,并且 errno 中的错误号以及文件描述符掩码保持不变。对于成功的返回,三个组指示哪些文件描述符可以读取、写入或具有暂挂的例外情况。
使用 FD_ISSET (fd, &mask) 宏在选择掩码中测试文件描述符的状态。如果 fd 位于组 mask 中,则此宏会返回非零值。否则,此宏会返回零。针对读取组先后使用 select(3C) 和 FD_ISSET (fd, &mask) 宏,以便检查套接字上的排队连接请求。
以下示例显示如何针对可读性在侦听套接字上做出选择,以确定何时可以通过调用 accept(3SOCKET) 接受新的连接。程序将接受连接请求,读取数据,并在单个套接字上断开连接。
#include <sys/types.h> #include <sys/socket.h> #include <sys/time/h> #include <netinet/in.h> #include <netdb.h> #include <stdio.h> #define TRUE 1 /* * This program uses select to check that someone is * trying to connect before calling accept. */ main() { int sock, length; struct sockaddr_in6 server; int msgsock; char buf[1024]; int rval; fd_set ready; struct timeval to; /* Open a socket and bind it as in previous examples. */ /* Start accepting connections. */ listen(sock, 5); do { FD_ZERO(&ready); FD_SET(sock, &ready); to.tv_sec = 5; to.tv_usec = 0; if (select(sock + 1, &ready, (fd_set *)0, (fd_set *)0, &to) == -1) { perror("select"); continue; } if (FD_ISSET(sock, &ready)) { msgsock = accept(sock, (struct sockaddr *)0, (int *)0); if (msgsock == -1) perror("accept"); else do { memset(buf, 0, sizeof buf); if ((rval = read(msgsock, buf, sizeof(buf))) == -1) perror("reading stream message"); else if (rval == 0) printf("Ending connection\n"); else printf("-->%s\n", buf); } while (rval > 0); close(msgsock); } else printf("Do something else\n"); } while (TRUE); exit(0); }
在早期版本的 select(3C) 例程中,其参数是指向整数的指针,而不是指向 fd_sets 的指针。在文件描述符数小于整数中的位数时,此风格的调用仍然有效。
select(3C) 例程提供一种同步多路复用方案。高级套接字主题中介绍的 SIGIO 和 SIGURG 信号提供有关输出完成、输入可用性以及例外情况的异步通知。
数据报套接字提供了对称数据交换接口,无需建立连接。每条消息都带有目标地址。下图显示了服务器与客户机之间的通信流。
服务器的 bind(3SOCKET) 步骤为可选步骤。
按照创建套接字中所述创建数据报套接字。如果需要特定本地地址,必须在首次数据传输之前执行 bind(3SOCKET) 操作。否则,系统会在首次发送数据时设置本地地址或端口。可以使用 sendto(3SOCKET) 发送数据。
sendto(s, buf, buflen, flags, (struct sockaddr *) &to, tolen);
s、buf、buflen 以及 flags 参数与面向连接的套接字中的相应参数相同。to 和 tolen 值指示消息预定接受者的地址。在本地检测到的错误情况(如无法访问网络)会导致返回 -1,并将 errno 设置为错误号。
recvfrom(s, buf, buflen, flags, (struct sockaddr *) &from, &fromlen);
要在数据报套接字上接收消息,请使用 recvfrom(3SOCKET)。在调用之前,fromlen 将会设置为 from 缓冲区的大小。返回时,fromlen 将会设置为接收数据报时的源地址的大小。
数据报套接字还可以使用 connect(3SOCKET) 调用将套接字与特定目标地址关联。然后,此套接字便可以使用 send(3SOCKET) 调用。如果在未显式指定目标地址的套接字上发送数据,则目标地址为已连接的对等方。只传送从该对等方接收的数据。一个套接字一次只能有一个已连接的地址。再次调用 connect(3SOCKET) 会更改目标地址。将立即返回数据报套接字上的连接请求。系统将记录对等方的地址。不能将 accept(3SOCKET) 或 listen(3SOCKET) 用于数据报套接字。
连接数据报套接字之后,便可以从先前的 send(3SOCKET) 调用中异步返回错误。此套接字可以在后续套接字操作中报告这些错误。或者,此套接字可以使用 getsockopt(3SOCKET) 的选项 SO_ERROR 来询问错误状态。
以下示例代码说明如何通过创建一个套接字、将名称绑定到该套接字以及将消息发送到该套接字来发送 Internet 调用。
#include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <netdb.h> #include <stdio.h> #define DATA "The sea is calm, the tide is full . . ." /* * Here I send a datagram to a receiver whose name I get from * the command line arguments. The form of the command line is: * dgramsend hostname portnumber */ main(int argc, char *argv[]) { int sock, errnum; struct sockaddr_in6 name; struct hostent *hp; /* Create socket on which to send. */ sock = socket(AF_INET6,SOCK_DGRAM, 0); if (sock == -1) { perror("opening datagram socket"); exit(1); } /* * Construct name, with no wildcards, of the socket to ``send'' * to. getinodebyname returns a structure including the network * address of the specified host. The port number is taken from * the command line. */ hp = getipnodebyname(argv[1], AF_INET6, AI_DEFAULT, &errnum); if (hp == (struct hostent *) 0) { fprintf(stderr, "%s: unknown host\n", argv[1]); exit(2); } bzero (&name, sizeof (name)); memcpy((char *) &name.sin6_addr, (char *) hp->h_addr, hp->h_length); name.sin6_family = AF_INET6; name.sin6_port = htons(atoi(argv[2])); /* Send message. */ if (sendto(sock,DATA, sizeof DATA ,0, (struct sockaddr *) &name,sizeof name) == -1) perror("sending datagram message"); close(sock); exit(0); }
以下样例代码说明如何通过创建套接字,将名称绑定到套接字然后从套接字进行读取来读取 Internet 调用。
#include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <stdio.h> /* * This program creates a datagram socket, binds a name to it, then * reads from the socket. */ main() { int sock, length; struct sockaddr_in6 name; char buf[1024]; /* Create socket from which to read. */ sock = socket(AF_INET6, SOCK_DGRAM, 0); if (sock == -1) { perror("opening datagram socket"); exit(1); } /* Create name with wildcards. */ bzero (&name, sizeof (name)); name.sin6_family = AF_INET6; name.sin6_addr = in6addr_any; name.sin6_port = 0; if (bind (sock, (struct sockaddr *)&name, sizeof (name)) == -1) { perror("binding datagram socket"); exit(1); } /* Find assigned port value and print it out. */ length = sizeof(name); if (getsockname(sock,(struct sockaddr *) &name, &length) == -1) { perror("getting socket name"); exit(1); } printf("Socket port #%d\n", ntohs(name.sin6_port)); /* Read from the socket. */ if (read(sock, buf, 1024) == -1 ) perror("receiving datagram packet"); /* Assumes the data is printable */ printf("-->%s\n", buf); close(sock); exit(0); }
本节介绍可以用来查找和构造网络地址的例程。除非另行说明,否则本节中介绍的接口只适用于 Internet 系列。
在客户机与服务器通信之前,在远程主机上查找服务需要许多级别的映射。为了便于用户使用,每个服务都具有一个名称。服务名和主机名必须转换为网络地址。最后,网络地址必须可用于查找并路由到主机。网络体系结构之间的具体映射情况可以不同。
标准例程将主机名映射到网络地址,将网络名映射到网络号,将协议名映射到协议号,将服务名映射到端口号。此外,标准例程还指明与服务器进程通信时所使用的相应协议。使用其中任一例程时,必须包括文件 netdb.h。
接口 getaddrinfo(3SOCKET)、getnameinfo(3SOCKET)、gai_strerror(3SOCKET) 以及 freeaddrinfo(3SOCKET) 提供了一种在主机上的服务名称与地址之间进行转换的简化方法。这些接口比 getipnodebyname(3SOCKET)、gethostbyname(3NSL) 以及 getservbyname(3SOCKET) API 更新。将透明地处理 IPv6 和 IPv4 地址。
getaddrinfo(3SOCKET) 例程返回指定主机名和服务名的组合地址和端口号。由于会动态分配 getaddrinfo(3SOCKET) 所返回的信息,因此必须通过 freeaddrinfo(3SOCKET) 释放信息以防止内存泄漏。getnameinfo(3SOCKET) 返回与指定地址和端口号关联的主机名和服务名。调用 gai_strerror(3SOCKET) 以便基于 getaddrinfo(3SOCKET) 和 getnameinfo(3SOCKET) 所返回的 EAI_xxx 代码列显错误消息。
以下是使用 getaddrinfo(3SOCKET) 的示例。
struct addrinfo *res, *aip; struct addrinfo hints; int error; /* Get host address. Any type of address will do. */ bzero(&hints, sizeof (hints)); hints.ai_flags = AI_ALL|AI_ADDRCONFIG; hints.ai_socktype = SOCK_STREAM; error = getaddrinfo(hostname, servicename, &hints, &res); if (error != 0) { (void) fprintf(stderr, "getaddrinfo: %s for host %s service %s\n", gai_strerror(error), hostname, servicename); return (-1); }
在 res 指向的结构中处理完 getaddrinfo(3SOCKET) 所返回的信息之后,应该通过 freeaddrinfo(res) 释放存储空间。
getnameinfo(3SOCKET) 例程在确定错误原因时特别有用,如以下示例所示:
struct sockaddr_storage faddr; int sock, new_sock, sock_opt; socklen_t faddrlen; int error; char hname[NI_MAXHOST]; char sname[NI_MAXSERV]; ... faddrlen = sizeof (faddr); new_sock = accept(sock, (struct sockaddr *)&faddr, &faddrlen); if (new_sock == -1) { if (errno != EINTR && errno != ECONNABORTED) { perror("accept"); } continue; } error = getnameinfo((struct sockaddr *)&faddr, faddrlen, hname, sizeof (hname), sname, sizeof (sname), 0); if (error) { (void) fprintf(stderr, "getnameinfo: %s\n", gai_strerror(error)); } else { (void) printf("Connection from %s/%s\n", hname, sname); }
Internet 主机名到地址映射由 hostent 结构表示,如 gethostent(3NSL) 中所定义:
struct hostent { char *h_name; /* official name of host */ char **h_aliases; /* alias list */ int h_addrtype; /* hostaddrtype(e.g.,AF_INET6) */ int h_length; /* length of address */ char **h_addr_list; /* list of addrs, null terminated */ }; /*1st addr, net byte order*/ #define h_addr h_addr_list[0]
将 Internet 主机名映射到 hostent 结构
将 Internet 主机地址映射到 hostent 结构
释放 hostent 结构的内存
将 Internet 主机地址映射到字符串
这些例程返回的 hostent 结构中包含主机名、主机别名、地址类型,以及以 NULL 结尾的长度可变地址的列表。此地址列表是必需的,因为主机可以具有许多地址。h_addr 定义用于向后兼容,并且是 hostent 结构的地址列表中的第一个地址。
用于将网络名映射到网络号以及将网络号映射到网络名的例程将返回 netent 结构:
/* * Assumes that a network number fits in 32 bits. */ struct netent { char *n_name; /* official name of net */ char **n_aliases; /* alias list */ int n_addrtype; /* net address type */ int n_net; /* net number, host byte order */ };
getnetbyname(3SOCKET)、getnetbyaddr_r(3SOCKET) 以及 getnetent(3SOCKET) 是前面介绍的主机例程的网络对应项。
protoent 结构定义用于 getprotobyname(3SOCKET)、getprotobynumber(3SOCKET) 以及 getprotoent(3SOCKET) 且在 getprotoent(3SOCKET) 中定义的协议到名称映射:
struct protoent { char *p_name; /* official protocol name */ char **p_aliases /* alias list */ int p_proto; /* protocol number */ };
Internet 系列服务驻留在特定的已知端口,并使用特定的协议。getprotoent(3SOCKET) 中定义的 servent 结构描述了服务名到端口号映射:
struct servent { char *s_name; /* official service name */ char **s_aliases; /* alias list */ int s_port; /* port number, network byte order */ char *s_proto; /* protocol to use */ };
getservbyname(3SOCKET) 将服务名以及限定协议(可选)映射到 servent 结构。调用
sp = getservbyname("telnet", (char *) 0);
将返回使用任意协议的 telnet 服务器的服务规范。调用
sp = getservbyname("telnet", "tcp");
将返回使用 TCP 协议的 telnet 服务器。还提供了 getservbyport(3SOCKET) 和 getservent(3SOCKET)。getservbyport(3SOCKET) 具有的接口类似于 getservbyname(3SOCKET) 使用的接口。可以指定一个可选协议名来限定查找。
可以使用其他一些例程来简化名称和地址的处理。下表概述了用于处理长度可变字节字符串以及字节交换网络地址和值的例程。
表 7–2 运行时库例程
接口 |
概要 |
---|---|
比较字节字符串;如果相同,则为 0,否则不为 0 |
|
将 n 个字节从 s2 复制到 s1 |
|
将 n 个字节设置为以 base 开始的 value |
|
从主机字节顺序转换到网络字节顺序的 32 位值 |
|
从主机字节顺序转换到网络字节顺序的 16 位值 |
|
从网络字节顺序转换到主机字节顺序的 32 位值 |
|
从网络字节顺序转换到主机字节顺序的 16 位值 |
因为操作系统希望以网络顺序提供地址,所以提供了字节交换例程。在某些体系结构中,主机字节顺序不同于网络字节顺序,因此有时程序必须对值进行字节交换。返回网络地址的例程以网络顺序执行此操作。仅在解释网络地址时会出现字节交换问题。例如,以下代码设置 TCP 或 UDP 端口的格式:
printf("port number %d\n", ntohs(sp->s_port));
在不需要这些例程的计算机上,将这些例程定义为空宏。
分布式应用程序的最常见形式为客户机/服务器模型。在此方案中,客户机进程从服务器进程请求服务。
备用方案是可以删除暂停服务器进程的服务服务器。Internet 服务守护进程 inetd(1M) 便是一个示例。inetd(1M) 可以侦听启动时通过读取配置文件而确定的不同端口。在 inetd(1M) 服务端口上请求连接时,inetd(1M) 会产生相应的服务器以便为客户机提供服务。客户机并不知道中间服务器已参与连接。inetd 守护进程中更详细地介绍了 inetd(1M)。
大多数服务器都是通过已知 Internet 端口号或 UNIX 系列名称进行访问的。服务 rlogin 便是已知的 UNIX 系列名称。示例 7–7 中给出了远程登录服务器的主循环。
服务器将从其调用方的控制终端分离出来,除非服务器在 DEBUG 模式下运行。
(void) close(0); (void) close(1); (void) close(2); (void) open("/", O_RDONLY); (void) dup2(0, 1); (void) dup2(0, 2); setsid();
进行分离可防止服务器从控制终端的进程组接收信号。服务器从控制终端分离之后,便不能将错误报告发送到终端。已分离的服务器必须使用 syslog(3C) 记录错误。
服务器通过调用 getaddrinfo(3SOCKET) 获取其服务定义。
bzero(&hints, sizeof (hints)); hints.ai_flags = AI_ALL|AI_ADDRCONFIG; hints.ai_socktype = SOCK_STREAM; error = getaddrinfo(NULL, "rlogin", &hints, &api);
api 中返回的结果包含程序侦听服务请求所用的 Internet 端口。/usr/include/netinet/in.h 中定义了某些标准端口号。
然后,服务器创建套接字并侦听服务请求。bind(3SOCKET) 例程可以确保服务器在预期位置进行侦听。由于远程登录服务器会侦听受限的端口号,因此该服务器将以超级用户身份运行。 服务器的主体是以下循环。
/* Wait for a connection request. */ for (;;) { faddrlen = sizeof (faddr); new_sock = accept(sock, (struct sockaddr *)api->ai_addr, api->ai_addrlen) if (new_sock == -1) { if (errno != EINTR && errno != ECONNABORTED) { perror("rlogind: accept"); } continue; } if (fork() == 0) { close (sock); doit (new_sock, &faddr); } close (new_sock); } /*NOTREACHED*/
accept(3SOCKET) 将阻止消息,直到客户机请求服务为止。此外,如果 accept 被某信号(如 SIGCHLD)中断,则 accept(3SOCKET) 会返回故障指示。如果发生错误,则会检查来自 accept(3SOCKET) 的返回值,并使用 syslog(3C) 记录错误。
然后,服务器派生一个子进程,并调用远程登录协议处理的主体。父进程用于对连接请求进行排队的套接字将在子进程中关闭。accept(3SOCKET) 所创建的套接字将在父进程中关闭。将客户机的地址传递到服务器应用程序的 doit() 例程,此例程用来验证客户机。
本节介绍客户机进程所执行的步骤。与在服务器中相同,第一步是查找远程登录的服务定义。
bzero(&hints, sizeof (hints)); hints.ai_flags = AI_ALL|AI_ADDRCONFIG; hints.ai_socktype = SOCK_STREAM; error = getaddrinfo(hostname, servicename, &hints, &res); if (error != 0) { (void) fprintf(stderr, "getaddrinfo: %s for host %s service %s\n", gai_strerror(error), hostname, servicename); return (-1); }
getaddrinfo(3SOCKET) 返回 res 中的地址列表头。通过创建套接字,并尝试连接列表中返回的每个地址直到一个地址有效,即可找到所需的地址。
for (aip = res; aip != NULL; aip = aip->ai_next) { /* * Open socket. The address type depends on what * getaddrinfo() gave us. */ sock = socket(aip->ai_family, aip->ai_socktype, aip->ai_protocol); if (sock == -1) { perror("socket"); freeaddrinfo(res); return (-1); } /* Connect to the host. */ if (connect(sock, aip->ai_addr, aip->ai_addrlen) == -1) { perror("connect"); (void) close(sock); sock = -1; continue; } break; }
套接字已经创建并已连接到所需的服务。connect(3SOCKET) 例程隐式绑定 sock,因为 sock 未绑定。
某些服务使用数据报套接字。rwho(1) 服务提供了有关连接到局域网的主机的状态信息。应避免运行 in.rwhod(1M),因为 in.rwho 会导致网络通信流量过大。rwho 服务将信息广播到所有连接到特定网络的主机。rwho 服务是数据报套接字用法示例。
运行 rwho(1) 服务器的主机上的用户可以使用 ruptime(1) 获取其他主机的当前状态。以下示例给出了典型输出。
itchy up 9:45, 5 users, load 1.15, 1.39, 1.31 scratchy up 2+12:04, 8 users, load 4.67, 5.13, 4.59 click up 10:10, 0 users, load 0.27, 0.15, 0.14 clack up 2+06:28, 9 users, load 1.04, 1.20, 1.65 ezekiel up 25+09:48, 0 users, load 1.49, 1.43, 1.41 dandy 5+00:05, 0 users, load 1.51, 1.54, 1.56 peninsula down 0:24 wood down 17:04 carpediem down 16:09 chances up 2+15:57, 3 users, load 1.52, 1.81, 1.86
在每台主机上,rwho(1) 服务器进程定期广播状态信息,并接收状态信息。此外,服务器还更新数据库。将对此数据库进行解释以了解每台主机的状态。仅通过本地网络及其广播功能连接的服务器将自主运行。
使用广播时效率非常低,因为广播会生成过多网络通信流量。除非广泛且频繁地使用该服务,否则所带来的简单性相对定期广播的开销而言,得不偿失。
以下示例给出了简化的 rwho(1) 服务器版本。样例代码接收网络中其他主机广播的状态信息,并提供运行此样例代码的主机的状态。第一项任务在程序的主循环中完成:检查在 rwho(1) 端口接收到的包,以确保这些包由其他 rwho(1) 服务器进程发送并标记有到达时间。然后,这些包使用主机的状态更新文件。如果长时间没有收到来自主机的消息,则数据库例程认为此主机已关闭并记录此信息。由于主机正常运行时服务器可能会关闭,因此该应用程序容易出现错误。
main() { ... sp = getservbyname("who", "udp"); net = getnetbyname("localnet"); sin.sin6_addr = inet_makeaddr(net->n_net, in6addr_any); sin.sin6_port = sp->s_port; ... s = socket(AF_INET6, SOCK_DGRAM, 0); ... on = 1; if (setsockopt(s, SOL_SOCKET, SO_BROADCAST, &on, sizeof on) == -1) { syslog(LOG_ERR, "setsockopt SO_BROADCAST: %m"); exit(1); } bind(s, (struct sockaddr *) &sin, sizeof sin); ... signal(SIGALRM, onalrm); onalrm(); while(1) { struct whod wd; int cc, whod, len = sizeof from; cc = recvfrom(s, (char *) &wd, sizeof(struct whod), 0, (struct sockaddr *) &from, &len); if (cc <= 0) { if (cc == -1 && errno != EINTR) syslog(LOG_ERR, "rwhod: recv: %m"); continue; } if (from.sin6_port != sp->s_port) { syslog(LOG_ERR, "rwhod: %d: bad from port", ntohs(from.sin6_port)); continue; } ... if (!verify( wd.wd_hostname)) { syslog(LOG_ERR, "rwhod: bad host name from %x", ntohl(from.sin6_addr.s6_addr)); continue; } (void) sprintf(path, "%s/whod.%s", RWHODIR, wd.wd_hostname); whod = open(path, O_WRONLY|O_CREAT|O_TRUNC, 0666); ... (void) time(&wd.wd_recvtime); (void) write(whod, (char *) &wd, cc); (void) close(whod); } exit(0); }
第二项服务器任务是提供其主机状态。这要求定期获取系统状态信息,将其打包在消息中,并在本地网络上广播,以使其他 rwho(1) 服务器收到这些信息。此任务由计时器运行,并由信号触发。
状态信息将在本地网络上广播。对于不支持广播的网络,使用多点传送。
对于大多数程序员而言,前面介绍的机制足以用来生成分布式应用程序。本节介绍其他功能。
流套接字概念包括带外数据。带外数据是一对连接的流套接字之间的逻辑上独立的传输通道。带外数据的传送独立于一般数据。带外数据功能必须支持每次至少可靠传送一条带外消息。此消息至少可以包含一个字节的数据。可以随时暂挂传送至少一条消息。
使用带内信号发送时,紧急数据将与一般数据一同按顺序传送,并从一般数据流中提取消息。提取的消息将单独存储。用户可以在顺序接收紧急数据与无序接收紧急数据之间进行选择,而不必缓冲中间数据。
使用 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);
如果返回时 yes 为 1,则下次读取将返回标记之后的数据。否则,假设带外数据已经到达,下次读取将提供由客户机在发送带外信号之前发送的数据。以下示例给出了远程登录进程中接收中断或退出信号时刷新输出的例程。此代码读取标记之前的一般数据以废弃这些一般数据,然后读取带外字节。
进程还可以读取或查看带外数据,而无需首先读取标记之前的数据。当底层协议将紧急数据与一般数据一起带内传送,并仅提前发送其存在的通知时,很难访问此数据。此类型协议的示例为 TCP,此协议用于在 Internet 系列中提供套接字流。如果使用此类协议,则使用 MSG_OOB 标志调用 recv(3SOCKET) 时,带外字节可能尚未到达。在这种情况下,此调用会返回 EWOULDBLOCK 错误。此外,输入缓冲区中的带内数据量可能会导致正常流控制阻止对等方发送紧急数据,直到清除缓冲区为止。然后,在对等方可以发送紧急数据之前,此进程必须读取足够的排队数据以清除输入缓冲区。
#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) 调用(如以下示例所示)使此套接字变为非阻止套接字。
#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))不能全部完成,但是部分写入有效(如使用流套接字时),则会处理所有可用的数据。返回值是实际发送的数据量。
在同时处理多个请求的应用程序中,要求在进程之间进行异步通信。异步套接字的类型必须为 SOCK_STREAM。要使套接字异步,请发出 fcntl(2) 调用,如以下示例所示。
#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 例程将完成数据传送,如下节中所述。
SIGIO 信号会在套接字或任何文件描述符完成数据传送时通知进程。使用 SIGIO 的步骤如下所示:
使用 signal(3C) 或 sigvec(3UCB) 调用设置 SIGIO 信号处理程序。
使用 fcntl(2) 设置进程 ID 或进程组 ID,以将信号路由到其自己的进程 ID 或进程组 ID。套接字的缺省进程组为组 0。
将套接字转换为异步,如异步套接字 I/O中所示。
使用以下样例代码,给定进程可以在发生套接字请求时接收有关暂挂请求的信息。添加 SIGURG 的处理程序之后,还可以使用此代码来准备接收 SIGURG 信号。
#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); }
对于 SIGURG 和 SIGIO,每个套接字都具有一个进程号和进程组 ID。这些值初始化为零,但是可以在稍后使用 F_SETOWN fcntl(2) 命令重新定义,如前面示例中所示。fcntl(2) 采用正值的第三个参数可以设置套接字的进程 ID。fcntl(2) 采用负值的第三个参数可以设置套接字的进程组 ID。SIGURG 和 SIGIO 信号的唯一允许接受者是调用进程。 类似的 fcntl(2)(即 F_GETOWN)将返回套接字的进程号。
还可以通过使用 ioctl(2) 将套接字指定给用户的进程组来接收 SIGURG 和 SIGIO。
/* 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 元组:
本地 IP 地址
本地端口号
外部 IP 地址
外部端口号
TCP 要求这些 4 元组具有唯一性。UDP 则不要求。用户程序并非总是了解用于本地地址和本地端口的正确值,因为主机可能驻留在多个网络中。用户不能直接访问已分配的端口号集合。要避免这些问题,请不要指定地址部分,让系统在需要时适当指定这些部分。这些元组的不同部分可以由套接字 API 的不同部分指定:
本地地址和/或本地端口
外部地址和外部端口
调用 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);
系统使用两个条件来选择本地端口号:
小于 1024 (IPPORT_RESERVED) 的 Internet 端口号保留供特权用户使用。非特权用户可以使用大于 1024 的任何 Internet 端口号。最大的 Internet 端口号是 65535。
当前并未将此端口号绑定到其他套接字。
可以通过 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) 调用的参数:
要应用选项的套接字
指定协议级别,例如由 sys/socket.h 中的符号常量 SOL_SOCKET 指示的套接字级别
在 sys/socket.h 中定义并指定选项的符号常量
指向选项值
指向选项值的长度
对于 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(1M) 守护进程在启动时调用,并从 /etc/inet/inetd.conf 文件中获取此守护进程侦听的服务。此守护进程针对 /etc/inet/inetd.conf 中列出的每个服务创建一个套接字,将相应的端口号绑定到每个套接字。有关详细信息,请参见 inetd(1M) 手册页。
inetd(1M) 守护进程轮询每个套接字,等待向对应于此套接字的服务发出连接请求。对于 SOCK_STREAM 类型套接字,inetd(1M) 在侦听套接字时接受 (accept(3SOCKET)),派生 (fork(2)),将新套接字复制 (dup(2)) 到文件描述符 0 和 1(stdin 和 stdout),关闭其他开放式文件描述符,并执行 (exec(2)) 相应服务器。
使用 inetd(1M) 的主要优点就是未使用的服务不占用计算机资源。次要优点是 inetd(1M) 会尽力建立连接。在文件描述符 0 和 1 中,由 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 结构。
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 标志。
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) 命令来获取接口的广播地址。
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),前提是此接口支持广播或点对点寻址。
只有类型为 SOCK_DGRAM 和 SOCK_RAW 的 AF_INET6 和 AF_INET 套接字支持 IP 多点传送。IP 多点传送仅在接口驱动程序支持多点传送的子网中受到支持。
要发送多点传送数据报,请在 224.0.0.0 到 239.255.255.255 的范围中指定一个 IP 多点传送地址作为 sendto(3SOCKET) 调用的目标地址。
缺省情况下,发送 IP 多点传送数据报时其生存时间 (time-to-live, TTL) 值为 1。此值可以阻止将数据报转发到单个子网之外。使用套接字选项 IP_MULTICAST_TTL,可以将后续多点传送数据报的 TTL 设置为 0 到 255 之间的任何值。此功能用于控制多点传送的范围。
u_char ttl; setsockopt(sock, IPPROTO_IP, IP_MULTICAST_TTL, &ttl,sizeof(ttl))
TTL 为 0 的多点传送数据报不能在任何子网中传输,但是在发送主机属于目标组并且发送套接字上未禁用多点传送回送的情况下可以进行本地传送。 如果有一个或多个多点传送路由器连接到第一跃点子网,则 TTL 大于 1 的多点传送数据报可以传送到多个子网。 为了提供有意义的范围控制,多点传送路由器应支持 TTL 阈值概念。这些阈值会阻止低于特定 TTL 值的数据报遍历某些子网。 这些阈值将针对具有以下初始 TTL 值的多点传送数据报强制实施相应约定:
限定在同一主机
限定在同一子网
限定在同一站点
限定在同一地区
限定在同一洲
范围不受限制
站点和地区并未严格定义,站点可以根据实际情况再分为更小的管理单元。
应用程序可以选择以上列出的 TTL 值以外的初始 TTL 值。例如,应用程序可以通过发送多点传送查询来对网络资源执行扩展环搜索,即第一个 TTL 值为 0,然后逐渐增大 TTL 的值,直到收到回复为止。
多点传送路由器不转发任何目标地址在 224.0.0.0 与 224.0.0.255(包括 224.0.0.0 和 224.0.0.255)范围之间的多点传送数据报,而不管其 TTL 值是多少。 此地址范围是为使用路由协议以及其他低级拓扑搜索或维护协议(如网关搜索和组成员关系报告)而保留的。
即使主机拥有多个具有多点传送功能的接口,每个多点传送传输也是通过单个网络接口发送的。 如果主机还用作多点传送路由器且 TTL 值大于 1,则多点传送可以转发到源接口之外的接口。 套接字选项可以覆盖来自给定套接字的后续传输的缺省设置:
struct in_addr addr; setsockopt(sock, IPPROTO_IP, IP_MULTICAST_IF, &addr, sizeof(addr))
其中,addr 是所需传出接口的本地 IP 地址。通过指定地址 INADDR_ANY 恢复到缺省接口。使用 SIOCGIFCONF ioctl 获取接口的本地 IP 地址。 要确定接口是否支持多点传送,请使用 SIOCGIFFLAGS ioctl 提取接口标志并测试是否设置了 IFF_MULTICAST 标志。此选项主要用于多点传送路由器以及其他专门针对 Internet 拓扑的系统服务。
如果将多点传送数据报发送到发送主机本身所属的组,则缺省情况下,本地传送的 IP 层将回送此数据报的副本。 另一套接字选项可为发送主机提供针对是否回送后续数据报的显式控制:
u_char loop; setsockopt(sock, IPPROTO_IP, IP_MULTICAST_LOOP, &loop, sizeof(loop))
其中,loop 为 0 即为禁用回送,为 1 即为启用回送。此选项通过消除因接收应用程序自己的传输内容而产生的开销,可提高单台主机上只有一个实例的应用程序的性能。对于可以在一台主机上具有多个实例或者其发送主机不属于目标组的应用程序,不应使用此选项。
如果发送主机属于其他接口的目标组,则发送初始 TTL 值大于 1 的多点传送数据报可以传送到其他接口上的发送主机。 回送控制选项不会影响此类传送。
主机必须成为一个或多个 IP 多点传送组的成员,才能接收 IP 多点传送数据报。 进程可以使用以下套接字选项请求主机加入多点传送组:
struct ip_mreq mreq; setsockopt(sock, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq))
其中,mreq 为以下结构:
struct ip_mreq { struct in_addr imr_multiaddr; /* multicast group to join */ struct in_addr imr_interface; /* interface to join on */ }
每个成员都与单个接口关联。可以在多个接口上加入同一组。 将 imr_interface 地址指定为 INADDR_ANY 以选择缺省的多点传送接口。还可以通过指定主机的本地地址之一来选择特定的具有多点传送功能的接口。
要删除成员关系,请使用:
struct ip_mreq mreq; setsockopt(sock, IPPROTO_IP, IP_DROP_MEMBERSHIP, &mreq, sizeof(mreq))
其中,mreq 包含用于添加成员关系的那些值。关闭套接字或中止保存套接字的进程将删除与此套接字关联的成员关系。可以有多个套接字请求成为特定组的成员,并且直到删除最后一个请求,主机才不再是此组的成员。
如果任一套接字请求成为数据报目标组的成员,则内核 IP 层将接受传入的多点传送包。给定套接字是否接收多点传送数据报取决于此套接字的关联目标端口和成员关系,或者取决于原始套接字的协议类型。要接收发送到特定端口的多点传送数据报,请将其绑定到本地端口,同时不指定本地地址,如使用 INADDR_ANY。
如果在 bind(3SOCKET) 之前存在以下内容,则可以将多个进程绑定到同一 SOCK_DGRAM UDP 端口:
int one = 1; setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one))
在这种情况下,每个目标为共享端口的传入多点传送或广播 UDP 数据报将传送到所有绑定到此端口的套接字。 为了向后兼容,此传送不适用于传入的单点传送数据报。无论单点传送数据报的目标端口绑定有多少套接字,此类数据报永远都不会传送到多个套接字。 SOCK_RAW 套接字不要求 SO_REUSEADDR 选项共享单一 IP 协议类型。
可以在 <netinet/in.h> 中找到与多点传送相关的新套接字选项所需的定义。 所有 IP 地址均以网络字节顺序传递。
要发送 IPv6 多点传送数据报,请在 ff00::0/8 范围中指定一个 IP 多点传送地址作为 sendto(3SOCKET) 调用的目标地址。
缺省情况下,IP 多点传送数据报的发送跃点限制为 1,此值可以阻止将数据报转发到单个子网之外。 使用套接字选项 IPV6_MULTICAST_HOPS,可以将后续多点传送数据报的跃点限制设置为 0 到 255 之间的任何值。此功能用于控制多点传送的范围:
uint_l; setsockopt(sock, IPPROTO_IPV6, IPV6_MULTICAST_HOPS, &hops,sizeof(hops))
不能在任何子网中传输跃点限制为 0 的多点传送数据报,但是在以下情况下可以在本地范围内传送数据报:
发送主机属于目标组
启用了发送套接字上的多点传送回送
如果第一跃点子网连接到一个或多个多点传送路由器,则可以将跃点限制大于 1 的多点传送数据报传送到多个子网。 与 IPv4 多点传送地址不同,IPv6 多点传送地址包含明确的范围信息,此信息在地址的第一部分进行编码。定义的范围如下,其中未指定 X:
节点-本地范围,限定在同一节点
链路-本地范围
站点-本地范围
组织-本地范围
全局范围
应用程序可独立于多点传送地址范围,使用不同的跃点限制值。例如,应用程序可以通过发送多点传送查询来对网络资源执行扩展环搜索,即第一个跃点限制值为 0,然后逐渐增大跃点限制值,直到收到回复为止。
即使主机拥有多个具有多点传送功能的接口,每个多点传送传输也是通过单个网络接口发送的。如果主机还用作多点传送路由器且跃点限制值大于 1,则多点传送可以转发到源接口之外的接口。 套接字选项可以覆盖来自给定套接字的后续传输的缺省设置:
uint_t ifindex; ifindex = if_nametoindex ("hme3"); setsockopt(sock, IPPROTO_IPV6, IPV6_MULTICAST_IF, &ifindex, sizeof(ifindex))
其中,ifindex 是所需传出接口的接口索引。通过指定值 0 恢复到缺省接口。
如果将多点传送数据报发送到发送主机本身所属的组,则缺省情况下,本地传送的 IP 层将回送此数据报的副本。另一套接字选项可为发送主机提供针对是否回送后续数据报的显式控制:
uint_t loop; setsockopt(sock, IPPROTO_IPV6, IPV6_MULTICAST_LOOP, &loop, sizeof(loop))
其中,loop 为 0 即为禁用回送,为 1 即为启用回送。 此选项通过消除因接收应用程序自己的传输内容而产生的开销,可提高单台主机上只有一个实例的应用程序(如路由器或邮件守护进程)的性能。 对于可以在一台主机上具有多个实例的应用程序(如会议程序)或者其发送主机不属于目标组的应用程序(如时间查询程序),不应使用此选项。
如果发送主机属于其他接口的目标组,则发送初始跃点限制大于 1 的多点传送数据报可以传送到其他接口上的发送主机。 回送控制选项不会影响此类传送。
主机必须成为一个或多个 IP 多点传送组的成员,才能接收 IP 多点传送数据报。 进程可以使用以下套接字选项请求主机加入多点传送组:
struct ipv6_mreq mreq; setsockopt(sock, IPPROTO_IPV6, IPV6_JOIN_GROUP, &mreq, sizeof(mreq))
其中,mreq 为以下结构:
struct ipv6_mreq { struct in6_addr ipv6mr_multiaddr; /* IPv6 multicast addr */ unsigned int ipv6mr_interface; /* interface index */ }
每个成员都与单个接口关联。可以在多个接口上加入同一组。 将 ipv6_interface 指定为 0 以选择缺省的多点传送接口。为主机的其中一个接口指定接口索引以选择此具有多点传送功能的接口。
要离开组,请使用:
struct ipv6_mreq mreq; setsockopt(sock, IPPROTO_IPV6, IP_LEAVE_GROUP, &mreq, sizeof(mreq))
其中,mreq 包含用于添加成员关系的那些值。当关闭套接字或中止保存套接字的进程时,此套接字将删除关联的成员关系。 可以有多个套接字请求成为特定组的成员。直到删除最后一个请求,主机才不再是此组的成员。
如果任一套接字已请求成为数据报目标组的成员,则内核 IP 层将接受传入的多点传送包。多点传送数据报是否传送到特定的套接字取决于与此套接字关联的目标端口和成员关系,或者取决于原始套接字的协议类型。要接收发送到特定端口的多点传送数据报,请将其绑定到本地端口,同时不指定本地地址,如使用 INADDR_ANY。
如果在 bind(3SOCKET) 之前存在以下内容,则可以将多个进程绑定到同一 SOCK_DGRAM UDP 端口:
int one = 1; setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one))
在这种情况下,所有绑定到此端口的套接字将接收每个目标为共享端口的传入多点传送 UDP 数据报。 为了向后兼容,此传送不适用于传入的单点传送数据报。无论单点传送数据报的目标端口绑定有多少套接字,此类数据报永远都不会传送到多个套接字。 SOCK_RAW 套接字不要求 SO_REUSEADDR 选项共享单一 IP 协议类型。
可以在 <netinet/in.h> 中找到与多点传送相关的新套接字选项所需的定义。 所有 IP 地址均以网络字节顺序传递。
流控制传输协议 (Stream Control Transmission Protocol, SCTP) 是一种可靠的传输协议,提供的服务与 TCP 提供的服务类似。此外,SCTP 还提供网络级别的容错功能。SCTP 支持关联两端具有多宿主。SCTP 套接字 API 支持模仿 TCP 的一对一的套接字风格。SCTP 套接字 API 还支持旨在用于发送信号的一对多套接字风格。一对多套接字风格可以减少在进程中使用的文件描述符数。必须链接 libsctp 库才能使用 SCTP 函数调用。
将在两个端点之间设置 SCTP 关联。这些端点使用的四次握手机制借助 cookie 来防止遭到某些类型的拒绝服务 (denial-of-service, DoS) 攻击。端点可由多个 IP 地址表示。
当 socket() 调用为 IPPROTO_SCTP 创建套接字时,它会调用特定于 SCTP 的套接字创建例程。针对 SCTP 套接字执行的套接字调用会自动调用相应的 SCTP 套接字例程。在一对一套接字中,每个套接字都对应一个 SCTP 关联。可以通过调用以下函数来创建一对一套接字:
socket(AF_INET[6], SOCK_STREAM, IPPROTO_STCP);在一对多风格套接字中,每个套接字都处理多个 SCTP 关联。每个关联都具有一个名为 sctp_assoc_t 的关联标识符。可以通过调用以下函数来创建一对多套接字:
socket(AF_INET[6], SOCK_SEQPACKET, IPPROTO_STCP);sctp_bindx() 函数管理 SCTP 套接字上的地址。如果 sock 参数为 IPv4 套接字,则传送给 sctp_bindx() 函数的地址必须为 IPv4 地址。如果 sock 参数为 IPv6 套接字,则传送给 sctp_bindx() 函数的地址可以为 IPv4 或 IPv6 地址。当传送给 sctp_bindx() 函数的地址为 INADDR_ANY 或 IN6ADDR_ANY 时,此套接字将绑定到所有可用地址。可以使用 bind(3SOCKET) 绑定 SCTP 端点。
*addrs 参数的值是指向包含一个或多个套接字地址的数组的指针。每个地址都包含在其相应的结构中。如果地址为 IPv4 地址,则它们可以包含在 sockaddr_in 结构或 sockaddr_in6 结构中。如果地址为 IPv6 地址,则它们可以包含在 sockaddr_in6 结构中。可以通过地址类型系列区分地址长度。调用方使用 addrcnt 参数指定数组中的地址数。
如果成功,则 sctp_bindx() 函数将返回 0。如果失败,则 sctp_bindx() 函数将返回 -1,并将 errno 的值设置为相应的错误代码。
如果没有为每个套接字地址提供同一端口,则 sctp_bindx() 函数将失败,并将 errno 的值设置为 EINVAL。
通过对以下零个或多个当前定义的标志执行按位 OR 运算,即可形成 flags 参数:
SCTP_BINDX_ADD_ADDR
SCTP_BINDX_REM_ADDR
SCTP_BINDX_ADD_ADDR 指示 SCTP 将给定地址添加到关联中。SCTP_BINDX_REM_ADDR 指示 SCTP 从关联中删除给定地址。这两个标志相互排斥。如果同时提供这两个标志,则 sctp_bindx() 将失败,并将 errno 的值设置为 EINVAL。
调用方无法删除关联中的所有地址。sctp_bindx() 函数拒绝此类尝试的表现为:函数失败并将 errno 的值设置为 EINVAL。应用程序可以在调用 bind() 函数之后使用 sctp_bindx(SCTP_BINDX_ADD_ADDR),将其他地址与端点关联。应用程序可以使用 sctp_bindx(SCTP_BINDX_REM_ADDR) 删除与侦听套接字关联的地址。使用 sctp_bindx(SCTP_BINDX_REM_ADDR) 删除地址之后,接受新关联将不会重新关联已删除的地址。如果端点支持动态地址,则可以使用 SCTP_BINDX_REM_ADDR 或 SCTP_BINDX_ADD_ADDR 向对等方发送消息来更改对等方的地址列表。在已连接的关联中添加和删除地址为可选功能。不支持此功能的实现将返回 EOPNOTSUPP。
如果地址族不是 AF_INET 或 AF_INET6,则 sctp_bindx() 函数将失败并返回 EAFNOSUPPORT。如果 sock 参数中传递给 sctp_bindx() 的文件描述符无效,则 sctp_bindx() 函数将失败并返回 EBADF。
sctp_opt_info() 函数将返回与 sock 参数中所述的套接字关联的 SCTP 级别选项。如果此套接字为一对多风格 SCTP 套接字,则 id 参数的值是某个特定关联。对于一对一风格 SCTP 套接字,将忽略 id 参数。opt 参数的值指定要获取的 SCTP 套接字选项。arg 参数的值是为调用程序而分配的特定于选项的结构缓冲区。*len 参数的值为选项长度。
opt 参数可以采用以下值:
返回用于初始化和绑定重新传输超时 (retransmission timeout, RTO) 可调参数的协议参数。这些协议参数使用以下结构:
struct sctp_rtoinfo { sctp_assoc_t srto_assoc_id; uint32_t srto_initial; uint32_t srto_max; uint32_t srto_min; };
调用程序提供此值,它指定所关注的关联。
此值为初始 RTO 值。
此值为最大 RTO 值。
此值为最小 RTO 值。
返回特定于关联的参数。这些参数使用以下结构:
struct sctp_assocparams { sctp_assoc_t sasoc_assoc_id; uint16_t sasoc_asocmaxrxt; uint16_t sasoc_number_peer_destinations; uint32_t sasoc_peer_rwnd; uint32_t sasoc_local_rwnd; uint32_t sasoc_cookie_life; };
调用程序提供此值,它指定所关注的关联。
此值指定关联的最大重新传输计数。
此值指定对等方具有的地址数。
此值指定对等方接收窗口的当前值。
此值指定对等方传输到的上一个已报告的接收窗口。
此值指定关联 cookie 的生命周期。可在发出 cookie 时使用此值。
所有使用时间值的参数均以毫秒为单位。
返回 sendto(3SOCKET) 函数调用在此关联中使用的缺省参数集。这些参数使用以下结构:
struct sctp_sndrcvinfo { uint16_t sinfo_stream; uint16_t sinfo_ssn; uint16_t sinfo_flags; uint32_t sinfo_ppid; uint32_t sinfo_context; uint32_t sinfo_timetolive; uint32_t sinfo_tsn; uint32_t sinfo_cumtsn; sctp_assoc_t sinfo_assoc_id; };
此值指定 sendmsg() 调用的缺省流。
此值始终为 0。
此值包含 sendmsg() 调用的缺省标志。此标志可以采用以下值:
MSG_UNORDERED
MSG_ADDR_OVER
MSG_ABORT
MSG_EOF
MSG_PR_SCTP
此值为 sendmsg() 调用的缺省有效负荷协议标识符。
此值为 sendmsg() 调用的缺省上下文。
此值指定时间段(以毫秒为单位)。在此时间段过后,如果消息传输尚未开始,则消息将过期。值为 0 指示消息尚未过期。如果设置了 MSG_PR_SCTP 标志,当消息传输未在 sinfo_timetolive 所指定的时间段内成功完成时,消息将过期。
此值始终为 0。
此值始终为 0。
此值由调用程序填充。它指定所关注的关联。
返回所指定对等地址的参数。这些参数使用以下结构:
struct sctp_paddrparams { sctp_assoc_t spp_assoc_id; struct sockaddr_storage spp_address; uint32_t spp_hbinterval; uint16_t spp_pathmaxrxt; };
调用程序提供此值,它指定所关注的关联。
此值指定所关注的对等地址。
此值指定心跳间隔(以毫秒为单位)。
此值指定在认为地址不可访问之前针对此地址尝试的最大重新传输数。
返回有关关联的当前状态信息。这些参数使用以下结构:
struct sctp_status { sctp_assoc_t sstat_assoc_id; int32_t sstat_state; uint32_t sstat_rwnd; uint16_t sstat_unackdata; uint16_t sstat_penddata; uint16_t sstat_instrms; uint16_t sstat_outstrms; uint32_t sstat_fragmentation_point; struct sctp_paddrinfo sstat_primary; };
调用程序提供此值,它指定所关注的关联。
此值为关联的当前状态。关联可以采用以下状态:
SCTP 端点没有任何与其关联的关联。一旦 socket() 函数调用打开一个端点或端点关闭,则端点便会处于此状态。
SCTP 端点在调用 bind() 之后绑定到一个或多个本地地址。
此端点在等待来自任何远程 SCTP 端点的关联请求。
此 SCTP 端点已发送 INIT 块并在等待 INIT-ACK 块。
此 SCTP 端点已将从其对等方的 INIT-ACK 块接收的 cookie 回显到对等方。
此 SCTP 端点可以与其对等方交换数据。
此 SCTP 端点已从其上层接收了 SHUTDOWN 元语。此端点不再从其上层接受数据。
处于 SCTP_SHUTDOWN_PENDING 状态的 SCTP 端点已向其对等方发送了 SHUTDOWN 块。仅在确认所有从此端点到其对等方的未完成数据之后,才发送 SHUTDOWN 块。当此端点的对等方发送 SHUTDOWN ACK 块时,此端点会发送 SHUTDOWN COMPLETE 块并认为关联已关闭。
SCTP 端点已从其对等方接收了 SHUTDOWN 块。此端点不再从其用户接受新数据。
处于 SCTP_SHUTDOWN_RECEIVED 状态的 SCTP 端点已向其对等方发送了 SHUTDOWN ACK 块。此端点仅在其对等方确认来自此端点的所有未完成数据之后发送 SHUTDOWN ACK 块。当此端点的对等方发送 SHUTDOWN COMPLETE 块时,将关闭关联。
此值为关联对等方的当前接收窗口。
此值为未确认的 DATA 块数。
此值为等待接收的 DATA 块数。
此值为传入的流数。
此值为外发的流数。
如果消息、SCTP 数据包头和 IP 数据包头的组合大小超出 sstat_fragmentation_point 的值,则消息会分段。此值等于包目标地址的路径最大传输单元 (Path Maximum Transmission Unit, P-MTU)。
此值包含有关主要对等地址的信息。此信息使用以下结构:
struct sctp_paddrinfo { sctp_assoc_t spinfo_assoc_id; struct sockaddr_storage spinfo_address; int32_t spinfo_state; uint32_t spinfo_cwnd; uint32_t spinfo_srtt; uint32_t spinfo_rto; uint32_t spinfo_mtu; };
调用程序提供此值,它指定所关注的关联。
此值为主要对等地址。
此值可以采用 SCTP_ACTIVE 或 SCTP_INACTIVE 两个值中的任意一个。
此值为对等地址的拥塞窗口。
此值为对等地址的当前平滑往返时间计算结果,以毫秒为单位。
此值为对等地址的当前重新传输超时值,以毫秒为单位。
此值为对等地址的 P-MTU。
如果成功,则 sctp_opt_info() 函数将返回 0。如果失败,则 sctp_opt_info() 函数将返回 -1,并将 errno 的值设置为相应的错误代码。如果 sock 参数中传递给 sctp_opt_info() 的文件描述符无效,则 sctp_opt_info() 函数将失败并返回 EBADF。如果 sock 参数中传递给 sctp_opt_info() 函数的文件描述符没有描述套接字,则 sctp_opt_info() 函数将失败并返回 ENOTSOCK。如果关联 ID 对于一对多风格 SCTP 套接字而言无效,则 sctp_opt_info() 函数将失败,并将 errno 的值设置为 EINVAL。如果输入缓冲区长度对于指定的选项而言过短,则 sctp_opt_info() 函数将失败,并将 errno 的值设置为 EINVAL。如果对等地址的地址族不是 AF_INET 或 AF_INET6,则 sctp_opt_info() 函数将失败,并将 errno 的值设置为 EAFNOSUPPORT。
使用 sctp_recvmsg() 函数,可以从 s 参数所指定的 SCTP 端点接收消息。调用程序可以指定以下属性:
此参数为消息缓冲区的地址。
此参数为消息缓冲区的长度。
此参数为指向包含发送主机地址的地址的指针。
此参数为与 from 参数中的地址关联的缓冲区的大小。
此参数仅在调用程序启用 sctp_data_io_events 时处于活动状态。要启用 sctp_data_io_events,请使用套接字选项 SCTP_EVENTS 调用 setsockopt() 函数。如果启用了 sctp_data_io_events,则应用程序将接收每个传入消息的 sctp_sndrcvinfo 结构的内容。此参数为指向 sctp_sndrcvinfo 结构的指针。此结构将在接收消息时进行填充。
此参数包含所有存在的消息标志。
sctp_recvmsg() 函数将返回其接收的字节数。sctp_recvmsg() 函数将在出现错误时返回 -1。
如果在 s 参数中传递的文件描述符无效,则 sctp_recvmsg() 函数将失败,并将 errno 的值设置为 EBADF。如果在 s 参数中传递的文件描述符没有描述套接字,则 sctp_recvmsg() 函数将失败,并将 errno 的值设置为 ENOTSOCK。如果 msg_flags 参数包括值 MSG_OOB,则 sctp_recvmsg() 函数将失败,并将 errno 的值设置为 EOPNOTSUPP。如果没有建立关联,则 sctp_recvmsg() 函数将失败,并将 errno 的值设置为 ENOTCONN。
sctp_sendmsg() 函数在发送来自 SCTP 端点的消息时启用高级 SCTP 功能。
此值指定发送消息的 SCTP 端点。
此值包含 sctp_sendmsg() 函数所发送的消息。
此值为消息的长度,以字节为单位。
此值为消息的目标地址。
此值为目标地址的长度。
此值为应用程序指定的有效负荷协议标识符。
此值为此消息的目标流。
此值为消息未能成功发送到对等方的情况下消息过期之前可以等待的时间段,以毫秒为单位。
如果在发送消息时出现错误,则返回此值。
此值在将逻辑运算 OR 以按位形式应用于以下零个或多个标志位时形成:
设置此标志之后,sctp_sendmsg() 函数将无序传送消息。
设置此标志之后,sctp_sendmsg() 函数将使用 to 参数中的地址,而不使用关联的主要目标地址。此标志仅用于一对多风格 SCTP 套接字。
设置此标志之后,指定的关联将异常中止,同时向其对等方发送 ABORT 信号。此标志仅用于一对多风格 SCTP 套接字。
设置此标志之后,指定的关联将进入正常关机状态。此标志仅用于一对多风格 SCTP 套接字。
设置此标志之后,如果消息传输未在 timetolive 参数所指定的时间段内成功完成,则消息将过期。
sctp_sendmsg() 函数将返回其发送的字节数。sctp_sendmsg() 函数将在出现错误时返回 -1。
如果在 s 参数中传递的文件描述符无效,则 sctp_sendmsg() 函数将失败,并将 errno 的值设置为 EBADF。如果在 s 参数中传递的文件描述符没有描述套接字,则 sctp_sendmsg() 函数将失败,并将 errno 的值设置为 ENOTSOCK。如果 flags 参数包括值 MSG_OOB,则 sctp_sendmsg() 函数将失败,并将 errno 的值设置为 EOPNOTSUPP。如果一对一风格套接字的 flags 参数包括 MSG_ABORT 或 MSG_EOF 值,则 sctp_sendmsg() 函数将失败,并将 errno 的值设置为 EOPNOTSUPP。如果没有建立关联,则 sctp_sendmsg() 函数将失败,并将 errno 的值设置为 ENOTCONN。如果套接字关闭,禁止进一步写入,则 sctp_sendmsg() 函数将失败,并将 errno 的值设置为 EPIPE。如果套接字为非阻止套接字并且传输队列已满,则 sctp_sendmsg() 函数将失败,并将 errno 的值设置为 EAGAIN。
如果控制消息长度不正确,则 sctp_sendmsg() 函数将失败,并将 errno 的值设置为 EINVAL。如果指定的目标地址不属于关联,则 sctp_sendmsg() 函数将失败,并将 errno 的值设置为 EINVAL。如果 stream_no 的值不在关联所支持的外发流数之内,则 sctp_sendmsg() 函数将失败,并将 errno 的值设置为 EINVAL。如果所指定的目标地址的地址族不是 AF_INET 或 AF_INET6,则 sctp_sendmsg() 函数将失败,并将 errno 的值设置为 EINVAL。
sctp_send() 函数可供一对一及一对多风格套接字使用。sctp_send() 函数在发送来自 SCTP 端点的消息时启用高级 SCTP 功能。
此值指定 socket() 函数所创建的套接字。
此值包含 sctp_send() 函数所发送的消息。
此值为消息的长度,以字节为单位。
此值包含用于发送消息的参数。对于一对多风格套接字,此值可以包含消息所发送到的关联 ID。
此值与 sendmsg() 函数中的标志参数相同。
sctp_send() 函数将返回其发送的字节数。sctp_send() 函数将在出现错误时返回 -1。
如果在 s 参数中传递的文件描述符无效,则 sctp_send() 函数将失败,并将 errno 的值设置为 EBADF。如果在 s 参数中传递的文件描述符没有描述套接字,则 sctp_send() 函数将失败,并将 errno 的值设置为 ENOTSOCK。如果 sinfo 参数的 sinfo_flags 字段包括值 MSG_OOB,则 sctp_send() 函数将失败,并将 errno 的值设置为 EOPNOTSUPP。如果一对一风格套接字中 sinfo 参数的 sinfo_flags 字段包括 MSG_ABORT 或 MSG_EOF 值,则 sctp_send() 函数将失败,并将 errno 值的设置为 EOPNOTSUPP。如果没有建立关联,则 sctp_send() 函数将失败,并将 errno 的值设置为 ENOTCONN。如果套接字关闭,禁止进一步写入,则 sctp_send() 函数将失败,并将 errno 的值设置为 EPIPE。如果套接字为非阻止套接字并且传输队列已满,则 sctp_send() 函数将失败,并将 errno 的值设置为 EAGAIN。
如果控制消息长度不正确,则 sctp_send() 函数将失败,并将 errno 的值设置为 EINVAL。如果指定的目标地址不属于关联,则 sctp_send() 函数将失败,并将 errno 的值设置为 EINVAL。如果 stream_no 的值不在关联所支持的外发流数之内,则 sctp_send() 函数将失败,并将 errno 的值设置为 EINVAL。如果所指定的目标地址的地址族不是 AF_INET 或 AF_INET6,则 sctp_send() 函数将失败,并将 errno 的值设置为 EINVAL。
应用程序可以将一对多风格套接字上已建立的关联分叉为独立的套接字和文件描述符。对于具有多个偶发消息发送者或接收者的应用程序,如果这些发送者或接收者需要存在于原始一对多风格套接字之下,则独立的套接字和文件描述符非常有用。应用程序会将传输大量数据通信流量的关联分叉为独立的套接字描述符。应用程序使用 sctp_peeloff() 调用将关联分叉为独立的套接字。新套接字为一对一风格套接字。sctp_peeloff() 函数的语法如下:
int sctp_peeloff(int sock, sctp_assoc_t id);从 socket() 系统调用返回的原始一对多风格套接字描述符
要分叉为独立的文件描述符的关联的标识符
如果在 sock 参数中传递的套接字描述符不是一对多风格 SCTP 套接字,则 sctp_peeloff() 函数将失败并返回 EOPTNOTSUPP。如果 id 的值为 0 或者 id 的值大于在 sock 参数中传递的套接字描述符的最大关联数,则 sctp_peeloff() 函数将失败并返回 EINVAL。如果 sctp_peeloff() 函数无法创建新的用户文件描述符或文件结构,则此函数将失败并返回 EMFILE。
sctp_getpaddrs() 函数将返回关联中的所有对等地址。
int sctp_getpaddrs(int sock, sctp_assoc_t id, void **addrs);当 sctp_getpaddrs() 函数成功返回时,**addrs 参数的值将指向每个地址相应类型的动态分配的压缩 sockaddr 结构数组。调用线程使用 sctp_freepaddrs() 函数释放内存。**addrs 参数的值不能为 NULL。如果 sock 中给定的套接字描述符用于 IPv4 套接字,则 sctp_getpaddrs() 函数将返回 IPv4 地址。如果 sock 中给定的套接字描述符用于 IPv6 套接字,则 sctp_getpaddrs() 函数将同时返回 IPv4 和 IPv6 地址。对于一对多风格套接字,id 参数指定要查询的关联。对于一对一风格套接字,sctp_getpaddrs() 函数将忽略 id 参数。当 sctp_getpaddrs() 函数成功返回时,它将返回关联中的对等地址数。如果此套接字上没有关联,则 sctp_getpaddrs() 函数将返回 0,并且不定义 **addrs 参数的值。如果出现错误,则 sctp_getpaddrs() 函数将返回 -1,并且不定义 **addrs 参数的值。
如果 sock 参数中传递给 sctp_getpaddrs() 函数的文件描述符无效,则 sctp_getpaddrs() 函数将失败并返回 EBADF。如果 sock 参数中传递给 sctp_getpaddrs() 函数的文件描述符没有描述套接字,则 sctp_getpaddrs() 函数将失败并返回 ENOTSOCK。如果 sock 参数中传递给 sctp_getpaddrs() 函数的文件描述符描述了未连接的套接字,则 sctp_getpaddrs() 函数将失败并返回 ENOTCONN。
sctp_freepaddrs() 函数将释放所有由之前的 sctp_getpaddrs() 调用所分配的资源。sctp_freepaddrs() 函数的语法如下:
void sctp_freepaddrs(void *addrs);*addrs 参数为包含 sctp_getpaddrs() 函数所返回的对等地址的数组。
sctp_getladdrs() 函数将返回套接字上的所有本地绑定的地址。sctp_getladdrs() 函数的语法如下:
int sctp_getladdrs(int sock, sctp_assoc_t id, void **addrs);当 sctp_getladdrs() 函数成功返回时,addrs 的值将指向动态分配的压缩 sockaddr 结构数组。sockaddr 结构为每个本地地址的相应类型。调用应用程序使用 sctp_freeladdrs() 函数释放内存。addrs 参数的值不能为 NULL。
如果 sd 参数引用的套接字为 IPv4 套接字,则 sctp_getladdrs() 函数将返回 IPv4 地址。如果 sd 参数引用的套接字为 IPv6 套接字,则 sctp_getladdrs() 函数将同时返回相应的 IPv4 或 IPv6 地址。
针对一对多风格套接字调用 sctp_getladdrs() 函数时,id 参数的值指定要查询的关联。sctp_getladdrs() 函数在一对一套接字上运行时将忽略 id 参数。
当 id 参数的值为 0 时,无论为何种特定关联,sctp_getladdrs() 函数都将返回本地绑定的地址。当 sctp_getladdrs() 函数成功返回时,它将报告绑定到套接字的本地地址数。如果未绑定套接字,则 sctp_getladdrs() 函数将返回 0,并且不定义 *addrs 的值。如果出现错误,则 sctp_getladdrs() 函数将返回 -1,并且不定义 *addrs 的值。
sctp_freeladdrs() 函数将释放所有由之前的 sctp_getladdrs() 调用所分配的资源。sctp_freeladdrs() 函数的语法如下:
void sctp_freeladdrs(void *addrs);*addrs 参数为包含 sctp_getladdrs() 函数所返回的对等地址的数组。
本节详细介绍 SCTP 套接字的两种用法。
/* * Copyright 2004 Sun Microsystems, Inc. All rights reserved. * Use is subject to license terms. */ /* To enable socket features used for SCTP socket. */ #define _XPG4_2 #define __EXTENSIONS__ #include <stdio.h> #include <string.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <stdlib.h> #include <unistd.h> #include <pthread.h> #include <netinet/sctp.h> #include <netdb.h> #define BUFLEN 1024 static void usage(char *a0) { fprintf(stderr, "Usage: %s <addr>\n", a0); } /* * Read from the network. */ static void readit(void *vfdp) { int fd; ssize_t n; char buf[BUFLEN]; struct msghdr msg[1]; struct iovec iov[1]; struct cmsghdr *cmsg; struct sctp_sndrcvinfo *sri; char cbuf[sizeof (*cmsg) + sizeof (*sri)]; union sctp_notification *snp; pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, NULL); fd = *(int *)vfdp; /* Initialize the message header for receiving */ memset(msg, 0, sizeof (*msg)); msg->msg_control = cbuf; msg->msg_controllen = sizeof (*cmsg) + sizeof (*sri); msg->msg_flags = 0; cmsg = (struct cmsghdr *)cbuf; sri = (struct sctp_sndrcvinfo *)(cmsg + 1); iov->iov_base = buf; iov->iov_len = BUFLEN; msg->msg_iov = iov; msg->msg_iovlen = 1; while ((n = recvmsg(fd, msg, 0)) > 0) { /* Intercept notifications here */ if (msg->msg_flags & MSG_NOTIFICATION) { snp = (union sctp_notification *)buf; printf("[ Receive notification type %u ]\n", snp->sn_type); continue; } msg->msg_control = cbuf; msg->msg_controllen = sizeof (*cmsg) + sizeof (*sri); printf("[ Receive echo (%u bytes): stream = %hu, ssn = %hu, " "flags = %hx, ppid = %u ]\n", n, sri->sinfo_stream, sri->sinfo_ssn, sri->sinfo_flags, sri->sinfo_ppid); } if (n < 0) { perror("recv"); exit(1); } close(fd); exit(0); } #define MAX_STREAM 64 /* * */ static void echo(struct sockaddr_in *addr) { int fd; uchar_t buf[BUFLEN]; ssize_t n; int perr; pthread_t tid; struct cmsghdr *cmsg; struct sctp_sndrcvinfo *sri; char cbuf[sizeof (*cmsg) + sizeof (*sri)]; struct msghdr msg[1]; struct iovec iov[1]; int ret; struct sctp_initmsg initmsg; struct sctp_event_subscribe events; /* Create a one-one SCTP socket */ if ((fd = socket(AF_INET, SOCK_STREAM, IPPROTO_SCTP)) == -1) { perror("socket"); exit(1); } /* * We are interested in association change events and we want * to get sctp_sndrcvinfo in each receive. */ events.sctp_association_event = 1; events.sctp_data_io_event = 1; ret = setsockopt(fd, IPPROTO_SCTP, SCTP_EVENTS, &events, sizeof (events)); if (ret < 0) { perror("setsockopt SCTP_EVENTS"); exit(1); } /* * Set the SCTP stream parameters to tell the other side when * setting up the association. */ memset(&initmsg, 0, sizeof(struct sctp_initmsg)); initmsg.sinit_num_ostreams = MAX_STREAM; initmsg.sinit_max_instreams = MAX_STREAM; initmsg.sinit_max_attempts = MAX_STREAM; ret = setsockopt(fd, IPPROTO_SCTP, SCTP_INITMSG, &initmsg, sizeof(struct sctp_initmsg)); if (ret < 0) { perror("setsockopt SCTP_INITMSG"); exit(1); } if (connect(fd, (struct sockaddr *)addr, sizeof (*addr)) == -1) { perror("connect"); exit(1); } /* Initialize the message header structure for sending. */ memset(msg, 0, sizeof (*msg)); iov->iov_base = buf; msg->msg_iov = iov; msg->msg_iovlen = 1; msg->msg_control = cbuf; msg->msg_controllen = sizeof (*cmsg) + sizeof (*sri); msg->msg_flags |= MSG_XPG4_2; memset(cbuf, 0, sizeof (*cmsg) + sizeof (*sri)); cmsg = (struct cmsghdr *)cbuf; sri = (struct sctp_sndrcvinfo *)(cmsg + 1); cmsg->cmsg_len = sizeof (*cmsg) + sizeof (*sri); cmsg->cmsg_level = IPPROTO_SCTP; cmsg->cmsg_type = SCTP_SNDRCV; sri->sinfo_ppid = 1; /* Start sending to stream 0. */ sri->sinfo_stream = 0; /* Create a thread to receive network traffic. */ perr = pthread_create(&tid, NULL, (void *(*)(void *))readit, &fd); if (perr != 0) { fprintf(stderr, "pthread_create: %d\n", perr); exit(1); } /* Read from stdin and then send to the echo server. */ while ((n = read(fileno(stdin), buf, BUFLEN)) > 0) { iov->iov_len = n; if (sendmsg(fd, msg, 0) < 0) { perror("sendmsg"); exit(1); } /* Send the next message to a different stream. */ sri->sinfo_stream = (sri->sinfo_stream + 1) % MAX_STREAM; } pthread_cancel(tid); close(fd); } int main(int argc, char **argv) { struct sockaddr_in addr[1]; struct hostent *hp; int error; if (argc < 2) { usage(*argv); exit(1); } /* Find the host to connect to. */ hp = getipnodebyname(argv[1], AF_INET, AI_DEFAULT, &error); if (hp == NULL) { fprintf(stderr, "host not found\n"); exit(1); } addr->sin_family = AF_INET; addr->sin_addr.s_addr = *(ipaddr_t *)hp->h_addr_list[0]; addr->sin_port = htons(5000); echo(addr); return (0); }
/* * Copyright 2004 Sun Microsystems, Inc. All rights reserved. * Use is subject to license terms. */ /* To enable socket features used for SCTP socket. */ #define _XPG4_2 #define __EXTENSIONS__ #include <stdio.h> #include <string.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <stdlib.h> #include <unistd.h> #include <netinet/sctp.h> #define BUFLEN 1024 /* * Given an event notification, print out what it is. */ static void handle_event(void *buf) { struct sctp_assoc_change *sac; struct sctp_send_failed *ssf; struct sctp_paddr_change *spc; struct sctp_remote_error *sre; union sctp_notification *snp; char addrbuf[INET6_ADDRSTRLEN]; const char *ap; struct sockaddr_in *sin; struct sockaddr_in6 *sin6; snp = buf; switch (snp->sn_header.sn_type) { case SCTP_ASSOC_CHANGE: sac = &snp->sn_assoc_change; printf("^^^ assoc_change: state=%hu, error=%hu, instr=%hu " "outstr=%hu\n", sac->sac_state, sac->sac_error, sac->sac_inbound_streams, sac->sac_outbound_streams); break; case SCTP_SEND_FAILED: ssf = &snp->sn_send_failed; printf("^^^ sendfailed: len=%hu err=%d\n", ssf->ssf_length, ssf->ssf_error); break; case SCTP_PEER_ADDR_CHANGE: spc = &snp->sn_paddr_change; if (spc->spc_aaddr.ss_family == AF_INET) { sin = (struct sockaddr_in *)&spc->spc_aaddr; ap = inet_ntop(AF_INET, &sin->sin_addr, addrbuf, INET6_ADDRSTRLEN); } else { sin6 = (struct sockaddr_in6 *)&spc->spc_aaddr; ap = inet_ntop(AF_INET6, &sin6->sin6_addr, addrbuf, INET6_ADDRSTRLEN); } printf("^^^ intf_change: %s state=%d, error=%d\n", ap, spc->spc_state, spc->spc_error); break; case SCTP_REMOTE_ERROR: sre = &snp->sn_remote_error; printf("^^^ remote_error: err=%hu len=%hu\n", ntohs(sre->sre_error), ntohs(sre->sre_length)); break; case SCTP_SHUTDOWN_EVENT: printf("^^^ shutdown event\n"); break; default: printf("unknown type: %hu\n", snp->sn_header.sn_type); break; } } /* * Receive a message from the network. */ static void * getmsg(int fd, struct msghdr *msg, void *buf, size_t *buflen, ssize_t *nrp, size_t cmsglen) { ssize_t nr = 0; struct iovec iov[1]; *nrp = 0; iov->iov_base = buf; msg->msg_iov = iov; msg->msg_iovlen = 1; /* Loop until a whole message is received. */ for (;;) { msg->msg_flags = MSG_XPG4_2; msg->msg_iov->iov_len = *buflen; msg->msg_controllen = cmsglen; nr += recvmsg(fd, msg, 0); if (nr <= 0) { /* EOF or error */ *nrp = nr; return (NULL); } /* Whole message is received, return it. */ if (msg->msg_flags & MSG_EOR) { *nrp = nr; return (buf); } /* Maybe we need a bigger buffer, do realloc(). */ if (*buflen == nr) { buf = realloc(buf, *buflen * 2); if (buf == 0) { fprintf(stderr, "out of memory\n"); exit(1); } *buflen *= 2; } /* Set the next read offset */ iov->iov_base = (char *)buf + nr; iov->iov_len = *buflen - nr; } } /* * The echo server. */ static void echo(int fd) { ssize_t nr; struct sctp_sndrcvinfo *sri; struct msghdr msg[1]; struct cmsghdr *cmsg; char cbuf[sizeof (*cmsg) + sizeof (*sri)]; char *buf; size_t buflen; struct iovec iov[1]; size_t cmsglen = sizeof (*cmsg) + sizeof (*sri); /* Allocate the initial data buffer */ buflen = BUFLEN; if ((buf = malloc(BUFLEN)) == NULL) { fprintf(stderr, "out of memory\n"); exit(1); } /* Set up the msghdr structure for receiving */ memset(msg, 0, sizeof (*msg)); msg->msg_control = cbuf; msg->msg_controllen = cmsglen; msg->msg_flags = 0; cmsg = (struct cmsghdr *)cbuf; sri = (struct sctp_sndrcvinfo *)(cmsg + 1); /* Wait for something to echo */ while ((buf = getmsg(fd, msg, buf, &buflen, &nr, cmsglen)) != NULL) { /* Intercept notifications here */ if (msg->msg_flags & MSG_NOTIFICATION) { handle_event(buf); continue; } iov->iov_base = buf; msg->msg_iov = iov; msg->msg_iovlen = 1; iov->iov_len = nr; msg->msg_control = cbuf; msg->msg_controllen = sizeof (*cmsg) + sizeof (*sri); printf("got %u bytes on stream %hu:\n", nr, sri->sinfo_stream); write(0, buf, nr); /* Echo it back */ msg->msg_flags = MSG_XPG4_2; if (sendmsg(fd, msg, 0) < 0) { perror("sendmsg"); exit(1); } } if (nr < 0) { perror("recvmsg"); } close(fd); } int main(void) { int lfd; int cfd; int onoff = 1; struct sockaddr_in sin[1]; struct sctp_event_subscribe events; struct sctp_initmsg initmsg; if ((lfd = socket(AF_INET, SOCK_STREAM, IPPROTO_SCTP)) == -1) { perror("socket"); exit(1); } sin->sin_family = AF_INET; sin->sin_port = htons(5000); sin->sin_addr.s_addr = INADDR_ANY; if (bind(lfd, (struct sockaddr *)sin, sizeof (*sin)) == -1) { perror("bind"); exit(1); } if (listen(lfd, 1) == -1) { perror("listen"); exit(1); } (void) memset(&initmsg, 0, sizeof(struct sctp_initmsg)); initmsg.sinit_num_ostreams = 64; initmsg.sinit_max_instreams = 64; initmsg.sinit_max_attempts = 64; if (setsockopt(lfd, IPPROTO_SCTP, SCTP_INITMSG, &initmsg, sizeof(struct sctp_initmsg)) < 0) { perror("SCTP_INITMSG"); exit (1); } /* Events to be notified for */ (void) memset(&events, 0, sizeof (events)); events.sctp_data_io_event = 1; events.sctp_association_event = 1; events.sctp_send_failure_event = 1; events.sctp_address_event = 1; events.sctp_peer_error_event = 1; events.sctp_shutdown_event = 1; /* Wait for new associations */ for (;;) { if ((cfd = accept(lfd, NULL, 0)) == -1) { perror("accept"); exit(1); } /* Enable ancillary data */ if (setsockopt(cfd, IPPROTO_SCTP, SCTP_EVENTS, &events, sizeof (events)) < 0) { perror("setsockopt SCTP_EVENTS"); exit(1); } /* Echo back any and all data */ echo(cfd); } }