APUE 网络IPC:套接字

《UNIX环境高级编程》第16章 网络IPC:套接字 笔记

套接字描述

套接字是通信端点的抽象。正如使用文件描述符访问文件,应用程序用套接字描述符访问套接字。套接字描述符在UNIX系统中被当作是一种文件描述符。事实上,许多处理文件描述符的函数(如read和 write)可以用于处理套接字描述符。

为创建一个套接字,调用socket函数:

1
2
3
4
5
#include <sys/socket.h> 

int socket(int domain, int type, int protocol);

// Returns: file (socket) descriptor if OK, −1 on error

参数 domain(域)确定通信的特性,包括地址格式

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

参数protocol通常是0,表示为给定的域和套接字类型选择默认协议。当对同一域和套接字类型支持多个协议时,可以使用 protocol选择一个特定协议

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

调用socket与调用open相类似。在两种情况下,均可获得用于I/O的文件描述符。当不再需要该文件描述符时,调用close来关闭对文件或套接字的访问,并且释放该描述符以便重新使用虽然套接字描述符本质上是一个文件描述符,但不是所有参数为文件描述符的函数都可以接受套接字描述符。

寻址

在学习用套接字做一些有意义的事情之前,需要知道如何标识一个目标通信进程。进程标识由两部分组成。一部分是计算机的网络地址,它可以帮助标识网络上我们想与之通信的计算机;另一部分是该计算机上用端口号表示的服务,它可以帮助标识特定的进程。

字节序

字节序是一个处理器架构特性用于指示像整数这样的大数据类型内部的字节如何排序。

如果处理器架构支持大端(big-endian)字节序,那么最大字节地址出现在最低有效字节( Least Significant Byte,LSB)上,小端(little-endian)字节序则相反:最低有效字节包含最小字节地址

网络协议指定了字节序,因此异构计算机系统能够交换协议信息而不会被字节序所混淆。TCPP协议栈使用大端字节序。应用程序交换格式化数据时,字节序问题就会出现

对于TCPP应用程序,有4个用来在处理器字节序和网络字节序之间实施转换的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <arpa/inet.h> 

uint32_t htonl(uint32_t hostint32);
// Returns: 32-bit integer in network byte order

uint16_t htons(uint16_t hostint16);
// Returns: 16-bit integer in network byte order

uint32_t ntohl(uint32_t netint32);

// Returns: 32-bit integer in host byte order

uint16_t ntohs(uint16_t netint16);

// Returns: 16-bit integer in host byte order

h表示“主机”字节序,n表示“网络”字节序。1表示“长”(即4字节)整数,s表示“短”

地址格式

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

不同的系统实现可能会不一样

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

有时,需要打印出能被人理解而不是计算机所理解的地址格式。

1
2
3
4
5
6
7
#include <arpa/inet.h>

const char *inet_ntop(int domain, const void *restrict addr, char *restrict str, socklen_t size);
// Returns: pointer to address string on success, NULL on error

int inet_pton(int domain, const char *restrict str, void *restrict addr);
// Returns: 1 on success, 0 if the format is invalid, or −1 on error

函数inet_ntop将网络字节序的二进制地址转换成文本字符串格式。inet_pton将文本字符串格式转换成网络字节序的二进制地址。

将套接字和地址关联

将一个客户端的套接字关联上一个地址没有多少新意,可以让系统选一个默认的地址。然而,对于服务器,需要给一个接收客户端请求的服务器套接字关联上一个众所周知的地址。客户端应有一种方法来发现连接服务器所需要的地址,最简单的方法就是服务器保留一个地址并且注册在/etc/services或者某个名字服务中。

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

1
2
3
4
5
#include <sys/socket.h>

int bind(int sockfd, const struct sockaddr *addr, socklen_t len);

// Returns: 0 if OK, −1 on error

对于使用的地址有以下一些限制:

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

可以调用 getsockname函数来发现绑定到套接字上的地址:

1
2
3
4
5
#include <sys/socket.h> 

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

// Returns: 0 if OK, −1 on error

调用 getsockname之前,将 alenp设置为一个指向整数的指针,该整数指定缓冲区sockaddr的长度。返回时,该整数会被设置成返回地址的大小。如果地址和提供的缓冲区长

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

1
2
3
4
5
#include <sys/socket.h> 

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

// Returns: 0 if OK, −1 on error

建立连接

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

1
2
3
4
5
#include <sys/socket.h> 

int connect(int sockfd, const struct sockaddr *addr, socklen_t len);

// Returns: 0 if OK, −1 on error

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

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

1
2
3
4
5
#include <sys/socket.h> 

int listen(int sockfd, int backlog);

// Returns: 0 if OK, −1 on error

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

1
2
3
4
5
#include <sys/socket.h> 

int accept(int sockfd, struct sockaddr *restrict addr, socklen_t *restrict len);

// Returns: file (socket) descriptor if OK, −1 on error

函数accept所返回的文件描述符是套接字描述符,该描述符连接到调用connect的客户端。这个新的套接字描述符和原始套接字(sockfd)具有相同的套接字类型和地址族。传给accept原始套接字没有关联到这个连接,而是继续保持可用状态并接收其他连接请求。

如果服务器调用accept,并且当前没有连接请求,服务器会阻塞直到一个请求到来。另外服务器可以使用poll或se1ect来等待一个请求的到来。在这种情况下,一个带有等待连接请求的套接字会以可读的方式出现

数据传输

既然一个套接字端点表示为一个文件描述符,那么只要建立连接,就可以使用read和 write来通过套接字通信。

尽管可以通过read和write交换数据,但这就是这两个函数所能做的一切。如果想指定选项,从多个客户端接收数据包,或者发送带外数据,就需要使用6个为数据传递而设计的套接字函数中的一个

3个函数用来发送数据,3个用于接收数据。首先,考查用于发送数据的函数。最简单的是send,它和 write很像,但是可以指定标志来改变处理传输数据的方式

1
2
3
4
5
#include <sys/socket.h> 

ssize_t send(int sockfd, const void *buf, size_t nbytes, int flags);

// Returns: number of bytes sent if OK, −1 on error

前面3个参数和write参数一样,不同的是第4个参数flags:

即使send成功返回,也并不表示连接的另一端的进程就一定接收了数据。我们所能保证的只是当send成功返回时,数据已经被无错误地发送到网络驱动程序上

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

1
2
3
4
5
#include <sys/socket.h> 

ssize_t sendto(int sockfd, const void *buf, size_t nbytes, int flags, const struct sockaddr *destaddr, socklen_t destlen);

// Returns: number of bytes sent if OK, −1 on error

对于面向连接的套接字,目标地址是被忽略的,因为连接中隐含了目标地址。对于无连接的套接字,除非先调用 connect设置了目标地址,否则不能使用send。 sendto提供了发送报文的另一种方式

通过套接字发送数据时,还有一个选择。可以调用带有msghdr结构的sendmsg来指定多重缓冲区传输数据,这和 writev函数很相似

1
2
3
4
5
#include <sys/socket.h>

ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);

// Returns: number of bytes sent if OK, −1 on error

与发送数据相对应的,接受数据也有三个函数:

1
2
3
4
5
#include <sys/socket.h> 

ssize_t recv(int sockfd, void *buf, size_t nbytes, int flags);

// Returns: length of message in bytes, 0 if no messages are available and peer has done an orderly shutdown, or −1 on error

如果有兴趣定位发送者,可以使用 recvfrom来得到数据发送者的源地址

1
2
3
4
5
#include <sys/socket.h> 

ssize_t recvfrom(int sockfd, void *restrict buf, size_t len, int flags, struct sockaddr *restrict addr, socklen_t *restrict addrlen);

// Returns: length of message in bytes, 0 if no messages are available and peer has done an orderly shutdown, or −1 on error

为了将接收到的数据送入多个缓冲区,类似于 readv,或者想接收辅助数据,可以使用 recvmso

1
2
3
4
5
#include <sys/socket.h> 

ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);

// Returns: length of message in bytes, 0 if no messages are available and peer has done an orderly shutdown, or −1 on error

套接字选项

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

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

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

1
2
3
4
5
#include <sys/socket.h> 

int setsockopt(int sockfd, int level, int option, const void *val, socklen_t len);

// Returns: 0 if OK, −1 on error

参数 level标识了选项应用的协议。

参数val根据选项的不同指向一个数据结构或者一个整数。一些选项是on/off开关。如果整数非0,则启用选项。如果整数为0,则禁止选项。参数len指定了val指向的对象的大小

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

1
2
3
4
5
6
#include <sys/socket.h> 

int getsockopt(int sockfd, int level, int option, void *restrict val, socklen_t *restrict lenp);


// Returns: 0 if OK, −1 on error

带外数据

带外数据( out-of-band data)是一些通信协议所支持的可选功能,与普通数据相比,它允许更高优先级的数据传输。带外数据先行传输,即使传输队列已经有数据。TCP支持带外数据,但是UDP不支持。套接字接口对带外数据的支持很大程度上受TCP带外数据具体实现的影响。TCP将带外数据称为紧急数据( urgent data)。

TCP仅支持一个字节的紧急数据,但是允许紧急数据在普通数据传递机制数据流之外传输。

非阻塞和异步I/O

在基于套接字的异步LO中,当从套接字中读取数据时,或者当套接字写队列中空间变得可用时,可以安排要发送的信号 SIGIO。

启用异步IO是一个两步骤的过程

  1. 建立套接字所有权,这样信号可以被传递到合适的进程
  2. 通知套接字当IO操作不会阻塞时发信号

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

  • 在fcnt1中使用 F_SETOWN命令
  • 在ioct1中使用 FIOSETOWN命令
  • 在ioctl中使用 SIOCSPGRP命令

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

  • 在fcnt1中使用F_SETFL命令并且启用文件标志O_ASYNO
  • 在ioctl中使用FI命令