编程接口指南

套接字基础知识

本节介绍基本套接字接口的用法。

创建套接字

socket(3SOCKET) 调用创建指定系列和指定类型的套接字。

s = socket(family, type, protocol);

如果未指定协议,则系统将选择支持所需套接字类型的协议。将返回套接字句柄。套接字句柄即为文件描述符。

familysys/socket.h 中定义的一个常量指定。名为 AF_suite 的常量指定要在解释名称时使用的地址格式:

AF_APPLETALK

Apple Computer Inc. Appletalk 网络

AF_INET6

Internet IPv6 和 IPv4 系列

AF_INET

仅 Internet IPv4 系列

AF_PUP

Xerox Corporation PUP internet

AF_UNIX

UNIX 文件系统

套接字类型在 sys/socket.h 中定义。AF_INET6AF_INETAF_UNIX 支持 SOCK_STREAMSOCK_DGRAMSOCK_RAW 这些类型。以下示例创建 Internet 系列的流套接字:

s = socket(AF_INET6, SOCK_STREAM, 0);

此调用生成流套接字。TCP 协议提供底层通信。在大多数情况下,将 protocol 参数设置为缺省值 0。可以指定缺省协议之外的其他协议,如高级套接字主题中所述。

绑定本地名称

创建套接字时不带名称。只有在套接字绑定到地址之后,远程进程才能引用此套接字。用于通信的进程通过地址连接。在 Internet 系列中,连接由本地和远程地址以及本地和远程端口组成。 不能存在重复排序组,如 protocollocal addresslocal portforeign addressforeign port。 在大多数系列中,连接必须唯一。

使用 bind(3SOCKET) 接口,进程可以指定套接字的本地地址。此接口组成 local addresslocal port 组。connect(3SOCKET)accept(3SOCKET) 通过添加地址元组的远程部分来完成套接字的关联。bind(3SOCKET) 调用的用法如下:

bind (s, name, namelen);

套接字句柄为 s。绑定名称是由支持协议解释的字节字符串。Internet 系列名称包含 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

主机拒绝服务。当服务器进程未在请求地址中显示时,会发生此错误。 

ENETDOWNEHOSTDOWN

这些错误是由底层通信接口传送的状态信息造成的。 

ENETUNREACHEHOSTUNREACH

由于不存在到网络或主机的路由,因此可能会发生这些操作错误。这些错误还可能是由于中间网关或切换节点所返回的状态信息造成的。返回的状态信息并不总是足以区分网络故障和主机故障。 

数据传送

本节介绍用于发送和接收数据的接口。可以使用常规 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 参数指定为非零值:

MSG_OOB

发送和接收带外数据

MSG_PEEK

查看数据而不读取

MSG_DONTROUTE

发送数据而不路由包

带外数据特定于流套接字。使用 recv(3SOCKET) 调用指定 MSG_PEEK 之后,所有显示的数据均返回到用户,但是仍视为不可读取。套接字上的下一个 read(2)recv(3SOCKET) 调用将返回相同数据。当前只有路由表管理进程使用发送数据而不路由包的选项(应用于传出包)。

关闭套接字

可以通过 close(2) 接口调用废弃 SOCK_STREAM 套接字。如果在 close(2) 接口调用之后数据排队到保证可靠传送的套接字,则协议会继续尝试传送数据。如果数据在任意时间之后还不能传送,则会将其废弃。

shutdown(3SOCKET) 可正常关闭 SOCK_STREAM 套接字。这两个进程均可确认不再发送。此调用的形式为:

shutdown(s, how);

其中 how 定义为

0

禁止进一步接收数据

1

禁止进一步传输数据

2

禁止进一步传输和接收

连接流套接字

以下两个示例说明启动和接受 Internet 系列流连接。

图 7–1 使用流套接字的面向连接的通信

此图形使用接受/连接和读/写功能对说明客户机与服务器之间的数据流。

以下是针对服务器的示例程序。服务器创建套接字并将名称绑定到此套接字,然后显示端口号。此程序调用 listen(3SOCKET) 将套接字标记为可以接受连接请求并初始化请求队列。此程序的其余部分为一个死循环。每次循环都接受一个新的连接并将其从队列中删除,从而创建一个新的套接字。服务器读取并显示此套接字中的消息,然后关闭此套接字。地址绑定中介绍了 in6addr_any 用法。


示例 7–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);
}

要启动连接,示例 7–2 中的客户机程序会创建一个流套接字,然后调用 connect(3SOCKET),指定用于连接的套接字地址。如果存在目标套接字,并且接受了请求,则连接会完成。现在,程序可以发送数据。数据按顺序传送,并且没有消息边界。此连接会在任何一个套接字关闭时销毁。有关此程序中的数据表示例程(如 ntohl(3SOCKET)ntohs(3SOCKET)htons(3SOCKET) 以及 htonl(3XNET))的更多信息,请参见 byteorder(3SOCKET) 手册页。


示例 7–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 添加到现有程序,使此程序可以指定要使用的协议。


示例 7–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) 接受新的连接。程序将接受连接请求,读取数据,并在单个套接字上断开连接。


示例 7–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) 例程提供一种同步多路复用方案。高级套接字主题中介绍的 SIGIOSIGURG 信号提供有关输出完成、输入可用性以及例外情况的异步通知。