APUE - 网络 IPC:套接字

套接字描述符

要创建一个套接字,可以调用 socket 函数。

#include <sys/socket.h>
int socket(int domain, int type, int protocol);

套接字通信是双向的。可以采用函数 shutdown 来禁止套接字上的输入/输出。

#include <sys/socket.h>
int shutdown(int sockfd, int how);

寻址

字节序

如果处理器架构支持大端(big-endian)字节序,那么最大字节地址对应于数字最低有效字节(LSB)上;小端(little-endian)字节序则相反:数字最低有效字节对应于最小字节地址。注意,不管字节任何排序,数字最高位总是在左边,最低位总是在右边。

对于 TCP/IP 应用程序,提供了四个通用函数以实施在处理器字节序和网络字节序之间的转换。

#include <arpa/inet.h>
uint32_t htonl(uint32_t hostint32);
uint16_t htons(uint16_t hostint16);
uint32_t ntohl(uint32_t netint32);
uint16_t ntohs(uint16_t netint16);

地址格式

地址标识了特定通信域的套接字端点,地址格式与特定的通信域相关。为使不同格式地址能够被传入到套接字函数,地址被强制转换成通用的地址结构 sockaddr 表示:

struct sockaddr {
    sa_family_t sa_family;  /* address family */
    char        sa_data[];  /* variable-length address */
    /* ... */
};

因特网地址定义在 中。在 IPv4 因特网域(AF_INET)中,套接字地址用如下结构 sockaddr_in 表示:

struct in_addr {
    in_addr_t       s_addr;     /* IPv4 address */
};
struct sockaddr_in {
    sa_family_t     sin_family; /* address family */
    in_port_t       sin_port;   /* port number */
    struct in_addr  sin_addr;   /* IPv4 address */
};

IPv6 因特网域(AF_INET6)套接字地址用如下结构 sockaddr_in6 表示:

struct in6_addr {
    uint8_t         s6_addr[16];    /* IPv6 address */
};
struct sockaddr_in6 {
    sa_family_t     sin6_family;    /* address family */
    in_port_t       sin6_port;      /* port number */
    uint32_t        sin6_flowinfo;  /* traffic class and flow info */
    struct in6_addr sin6_addr;      /* IPv6 address */
    uint32_t        sin6_scope_id;  /* set of interfaces for scope */
};

下列函数用于在二进制地址格式与点分十进制字符串表示之间相互转换。

#include <arpa/inet.h>
const char *inet_ntop(int domain, const void *restrict addr,
                      char *restrict str, socklen_t size);
int inet_pton(int domain, const char *restrict str,
              void *restrict addr);

地址查询

获得给定计算机的主机信息:

#include <netdb.h>
struct hostent *gethostent(void);
void sethostent(int stayopen);
void endhostent(void);

struct hostent {
    char   *h_name;         /* name of host */
    char  **h_aliases;      /* pointer to alternate host name array */
    int     h_addrtype;     /* address type */
    int     h_length;       /* length in bytes of address */
    char  **h_addr_list;    /* pointer to array of network addresses */
    /* ... */
};

获得网络名字和网络号:

#include <netdb.h>
struct netent *getnetbyaddr(uint32_t net, int type);
struct netent *getnetbyname(const char *name);
struct netent *getnetent(void);
void setnetent(int stayopen);
void endnetent(void);

struct netent {
    char       *name;       /* network name */
    char      **n_aliases;  /* alternate network name array pointer */
    int         n_addrtype; /* address type */
    uint32_t    n_net;      /* network number */
    /* ... */
};

可以将协议名字和协议号采用以下函数映射。

#include <netdb.h>
struct protoent *getprotobyname(const char *name);
struct protoent *getprotobynumber(int proto);
struct protoent *getprotoent(void);
void setprotoent(int stayopen);
void endprotoent(void);

struct protoent {
    char   *p_name;     /* protocol name */
    char  **p_aliases;  /* pointer to alternate protocol name array */
    int     p_proto;    /* protocol number */
    /* ... */
};

函数 getservbyname 可以将一个服务名字映射到一个端口号,函数 getservbyport 将一个端口号映射到一个服务名,函数 getservent 顺序扫描服务数据库。

#include <netdb.h>
struct servent *getservbyname(const char *name, const char *proto);
struct servent *getservbyport(int port, const char *proto);
struct servent *getservent(void);
void setservent(int stayopen);
void endservent(void);

struct servent {
    char   *s_name;     /* service name */
    char  **s_aliases;  /* pointer to alternate service name array */
    int     s_port;     /* port number */
    char   *s_proto;    /* name of protocol */
    /* ... */
};

函数 getaddrinfo 允许将一个主机名字和服务名字映射到一个地址。

#include <sys/socket.h>
#include <netdb.h>
int getaddrinfo(const char *restrict host,
                const char *restrict service,
                const struct addrinfo *restrict hint,
                struct addrinfo **restrict res);
void freeaddrinfo(struct addrinfo *ai);

struct addrinfo {
    int                 ai_flags;       /* customize behavior */
    int                 ai_family;      /* address family */
    int                 ai_socktype;    /* socket type */
    int                 ai_protocol;    /* protocol */
    socklen_t           ai_addrlen;     /* length in bytes of address */
    struct sockaddr    *ai_addr;        /* address */
    char               *ai_canonname;   /* canonical name of host */
    struct addrinfo    *ai_next;        /* next in list */
    /* ... */
};

如果 getaddrinfo 失败,不能使用 perror 或 strerror 来生成错误消息。替代地,调用 gai_strerror 将返回的错误码转换成错误消息。

#include <netdb.h>
const char *gai_strerror(int error);

函数 getnameinfo 将地址转换成主机名或者服务名。

#include <sys/socket.h>
#include <netdb.h>
int getnameinfo(const struct sockaddr *restrict addr,
                socklen_t alen, char *restrict host,
                socklen_t hostlen, char *restrict service,
                socklen_t servlen, unsigned int flags);

将套接字与地址绑定

可以用 bind 函数将地址绑定到一个套接字。

#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t len);

对于所能使用的地址有一些限制:

  • 在进程所运行的机器上,指定的地址必须有效,不能指定一个其他机器的地址。
  • 地址必须和创建套接字时的地址族所支持的格式相匹配。
  • 端口号必须不小于 1024,除非该进程具有相应的特权(即为超级用户)。
  • 一般只有套接字端点能够与地址绑定,尽管有些协议允许多重绑定。

可以调用 getsockname 来发现绑定到一个套接字的地址。

#include <sys/socket.h>
int getsockname(int sockfd, struct sockaddr *restrict addr,
                socklen_t *restrict alenp);

如果套接字已经和对方连接,调用 getpeername 来找到对方的地址。

#include <sys/socket.h>
int getpeername(int sockfd, struct sockaddr *restrict addr,
                socklen_t *restrict alenp);

建立连接

如果处理的是面向连接的网络服务(SOCK_STREAM或SOCK_SEQPACKET),在开始交换数据以前,需要在请求服务的进程套接字(客户端)和提供服务的进程套接字(服务器)之间建立一个连接。可以用 connect 建立一个连接。

#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t len);

服务器调用 listen 来宣告可以接受连接请求。

#include <sys/socket.h>
int listen(int sockfd, int backlog);

一旦服务器调用了 listen,套接字就能接收连接请求。使用 accept 获得连接请求并建立连接。

#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *restrict addr,
           socklen_t *restrict len);

数据传输

#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t nbytes, int flags);
ssize_t sendto(int sockfd, const void *buf, size_t nbytes, int flags,
               const struct sockaddr *destaddr, socklen_t destlen);
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
ssize_t recv(int sockfd, void *buf, size_t nbytes, int flags);
ssize_t recvfrom(int sockfd, void *restrict buf, size_t len, int flags,
                 struct sockaddr *restrict addr,
                 socklen_t *restrict addrlen);
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);

struct msghdr {
    void           *msg_name;           /* optional address */
    socklen_t       msg_namelen;        /* address size in bytes */
    struct iovec   *msg_iov;            /* array of I/O buffers */
    int             msg_iovlen;         /* number of elements in array */
    void           *msg_control;        /* ancillary data */
    socklen_t       msg_controllen;     /* number of ancillary bytes */
    int             msg_flags;          /* flags for received message */
    /* ... */
};

套接字选项

套接字机制提供两个套接字选项接口来控制套接字行为。一个接口用来设置选项,另一个接口允许查询一个选项的状态。可以获取或设置三种选项:

  1. 通用选项,工作在所有套接字类型上。
  2. 在套接字层次管理的选项,但是依赖于下层协议的支持。
  3. 特定于某协议的选项,为每个协议所独有。

可以采用 setsockopt 函数来设置套接字选项。

#include <sys/socket.h>
int setsockopt(int sockfd, int level, int option, const void *val,
               socklen_t len);

可以使用 getsockopt 函数来发现选项的当前值。

#include <sys/socket.h>
int getsockopt(int sockfd, int level, int option, void *restrict val,
               socklen_t *restrict lenp);

带外数据

带外数据是一些通信协议所支持的可选特征,允许更高优先级的数据比普通数据优先传输。即使传输队列已经有数据,带外数据先行传输。TCP 支持带外数据,但是 UDP 不支持。

TCP 将带外数据称为“紧急”数据。TCP 仅支持一个字节的紧急数据,但是允许紧急数据在普通数据传递机制数据流之外传输。为了产生紧急数据,在三个 send 函数中任何一个指定标志 MSG_OOB。如果带 MSG_OOB 标志传输字节超过一个时,最后一个字节被看作紧急数据字节。

如果安排发生套接字信号,当接收到紧急数据时,那么发送信号 SIGURG。

TCP 支持紧急标记的概念:在普通数据流中紧急数据所在的位置。如果采用套接字选项 SO_OOBINLINE,那么可以在普通数据中接收紧急数据。为帮助判断是否接收到紧急标记,可以使用 sockatmark 函数。

#include <sys/socket.h>
int sockatmark(int sockfd);

非阻塞和异步 I/O

在基于套接字的异步 I/O 中,当能够从套接字中读取数据,或者套接字写队列中的空间变得可用时,可以安排发送信号 SIGIO。通过两个步骤来使用异步 I/O:

  1. 建立套接字拥有者关系,信号可以被传送到合适的进程。
  2. 通知套接字当 I/O 操作不会阻塞时发信号告知。

可以使用三种方式来完成第一个步骤:

  1. 在 fcntl 中使用 F_SETOWN 命令。
  2. 在 ioctl 中使用 FIOSETOWN 命令。
  3. 在 ioctl 中使用 SIOCSPGRP 命令。

要完成第二个步骤,有两个选择:

  1. 在 fcntl 中使用 F_SETFL 命令并且启用文件标志 O_ASYNC。
  2. 在 ioctl 中使用 FIOASYNC。