暗无天日

=============>DarkSun的个人博客

网络IPC

1 套接字描述符

1.1 创建套接字

#include <sys/socket.h>

/* 成功返回套接字描述符,出错返回-1 */
int socket(int domain,int type,int protocol);
  • 参数domain确定通讯的特征,包括地址格式

    domain 描述
    AF_INET IPv4因特网域
    AF_INET6 IPv6因特网域
    AF_UNIX UNIX域
    AF_UNSPEC 未指定
  • 参数type确定套接字的类型,进一步确定通讯特征

    type 描述
    SOCK_DGRAM 长度固定的,无连接的不可靠报文传递
    SOCK_STREAM 有序,可靠,双向的面向连接的字节流,数据传送前需使用connect()来建立连接状态
    SOCK_RAW 原始的IP协议访问
    SOCK_SEQPACKET 连续可依赖的数据报连接
    • 对于SOCK_STREAM套接字是基于字节流服务的,应用程序无法直到对方发来了多少数据量,因此可能需要通过 多次read调用 才能获取完所有的发来的数据.
    • SOCK_SEQPACKET与SOCK_STREAM类似,但它是基于报文服务的,因此SOCK_SEQPACKET套接字 一次读入的数据量与对方所发送的一致.
    • SOCK_RAW套接字提供接口直接访问IP层,应用程序需要负责构造自己的协议首部.
  • 参数protocol用来指定socket所使用的具体传输协议编号

    参数protocol通常为0,表示按给给定的域(domain)和套接字类型(type)选择默认协议.

    AF_INET域+SOCK_STREAM套接字类型的默认协议是TCP

    AF_INET域+SOCK_DGRAM套接字类型的默认协议是UDP

1.2 使用文件描述符函数操作套接字描述符

虽然套接字描述符本质上是一个文件描述符,但不是所有参数为文件描述符的函数都可以接受套接字描述符.

函数 处理套接字时的行为
close 释放套接字
dup,dup2 和一般文件描述符一样复制
fchdir 失败,errno为ENOTDIR
fchmod 未定义
fchown 由实现定义
fcntl 支持某些命令,例如F_DUPFD,F_GETFD,F_GETFL,F_GETOWN,F_SETFD,F_SETFL,F_SETOWN
fdatasync,fsync 由实现定义
fstat 支持某些stat结构成员,但如何支持由实现定义
ftruncate 未定义
getmsg,getpmsg 若套接字由STREAMS实现则支持
ioctl 支持部分命令,依赖低层设备驱动
lseek 由实现定义(一般失败,errno为ESPIPE)
nmap 未定义
poll 正常工作
putmsg,putpmsg 若套接字由STREAMS实现则支持
read,readv 与没有任何标志位的recv等价
select 正常工作
write,writev 与没有任何标志位的send等价

1.3 shutdown函数

可以使用函数shutdown来禁止套接字上的输入/输出

#include <sys/socket.h>

int shutdown(int sockfd,int how);

参数how可以是:

SHUT_RD
关闭读
SHUT_WR
关闭写
SHUT_RDWR
关闭读写

shutdown与close的区别在于:

  • 若通过dup等操作复制过套接字,则只有在最后一个套接字被关闭后才回释放网络通路.
  • shutdown不管有多少个套接字连接,都使得连接立即关闭

2 寻址

TCP/IP协议栈规定了采用大端字节序,而处理器字节序分大端和小端两种,因此应用程序需要在处理器的字节序与网络字节序之间进行转换.

2.1 处理器字节序与网络字节序的转换函数

#include <arpa/inet.h>

/* 返回以网络字节序表示的32位整型数 */
uint32_t htonl(uint32_t hostint32);

/* 返回以网络字节序表示的16位整型数 */
uint16_t htons(uint16_t hostint16);

/* 返回以主机字节序表示的32位整型数 */
uint32_t ntohl(uint32_t netint32);

/* 返回以主机字节序表示的16位整型数 */
uint16_t ntohs(uint16_t netint16);

2.2 地址格式

一个地址用于标识一个特定通讯域的套接字端点,因此地址格式与特定的通讯域相关.

为了使不同格式地址能够传入到套接字函数,地址会被 强制转换为一个通用的地址结构sockaddr:

struct sockaddr{
  sa_family_t sa_family;        /* address的协议种类 */
  char sa_data[];               /* 大小可变的地址 */
  /* 其他成员 */
}

因特网地址定义在<netinet/in.h>头文件中. 在IPv4因特网域(AF_INET)中,套接字的地址用结构sockaddr_in表示:

struct sockaddr_in
{
  sa_family_t sin_family;       /* 地址family */
  in_port_t sin_port;           /* 端口号,实际一般为uint16_t */
  struct in_addr sin_addr;      /* IPv4地址 */
};

struct in_addr
{
  in_addr_t s_addr;             /* IPv4地址,实际一般为uint32_t */
};

IPv6则用结构sockaddr_in6表示:

struct sockaddr_in6{
  sa_family_t sin6_family;      /* 地址family */
  in_port_t sin6_port;          /* 端口号 */
  uint32_t sin6_flowinfo;       /* traffic class and flow info */
  struct in6_addr sin6_addr;    /* IPv6地址 */
  uint32_t sin6_scope_id;       /* set of interfaces for scope */
};

struct in6_addr{
  uint8_t sa_addr[16];          /* IPv6地址 */
};

2.3 地址格式转换

通过inet_ntop能把IPv4和IPv6的地址转换为人能理解的字符串格式

#include <arpa/inet.h>

/* 成功返回地址字符串指针. 格式无效返回0. 出错返回-1 */
const char* inet_ntop(int domain,const void* addr,char* str,socklen_t size);
  • 参数domain仅支持AF_INET和AF_INET6
  • 参数size指定了str缓存区的大小,INET_ADDRSTRLEN/INET6_ADDRSTRLEN定义了足够大的空间存放存放表示IPv4/IPv6地址的文本字符串.

通过inet_pton能把人理解的字符串格式转换成网络字节序的二进制格式.

#include <arpa/inet.h>

/* 若成功,返回1;格式无效返回0;出错返回-1 */
int inet_pton(int domian,const char* str,void* addr);

2.4 地址映射

POSIX.1定义了若干新函数,用于将一个主机名和服务器名映射到一个地址或者反之.

2.4.1 getaddrinfo

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

#include <sys/socket.h>
#include <netdb.h>

int getaddrinfo(const char* host,
                const char* service,
                const struct addrinfo* hint,
                struct addrinfo** res);

void freeaddrinfo(struct addrinfo* ai);

struct addrinfo
{
  int ai_flags;                 /*  */
  int ai_family;                /* 地址族,如AF_INET,AF_INET6,AF_UNIX...*/
  int ai_socktype;              /* socket类型,如SOCK_STREAM,SOCK_DGRAM,SOCK_SEQPACKET,SOCK_RAW */
  int ai_protocol;              /* 协议,如IPPROTO_TCP,IPPROTO_UDP,IPPROTO_RAW */
  socklen_t ai_addrlen;         /* 地址的字节长度 */
  struct sockaddr* ai_addr;     /* 地址 */
  char* ai_canonname;           /* 主机的canonical name */
  struct addrinfo* ai_next;     /* 列表中的下一个addrinfo元素 */
  /* 其他成员 */
};
  • 参数host和service必须至少指定一个值,如果仅提供一个值,那么另一个必须是 空指针
  • 参数host可以是一个节点名或点分结构的主机地址
  • 参数hint为一个过滤模板,用来选择符合特定条件的地址. 其包含ai_family,ai_flags,ai_protocol和ai_socktype字段, 剩余的整数字段必须设置为0,指针必须为NULL
  • 结果res为一个元素为addrinfo的链表结构
  • ai_flags字段中的标志意义为:

    标志 描述
    AI_ADDRCONFIG 查询配置的地址类型(IPv4/IPv6)
    AI_ALL 查找IPv4和IPv6地址(仅用于AI_V4MAPPED)
    AI_CANONNAME 需要一个规范的别名(非别名)
    AI_NUMERICHOST 以数字格式指定主机地址
    AI_NUMERICSERV 以数字端口号指定服务名
    AI_PASSIVE 套接字地址用于监听绑定
    AI_V4MAPPED 将IPv4的地址映射为IPv6

2.4.2 gai_strerror

如果getaddrinfo失败,不能使用perror或strerror来生成错误信息,而需要用gai_strerror将返回值转换成错误信息

const char* gai_strerror(int error);

2.4.3 getnameinfo

getnameinfo函数将一个地址转换成一个主机名和服务名

#include <sys/socket.h>
#include <netdb.h>

int getnameinfo(const struct sockaddr* addr, socklen_t alen,
                char* host, socklen_t hostlen,
                char* service,socklen_t servlen,
                int flags);
  • flags参数提供了一些控制翻译的方式

    标志 描述
    NI_DGRAM 服务基于数据报而非流
    NI_NAMEREQD 如果找不到主机名,则报错
    NI_NOFQDN 对于本地主机,仅返回全限定域名的节点名部分
    NI_NUMERICHOST 返回主机地址的数字形式,而非主机名
    NI_NUMERICSCOPE 对于IPv6,返回返回ID的数字形式,而非名字
    NI_NUMERICSERV 返回服务地址的数字形式(端口号),而非名字

2.5 将套接字与地址关联

2.5.1 用bind函数来关联地址和套接字

#include <sys/socket.h>

int bind(int sockfd,const struct sockaddr* addr,socklen_t len);
  • 绑定的地址必须是本地计算机的地址
  • 地址格式必须与创建套接字时指定的地址族相匹配
  • 地址中的端口号大于或等于1024,除非进程拥有root权限
  • bind操作是非必须的,若connect或listen时没有将地址绑定到套接字上,系统会选择一个地址绑定到套接字上

2.5.2 getsockname函数来发现绑定到套接字上的地址

#include <sys/socket.h>

int getsockname(int sockfd,struct sockaddr* addr, socklen_t* alenp);

调用getsockname前,alenp指向一个整数,且该整数为缓冲区sockaddr的长度,返回时, 该整数会被设置成返回地址的大小

2.5.3 getperrname函数查找对方的地址

#include <sys/socket.h>

int getpeername(int sockfd,struct sockaddr* addr,socklen_t* alenp)

与getsockname类似,但sockfd需已经和对方建立连接,且返回的是对方的地址.

3 建立连接

3.1 客户端建立连接

在客户端上使用connect函数来建立与服务端之间的连接

#include <sys/socket.h>

int connect(int sockfd,const struct sockaddr* addr,socklen_t len);
  • addr为服务器地址
  • 参数len为结构体sockaddr的长度(sockaddr的长度是可变的,还记得吗?)
  • connect将sockfd与远程服务器连接,之后即可通过对sockfd进行读写的方式与远程服务器交互了.

3.2 服务端建立连接

在服务端调用listen函数监听端口

#include <sys/socket.h>

/* 成功返回0,不成功返回-1 */
int listen(int sockfd,int backlog);
  • 参数backlog指定了同时能处理的最大连接数,其上限由<sys/socket.h>中的SOMAXCONN指定
  • 如果连接数目达到上限则client端将收到ECONNREFUSED操作
  • listen函数并未开始接受连接,它只是设置socket为listen模式,真正接受client端连接的是accept函数
  • 通常的调用顺序为socket(),bind(),listen(),accept()

服务端使用accept函数获取连接请求并建立连接

#include <sys/socket.h>

/* 若成功则返回新的已建立连接的套接字描述符 */
int accept(int sockfd,struct sockaddr* addr,socklen_t* len);
  • 参数sockfd为调用listen之后的套接字
  • 参数addr可以用来查看是哪个客户端发起的连接请求, 若对客户端无要求,可以将addr与len都设置为NULL
  • 如果没有连接请求在等待,accept会阻塞直到一个请求的到来. 若sockfd为非阻塞模式,则accept返回-1,且errno为EAGAIN或EWOULDBLOCK.
  • 服务器也可以使用poll或select来等待一个请求的到来,这时,一个带有等待连接请求的套接字会以可读的方式出现

4 数据传输

除了read和write外,还有三对为数据传递而设计的套接字函数

4.1 send和recv

send和write很类似,但可以指定标志来改变传输数据的方式

#include <sys/socket.h>

ssize_t send(int sockfd,const void* buf,size_t nbytes,int flags);
  • flag参数说明

    标志 描述
    MSG_CONFIRM 提供链路层以保持地址映射有效
    MSG_DONTROUTE 不将数据报邮路出本地网络
    MSG_DONTWAIT 非阻塞操作(等价于O_NONBLOCK)
    MSG_EOF 发送数据后关闭套接字的发送端
    MSG_EOR 如果协议支持,标记记录结束
    MSG_MORE 延迟发送数据包,以允许写更多数据
    MSG_NOSIGNAL 在写无连接的套接字时不产生SIGPIPE信号
    MSG_OOB 如果协议支持,发送带外数据
  • send函数返回,表示数据已经无错误地发送到网络驱动程序上,但 不代表连接的另一端进程就接受了数据
  • 对于支持报文边界的协议,如果尝试发送的单个报文的长度超过协议支持的最大长度,那么send会失败,且errno为EMSGSIZE
  • 对于字节流协议,send会阻塞直到整个数据传输完成

recv函数与read类似,但recv可以指定标志来控制如何接受数据.

#include <sys/socket.h>

ssize_t recv(int sockfd,void* buf,size_t nbytes,int flags);
  • 参数flag说明

    标志 描述
    MSG_CMSG_CLOEXEC 为UNIX域套接字上接收的文件描述符设置执行时关闭标志
    MSG_DONTWAIT 非阻塞操作(类似O_NONBLOCK)
    MSG_ERRQUEUE 接收错误信息作为辅助数据
    MSG_OOB 如果协议支持,获取带外数据
    MSG_PEEK 返回数据包内容,但不真正取走数据
    MSG_TRUNC 即使数据包被截断,也返回数据包的实际长度
    MSG_WAITALL 强迫接收到nbytes大小的数据后才返回(仅SOCK_STREAM),除非有错误或信号产生
    MSG_NOSIGNAL 该操作不能被SIGPIPE信号中断

4.2 sendto和recvfrom

sendto和send很类似,区别在于sendto可以在无连接的套接字上指定一个目标地址.

#include <sys/socket.h>

/* 若成功返回发送的字节数,出错返回-1 */
ssize_t sendto(int sockfd,const void* buf,size_t nbytes,int flags,
               const struct sockaddr* destaddr,socklen_t destlen);
  • 对于面向连接的套接字,目标地址参数被忽略,因为连接中隐含了目标地址. 因此sendto函数一般用于无连接的套接字

recvfrom也可以在无连接的套接字上指定一个目标地址

#include <sys/socket.h>

ssize_t recvfrom(int sockfd,void* buf,size_t len,int flags,
                 struct sockaddr* addr,socklen_t* addrlen);
  • 对于面向连接的套接字,目标地址参数被忽略,因为连接中隐含了目标地址. 因此recvfrom函数一般用于无连接的套接字

4.3 sendmsg和recvmsg

调用msghdr结构的sendmsg可以指定多重缓冲区传输数据,这和writev函数很类似

#include <sys/socket.h>

/* 成功返回发送的字节数,出错返回-1 */
ssize_t sendmsg(int sockfd,const struct msghdr* msg,int flags);

struct msghd
{
  void* msg_name;               /* optional address */
  socklen_t msg_namelen;        /* address size in bytes */
  struct iovec* msg_iov;        /* array of IO buffer */
  int msg_iovlen;               /* number of elements in array */
  void* msg_control;            /* 附加数据 */
  socklen_t msg_controllen;     /* 附加数据的字节长度 */
  int msg_flags;                /* flags for received message */
  /* 其他成员 */
};

recvmsg类似readv,可以将接收到的数据送入多个缓冲区.

#include <sys/socket.h>

ssize_t recvmsg(int sockfd,struct msghdr* msg,int flags);
  • 进入recvmsg时,msghdr结构中的msg_flags字段被忽略,但在返回时, 它会被设置以表示所接收数据的各种特征

    msg_flags 描述
    MSG_CTRUNC 控制数据被截断
    MSG_EOR 接收记录结束符
    MSG_ERRQUEUE 接收错误信息作为辅助数据
    MSG_OOB 接收带外数据
    MSG_TRUNC 一般数据被截断

5 套接字选项

套接字选项可以控制套接字行为,

5.1 setsockopt函数设置套接字选项

#include <sys/socket.h>

/* 成功返回0,失败返回-1 */
int setsockopt(int sockfd,int level,int option,const void* val,socklen_t len);
  • 参数level表示欲设置的网络层

    level 说明
    SOL_SOCKET 通用的套接字层次选项
    IPPROTO_TCP TCP协议选项
    IPPROTO_IP IP协议选项
  • 参数option表示选项,val表示该选项设置为那个值, val的具体参数类型根据不同的option而不同.

    选项 参数val的类型 描述
    SO_ACCEPTCONN int 返回信息指示该套接字是否能被监听(仅getsockopt)
    SO_BROADCAST int 若*val非0,则广播数据报
    SO_DEBUG int 若*val非0,启用网络驱动调试功能
    SO_DONTROUTE int 若*val非0,不将报文由路出网络
    SO_ERROR int 返回挂起的套接字错误并清除(仅getsockopt)
    SO_KEEPALIVE int 若*val非0,启用周期性keep-alive报文
    SO_LINGER struct linger 确保数据安全且可靠的传送出去
    SO_OOBINLINE int 若*val非0,将带外数据放在普通数据中
    SO_RCVBUF int 接收缓冲区的字节长度
    SO_RCVLOWAT int 接收调用中返回的最小数据字节数
    SO_RCVTIMEO struct timeval 套接字接收调用超时值
    SO_REUSEADDR int 若*val非0,重用bind中的地址
    SO_SNDBUF int 发送缓冲区的字节长度
    SO_SNDLOWAT int 发送调用中传送的最小数据字节数
    SO_SNDTIMEO struct timeval 套接字发送调用超时值
    SO_TYPE int 标识套接字类型(仅getsockopt)
  • 参数len指定了val指向的对象的大小.

可以使用getsockopt函数来查看选项的当前值

#include <sys/socket.h>

int getsockopt(int sockfd,int level,int option,void* val,socklen_t* lenp);
  • 参数lenp是一个指向整数的指针,在调用getsockopt之前该整数为val缓冲区的长度,调用后,该值更新为实际长度.

6 带外数据

带外数据是一些通讯协议所支持的可选功能,它具有比普通数据更高优先级的数据传输. 因此带外数据也被成为紧急数据.

TCP支持一个字节的带外数据,但UDP不支持.

为了产生带外数据,可以在3个send函数中的任何一个里指定MSG_OOB标志. 如果代MSG_OOB标志发送的字节数超过一个时,最后一个字节将被视为紧急数据字节.

如果通过套接字安排了信号的产生,那么紧急数据被接收时,会发送SIGURG信号.

当带外数据出现在套接字读取队列时,select函数会返回一个文件描述符并且有一个待处理的异常条件.

可以在普通数据流上接收带外数据,也可以在recv函数中使用MSG_OOB标志优先接收紧急数据. 由于TCP队列仅使用一个字节的带外数据,因此若在接收当前的紧急数据字节之前,又有新的紧急数据到来,那么已有的字节会被丢弃

使用函数sockatmark可以判断将要读取的下一个字节是否为带外数据

#include <sys/socket.h>

/* 将要读取带外数据返回1,否则返回0,出错返回-1 */
int sockatmark(int sockfd);