引言
前一章中讲了经典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_LOCAL
是PF_UNIX
的别名,并且PF_UNIX
已经废弃了,然后就是IPv4/6的协议,和其他几个协议。
参数type将会更进一步确定套接字的类型,其中有3种:
SOCKET_STREAM
- 有序的、可靠地、双向的、面向连接的字节流SOCKET_DGRAM
- 固定长度的、无连接的、不可靠的报文传输SOCKET_RAW
- IP协议的数据报接口
参数protocol通常是0,表示为给定的域和类型选择默认协议,一般情况下,都只支持单协议,当域和套接字支持多协议的时候,可以使用protocol参数给定一个特定协议,每个系统都有自己实现的协议,在苹果系统下,可以通过查看/etc/protocols
文件来查询具体的协议。
其中,用的最多的就是TCP和UDP协议,也就是SOCK_DGRAM
和SOCK_STREAM
,当使用数据报的时候,不需要连接建立,因此数据报是一种面向无连接的服务,而字节流会要求在交换数据之前建立连接,所以这是面向连接的服务。
对于一个用完的套接字,可以使用shutdown函数禁止IO
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的范畴了。而属于网络通信的基本原理。
套接字和地址绑定
可能有一些朋友已经学过有关于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操作不会阻塞的时候发送信号,从而进程得到了阻塞非阻塞的情况。