[单刷APUE系列]第十六章——网络IPC:套接字

引言

前一章中讲了经典Unix进程间通讯,可是对于不一样计算机的不一样进程通讯是没法使用这种技术的,因此就有了网络间新进程通讯的机制。而网络套接字解释一种很是实用的技术。进程将套接字绑定在端口上,经过该接口向其余进程通讯,这一章其实是很重要的一章。编程

套接字描述符

就如同文件描述符,套接字也有描述符,在文件系统中,套接字也被认为是一种文件,因此套接字描述符在Unix系统中也能被当作是一种文件描述符。服务器

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

PF_LOCAL        Host-internal protocols, formerly called PF_UNIX,
PF_UNIX         Host-internal protocols, deprecated, use PF_LOCAL,
PF_INET         Internet version 4 protocols,
PF_ROUTE        Internal Routing protocol,
PF_KEY          Internal key-management function,
PF_INET6        Internet version 6 protocols,
PF_SYSTEM       System domain,
PF_NDRV         Raw access to network device

The socket has the indicated type, which specifies the semantics of communication.  Currently defined types are:

SOCK_STREAM
SOCK_DGRAM
SOCK_RAW

domain参数指定通讯将会发生的域,将会选择即将使用的协议族。咱们能够看到上面有8种协议族,其中PF_LOCALPF_UNIX的别名,而且PF_UNIX已经废弃了,而后就是IPv4/6的协议,和其余几个协议。
参数type将会更进一步肯定套接字的类型,其中有3种:网络

  1. SOCKET_STREAM - 有序的、可靠地、双向的、面向链接的字节流架构

  2. SOCKET_DGRAM - 固定长度的、无链接的、不可靠的报文传输dom

  3. SOCKET_RAW - IP协议的数据报接口异步

参数protocol一般是0,表示为给定的域和类型选择默认协议,通常状况下,都只支持单协议,当域和套接字支持多协议的时候,可使用protocol参数给定一个特定协议,每一个系统都有本身实现的协议,在苹果系统下,能够经过查看/etc/protocols文件来查询具体的协议。
其中,用的最多的就是TCP和UDP协议,也就是SOCK_DGRAMSOCK_STREAM,当使用数据报的时候,不须要链接创建,所以数据报是一种面向无链接的服务,而字节流会要求在交换数据以前创建链接,因此这是面向链接的服务。
对于一个用完的套接字,可使用shutdown函数禁止IOsocket

int shutdown(int socket, int how);

The shutdown() call causes all or part of a full-duplex connection on the socket associated with socket to be shut down.  If how is SHUT_RD, further receives will be disallowed.  If how is SHUT_WR, further sends will be disallowed.  If how is SHUT_RDWR, further sends and receives will be disallowed.

若是how参数为SHUT_RD,则没法从套接字读取数据,若是how是SHUT_WR,则没法向套接字写入数据,前面讲过,套接字描述符基本上能够认为是文件描述符,那为何咱们不用close函数关闭呢?由于套接字做为相似文件描述符这样的资源,是能够被复制的,咱们讲过,文件描述符其实是引用一个内核维护的链表文件项,当复制的时候实际上支付至了文件描述符自己,若是使用close函数,则必需要等到全部关联到这个套接字的套接字描述符所有关闭才能真正关闭,而使用shutdown函数则能够无视描述符,直接操做文件项,很方便的就能关闭其中一个方向。函数

寻址

你们对网络通讯应该也已经有过一些粗浅的了解了,对于一个端对端的通讯,最重要的一步就是寻找目标位置,咱们知道,TCP/IP协议包含了网络层和传输层,其中网络层是IP协议,而传输层是TCP协议、UDP协议和ICMP协议,IP地址是标志了一台主机的位置,而port部分则是标志了传输层目标位置,也就是说,port是传输层对网络的封装。咱们知道,TCP/IP协议其实是一个很是抽象良好的分层架构,每一层只对上一层负责,而无需了解上层内容,同时也屏蔽了上层对下层的了解,因此,有一些东西是须要注意的,好比字节序、地址格式、地址查询,这里再也不对其讲解,由于笔者认为这已经超出了Unix的范畴了。而属于网络通讯的基本原理。this

套接字和地址绑定

可能有一些朋友已经学过有关于socket编程的内容了,socket编程对于服务器和客户端是不同的,服务器须要固定一个端口,而后一直侦听端口,客户端则不须要侦听固定端口,只须要在进行联系的时候随意分配一个便可,因此这一小节实际上应当是属于服务端开发的内容。咱们可使用bind函数来将套接字和地址绑定在一块儿翻译

int bind(int socket, const struct sockaddr *address, socklen_t address_len);

对于socket编程,你们应该也有一些了解,好比非root权限不能使用1024之内端口,一个进程只能使用一个端口,端口不能被多个进程使用。这里的绑定规则就是和上面的差很少,

创建链接

对于客户端来讲,不须要固定使用一个端口,彻底能够随机分配,因此接口API也是不一样的,通常使用connect函数来链接。

int connect(int socket, const struct sockaddr *address, socklen_t address_len);

原著关于这段的讲解很烦,笔者这里就直接讲述本身的见解。socket参数是一个套接字,若是类型是SOCK_DGRAM,函数调用就会指定套接字关联的对方的地址为address参数,而且在接收的时候只能接收此地址传过来的数据。若是套接字是SOCK_STREAM类型,函数调用将会尝试链接另外一个套接字,另外一个套接字经过address参数对应的地址链接,对于UDP数据报来讲,能够屡次调用这个函数用于改变对应地址,而TCP流则只能使用一次用于创建链接。
对于服务端进程来讲,只须要调用listen命令侦听套接字就好了。

int listen(int socket, int backlog);

原著上面关于backlog参数写的很是迷,固然,多是笔者看的是中文版的,因此翻译很迷,实际上这个backlog参数是用来定义阻塞请求队列的最大长度的,若是超出了这个范围,就会有ECONNREFUSED提示。一旦服务器调用listen,所用的套接字就能接收链接请求,使用accept函数得到链接请求并创建链接。

int accept(int socket, struct sockaddr *restrict address, socklen_t *restrict address_len);

咱们能够看到,上面accept函数接收一个socket参数,一个address参数,一个address_len参数,其中,socket参数是一个已经建立的套接字描述符,而且使用bind函数将其绑定到了端口上,而且正在使用listen函数侦听端口,accept函数取出请求队列中的第一个请求,而后生成与socket参数相同属性的一个套接字,而且为其分配一个新的文件描述符。若是调用时候请求队列没有任何请求,而且套接字没有被标记为非阻塞,则accept函数将会阻塞当前进程直到链接到来,而原始的socket参数套接字将会继续侦听端口。

数据传输

套接字属于文件描述符,那么当套接字描述符存在的时候,就能使用read和write等文件IO函数对其读写,这样就能简化操做。可是,若是想要作到更多的选项和操做,则必须使用socket库提供的6个函数。

ssize_t send(int socket, const void *buffer, size_t length, int flags);
ssize_t sendmsg(int socket, const struct msghdr *message, int flags);
ssize_t sendto(int socket, const void *buffer, size_t length, int flags, const struct sockaddr *dest_addr, socklen_t dest_len);

ssize_t recv(int socket, void *buffer, size_t length, int flags);
ssize_t recvmsg(int socket, struct msghdr *message, int flags);
ssize_t recvfrom(int socket, void *restrict buffer, size_t length, int flags, struct sockaddr *restrict address, socklen_t *restrict address_len);

咱们能够看到,这六个函数是一一对应的,三个发送函数,三个接收函数,咱们先来观察发送函数的参数。send函数是一个通用的发送函数,它能发送任何的buffer数据,只须要开发者手动指定长度和flags发送参数,而sendmsg则是使用msghdr结构体发送数据,下面是msghdr的结构体内容

struct msghdr {
        void            *msg_name;      /* [XSI] optional address */
        socklen_t       msg_namelen;    /* [XSI] size of address */
        struct          iovec *msg_iov; /* [XSI] scatter/gather array */
        int             msg_iovlen;     /* [XSI] # elements in msg_iov */
        void            *msg_control;   /* [XSI] ancillary data, see below */
        socklen_t       msg_controllen; /* [XSI] ancillary data buffer len */
        int             msg_flags;      /* [XSI] flags on received message */
};

因为结构体可以作到定长,因此也就不须要指定length参数,sendmsg实际上就是个send函数的变体。
经过对比send函数和write函数,咱们发现,实际上send函数只是多了个flags参数,经过查看苹果系统Unix系统手册,能够发现如下内容。

The flags parameter may include one or more of the following:

#define MSG_OOB        0x1  /* process out-of-band data */
#define MSG_DONTROUTE  0x4  /* bypass routing, use direct interface */

The flag MSG_OOB is used to send ``out-of-band'' data on sockets that support this notion (e.g.  SOCK_STREAM); the underlying protocol must also support ``out-of-band'' data.  MSG_DONTROUTE is usually used only by diagnostic or routing programs.

咱们能够发现上面就列举出了两个常量,上面只写了包括并不限于下面两个值,因此咱们来看看头文件是怎么定义的

#define MSG_OOB         0x1             /* process out-of-band data */
#define MSG_PEEK        0x2             /* peek at incoming message */
#define MSG_DONTROUTE   0x4             /* send without using routing tables */
#define MSG_EOR         0x8             /* data completes record */
#define MSG_TRUNC       0x10            /* data discarded before delivery */
#define MSG_CTRUNC      0x20            /* control data lost before delivery */
#define MSG_WAITALL     0x40            /* wait for full request or error */
#if !defined(_POSIX_C_SOURCE) || defined(_DARWIN_C_SOURCE)
#define MSG_DONTWAIT    0x80            /* this message should be nonblocking */
#define MSG_EOF         0x100           /* data completes connection */
#ifdef __APPLE__
#ifdef __APPLE_API_OBSOLETE
#define MSG_WAITSTREAM  0x200           /* wait up to full request.. may return partial */
#endif
#define MSG_FLUSH       0x400           /* Start of 'hold' seq; dump so_temp */
#define MSG_HOLD        0x800           /* Hold frag in so_temp */
#define MSG_SEND        0x1000          /* Send the packet in so_temp */
#define MSG_HAVEMORE    0x2000          /* Data ready to be read */
#define MSG_RCVMORE     0x4000          /* Data remains in current pkt */
#endif
#define MSG_NEEDSA      0x10000         /* Fail receive if socket address cannot be allocated */
#endif  /* (!_POSIX_C_SOURCE || _DARWIN_C_SOURCE) */

上面就是苹果系统头文件的定义,确实比Unix系统手册上讲的多了很是多,可是也能推测出来,具体flags的实现实际上根据系统不一样是不一样的。
sendto函数跟send函数基本同样,除了sendto函数能在一个无链接的套接字上面向指定目标发送数据。
recv函数族基本和send函数族同样,因此这里就再也不继续讲解了,有兴趣的能够本身查询Unix系统手册和原著。

套接字选项

为了能让开发者基础套接字的编程,系统也会提供set、get函数,咱们知道,套接字其实是网络抽象模型,它能工做在任何协议上,而有些特定协议具备一些特殊行为,因此,套接字编程API也具备其特殊性。

int getsockopt(int socket, int level, int option_name, void *restrict option_value, socklen_t *restrict option_len);
int setsockopt(int socket, int level, int option_name, const void *option_value, socklen_t option_len);

这两个函数操做socket关联的选项,前面说过,套接字能工做在不少协议上,而一些特殊协议会有一些特殊选项,因此需呀使用level参数指定操做的级别,若是选项是通用套接字选项,则level设置为SOL_SOCKET,不然,level设置为控制这个选项的协议的编号。好比TCP协议则是IPPIPPROTO_TCP,下面是option_name 可用的值

SO_DEBUG        enables recording of debugging information
SO_REUSEADDR    enables local address reuse
SO_REUSEPORT    enables duplicate address and port bindings
SO_KEEPALIVE    enables keep connections alive
SO_DONTROUTE    enables routing bypass for outgoing messages
SO_LINGER       linger on close if data present
SO_BROADCAST    enables permission to transmit broadcast messages
SO_OOBINLINE    enables reception of out-of-band data in band
SO_SNDBUF       set buffer size for output
SO_RCVBUF       set buffer size for input
SO_SNDLOWAT     set minimum count for output
SO_RCVLOWAT     set minimum count for input
SO_SNDTIMEO     set timeout value for output
SO_RCVTIMEO     set timeout value for input
SO_TYPE         get the type of the socket (get only)
SO_ERROR        get and clear error on the socket (get only)
SO_NOSIGPIPE    do not generate SIGPIPE, instead return EPIPE
SO_NREAD        number of bytes to be read (get only)
SO_NWRITE       number of bytes written not yet sent by the protocol (get only)
SO_LINGER_SEC   linger on close if data present with timeout in seconds

option_value根据option_name的不一样指向不一样的数据类型。

带外数据

带外数据可能翻译不许确,原文是out-of-band data,也就是超范围数据,熟悉网络基础的朋友应该知道,各层会对上层数据封装,好比使用限定字符将数据限定范围,而后先后加上头尾,组成一个封包,某些通讯协议支持带外数据,容许其做为更高优先级传输,至于具体内容,能够看原著讲解,由于这小节实际上并非特别重要。

非阻塞和异步IO

recv在没有数据可用的状况下会阻塞等待,而套接字没有足够空间发送的状况下send也会阻塞等待。若是在套接字建立的时候指定非阻塞,行为就会改变。这样函数就不会阻塞而是会直接返回失败,而且设置errno。咱们也知道,套接字描述符和文件描述符基本能够等价,那么咱们是否是可使用select和poll这种函数来判断文件描述符是否已经准备完毕。前面讲到过SUS标准实际上包含了异步IO的内容,可是套接字实际上也是有着本身的一套异步IO模型,也就是基于信号的异步IO模型。模型很是简单,就是当套接字IO操做不会阻塞的时候发送信号,从而进程获得了阻塞非阻塞的状况。

相关文章
相关标签/搜索