网络进程间通信:socket API简介
不同计算机(通过网络相连)上运行的进程相互通信机制称为网络进程间通信(network IPC)。
在本地可以通过进程PID来唯一标识一个进程,但是在网络中这是行不通的。其实TCP/IP协议族已经帮我们解决了这个问题,网络层的“ip地址”可以唯一标识网络中的主机,而传输层的“协议+端口”可以唯一标识主机中的应用程序(进程)。这样利用三元组(ip地址,协议,端口)构成套接字,就可以标识网络的进程了,网络中的进程通信就可以利用这个标志与其它进程进行交互。
套接字是通信端口的抽象!通过套接字网络IPC接口,进程能够使用该接口和其他进程通信。
几个定义:
- IP地址:即依照TCP/IP协议分配给本地主机的网络地址,两个进程要通讯,任一进程首先要知道通讯对方的位置,即对方的IP。
- 端口号:用来辨别本地通讯进程,一个本地的进程在通讯时均会占用一个端口号,不同的进程端口号不同,因此在通讯前必须要分配一个没有被访问的端口号。
- 连接:指两个进程间的通讯链路。
- 半相关:网络中用一个三元组可以在全局唯一标志一个进程:(协议,本地地址,本地端口号)这样一个三元组,叫做一个半相关,它指定连接的每半部分。
- 全相关:一个完整的网间进程通信需要由两个进程组成,并且只能使用同一种高层协议。也就是说,不可能通信的一端用TCP协议,而另一端用UDP协议。因此一个完整的网间通信需要一个五元组来标识:(协议,本地地址,本地端口号,远地地址,远地端口号),这样一个五元组,叫做一个相关(association),即两个协议相同的半相关才能组合成一个合适的相关,或完全指定组成一连接。
套接字描述符
套接字是端点的抽象。与应用进程要使用文件描述符访问文件一样,访问套接字也需要用套接字描述符。套接字描述符在UNIX系统中是用文件描述符实现的。
要创建一个套接字,可以调用socket函数。
1 2 |
|
参数:
作用:socket()用于创建一个socket描述符(socket descriptor),它唯一标识一个socket。
网络字节序
网络协议指定了字节序,因此异构计算机系统能够交换协议信息而不会混淆字节序。TCP/IP协议栈采用大端字节序。应用进程交换格式化数据时,字节序问题就会出现。对于TCP/IP,地址用网络字节序来表示,所以应用进程有时需要在处理器的字节序与网络字节序之间转换。
1 2 3 4 5 |
|
这些函数名很好记,h表示host,n表示network, l表示32位长整数,s表示16位短整数
在将一个地址绑定到socket的时候,请先将主机字节序转换成为网络字节序,对主机字节序不要做任何假定,务必将其转化为网络字节序再赋给socket!
将套接字与地址绑定
与客户端的套接字关联的地址意义不大,可以让系统选择一个默认的地址。然而,对于服务器,需要给一个接收客户端请求的套接字绑定一个众所周知的地址。客户端应有一种方法用以连接服务器的地址,最简单的方法就是为服务器保留一个地址并且在/etc/services或某个名字服务(name service)中注册。
可以用bind函数来搞定这个问题:
1 2 3 |
|
参数:
第一个参数:bind()函数把一个地址族中的特定地址赋给该sockfd(套接字描述字)。例如对应AF_INET、AF_INET6就是把一个ipv4或ipv6地址和端口号组合赋给socket。
第二个参数:struct sockaddr *指针,指向要绑定给sockfd的协议地址。这个地址结构根据地址创建socket时的地址协议族的不同而不同:
地址格式
地址标识了特定通信域中的套接字端点,地址格式与特定的通信域相关。为使不同格式地址能够被传入到套接字函数,地址需被强转为通用的地址结构sockaddr表示。
1
2
//头文件
#include<netinet/in.h>
struct sockaddr 是一个通用地址结构,该结构定义如下:
1
2
3
4
5
struct
sockaddr
{
sa_family_t sa_family;
char
sa_data[14];
}
IPV4因特网域:
1
2
3
4
5
6
7
8
9
10
11
12
//ipv4对应的是:
/* 网络地址 */
struct
in_addr
{
uint32_t s_addr;
/* address in network byte order */
};
struct
sockaddr_in {
sa_family_t sin_family;
/* address family: AF_INET */
in_port_t sin_port;
/* port in network byte order */
struct
in_addr sin_addr;
/* internet address */
};
IPv6因特网域:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//ipv6对应的是:
struct
in6_addr
{
unsigned
char
s6_addr[16];
/* IPv6 address */
};
struct
sockaddr_in6
{
sa_family_t sin6_family;
/* AF_INET6 */
in_port_t sin6_port;
/* port number */
uint32_t sin6_flowinfo;
/* IPv6 flow information */
struct
in6_addr sin6_addr;
/* IPv6 address */
uint32_t sin6_scope_id;
/* Scope ID (new in 2.4) */
};
Unix域对应的是:
1
2
3
4
5
6
7
#define UNIX_PATH_MAX 108
struct
sockaddr_un
{
sa_family_t sun_family;
/* AF_UNIX */
char
sun_path[UNIX_PATH_MAX];
/* pathname */
};
第三个参数:addrlen 对应的是地址的长度
返回值:成功返回0,出错返回-1
作用:将套接字与端口号绑定,即把一个ip地址和端口号组合赋给socket
点分十进制IP与网络字节序IP之间的转换
有时需要打印出能被人而不是计算机所理解的地址格式。我们可以利用函数来进行二进制地址格式与点分十进制格式的相互转换。但是这些函数仅支持IPv4地址。
1 2 3 4 5 6 7 8 9 |
|
其中inet_pton和inet_ntop不仅可以转换IPv4的in_addr,还可以转换IPv6的in6_addr,因此函数接口是void* 类型!
1 2 3 4 5 |
|
监听
如果作为一个服务器,在调用socket()、bind()之后就会调用listen()来监听这个socket,如果客户端这时调用connect()发出连接请求,服务器端就会接收到这个请求。
服务器调用 listen 来宣告可以接收连接请求!
1 2 3 |
|
参数:sockfd为要监听的socket描述字,backlog为相应socket可以排队的最大连接个数
返回值:成功返回0,出错返回-1
作用:socket函数创建一个套接字时,默认是一个主动套接字,listen函数把一个未调用connect的未连接的套接字转换成一个被动套接字,指示内核应接收指向该套接字的连接请求。(主动/客户 -> 被动/服务器)
连接
如果是面向连接的网络服务,在开始交换数据前,都要在请求服务的进程套接字(客户端)和提供服务的进程套接字(服务器)之间建立一个连接,使用connect函数:
1 2 3 |
|
参数:第一个参数sockfd为客户端的socket描述字,第二参数为服务器的socket地址,第三个参数为socket地址的长度。
返回值:成功返回0,出错返回-1
作用:客户端通过调用connect函数来建立与TCP服务器的连接
注意:在connect中所指定的地址是想与之通信的服务器地址。如果sockfd没有绑定到一个地址,connect会给调用者绑定一个默认地址!
使用accept函数获得连接请求并建立连接
1 2 3 |
|
参 数 :第一个参数为服务器的socket描述字,第二个参数为指向struct sockaddr *的指针,用于返回客户端的协议地址,第三个参数为协议地址的长度
返回值:如果accpet成功,那么其返回值是由内核自动生成的一个全新的描述字,该描述符连接到调用connect的客户端。这个新的套接字描述符和原始的套接字描述符具有相同的套接字类型和地址族。
注 意:传给accept的原始套接字没有关联到这个连接,而是继续保持可用状态并接受其它连接请求!
通俗点来说,accept的第一个参数为服务器的socket描述字,是服务器开始调用socket()函数生成的,称为监听socket描述字;而accept函数返回的是已连接的socket描述字。一个服务器通常通常仅仅只创建一个监听socket描述字,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户连接创建了一个已连接socket描述字,当服务器完成了对某个客户的服务,相应的已连接socket描述字就被关闭。
数据传输
既然套接字端点表示文件描述符,那么只要建立连接,就可以使用write和read来通过套接字通信了。
1 2 3 |
|
write()会把指针buf所指的内存写入count个字节到参数fd所指的文件内(文件读写位置也会随之移动),如果顺利write()会返回实际写入的字节数。当有错误发生时则返回-1,错误代码存入errno中!
read()会把参数fd所指的文件传送nbyte个字节到buf指针所指的内存中,成功返回读取的字节数,出错返回-1并设置errno,如果在调read之前已到达文件末尾,则这次read返回0 。
如果想指定多个选项、从多个客户端接收数据包或发送带外数据,需要采用6个传递数据的套接字函数中的一个。
三个函数用来发送数据:
1 2 3 4 5 |
|
sendto()适用于已连接的数据报或流式套接口发送数据。
参数:
- sockfd:一个标识套接口的描述字。
- buf:包含待发送数据的缓冲区。
- len:buf缓冲区中数据的长度。
- flags:调用方式标志位。
- dest_addr:(可选)指针,指向目的套接口的地址。
- addrlen:所指地址的长度。
三个函数用来接收数据:
1 2 3 4 5 |
|
recvfrom()函数用于从已连接的套接口上接收数据,并捕获数据发送源的地址。
参数:
- sockfd:用来标识一个已连接套接口的描述字;
- buf:接收数据缓冲区;
- len:缓冲区长度;
- flags:调用操作方式,一般情况下为0;
- src_addr:指向装有源地址缓冲区的指针;
关闭套接字描述符
close函数用来关闭文件描述符:
1 2 |
|
注意:close操作只是使相应socket描述字的引用计数-1,只有当引用计数为0的时候,才会触发TCP客户端向服务器发送终止连接请求。
地址“重用”
缺省条件下,一个套接字不能与一个已在使用中的本地地址捆绑。但有时会需要“重用”地址。因为每一个连接都由本地地址和远端地址的组合唯一确定,所以只要远端地址不同,两个套接口与一个地址捆绑并无大碍。为了通知套接口实现不要因为一个地址已被一个套接口使用就不让它与另一个套接口捆绑,应用程序可在bind()调用前先设置SO_REUSEADDR选项。请注意仅在bind()调用时该选项才被解释;故此无需(但也无害)将一个不会共用地址的套接字设置该选项,或者在bind()对这个或其他套接口无影响情况下设置或清除这一选项。
解决这个问题的方法是使用setsockopt()设置socket描述符的 选项SO_REUSEADDR为1,表示允许创建端口号相同但IP地址不同的多个socket描述符。 在server代码的socket()和bind()调用之间插入如下代码:
1 2 |
|
基于TCP的socket通信基本流程:
- TCP服务器端依次调用socket()、bind()、listen()之后,就会监听指定的socket地址了。
- TCP客户端依次调用socket()、connect()之后就向TCP服务器发送了一个连接请求。
- TCP服务器监听到这个请求之后,就会调用accept()函数取接收请求,这样连接就建立好了。
- 之后就可以开始网络I/O操作了,即类同于普通文件的读写I/O操作。
建立一个基于TCP的socket API :
服务器:
客户端:
服务器:
客户端:
基于UDP协议的Socket编程
注意:
UDP没有建立连接的过程!
创建一个基于udp协议的套接字,使用socket函数时第二个参数不能传递SOCK_STREAM,而是传递SOCK_DGRAM
如创建一个基于IPv4地址族的UDP套接字: socket(AF_INET,SOCK_DGRAM, 0);
通常用于基于UDP协议的I/O一般使用 recvfrom 和 sendto 两个函数进行数据收发!
服务器:
客户端:
运行结果:
服务器:
客户端:
附:
POSIX( Portable Operater System Interface X代表unix)
MSB(Most Significant Bit):最高有效位,二进制中代表最高值的比特位,这一位对数值的影响最大。
LSB(Least Significant Bit):最低有效位,二进制中代表最低值的比特位