JOE'S BLOG

好记性不如烂键盘

0%

网络IPC: 套接字(十六)

引言

UNIX系统提供的经典进程间通信机制(IPC): 管道、FIFO、消息队列、信号量以及共享存储。这些机制允许在同一台计算机上运行的进程可以相互通信。本章内容主要是研究在不同计算机上的进程相互通信的机制: 网络进程间通信(network IPC)。

套接字描述符

套接字描述符在UNIX系统上被当做一种文件描述符,UNIX系统一切皆文件。
调用socket函数,创建一个套接字

1
2
3
#include <sys/socket.h>
int socket(int damain, int type, int protocol);
//成功:返回描述符,出错:返回-1

参数domain确定通信特征,包括地址格式

描述
AF_INET IPv4
AF_INET6 IPv6
AF_UNIX UNIX域
AF_UPSPEC 未指定

参数type确定套接字类型,进一步确定通信特征

类型 描述
SOCK_DGRAM 固定长度、无连接、不可靠的报文传递
SOCK_RAW IP协议的数据报接口
SOCK_SEQPACKET 固定长度、有序的、可靠的、面向连接的报文传递
SOCK_STREAM 有序的、可靠的、双向的、面向连接的字节流

参数protocal通常是0、表示为给定的域和套接字类型选择默认协议

数据报(SOCK_DGRAM)接口,类似于UDP通信,两个对等进程之间通信时不需要逻辑连接。只需要向对等进程所使用的套接字送出一个报文。
字节流(SOCK_STREAM),类似于TCP,在交换数据前,需要建立一个逻辑连接。

套接字通信时双向的。可以采用shutdown函数来禁止一个套接字的I/O。

1
2
3
#include <sys/socket.h>
int shutdown(int sockfd, int how);
//成功:返回0 失败:返回-1

how参数

  • SHUT_RE(关闭读端),无法从套接字读取数据
  • SHUT_WR(关闭写端),无法使用套接字发送数据
  • SHUT_RDWR,无法读,也无法读

有两个函数用于点分十进制和二进制地址格式之前的相互转换

1
2
3
4
5
6
7
8
#include <arpa/inet.h>
const char *inet_ntop(int domain, const void *restrict addr,
char *restrict str, socklen_t size);
//成功:返回地址字符串指针,出错:返回NULL

int inet_pton(int domain, const char *restrict str,
void *restrict addr);
//成功:返回1,格式无效返回0,失败:返回-1

将套接字与地址关联

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

1
2
3
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t len);
//成功:返回0,失败:返回-1

建立连接

使用connect函数来建立连接

1
2
3
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t len);
//成功:返回0,失败:返回-1

在connect中指定的地址是我们想与之通信的服务器地址。如果sockfd没有绑定到一个地址,connect会给调用者绑定一个默认地址。

服务器调用listen函数宣告它愿意接受连接请求

1
2
3
#include <sys/socket.h>
int listen(int sockfd, int backlog);
//成功:返回0,失败:返回-1

参数backlog提供了一个提示,提示系统该进程所要入队的未完成连接请求数量。实际值由系统决定
一旦队列满,系统就会拒绝多余的连接请求,所以backlog的值应该基于服务器期望负载和处理量来选择,其中处理量是指接受连接请求与启动服务的数量。
一旦服务器调用了listen,所用的套接字就能接收连接请求。使用accept函数来获得连接请求并建立连接。

1
2
3
4
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *restrict addr,
socklen_t *restrict len);
//成功:返回文件描述符,出错:返回-1

函数accept返回的是套接字描述符,该描述符连接到调用connect的客户端。这个新的套接字描述符和原始套接字(sockfd)具有相同的套接字类型和地址族。传给accept的原始套接字没有关联到这个连接,而是继续保持可用状态并接收其他连接请求。
如果不关心客户端标识,可以将参赛addr和len设为NULL。否则,在调用accept之前,将addr参数设为足够大的缓冲区来存放地址,并且将len指向的整数设为这个缓冲区的字节大小。返回时,accept会在缓冲区填充客户端的地址,并且更新指向len的整数来反映该地址的大小。
如果没有连接请求等待,accept会阻塞直到一个请求到来。如果sockfd处于非阻塞模式,accept会返回-1,并将errno设置为EAGAIN或EWOULDBLOCK。
如果服务器调用accept,并且当前没有连接请求,服务器会阻塞直到一个请求到来。另外,服务器可以使用poll或select来等待一个请求的到来。在这种情况下,一个带有等待连接请求的套接字会以可读的方式出现。

数据传输

1
2
3
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t nbytes, int flags);
//成功: 返回发送的字节数,失败:返回-1

对于支持报文边界的协议,如果尝试发送单个报文的长度超过协议锁支持的最大长度,那么send会失败,并将errno设为EMSGSIZE。对于字节流协议,send会阻塞直到整个数据传输完成。函数sendto和send类似。区别在于sendto可以在无连接的套接字上指定一个目标地址。

1
2
3
#include <sys/socket.h>
ssize_t sendto(int sockfd, const void *buf, size_t nbytes, int flags, const struct sockaddr * destaddr, socklen_t destlen);
//成功:返回发送的字节数,失败:返回-1

使用recv函数接收数据

1
2
3
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t nbytes, int flags);
//返回数据的字节长度,无数据或等放已经按序结束,返回0,出错:返回-1

非阻塞和异步I/O

通常,recv函数没有数据可用时会阻塞等待。同样的,当套接字输出队列没有足够空间来发送消息时,send函数会阻塞。在套接字非阻塞模式下,行为会改变。在这种情况下,这些函数不会阻塞而是会失败,将errno设置为EWOULDBLOCK或者EAGAIN。当这种情况发生时,可以用poll或select来判断能否接收或者传输数据。

Single UNIX Specification 包含通用异步I/O机制的支持。套接字机制有自己的处理异步I/O的方式。
在基于套接字的异步I/O中,当从套接字中读取数据时,或者当套接字写队列中空间变得可用时,可以安排要发送的信号SIGIO。
启用异步I/O的步骤

  1. 建立套接字所有权,这样信号可以被传递到合适的进程。
    • 在fcntl中使用F_SETOWN命令
    • 在ioctl中使用FIOSETOWN命令
    • 在ioctl中使用SIOCSGRP命令
  2. 通知套接字当I/O操作不会阻塞时发信号。
    • 在fcntl中使用F_SETFL命令并且启用文件标志O_ASYNC
    • 在ioctl中使用FIOASYNC命令。

但没有得到普遍支持