网络进程间通信(network IPC)
- 我们接下来描述网络进程间通信接口,进程用该接口能够和其他进程通信,无论他们是处于同一台计算机还是处于不同的计算机上。实际上,这也正是套接字接口设计的主要目标之一;同样的接口既可以用于计算机间通信,也可以用于计算机内通信。尽管套接字接口可以采用许多不同的网络协议进行通信,但是我们还是讨论在因特网事实上的通信标准:TCP/IP协议栈。
套接字描述符
- 套接字是通信端点的抽象。正如使用文件描述符访问文件,应用程序用套接字描述符访问套接字。套接字描述符在UNIX系统中被当做一种文件描述符。事实上,许多处理文件描述符的函数(read()和write())可以用于处理套接字描述符。
#include<sys/socket.h>
int socket( int domain, int type, int prootocol);
返回值:若成功,返回文件(套接字)描述符,若出错,返回 -1
- 参数domain(域) 确定通信的特性,包括地址格式。各个域都有自己表示地址的格式,而表示各个域的常数都以AF_开头,意指地址族(address family)。
域 | 描述 |
---|
AF_INET | IPv4 因特网域 |
AF_INET6 | IPv6因特网域 |
AF_UNIX | UNIX域 |
AF_UPSPEC | 未指定 |
- 上面是UNIX下的域,下面我们来看一下linux下的域
Name Purpose Man page
AF_UNIX, AF_LOCAL Local communication unix(7)
AF_INET IPv4 Internet protocols ip(7)
AF_INET6 IPv6 Internet protocols ipv6(7)
AF_IPX IPX - Novell protocols
AF_NETLINK Kernel user interface device netlink(7)
AF_X25 ITU-T X.25 / ISO-8208 protocol x25(7)
AF_AX25 Amateur radio AX.25 protocol
AF_ATMPVC Access to raw ATM PVCs
AF_APPLETALK Appletalk ddp(7)
AF_PACKET Low level packet interface packet(7)
- 我就不多介绍了,读者如有需要,可以去 linux下的man手册中查看。我来说几个常用的。
- AF_INET 这是大多数用来产生socket的协议,使用TCP或UDP来传输,用IPv4的地址
- AF_INET6 与上面类似,不过是来用IPv6的地址
- AF_UNIX 本地协议,使用在Unix和Linux系统上,一般都是当客户端和服务器在同一台及其上的时候使用
- 参数 type 确定套接字的类型,进一步确定通信特征。
类型 | 描述 |
---|
SOCK_DGRAM | 固定长度的、无连接的、不可靠的报文传递 |
SOCK_RAW | IP协议的数据报连接 |
SOCK_SEQPACKET | 固定长度的、有序的、可靠的、面向连接的报文传递 |
SOCK_STREAM | 有序的、可靠的、双向的、面向连接的字节流 |
- 参数protocol 通常是0,表示给定的域和套接字类型选择默认协议。当对同一域和套接字类型支持多个协议时,可以使用protocol 选择一个特定协议。
- 例如:在AF_INET 通信域中,套接字类型SOCK_STREAM的默认协议时传输控制协议(TCP)。
- 在AF_INET通信域中,套接字SOCK_DGRAM的默认协议是UDP。
- 英特网域套接字定义的协议
协议 | 描述 |
---|
IPPROTO_IP | IPv4 网际协议 |
IPPROTO_IPv6 | IPv6 网际协议 |
IPPROTO_ICMP | 因特网控制报文协议 |
IPPROTO_RAW | 原始IP数据包协议 |
IPPROTO_TCP | 传输控制协议 |
IPPROTO_UDP | 用户数据报协议 |
我们在来区分一下SOCK_DGRAM和SOCK_STREAM这两种套接字类型
- 对于数据包(SOCK_DGRAM)接口,两个对等进程之间通信时不需要逻辑连接。只需要向对等进程所使用的套接字送出一个报文。
- 因此数据提供一个无连接的服务。另一方面,字节流(SOCK_STREAM)要求在交换数据之前,在本地套接字和通信的对等进程的套接字之间建立一个逻辑连接。
- 数据报包含报文,发送数据报近似于给某人邮寄信件。你可以邮递很多信,但你不能保证邮递的次序,并且可能有些信件会丢失在路上。每封信件包含接收者地址,使这封信件独立于所有其他信件。每封信件可能送达不同的接收者。
- 相反的,使用面向连接的协议通信就像与对方打电话。首先,需要通过电话建立一个连接,连接建立好之后,彼此能双向的通信。每个连接是端到端的通信链路。对话中不包含地址信息,就像呼叫两端存在一个点对点的虚拟连接,并且连接本身暗示特定的源和目的地。
- SOCK_STREAM套接字提供字节流服务,所以应用程序分辨不出报文的界限。这意味着从SOCK_STREAM套接字读数据时,它也许不会返回所有由发送进程所写的字节数。最终可以获得发送过来的所有数据,但也许要通过若干次函数调用才能得到。
套接字的shutdown
- 套接字通信是双向的。可以采用shutdown 函数来禁止一个套接字的I/O
#include<sys/socket.h>
int shutdown(int sockfd, int how);
返回值:若成功,返回0,若失败,返回 -1
- 如果how是SHUT_RD(关闭读端),那么无法从套接字读取数据。如果how是SHUT_WR(关闭写端),那么无法从套接字发送数据。如果how是SHUT_RDWR,则既无法读取数据,又无法发送数据。
- 这是肯定有读者有问题了,既然已经有了close了,为何还要有shutdown呢?这里我解释一下。
- 首先,只有最后一个活动引用关闭时,close才能释放网络端点。这意味着如果复制了一个套接字(如采用dup),要直到关闭了最后一个引用它的文件描述符才会释放这个套接字。而shutdown 允许使一个套接字处于不活动状态,和引用它的文件描述符数目无关。其次,有时可以很方便的关闭套接字双向传输的一个方向。例如,如果想让通信的进程能够确定数据传输何时结束,可以关闭该套接字的写端,然而通过该套接字读端仍可以继续接受数据。
字节序
- 字节序有大端和小端之分。
- 大端:最大字节地址出现在最低有效字节上。
- 小端:最低有效字节包含最小字节地址
- 这个相信大家已经很熟悉了,我就不多说了。
- 重要的是:TCP/IP协议栈使用大端字节序
- 对于TCP/IP,地址用网络字节序来表示,所以应用程序有时需要在处理器的字节序与网络字节序之间转换他们,对于TCP/IP应用程序,有4个用来处理字节序和网络字节序之间实施转换的函数。
#include<arpa/inet.h>
uint32_t htonl( uint32_t hostint32 ) ; 返回值:以网络字节序表示的32位整数
uint16_t htons( uint16_t hostint16); 返回值:以网络字节序表示的16位整数
uint32_t ntohl( uint32_t netint32); 返回值:以主机字节序表示的32位整数
uint16_t ntohs( uint16_t netint16); 返回值:以主机字节序表示的16位整数
- h表示“主机”字节序,n表示“网络”字节序。l 表示“长”(即4字节)整数,s表示“短”(即2字节)整数。虽然在使用这些函数时包含的是<arpa/inet.h>头文件,但系统实现经常是在其他头文件中声明这些函数的,只是这些头文件都包含在<arpa/inet.h>。对于系统来说,把这些函数实现为宏也是常见的。
地址格式
- 一个地址标识一个特定通信域的套接字端点,地址格式与这个特定的通信域相关。为使不同格式地址能够传入到套接字函数,地址会强迫转换成一个通用的地址结构sockaddr:
- 在linux下中,该结构定义为:
struct sockaddr{
sa_family_t sa_family;
char sa_data[4];
}
- 英特网地址定义在<netinet/in.h>头文件中。在IPv4英特网域中,套接字地址用结构sockaddr_in表示
struct in_addr{
in_addr_t s_addr;
}
struct sockaddr_in{
sa_family_t sin_family; 地址名
in_port_t sin_port; 端口号
struct in_addr sin_addr; IPv4地址
}
- 数据类型 in_port_t 定义成 uint16_t。数据类型 in_addr_t 定义成 uint32_t。这些整数类型在<stdint.h>中定义并指定了相应的位数。
- 有时需要打印出被人理解而不是计算机所理解的地址格式。有两个函数 inet_ntop 和 inet_pton 满足这样的功能。
#include<arpa/inet.h>
const char *inet_ntop(int domain,const void *restrict addr, char *restrict str, socklen_t size);
返回值:若成功,返回地址字符串指针;若出错,返回NULL
int inet_pton(int damain, const char* restrict str, void *restrict addr);
返回值:若成功,返回1,;若格式无效,返回0;若出错,返回 -1
- 函数inet_ntop 将网络字节序的二进制地址转换成文本字符串格式。inet_pton将文本字符串格式转换成网络字节序的二进制地址。
将套接字与地址关联
- 将一个客户端的套接字关联上一个地址没有多少新意,可以让系统选一个默认的地址。然后,对于服务器,需要给一个接收客户端请求的服务器套接字关联上一个众所周知的地址。客户端应有一种方法来发现连接服务器所需要的地址,最简单的方法就是服务器保留一个地址并且注册在/etc/services或者某个名字服务中。
- 使用 bind 函数关联地址和套接字。
#include<sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t len);
返回值:若成功,返回0;若出错,返回 -1
- 对于使用的地址有一下一些限制
- 在进程正在运行的计算机上,指定的地址必须有效;不能指定一个其他机器的地址。
- 地址必须和创建套接字时的地址族所支持的格式相匹配。
- 地址中的端口号必须不小于1024,除非该进程具有相应的特权。
- 一般只能将一个套接字端点绑定到一个给定地址上,尽管有些协议允许多重绑定。
建立连接
- 如果要处理一个面向连接的网络服务(SOCK_STREAM或SOCK_SEQPACKET),那么在开始交换数据以前,需要在请求服务的进程套接字(客户端)和提供服务的进程套接字(服务器)之间建立一个连接。使用connect 函数来建立连接。
#include<sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t len);
返回值:若成功,返回0;若出错,返回 -1
- 在connect 中指定的地址是我们想与之通信的服务器地址。如果sockfd 没有绑定到一个地址,connect会给调用者绑定一个默认地址。
- 当尝试连接服务器时,出于一些原因,连接可能会失败。要想一个连接请求成功,要连接的计算机必须是开启的,并且正在运行,服务器必须绑定到一个想与之连接的地址上,并且服务器的等待队列要有足够的空间。
代码的介绍
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <sys/types.h>
4 #include <sys/socket.h>
5 #include <strings.h>
6 #include <string.h>
7 #include <ctype.h>
8 #include <arpa/inet.h>
9
10 #define SERV_PORT 9527
11
12 int main(void)
13 {
14 int sfd, cfd;
15 int len, i;
16 char buf[BUFSIZ], clie_IP[BUFSIZ];
17
18 struct sockaddr_in serv_addr, clie_addr;
19 socklen_t clie_addr_len;
20
21 /*创建一个socket 指定IPv4协议族 TCP协议*/
22 sfd = socket(AF_INET, SOCK_STREAM, 0);
23
24 /*初始化一个地址结构 man 7 ip 查看对应信息*/
25 bzero(&serv_addr, sizeof(serv_addr)); //将整个结构体清零
26 serv_addr.sin_family = AF_INET; //选择协议族为IPv4
27 serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); //监听本地所有IP地址
28 serv_addr.sin_port = htons(SERV_PORT); //绑定端口号
29
30 /*绑定服务器地址结构*/
31 bind(sfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
32
33 /*设定链接上限,注意此处不阻塞*/
34 listen(sfd, 64); //同一时刻允许向服务器发起链接请求的数量
35
36 printf("wait for client connect ...\n");
37
38 /*获取客户端地址结构大小*/
39 clie_addr_len = sizeof(clie_addr_len);
40 /*参数1是sfd; 参2传出参数, 参3传入传入参数, 全部是client端的参数*/
41 cfd = accept(sfd, (struct sockaddr *)&clie_addr, &clie_addr_len); /*监听客户端链接, 会阻塞*/
42
43 printf("client IP:%s\tport:%d\n",
44 inet_ntop(AF_INET, &clie_addr.sin_addr.s_addr, clie_IP, sizeof(clie_IP)),
45 ntohs(clie_addr.sin_port));
46
47 while (1) {
48 /*读取客户端发送数据*/
49 len = read(cfd, buf, sizeof(buf));
50 write(STDOUT_FILENO, buf, len);
51
52 /*处理客户端数据*/
53 for (i = 0; i < len; i++)
54 buf[i] = toupper(buf[i]);
55
56 /*处理完数据回写给客户端*/
57 write(cfd, buf, len);
58 }
59
60 /*关闭链接*/
61 close(sfd);
62 close(cfd);
63
64 return 0;
65 }
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <string.h>
4 #include <sys/socket.h>
5 #include <arpa/inet.h>
6
7 #define SERV_IP "127.0.0.1"
8 #define SERV_PORT 9527
9
10 int main(void)
11 {
12 int sfd, len;
13 struct sockaddr_in serv_addr;
14 char buf[BUFSIZ];
15
16 /*创建一个socket 指定IPv4 TCP*/
17 sfd = socket(AF_INET, SOCK_STREAM, 0);
18
19 /*初始化一个地址结构:*/
20 bzero(&serv_addr, sizeof(serv_addr)); //清零
21 serv_addr.sin_family = AF_INET; //IPv4协议族
22 inet_pton(AF_INET, SERV_IP, &serv_addr.sin_addr.s_addr); //指定IP 字符串类型转换为网络字节序 参3:传出参数
23 serv_addr.sin_port = htons(SERV_PORT); //指定端口 本地转网络字节序
24
25 /*根据地址结构链接指定服务器进程*/
26 connect(sfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
27
28 while (1) {
29 /*从标准输入获取数据*/
30 fgets(buf, sizeof(buf), stdin);
31 /*将数据写给服务器*/
32 write(sfd, buf, strlen(buf)); //写个服务器
33 /*从服务器读回转换后数据*/
34 len = read(sfd, buf, sizeof(buf));
35 /*写至标准输出*/
36 write(STDOUT_FILENO, buf, len);
37 }
38
39 /*关闭链接*/
40 close(sfd);
41
42 return 0;
43 }