.
.
.
.
.
目录
(一) 一起学 Unix 环境高级编程 (APUE) 之 标准IO
(二) 一起学 Unix 环境高级编程 (APUE) 之 文件 IO
(三) 一起学 Unix 环境高级编程 (APUE) 之 文件和目录
(四) 一起学 Unix 环境高级编程 (APUE) 之 系统数据文件和信息
(五) 一起学 Unix 环境高级编程 (APUE) 之 进程环境
(六) 一起学 Unix 环境高级编程 (APUE) 之 进程控制
(七) 一起学 Unix 环境高级编程 (APUE) 之 进程关系 和 守护进程
(八) 一起学 Unix 环境高级编程 (APUE) 之 信号
(九) 一起学 Unix 环境高级编程 (APUE) 之 线程
(十) 一起学 Unix 环境高级编程 (APUE) 之 线程控制
(十一) 一起学 Unix 环境高级编程 (APUE) 之 高级 IO
(十二) 一起学 Unix 环境高级编程 (APUE) 之 进程间通信(IPC)
(十三) [终篇] 一起学 Unix 环境高级编程 (APUE) 之 网络 IPC:套接字
在前面的博文中我们讨论了进程间通讯(IPC)的各种常用手段,但是那些手段都是指通讯双方在同一台机器上的情况。在现实生活中我们会经常接触到各种各样的网络应用程序,比如大家经常使用的 ftp、svn、甚至QQ、迅雷等等,它们的通讯双方通常都是在不同的机器上的,那么它们的通讯就是跨主机的进程间通讯了,所以网络通讯也是一种进程间通讯的手段。
跨主机的程序在传输数据之前要制定严谨的协议,不然对方可能会看不懂你发送的数据,从而导致数据传送失败,甚至造成安全类bug,所以跨主机的通讯就不像我们之前学习的在同一台主机上的进程间通讯那么简单了。
制定协议要考虑的问题至少包括以下几点:
1)告诉对方自己的 IP 和端口;
先来看看 IP 和端口的概念。
当我们的程序在进行网络通讯之前,需要先与自己的机器进行约定,告诉操作系统我需要使用哪个端口,这样操作系统的某个端口在收到数据的时候就会发送给我们的进程。当另一个程序也来通知操作系统它要使用这个端口时,操作系统要保证这个端口只有我们使用而不能再让别人使用,否则当它收到数据的时候就不知道应该发送给谁了。
当我们需要发送数据的时候,也会使用这个端口进行发送,只有特殊情况才会使用别的端口或者使用多个端口。
2)还要考虑的问题是通信的双方应该采用什么数据类型呢?
假如通讯双方要传送一个 int 类型的数据,那么对方机器上 int 类型的位数与我们机器上的位数是否相同呢?
也就是说 int 类型在我的机器上是 32bit,但是在对方的机器上也是 32bit 吗?假设在对方机器上是 16bit,那么我发送给它的 int 值它能正确解析吗?
所以通信双方的数据类型要采用完全一致的约定,这个我们在下面会讨论如何让数据类型一致。
3)还要考虑字节序问题,这个说的是大小端的问题。
大端格式是:低地址存放高位数据,高地址存放低位数据。
小端格式是:低地址存放低位数据,高地址存放高位数据。
图1 大小端
如图1 所示,假设要存放的数据是 0x30313233,那么 33 是低位,30 是高位,在大端存储格式中,30 存放在低位,33 存放在高位;而在小端存储格式中,33 存放在低位,30 存放在高位。
这个东西有什么作用呢?它其实就是我们使用的网络设备(计算机、平板电脑、智能手机等等)在内存当中存储数据的格式。所以如果通讯双方的设备存储数据的格式不同,那么一端发送过去的数据,另一端是无法正确解析的,这可怎么办呢?
没关系,还好系统为我们准备了一组函数可以帮我们实现字节序转换,我们可以像使用公式一样使用它们。
1 htonl, htons, ntohl, ntohs - convert values between host and network byte order 2 3 #include <arpa/inet.h> 4 5 uint32_t htonl(uint32_t hostlong); 6 7 uint16_t htons(uint16_t hostshort); 8 9 uint32_t ntohl(uint32_t netlong); 10 11 uint16_t ntohs(uint16_t netshort);
这组函数的名字好奇怪是吧,所以为了便于记忆,在讨论它们的功能之前我们先来分析一下它们名字里的玄机:
h 是 host,表示主机;n 是 network,表示网络。l 表示 long,s 表示 short。
这样一来就好理解多了吧?它们的作用从名字中就可以看出来了,就是把数据从主机序转换为网络序,或者把数据从网络序转换为主机序。
网路字节序一般都是大端的,而主机字节序则根据硬件平台的不同而不同(在 x86 平台和绝大多数的 ARM 平台都是小端)。所以为了简化我们编程的复杂度,这些函数的内部会根据当前机器的结构自动为我们选择是否要转换数据的字节序。我们不用管到底我们自己的主机采用的是什么字节序,只要是从主机发送数据到网络就需要调用 hton 函数,从网络接收数据到主机就需要调用 ntoh 函数。
4)最后一项约定是结构体成员不对齐,由于数据对齐也是与硬件平台相关的,所以不同的主机如果使用不同的对齐方式,就会导致数据无法解析。
如何使数据不对齐呢,只需要在定义结构体的时候在结尾添加 __attribute__((packed)) 就可以了,见如下栗子:
1 struct msg_st 2 { 3 uint8_t name[NAMESIZE]; 4 uint32_t math; 5 uint32_t chinese; 6 }__attribute__((packed));
网络传输的结构体中的成员都是紧凑的,所以不能地址对齐,需要在结构体外面增加 __attribute__((packed))。
关于字节对齐的东西就足够写一篇博文了,LZ 在这里仅仅简单介绍一下什么是字节对齐,如果感兴趣大家可以去查阅专门的资料。
结构体的地址对齐是通过 起始地址 % sizeof(type) == 0 这个公式计算的,也就是说存放数据的起始地址位于数据类型本身长度的整倍数。
如果当前成员的起始地址能被 sizeof 整除,就可以把数据存放在这;否则就得继续看下一个地址能不能被 sizeof 整除,直到找到合适的地址为止。不适合作为起始地址的空间将被空(lang)闲(fei)。
图2 字节对齐
从进程间通信开始,我们写程序就是一步一步按部就班的写就可以了,编写网络应用也一样,网络通信本质上就是一种跨主机的进程间通信(IPC)。
在上一篇博文中我们了解了主动端和被动端的概念,那么接下来看看在 Socket 中主动端和被动端都要做什么。
主动端(先发包的一方)
1.取得 Socket
2.给 Socket 取得地址(可省略,不必与操作系统约定端口,由操作系统指定随机端口)
3.发/收消息
4.关闭 Socket
被动端(先收包的一方,先运行)
1.取得 Socket
2.给 Socket 取得地址
3.收/发消息
4.关闭 Socket
首先我们来看一个栗子,看不懂没关系,稍后 LZ 会告诉大家用到的函数都是什么意思。
proto.h 里面主要是通讯双方约定的协议,包含端口号、传送数据的结构体等等。
1 /* proto.h */ 2 #ifndef PROTO_H__ 3 #define PROTO_H__ 4 5 #include <stdint.h> 6 7 #define RCVPORT "1989" 8 9 #define NAMESIZE 13 10 11 12 struct msg_st 13 { 14 uint8_t name[NAMESIZE]; 15 uint32_t math; 16 uint32_t chinese; 17 }__attribute__((packed)); 18 19 20 #endif
rcver.c 是被动端的代码,也是通讯双方先启动的一端。
1 /* rcver.c */ 2 #include <stdio.h> 3 #include <stdlib.h> 4 5 #include <arpa/inet.h> 6 #include <sys/types.h> 7 #include <sys/socket.h> 8 9 #include "proto.h" 10 11 #define IPSTRSIZE 64 12 13 int main() 14 { 15 int sd; 16 struct sockaddr_in laddr,raddr; 17 socklen_t raddr_len; 18 struct msg_st rbuf; 19 char ipstr[IPSTRSIZE]; 20 21 sd = socket(AF_INET,SOCK_DGRAM, 0/*IPPROTO_UDP*/); 22 if(sd < 0) 23 { 24 perror("socket()"); 25 exit(1); 26 } 27 28 laddr.sin_family = AF_INET; 29 laddr.sin_port = htons(atoi(RCVPORT)); 30 inet_pton(AF_INET,"0.0.0.0",&laddr.sin_addr.s_addr); 31 32 if(bind(sd,(void *)&laddr,sizeof(laddr)) < 0) 33 { 34 perror("bind()"); 35 exit(1); 36 } 37 38 raddr_len = sizeof(raddr); 39 while(1) 40 { 41 if(recvfrom(sd,&rbuf,sizeof(rbuf),0,(void *)&raddr,&raddr_len) < 0) 42 { 43 perror("recvfrom()"); 44 exit(1); 45 } 46 47 inet_ntop(AF_INET,&raddr.sin_addr,ipstr,IPSTRSIZE); 48 printf("---MESSAGE FROM:%s:%d--- ",ipstr,ntohs(raddr.sin_port)); 49 printf("Name = %s ",rbuf.name); 50 printf("Math = %d ",ntohl(rbuf.math)); 51 printf("Chinese = %d ",ntohl(rbuf.chinese)); 52 } 53 54 close(sd); 55 56 57 exit(0); 58 }
snder.c 是主动端,主动向另一端发送消息。这端可以不用向操作系统绑定端口,发送数据的时候由操作系统为我们分配可用的端口即可,当然如果想要自己绑定特定的端口也是可以的。
1 /* snder.c */ 2 #include <stdio.h> 3 #include <stdlib.h> 4 #include <arpa/inet.h> 5 #include <sys/types.h> 6 #include <sys/socket.h> 7 #include <string.h> 8 9 #include "proto.h" 10 11 12 int main(int argc,char **argv) 13 { 14 int sd; 15 struct msg_st sbuf; 16 struct sockaddr_in raddr; 17 18 if(argc < 2) 19 { 20 fprintf(stderr,"Usage... "); 21 exit(1); 22 } 23 24 sd = socket(AF_INET,SOCK_DGRAM,0); 25 if(sd < 0) 26 { 27 perror("socket()"); 28 exit(1); 29 } 30 31 // bind(); // 主动端可省略绑定端口的步骤 32 33 memset(&sbuf,'