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

时间:2021-11-11 22:20:06

16.1 引言

本章将考察不同计算机(通过网络连接)上的进程相互通信的机制:网络进程间通信(network IPC)
套接字网络进程间通信接口,进程用该接口能够和其他进程通信,无论他们是在同一台计算机上还是在不同的计算机上。

16.2 套接字描述符

套接字是通信端点的抽象。正如使用文件描述符访问文件,应用程序用套接字描述符访问套接字。套接字描述符在unix系统中被当作是一种文件描述符。
因此能够操作文件的函数也能作用于套接字。

使用socket函数创建一个套接字。

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

- 参数domain(域)确定通信的特性,每个域都有自己表示地址的格式,而表示各个域常数都以AF_开头。意指地址族(address family)
- 参数type确定套接字的类型,进一步确定通信特征。
- 参数protocol通常是0,表示为给定的域和套接字选择默认协议。


套接字通信是双向的。可以采用shutdown函数来禁止一个套接字的io。

int shutdown(int sockfd,int how);
  • 参数how可以是SHUT_RD(关闭读)、SHUT_WR(关闭写)、SHUT_RDWR(关闭读写)。
  • 参数socked是套接字描述符。

既然能够关闭(close)套接字,为何还使用shutdown呢?

- 首先,只有最后一个活动引用关闭时,close才释放网络端点。这意味着,如果复制一个套接字(如采用dup),要直到关闭了最后一个引用它的文件描述符才会释放这个套接字(意思是,如果一个套接字描述符被多个进程引用,只有最后一个进程关闭引用,才会释放该套接字)。
- 而shutdown允许一个套接字处于不活动状态,和引用它的文件描述符的数目无关。
- 其次,有时可以很方便地关闭套接字双向传输中的一个方向。

16.3 寻址

上一节学习了如何创建和销毁一个套接字。在学习用套接字做一些有意义的事情之前,需要知道如何标识一个目标通信进程。进程标识由两部分组成:

  • 计算机网络地址:用于标识网络上我们想与之通信的计算机。
  • 端口号:计算机上用端口号表示服务,用于标识特定的进程。

16.3.1 字节序

同一台计算机上的进程通信时,一般不用考虑字节序。字节序是一个处理器架构特性,用于指示像整形这样的大数据类型在内存中字节是如何排序的。


对于一个数据,最高有效字节(Most significant Byre,MSB)总是在左边,LSB总是在右边。例如0x04030201,不管字节序如何,MSB总是04,LSB总是01。


而在不同的字节序架构上,存储顺序不同。
大端(big-endian),内存字节增长是从MSB开始的。
《UNIX环境高级编程》第16章 网络IPC:套接字

小端(little-endian),内存内存字节增长从LSB开始的。
《UNIX环境高级编程》第16章 网络IPC:套接字


下图总结了4种平台的字节序。
《UNIX环境高级编程》第16章 网络IPC:套接字


网络协议指定了字节序,因此异构计算机系统能够交换协议信息而不会被字节序所混淆。TCP/IP协议栈使用了大端字节序
对于TCP/IP应用程序,有4个用来在处理器字节序和网络字节序之间实施转换的函数。

#include <arpa/inet.h>
uint32 htonl(uint32 hostint32);
uint16 htons(uint16 hostint16);

uint32 ntohl(uint32 hostint32);
uint16 ntohs(uint16 hostint16);

h表示“主机”字节序,n表示“网络字节序”。l表示32位整数,s表示16位帧数。

16.3.2 地址格式

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

struct sockaddr{
sa_family_t sa_family;
char sa_data[];
...
//可以*填充
};

IPv4因特网域(AF_INET)中,套接字地址用结构sockaddr_in表示,IPv6网域用sockaddr_in6表示,根据SUS的要求,每个实现都可以*添加更多字段。
在linux中,sockaddr_in定义如下

struct sockaddr_in{
sa_family_t sin_family; //网域
in_port_t sin_port; //端口号
struct in6_addr sin6_addr; //IPv4地址
unsigned char sin_zero[8]; //为了让sockaddr与sockaddr_in两个数据结构保持大小相同而保留的空字节
};

sockaddr_insockaddr_in6最终都会转换成sockaddr6结构输入到套接字例程中。

例如:

struct sockaddr_in mysock;

mysock.sin_family = AF_INET; //TCP地址结构

mysock.sin_port = htons(3490); //字节顺序转换函数(后面我会介绍的)

mysock.sin_addr.s_addr = inet_addr("166.111.160.10");//设置IP地址

bzero(&(mysock.sin_zero),8);//设置sin_zero为8位保留字节

//如果mysock.sin_addr.s_addr = INADDR_ANY,则不指定IP地址(用于server程序)

有时候需要打印出能被人理解而不是计算机所理解的地址格式。inet_addr和inet_ntoa函数,用于二进制地址格式与点分十进制字符表示(a.b.c.d)之间的相互转换。但这两个函数仅适用于IPv4地址和IPv6地址。

#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 afddr);

16.3.3 地址查询

理想情况下,应用程序不需要了解一个套接字地址的内部结构。
这里引入一些函数来查询地址信息。这些信息存放在静态文件(如/etc/hosts和/etc/services)也可以由名字服务管理,如域名系统(DNS)或者(NIS),无论这些信号在何处,都可以用相同的函数访问它们。
函数gethostent,可以找到给定计算机系统的主机信息。

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

服务是由地址的端口号部分表示的。每个服务由一个唯一的众所周知的端口号来支持。可以使用函数getservbyname将一个服务映射到一个端口号,可以使用getservbyport将一个端口号映射到一个服务名,使用函数getservent顺序扫描服务数据库。



16.3.4 将套接字与地址关联

将一个客户端的套接字关联上一个地址不是必须的,因为可以让系统选一个默认的地址。
而对于服务器,需要一个众所周知的地址。最简单的方法就是服务器保留一个地址并且注册在/etc/services或者某个名字服务器中。
是用bind函数来关联地址和套接字。

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

对于使用的地址有一下限制。

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

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

int getsockname(int sockfd,struct sockaddr *restrict addr,socklen_t *restrict alenp);
  • sockfd为socket描述符。
  • addr为一个地址指针,得到地址后存在这里。
  • socklen_t 为地址长度,指定了addr的大小。

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

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

16.4 建立连接

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

int connect(int sockfd,const struct sockaddr *addr,socklen_t len);
  • sockfd 为本地套接字描述符。
  • addr为需要链接的服务器地址。
  • len为地址长度。

尝试连接时可能会出错,因此应用程序必须能处理connect返回的错误。
以下函数采用了指数补偿(exponential backoff)算法。如果调用connect失败,进程会休眠一小段时间,然后进入下次循环再次尝试。直到最大延时为2分钟左右。

#define MAXSLEEP 120
int connect_retry(int sockfd,const struct sockaddr *addr,socklen_t len)
{
int numsec,fd;
/*
try to connect with exponential backoff
*/



for(numsec=1;numsec<=MAXSLEEP;numsec<<=1)
{
if((fd=socket(domain,type,protocol))<0)
return (-1);

if(connect(fd,addr,alen)==0)
{
return(fd);
}
close (fd); //不成功就关闭socket

if(numsec<=MAXSLEEP/2)
sleep(numsec);
}

return (-1);
}

如果套接字描述符处于非阻塞模式,那么链接不能马上建立,connect会返回-1并且将errno设置为特殊的错误码EINPROGRESS.应用程序可以使用poll或者select来判断文件描述符何时可写,如果可写,连接完成。
connect函数还可以用于无连接的网络服务(SOCK_DGRAM)。
如果用SOCK_DGRAM套接字调用connect,传输报文的目的地址会设置成connect调用中所指定的地址,这样每次传送报文时就不需要再提供地址。另外,只能接收来自指定地址的报文。


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

int listen(int sockfd,int backlog);

backlog参数指定了该进程所要入队的未完成链接请求数量。系统限定不大于128.一旦队列满,系统就会拒绝多余的链接请求,所以backlog的值应该基于服务器期望负载和处理量来选择。


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

int accept(int sockfd,struct sockaddr *restrict addr,aoklen_t *restrict len);
  • 返回值为一个新的套接字描述符,该描述符连接到了调用connect的客户端。(一旦accept接收到就说明已经connect了)。
  • sockfd并没有改变,任然保持可用状态并接收其他连接请求。
  • addr为客户端的地址;
  • len为客户端地址长度。

如果没有连接请求在等待,accept会阻塞直到一个请求到来。如果sockfd处于非阻塞模式,accept返回-1,并设errno为EAGAIN或EWOULDBLOCK.


服务器可用调用poll或select来等待一个请求的到来。在这种情况下,一个带有等待连接请求的套接字会以可读的方式出现。

16.5 数据传输

既然一个套接字端点表示为一个文件描述符,那么只有建立连接,就可以使用read或write来通过套接字通信。
但是read和write函数功能有限,有6个专门用于传递的函数。3个用于发送,3个用于接收。

#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,int flags);
#include <sys/socket.h>
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);

16.6 套接字选项

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

  1. 通用选项,工作在所有套接字类型上。
  2. 在套接字层次管理的选项,每个协议独有。
  3. 特定于某个协议的选项,每个协议独有。
#include <sys/socket.h>
int setsockopt(int sockfd,int level,int option,const void *val,socklen_t *restrinct lenp);

参数level表示了选项应用的协议。


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

参数lenp是一个整数指针,在调用getsockopt之前,设置该整数为复制选项缓冲区的长度。如果选项实际长度大于此值,则选项会被截断。如果实际长度小于此值,那么返回时将此值更新此值为实际长度。

16.7 带外数据

带外数据(out-of-band data)是一些通信协议所支持的可选功能,与普通数据相比,它允许更高优先级的数据传输。带外数据先行传输,即使传输队列已经有数据。TCP支持带外数据,但是UDP不支持。
TCP将带外数据称为紧急数据(urgent data)。TCP仅支持一个字节的紧急数据,但是允许紧急数据在普通数据传递机制数据流之外传输。为了产生紧急数据,可以在3个send函数中的任意一个里指定MSG_OOB标志。如果带MSG_OOB标志发送的字节数超过一个时,最后一个字节将被视为紧急数据字节。
在紧急数据被接收时,会发送SIGURE信号。
TCP支持紧急标记(urgent mark)的概念,即在普通数据流中紧急数据所在的位置。如果采用套接字选项SO_OOBINLINE,那么可以在普通数据中接收紧急数据。

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

当一个要读取的字节在紧急标志处时,sockatmark返回1.

16.8 非阻塞和异步IO

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

16.9 小结

本章考察了IPC机制,这些机制允许进程与不同计算机上的以及同一计算机上的其他进程通信。

  • 讨论了套接字端点如何命名,在链接服务器时,如何发现所有的地址。
  • 各处了采用无连接的(即基于数据包的)套接字和面向连接的套接字的客户端和服务器实例。
  • 简要讨论了异步非阻塞套接字IO,以及用于管理套接字选项的接口。