0. 前言
进程通信的概念最初来源于单机系统。由于每个进程都在自己的地址范围内运行,为保证两个相互通信的进
程之间既互不干扰又协调一致工作,操作系统为进程通信提供了相应设施,如:
UNIX BSD:管道(pipe)、命名管道(fifo)、信号(sinal)
UNIX system:消息队列(message)、共享内存(shm)、信号量(semaphore)
它们都仅限于本地进程间通信。而网络间通信要解决的是不同主机进程间的通信问题(可把同机进程间通信看成一个特例)。同一主机上,不同进程可用进程号(process ID)唯一标识。但在网络环境下,各主机独立分配的进程号不能唯一标识该进程。例如,主机A赋于某进程号5,在B机中也可以存在5号进程,因此,“5号进程”这句话就没有意义了。 其次,操作系统支持的网络协议众多,不同协议的工作方式不同,地址格式也不同。因此,网间进程通信还要解决多重协议的识别问题。
TCP/IP协议族已经帮我们解决了这个问题,网络层的 “ip地址” 可以唯一标识网络中的主机,而传输层的 “协议+端口” 可以唯一标识主机中的应用程序(进程)。这样利用三元组(ip地址,协议,端口)就可以标识网络的进程了,网络中的进程通信就可以利用这个标志与其它进程进行交互。
使用TCP/IP协议的应用程序通常采用应用编程接口:UNIX BSD的套接字(socket)和UNIX System V的TLI(已经被淘汰),来实现网络进程之间的通信。就目前而言,几乎所有的应用程序都是采用 socket,而现在又是网络时代,网络中进程通信是无处不在,这就是我为什么说“一切皆socket”。
1. socket 套接字
socket 套接字是通信断点的抽象。与应用程序要使用文件描述符访问文件一样,访问套接字也需要套接字描述符。套接字描述符在 UNIX 系统是用文件描述符实现的,因此很多处理文件描述符的函数(如 read 和write)也可以调用socket 描述符。
2. socket()
#include <sys/socket.h>int socket(int domain, int type, int protocol);
该函数用以创建一个套接字描述符,该描述符唯一标识一个 socket。
返回值:
- 若成功,返回socket描述符;
- 若出错,放回-1;
参数:
- domain:域,确定通信的特性,详细看第 2.1 节;
- type:确定socket 的类型,进一步确定通信特征,详细看第 2.2 节;
- protocol:通常为0,表示按照给定的域和套接字type 选择默认协议。当然,对同一域和socket type支持多个协议时,可以使用 protocol 参数选择一个特定的协议。
- 在AF_INET 通信域、socket type为 SOCK_STREAM 默认协议是TCP;
- 在AF_INET 通信域、socket type为 SOCK_DGRAM 默认协议是UDP;
2.1 参数domain
域的类型大致分:
- AF_INET: IPv4 因特网域;
- AF_INET6: IPv6 因特网域;
- AF_UNIX: UNIX 域,多数系统还会定义 AF_LOCAL,这个是AF_UNIX 别名;
- AF_UNSPEC:未指定,可以代表任何域;
UNIX 域套接字用于在同一台机器上运行的进程之间的通信。虽然因特网域套接字可以用于同一目的,但UNIX 域套接字的效率更高。UNIX 域仅仅复制数据,它们并不执行协议处理,不需要添加或删除网络报头,无需计算校验和,不要产生顺序号,无需发送确认报文。
UNIX 域套接字提供流和数据报两种接口。UNIX域数据报服务是可靠的,既不会丢失消息也不会传递出错。UNIX 域套接字是 套接字 和 管道 之间的混合物。
为了创建一对非命名的、相互连接的 UNIX 域套接字,用户可以使用它们面向网络的域套接字接口,也可以使用 socketpair() 函数。
#include <sys/socket.h>int socketpair(int domain, int type, int protocol, int sockfd[2]);返回值:若成功返回0,若出错返回-1
2.2 参数type
type 用于确定套接字的类型:
- SOCK_STREAM:又称字节流,有序、可靠、双向的面向连接字节流;
- SOCK_DGRAM:又称数据报,长度固定、无连接的不可靠的报文传递;
- SOCK_RAW:IP 协议的数据报接口;
- SOCK_SEQPACKET:长度固定、有序、可靠的面向连接报文传递;
数据报(SOCK_DGRAM)是一种自包含报文。发送数据报近似于给某人邮寄信件。可以邮寄很多信,但不能保证投递的次序,并且可能有些信件丢失在路上。每一封信件包含接收者的地址,使这封信件独立于所有其他信件。每一封信件可能送达不同的接收者。
对于字节流(SOCK_STREAM),应用程序意识不到报文界限,因为套接字提供的是字节流服务。这意味着当从套接字读出数据时,它也许不会返回所有由发送进程所写的字节数。最终可以获取发送过来的所有数据,但也许要通过若干次函数调用得到。
SOCK_SEQPACKET 与 SOCK_STREAM 套接字类似,但从该套接字的大的是基于报文的服务而不是字节流服务。这意味着从 SOCK_SEQPACKET 套接字接收的数据量与对方所发送的一致。流控制传输协议(Stream Control Transmission Protocol,SCTP)提供了因特网域上的顺序数据包服务。
SOCK_RAW 套接字提供一个数据报接口用于直接访问下面的网络层(在因特网域中为 IP)。使用这个套接字,应用程序负责构造自己的协议首部,用以防止恶意程序绕过内建安全机制来创建报文。
3. bind()
#include <sys/socket.h>int bind(int sockfd, const struct sockaddr *addr, socklen_t len);返回值:若成功返回0,若出错返回-1
对于服务器来说,需要给一个接收客户端请求的套接字绑定一个众所周知的地址。使用 bind() 函数将地址绑定到套接字。
而客户端不用指定,由系统自动分配一个端口号和自身的IP 地址组合。
参数:
- sockfd:通过 socket() 创建的套接字描述符,将会是addr 绑定到该套接字上;
- addr:指向要绑定给 sockfd 的协议地址,这个地址结构根据创建 socket 时的地址协议族不同而不同;
- len:对应地址的长度;
重点来关注下参数 addr,这个地址结构根据创建 socket 时的地址协议族不同而不同。
地址的格式与特定的通信域是有关联的,为了让不同格式的地址能够传入到套接字函数,地址被强制转换成通用的 sockaddr 表示:
struct sockaddr {sa_family_t sa_family; /* address family, AF_xxx */char sa_data[];...
};
例如在linux 中,该结构定义如下:
struct sockaddr {sa_family_t sa_family; /* address family, AF_xxx */char sa_data[14]; /* 14 bytes of protocol address */
};
在freeBSD 中,该结构定义如下:
struct sockaddr {unsigned char sa_len; /*total length*/sa_family_t sa_family; /*address family*/char sa_data[14]; /*variable-length address*/
};
在IPv4 因特网域中,该结构定义如下:
struct sockaddr_in {sa_family_t sin_family; /* Address family */in_port_t sin_port; /* Port number */struct in_addr sin_addr; /* Internet address */
}struct in_addr {uint32_t s_addr; /* address in network byte order */
};
对于所能使用的地址有一些限制:
- 在进程所运行的机器上,指定的地址必须有效,不能指定一个其他机器的地址;
- 地址必须和创建套接字时的地址族所支持的格式相匹配;
- 端口号必须不小于1024,除非该进程具有相应的特权(即为超级用户);
- 一般只有套接字断点能够与地址绑定,尽管有些协议允许多重绑定;
3.1 网络字节序和主机字节序
主机字节序就是我们平常说的大端和小端模式:不同的CPU有不同的字节序类型,这些字节序是指整数在内存中保存的顺序,这个叫做主机序。引用标准的 Big-Endian和 Little-Endian的定义如下:
- Little-Endian 就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。
- Big-Endian 就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。
网络字节序,4个字节的32 bit值以下面的次序传输:首先是 0~7bit,其次 8~15bit,然后16~23bit,最后是 24~31bit。这种传输次序称作大端字节序。由于 TCP/IP 首部中所有的二进制整数在网络中传输时都要求以这种次序,因此它又称作网络字节序。字节序,顾名思义字节的顺序,就是大于一个字节类型的数据在内存中的存放顺序,一个字节的数据没有顺序的问题了。
所以,在将一个地址绑定到socket的时候,请先将主机字节序转换成为网络字节序,而不要假定主机字节序跟网络字节序一样使用的是Big-Endian。由于这个问题曾引发过血案!公司项目代码中由于存在这个问题,导致了很多莫名其妙的问题,所以请谨记对主机字节序不要做任何假定,务必将其转化为网络字节序再赋给socket。
3.2 getsockname()
#include <sys/socket.h>int getsockname(int sockfd, struct sockaddr *restrict addr, socklen_t *restrict alenp);返回值:若成功返回0,若出错返回-1
可以通过调用函数 getsockname() 来发现绑定到一个套接字的地址。
在调用 getsockname() 之前,设置 alenp 为一个指向整数的指针,该整数指定缓冲区 sockaddr 的大小。返回时,该整数会被设置成返回地址的大小。如果该地址和提供的缓冲区长度不匹配,则将其截断而不报错。如果当前没有绑定到套接字的地址,其结果没有定义。
4. connect()
如果处理的是面向连接的网络服务(SOCK_STREAM 或 SOCK_SEQPACKET),在开始交换数据之前,需要在请求服务的进程套接字 (客户端) 和提供服务的进程套接字(服务端)之间建立一个连接。
#include <sys/socket.h>int connect(int sockfd, const struct sockaddr *addr, socklen_t len);返回值:若成功返回0,若出错返回-1
在connect() 中所指定的地址是想与之通信的服务器地址。如果 sockfd 没有绑定到一个地址,connect() 会给调用者绑定一个默认的地址。
当连接一个服务器时,出于一些原因,连接可能会失败。要连接的机器必须开启并且正在运行,服务器必须绑定到一个想与之连接的地址,并且在服务器的等待连接队列中应有足够的空间。因此,应用程序必须能够处理 connect() 返回的错误,这些错误可能由一些瞬时变化条件引起。
5. listen()
#include <sys/socket.h>int listen(int sockfd, int backlog);返回值:若成功返回0,若出错返回-1
参数:
- sockfd:需要监听的套接字描述符;
- backlog:用以限定连接请求的数量,一旦队列满,系统会拒绝多余连接请求;
socket() 函数创建的socket 默认是一个主动类型的,listen() 将socket 变成被动类型的,等待客户的连接请求。
6. accept()
#include <sys/socket.h>int accept(int sockfd, struct sockaddr *restrict addr, socklen_t *restrict len);返回值:若成功返回socket描述符,若出错返回-1
当服务器调用 listen() 监听函数之后,socket 就能接受连接请求,使用 accept() 获取连接请求并建立连接。
当accept() 成功返回,这个返回值为一个 socket 描述符,用以连接到调用 connect() 的客户端。这个新的套接字描述符 (又称连接socket)与 原始的sockfd (又称监听socket) 具有相同的socket type和地址族。
如果不关心客户端标识,可以将参数 addr 和 len 设为NULL,否则在调用 accept() 之前,应将参数 addr 设为足够大的缓冲区来存放地址,并且将 len 设为指向代表缓冲区大小的整数的指针。返回时,accept() 会在缓冲区填充客户端的地址并且更新指针 len 所指向的整数为该地址的大小。
如果没有连接到来,accept() 会阻塞直到连接请求的到来。
7. 数据传输
- read() / write()
- sned() / recv()
- sendto() / recvfrom()
- sendmsg() / recvmsg()
socket 数据传输大致分为上面 4 组,除了 read() 、write() 系统调用,socket 还提供了三组数据传输接口。
#include <sys/socket.h>ssize_t send(int sockfd, const void *buf, size_t nbytes, int flags);返回值:若成功返回发送字节数,若出错返回-1
类似于 write(),使用send() 时套接字必须已经连接。
如果 send() 成功返回,并不必然表示连接另一端的进程接收数据。所保证的仅是当send() 成功返回,数据已经无错误地发送到网络上。
相关博文:
进程间通信(0)——序
进程间通信(1)——信号(Signal)
进程间通信(2)——管道(PIPE)
进程间通信(3)——命名管道(FIFO)
进程间通信(4)——消息队列
进程间通信(5)——共享内存
进程间通信(6)——信号量(semaphore)
进程间通信(7)——套接字(socket)