在文章《linux基础编程:进程通信之System V IPC:消息队列,信号量,共享内存》开头部分,我们介绍了linux环境下的进程通信方式IPC分类。本节我们将介绍由贝尔实验室及BSD(加州大学伯克利分校的伯克利软件发布中心)提出的IPC工具:套接字接口。
套接字机制是管道概念的一个扩展。凭借这种机制,客户或者服务器系统的开发工作既可以在本地单机运行,也可以跨网络进行分布式进程间进行。但是套接字的创建和使用和管道是有区别的,它明确地将通信的两端应该分为客户端和服务端。
要使用套接字进行进程通信之间,首先必须使用系统调用socket,从系统中分配一个类似文件描述符的套接字描述符,下面我们首先来学习一下套接字描述符。
套接字由三个属性来确定一个套接字的基本特性:域(domain),类型(type)和协议(protocol),并且利用套接字的地址作为套接字的名字,从而使得其他进程可以找到。
和管道通信不同,套接字可以在基于本地文件系统,以及各种类型的互联网的环境下的进程间通信。因此需要通过一个参数指定套接字的在通信过程中使用的通信介质(互联网,本地文件系统等)。我们把这个参数称为域。 在linux文档中介绍了AF_UNIX/AF_LOCAL,AF_INET,AF_INET6 等10多种域。而其中以下面三种种最为常见:
套接字类型是指创建套接字的应用程序要使用的通信服务类型。主要有下面三种:流套接字(SOCK_STREAM),数据报套接字(SOCK_DGRAM),原始套接字(SOCK_RAW)。
对于AF_UNIX/AF_LOCAL本地套接字来说,流套接字(SOCK_STREAM)相当于在本地进程之间建立起一条数据通道;数据报式套接字(SOCK_DGRAM)相当于单纯的发送消息,在进程通信过程中,理论上可能会有信息丢失、复制或者不按先后次序到达的情况,但由于其在本地通信,不通过外界网络,这些情况出现的概率很小。
对于AF_INET/AF_INET6互联网套接字来说,不同的套接字类型对应了不同的底层的网络协议。流套接字(SOCK_STREAM):网络套接字使用了传输控制协议,即TCP(The Transmission Control Protocol)协议,来保证实现可靠的数据服务。数据报套接字(SOCK_DGRAM):数据报套接字使用UDP(User Datagram Protocol)协议进行数据的传输。由于数据包套接字不能保证数据传输的可靠性,对于有可能出现的数据丢失情况,需要在程序中做相应的处理。原始套接字(SOCK_RAW):原始套接字与标准套接字(标准套接字指的是前面介绍的流套接字和数据报套接字)的区别在于:原始套接字可以读写内核没有处理的IP数据包,而流套接字只能读取TCP协议的数据,数据报套接字只能读取UDP协议的数据。因此,如果要访问其他协议发送数据必须使用原始套接字。
另外,从linux2.6版本以后,还可以通过套接字类型来指定套接字的行为。通过上面三种和SOCK_NONBLOCK 进行OR操作,可以指定套接字为非阻塞套接字
#include <sys/types.h> /* See NOTES */ #include <sys/socket.h> #include <unistd.h> int socket(int domain, int type, int protocol); int close(int fd);通过该系统调用,可以创建一个匿名套接字描述符,三个参数分别对应了上面三种特性。
struct sockaddr_un{ sa_family_t sun_family;//AF_UNIX char sun_path[];//文件系统路径 };
AF_INET套接字地址:该地址通过一个包含了协议,IP地址加上端口的结构体来唯一标识一个socket。它定义在netinet/in.h中的struct sockaddr_in:
struct sockaddr_in { short int sin_family; /* AF_INET */ unsigned short int sin_port; /* 端口 */ struct in_addr sin_addr; /* 一个四个字节的32位的值 */ unsigned char sin_zero[8]; /* Same size as struct sockaddr */ }; struct in_addr { unsigned long s_addr; }; typedef struct in_addr { union { struct{ unsigned char s_b1, s_b2, s_b3, s_b4; } S_un_b; struct { unsigned short s_w1, s_w2; } S_un_w; unsigned long S_addr; } S_un; } IN_ADDR;
struct sockaddr_un和struct sockaddr_in分别针对AF_UNIX和AF_INET两种域的特定的地址结构,而套接字命名的是一个struct sockaddr的通用套接口地址结构,在头文件<sys/socket.h>中定义,该结构和前面两个结构是并行的。可以把前面两个结构体指针转化为一个struct sockaddr结构。
struct sockaddr { unsigned short sa_family; /* 协议类型 */ char sa_data[14]; /* 14 字节的协议地址 */ };对套接字进行命名可以通过bind系统调用,成功时返回0,失败返回-1。
#include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
#include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int listen(int sockfd, int backlog);
对于一个已经命名的套接字,如果想可以接受来自其他进程的访问,必须创建一个队列来存储连接请求。其中sockfd为前面创建的套接字描述符,队列长度为backlog。成功返回0,错误返回-1。
#include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);因此调用accept的套接字,必须已命名和创建监听队列。调用将会创建并返回一个新的套接字和客户端进行连接,并且由addr指针返回客户端的地址信息。
#include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);其中addr地址表示的是服务端套接字的名字。函数成功时,返回0。失败时,返回-1。如果连接不能立即建立,函数将会阻塞一段不确定的超时时间,一旦时间过或者被信号中断,connect将会失败返回。
由于不同的计算机上字节序是不一样的,为了使不同类型的计算机可以就通过网络传输的多字节整数达成一致,需要定义一个网络字节序,客户端和服务器程序在传输之前,将它们的内部整数表示方式转换为网络字节序。可以通过相关函数来完成这一工作:
#include <arpa/inet.h> uint32_t htonl(uint32_t hostlong); uint16_t htons(uint16_t hostshort); uint32_t ntohl(uint32_t netlong); uint16_t ntohs(uint16_t netshort);
函数名是与之对应的转换操作的简写形式。例如"host to network,long"--->htonl。
上面的函数主要是完成整数类型在网络字节序和主机字节序之间的转换。但是在网络编程过程,以"."隔开字符串格式的网络地址其实也可以表示为一个32位的整数类型。socket也提供一套函数,可以在字符串IP,32位整数以及struct in_addr之间转换:
#include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> int inet_aton(const char *cp, struct in_addr *inp); in_addr_t inet_addr(const char *cp); in_addr_t inet_network(const char *cp); char *inet_ntoa(struct in_addr in); struct in_addr inet_makeaddr(int net, int host); in_addr_t inet_lnaof(struct in_addr in); in_addr_t inet_netof(struct in_addr in);
inet_aton()把由cp指向的字符串类型的IP地址(格式可以a.b.c.d(a,b,c,d都为单字节值);a.b.c(a,b为单字节,c为双字节);a.b(a为单字节,c为三字节);a(a为四字节))转换为网络字节序的struct in_addr结构体类型。如果这个函数成功,函数的返回IP的网络字节序的整数,如果输入地址不正确则会返回零。
inet_addr()把由cp指向的字符串类型的IP地址转换为unsigned int的网络字节序的整数in_addr_t。此函数已经过时,推荐使用 inet_aton()。因为对于有效地址 "255.255.255.255" 它也返回 -1 (因为 -1 的补码形式为 0xFFFFFFFF ),使得用户可能将 255.255.255.255 也当成是无效的非法地址,而使用 inet_aton() 则不存在这个问题。
inet_network()把由cp指向的字符串类型的IP地址转换为unsigned int的主机字节序的整数in_addr_t。
一般情况下,尽量使用inet_aton函数。
inet_ntoa()函数把struct in_addr地址结构体输出为字符串类型的IP地址。
inet_lnaof()该函数从参数 in 中提取出主机地址,执行成功后返回主机字节顺序形式的主机地址。如 192.168.2.100 属于 C 类地址,则主机号为低 8 位,主机地址为 0.0.0.100 ,按主机字节顺序输出则为 0x64。
inet_netof()该函数从参数 in 中提取出网络地址,执行成功返回主机字节顺序形式的网络地址。如 192.168.2.100,属于 C 类地址,则高 24 位表示网络号,网络地址为 192.168.2.0 ,按主机字节顺序输出则为 0xc0a802.
inet_makeaddr()该函数将网络号为参数 net ,主机号为参数 host 的两个地址组合成一个网络地址,如 net 取 0xc0a802 (192.168.2.0 ,C 类网络,主机字节顺序形式),host 取 0x64 ( 主机号 0.0.0.100 ,主机字节顺序形式 ),则组合后的网络地址为:192.168.2.100,并表示为网络字节顺序形式 0x6402a8c0 。
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> int main() { char buffer[32]; int ret = 0; int host = 0; int network = 0; unsigned int address = 0; struct in_addr in; in.s_addr = 0; /*输入一个以 "." 分隔的字符串形式的IP地址*/ printf("please input your ip address:"); fgets(buffer, 31, stdin); buffer[31] = '\0'; /*测试使用 inet_aton() 函数*/ if ((ret = inet_aton(buffer, &in)) == 0) { printf("inet_aton: \tinvailid address\n"); } else { printf("inet_aton: \t0x%x\n", in.s_addr); } /*测试使用 inet_addr() 函数*/ if ((address = inet_addr(buffer)) == INADDR_NONE) { printf("inet_addr: \tinvalid address \n"); } else { printf("inet_addr:\t0x%lx\n", address); } /*测试使用 inet_netwrok()函数*/ if ((address = inet_network(buffer)) == -1) { printf("inet_network: \tinvalid address\n"); } else { printf("inet_network:\t0x%lx\n", address); } /*测试使用 inet_ntoa() 函数*/ if (inet_ntoa(in) == NULL) { printf("inet_ntoa: \tinvalid address\n"); } else { printf("inet_ntoa: \t%s\n", inet_ntoa(in)); } /*测试使用 inet_lnaof() 与 inet_netof() 函数*/ host = inet_lnaof(in); network = inet_netof(in); printf("inet_lnaof:\t0x%x\n", host); printf("inet_netof:\t0x%x\n", network); in = inet_makeaddr(network, host); printf("inet_makeaddr:0x%x\n", in.s_addr); return 0; } //please input your ip address:192.168.2.100 //inet_aton: 0x6402a8c0 //inet_addr: 0x6402a8c0 //inet_network: 0xc0a80264 //inet_ntoa: 192.168.2.100 //inet_lnaof: 0x64 //inet_netof: 0xc0a802 //inet_makeaddr:0x6402a8c0
#include <netdb.h> struct hostent *gethostbyname(const char *name); struct hostent { char *h_name; /* official name of host 主机的官方名*/ char **h_aliases; /* alias list 主机备选名称,以NULL结尾的列表*/ int h_addrtype; /* host address type 返回的地址类型,只能是 AF_INET 或 AF_INET6 两种类型*/ int h_length; /* length of address 地址长度(以字节为单位)*/ char **h_addr_list; /* list of addresses (主机的网络地址的指针数组,以NULL结尾的列表)*/ } //拿主机 www.sina.com 的信息为例来讲解该函数的使用: //主机的官方名:ara.sina.com.cn //主机的备选名称为: //主机备选名称0:www.sina.com //主机备选名称1:us.sina.com.cn //主机备选名称2:news.sina.com.cn //主机备选名称3:jupiter.sina.com.cn //地址类型: AF_INET //IP地址: 58.63.236.46 //IP地址: 58.63.236.47 //IP地址: 58.63.236.48 //IP地址: 58.63.236.49 //IP地址: 58.63.236.50 //IP地址: 58.63.236.26 //IP地址: 58.63.236.27 //IP地址: 58.63.236.28 //IP地址: 58.63.236.29 //IP地址: 58.63.236.30 //IP地址: 58.63.236.31 //IP地址: 58.63.236.32 //IP地址: 58.63.236.42 //IP地址: 58.63.236.43 //IP地址: 58.63.236.44 //IP地址: 58.63.236.45
#include <sys/socket.h> /* for AF_INET */ struct hostent *gethostbyaddr(const cost *addr,socklen_t len, int type);addr是一个IP信息;第 3 个参数 type 指定需要查询主机的 IP 地址的类型,在 IPv4 的情况下为 AF_INET,这个过程实际上是一个反向DNS 查询过程。
#include <unistd.h> int gethostname(char *name, size_t len); int sethostname(const char *name, size_t len);
#include <netdb.h> struct servent *getservbyname(const char *name, const char *proto); struct servent { char *s_name; /* 表示服务的正式名称。 */ char **s_aliases; /* 别名链表。 */ int s_port; /* 端口号。r */ char *s_proto; /*服务所使用的协议(如 tcp 或 udp) */ }; //ftp tcp:s_name: ftp s_port: 21 s_proto: tcp //domain udp:s_name: domain s_port: 53 s_protol: udp
#include <netdb.h> struct servent *getservbyport(int port, const char *proto);