第16章 网络IPC: 套接字
套接字描述符
正如使用文件描述符访问文件,应用程序用套接字描述符访问套接字。套接字描述符在UNIX系统中被当作是一种文件描述符。事实上,许多处理文件描述符的函数(如read和write)可以用于处理套接字描述符号。
socket函数:创建一个套接字
#include <sys/socket.h>
// 成功,返回套接字描述符;出错,返回-1
int socket(int domain, int type, int protocol);
-
domain参数:
- AF_INET: IPv4
- AF_INET6: IPv6
- AF_UNIX: UNIX域套接字
- AF_UPSPEC: 未指定
-
type参数:确定套接字类型
- SOCK_DGRAM: 固定长度、无连接、不可靠的报文传递,默认的protocol是UDP
- SOCK_RAW:IP协议的数据报接口
- SOCK_SEQPACKET:固定长度、有序的、可靠的、面向连接的报文传递
- SOCK_STREAM:有序的、可靠的、双向的、面向连接的字节流, 默认protocol是TCP
-
protocol参数:一般为0,表示为给定的域和套接字类型选择默认的协议。
SOCK_STREAM 套接字:提供字节流服务,所以应用程序分辨不出报文的界限。这意味着从SOCK_STREAM 套接字读数据时,它也许不会返回所有由发送进程所写的字节数。最终可以获得发送过来的所有数据,但也许要通过若干次函数调用才能得到。
SOCK_SEQPACKET 套接字:和SOCK_STREAM 套接字很类似,只是从该套接字得到的是基于报文的服务而不是字节流服务。
SOCK_RAW 套接字:提供一个数据报接口,用于直接访问下面的网络层(即因特网域中的 IP层)。使用这个接口时,应用程序负责构造自己的协议头部,这是因为传输协议(如TCP和UDP)被绕过了。
调用socket与调用open相类似。在两种情况下,均可获得用于I/O的文件描述符。
当不再需要该文件描述符时,调用close来关闭对文件或套接字的访问,并且释放该描述符以便重新使用。
shutdown函数:禁止一个套接字的I/O.
#include <sys/socket.h>
int shutdown(int sockfd, int how);
how可能是:
- SHUT_RD 关闭读端
- SHUT_WR 关闭写端
- SHUT_RDWR 关闭读端和写端
能够关闭(close)一个套接字,为何还使用shutdown呢? 有若干理由。
- 首先,只有最后一个active引用关闭时,close才释放网络端点。这意味着如果复制一个套接字(如采用dup),要直到关闭了最后一个引用它的文件描述符才会释放这个套接字。而 shutdown 允许使一个套接字处于不活动状态,和引用它的文件描述符数目无关。
- 其次,有时可以很方便地关闭套接字双向传输中的一个方向。例如,如果想让所通信的进程能够确定数据传输何时结束,可以关闭该套接字的写端,然而通过该套接字读端仍可以继续接收数据。
寻址
进程标识由两部分组成。一部分是计算机的网络地址,它可以帮助标识网络上我们想与之通信的计算机;另一部分是该计算机上用端口号表示的服务,它可以帮助标识特定的进程。
为使不同格式地址能够传入到套接字函数,地址会被强制转换成一个通用的地址结构sockaddr,在Linux中,该结构定义如下:
struct sockaddr {
sa_family_t sa_family; /* address family */
char sa_data[14]; /* variable-length address*/
};
套接字实现可以自由地添加额外的成员并且定义 sa_data 成员的大小。
因特网地址定义在 <netinet/in.h> 头文件中。在IPv4因特网域(AF_INET),套接字地址用结构 sockaddr_in 表示:
struct in_addr {
in_addr_t s_addr; // IPv4 address, in_addr_t is uint32_t
};
struct sockaddr_in {
sa_family_t sin_family; // address family
in_port_t sin_port; // port number, in_port_t is unit16_t
struct in_addr sin_addr; // IPv4 address
};
IPv6因特网域(AF_INET6)套接字地址用结构 sockaddr_in16 表示。详情略。
尽管sockaddr_in 和 sockaddr_in6 结构相差较大,但它们均被强制转换成 sockaddr 结构输入到套接字程序中。
有时,需要打印出能被人理解而不是计算机所理解的地址格式。BSD 网络软件包含函数 inet_addr 和 inet_ntoa,用于二进制地址格式与点分十进制字符表示(a.b.c.d)之间的相互转换。但是这些函数仅适用于IPv4地址。
有两个新函数 inet_ntop 和 inet_pton 具有相似的功能,而且同时支持IPv4地址和IPv6地址。
将套接字与地址关联
bind函数:关联地址和套接字
#include <sys/socket.h>
// 成功,返回0; 出错,返回-1
int bind(int sockfd,
cont struct sockaddr * addr,
socklen_t len);
对于因特网域,如果指定IP地址为 INADDR_ANY (<netinet/in.h>中定义),套接字端点可以绑定到所有的系统网络接口上。这意味着可以接收这个系统的任何一个网卡的数据包。
getsockname函数: 发现绑定到套接字上的地址。
int getsockname(int sockfd,
sturct sockaddr* restrict addr,
socklen_t *restrict alenp);
getpeername函数:若套接字已经和对方连接,找到对方的地址
int getpeername(int sockfd,
struct sockaddr *restrict addr,
socklen_t *restrict alenp);
建立连接
connect函数:建立socket之间的连接
int connect(int sockfd,
const struct sockaddr *addr,
socklen_t len);
connct函数也可用于无连接的网络服务(SOCK_DGRAM)。如果用SOCK_DGRAM套接字调用connect,传送的报文的目标地址会设为connect调用者所指定的地址。这样每次传送报文的时候就不再需要提供地址了。另外,也仅能接收来自指定地址的报文。
listen函数:宣告服务端愿接受连接请求。
int listen(int sockfd, int backlog);
accept函数: 一旦服务器调用了listen,所用的套接字就能接收连接请求。使用accept函数获得连接请求并建立连接。
int accept(int sockfd,
struct sockaddr *restrict addr,
socklen_t *restrict len);
如果不关心客户端标识,可以将参数addr和len设为NULL。否则,在调用accept之前,将addr参数设为足够大的缓冲区来存放地址,并且将len指向的整数设为这个缓冲区的字节大小。
如果服务器调用accept,并且当前没有连接请求,服务器会阻塞直到一个请求到来。
另外,服务器可以使用poll或select来等待一个请求的到来。在这种情况下,一个带有等待连接请求的套接字会以可读的方式出现。
init init_server()
{
int fd = socket();
bind();
listen(fd, ...);
return fd;
}
数据传输
在套接字描述符上使用read和write是非常有意义的,因为这意味着可以将套接字描述符传递给那些原先为处理本地文件而设计的函数。而且还可以安排将套接字描述符传递给子进程,而该子进程执行的程序并不了解套接字。
但是,常用的数据传输的函数是另外6个。
- send函数:和write很像,但是可以指定标志来改变处理传输数据的方式。
- sendto函数:和send很类似。区别在于sendto可以在无连接的套接字上指定一个目标地址。
- sendmsg函数:带有msghdr结构,可指定多重缓冲区来传输数据(这和writev函数很相似)
- recv函数:和read相似。但是recv可以指定标志来控制如何接收数据。
- recvfrom函数:若有兴趣定位发送者,可以使用recvfrom函数来得到数据发送者的源地址。
- recvmsg函数:将接收到的数据送入多个缓冲区,类似于readv.
#include <sys/socket.h>
// 成功,返回发送的字节数;出错,返回-1
// 关于第4个参数flags,详见图16-13. 此处从略。
ssize_t send(int sockfd, const void *buf, size_t nbytes, int flags);
// 成功,返回发送的字节数;出错,返回-1
ssize_t sendto(int sockfd, const void *buf, size_t nbytes, int flags, const struct sockaddr *destaddr, socklen_t destlen);
// 若成功,返回发送的字节数;若出错,返回-1
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
// 返回数据的字节长度;若无可用数据或对等方已经按序结束,返回0;若出错,返回-1
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);
// 返回数据的字节长度;若无可用数据或对等方已经按序结束,返回0;若出错,返回-1
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
一个实例:(图16-18 用于说明命令直接写到套接字的服务器程序)
Fork子进程,令其execl一个uptime程序。然后, dup2子进程的stdout到传输的socket, 并close 原socket的fd. 这样,uptime的输出就可以写到socket了。
套接字选项
- setsockopt函数: 可以使用setsockopt函数来设置套接字
- getsockopt函数: 查看选项的当前值
int setsockopt(int sockfd,
int level,
int option,
const void *val,
socklen_t len);
int getsockopt(int sockfd,
int level,
int option,
void *restrict val,
socklen_t *restrict lenp);
带外数据
带外数据(out-of-band data)是一些通信协议所支持的可选功能,与普通数据相比,它允许更高优先级的数据传输。带外数据先行传输,即使传输队列已经有数据。
TCP 支持带外数据,但是UDP不支持。套接字接口对带外数据的支持很大程度上受TCP带外数据具体实现的影响。
TCP将带外数据称为紧急数据(urgent data)。TCP仅支持一个字节的紧急数据,但是允许紧急数据在普通数据传递机制数据流之外传输。
为了产生紧急数据,可以在3个send函数中的任何一个里指定MSG_OOB标志。如果带MSG_OOB标志发送的字节数超过一个时,最后一个字节将被视为紧急数据字节。
非阻塞和异步I/O
通常,recv函数没有数据可用时会阻塞等待。同样地,当套接字输出队列没有足够空间来发送消息时,send 函数会阻塞。
在套接字非阻塞模式下,行为会改变。在这种情况下,这些函数不会阻塞而是会失败,将errno设置为EWOULDBLOCK或者EAGAIN。当这种情况发生时,可以使用poll或select来判断能否接收或者传输数据。
在基于socket的异步I/O中,当从socket读取数据时,或者当socket写队列中空间变得可用时,可以安排要发送的信号SIGIO.
启用异步I/O有2个步骤:
- 建立socket所有权,这样信号可以被传递到合适的进程;
- 通知socket当I/O操作不会阻塞时发信号;
可以有3种方式来完成第一个步骤“建立socket所有权”:
- 在fcntl中使用F_SETOWN命令
- 在ioctl中使用FIOSETOWN命令
- 在ioctl中使用SIOCSPGRP命令
可以有2种方法来完成第2个步骤“通知socket当I/O操作不会阻塞时发信号”:
- 在fcntl中使用F_SETFL命令并且启用文件标志O_ASYNC
- 在ioctl中使用FIOASYNC命令
以上的这些方法在Linux 3.2.0中都得到了支持。
第17章 高级进程间通信(UNIX域套接字)
UNIX域套接字:
这种形式的IPC可以在同一计算机系统上运行的两个进程之间传送打开文件描述符。
UNIX 域套接字用于在同一台计算机上运行的进程之间的通信。虽然因特网域套接字可用于同一目的,但 UNIX 域套接字的效率更高。UNIX 域套接字仅仅复制数据,它们并不执行协议处理,不需要添加或删除网络报头,无需计算校验和,不要产生顺序号,无需发送确认报文。
UNIX 域套接字提供流和数据报两种接口。UNIX 域数据报服务是可靠的,既不会丢失报文也不会传递出错。
UNIX 域套接字就像是套接字和管道的混合。可以使用它们面向网络的域套接字接口或者使用socketpair函数来创建一对无命名的、相互连接的UNIX域套接字。
#include <sys/socket.h>
int socketpair(int domain,
int type,
int protocol,
int sockfd[2]);
一对相互连接的UNIX域套接字可以起到全双工管道的作用:两端对读和写开放(见图17-1)。将其称为 fd 管道(fd-pipe),以便与普通的半双工管道区分开来。
套接字是和文件描述符相关联的,消息到达时,可以用套接字来通知。对每个消息队列使用一个线程。每个线程都会在msgrcv调用中阻塞。当消息到达时,线程会把它写入一个UNIX域套接字的一端。当poll指示套接字可以读取数据时,应用程序会使用这个套接字的另外一端来接收这个消息。
本章后略。
(全笔记完)