本节介绍基本套接字接口的用法。
socket(3SOCKET) 调用创建指定系列和指定类型的套接字。
s = socket(family, type, protocol);
如果未指定协议,则系统将选择支持所需套接字类型的协议。将返回套接字句柄。套接字句柄即为文件描述符。
family 由 sys/socket.h 中定义的一个常量指定。名为 AF_suite 的常量指定要在解释名称中使用的地址格式:
Apple Computer Inc. Appletalk 网络
适用于 IPv6 和 IPv4 的 Internet 系列
仅适用于 IPv4 的 Internet 系列
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) 时阻塞。高级套接字主题中介绍了这些技术。
如果连接失败,则会返回错误,但是由系统绑定的地址保持不变。如果连接成功,则套接字与服务器关联,并且可以开始数据传输。
下表列出在连接尝试失败时返回的一些比较常见的错误。
表 8-1 套接字连接错误
|
本节介绍用于发送和接收数据的接口。可以使用常规 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 系列流连接。
图 8-1 使用流套接字的面向连接的通信
以下是针对服务器的示例程序。服务器创建套接字并将名称绑定到此套接字,然后显示端口号。此程序调用 listen(3SOCKET) 将套接字标记为可以接受连接请求并初始化请求队列。此程序的其余部分为一个死循环。每次循环都接受一个新的连接并将其从队列中删除,从而创建一个新的套接字。服务器读取并显示此套接字中的消息,然后关闭此套接字。地址绑定中介绍了 in6addr_any 的用法。
示例 8-1 接受 Internet 流连接(服务器)
#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);
}
要启动连接,示例 8-2 中的客户机程序会创建一个流套接字,然后调用 connect(3SOCKET),指定用于连接的套接字地址。如果存在目标套接字,并且接受了请求,则连接会完成。现在,程序可以发送数据。数据按顺序传送,并且没有消息边界。此连接会在任何一个套接字关闭时销毁。 有关此程序中的数据表示例程(如 ntohl(3SOCKET)、ntohs(3SOCKET)、htons(3SOCKET) 和 htonl(3XNET))的更多信息,请参见 byteorder(3SOCKET) 手册页。
示例 8-2 Internet 系列流连接(客户机)
#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 添加到现有程序,使此程序可以指定要使用的协议。
示例 8-3 将 SCTP 支持添加到流套接字
#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) 接受新的连接。程序将接受连接请求,读取数据,并在单个套接字上断开连接。
示例 8-4 使用 select(3C) 检查暂挂连接
#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 信号提供有关输出完成、输入可用性以及例外情况的异步通知。