linux基础编程:进程通信之套接字

在文章《linux基础编程:进程通信之System V IPC:消息队列,信号量,共享内存》开头部分,我们介绍了linux环境下的进程通信方式IPC分类。本节我们将介绍由贝尔实验室及BSD(加州大学伯克利分校的伯克利软件发布中心)提出的IPC工具:套接字接口。

套接字机制是管道概念的一个扩展。凭借这种机制,客户或者服务器系统的开发工作既可以在本地单机运行,也可以跨网络进行分布式进程间进行。但是套接字的创建和使用和管道是有区别的,它明确地将通信的两端应该分为客户端和服务端。

要使用套接字进行进程通信之间,首先必须使用系统调用socket,从系统中分配一个类似文件描述符的套接字描述符,下面我们首先来学习一下套接字描述符。


套接字描述符


套接字由三个属性来确定一个套接字的基本特性:域(domain),类型(type)和协议(protocol),并且利用套接字的地址作为套接字的名字,从而使得其他进程可以找到。


套接字域(domain)

和管道通信不同,套接字可以在基于本地文件系统,以及各种类型的互联网的环境下的进程间通信。因此需要通过一个参数指定套接字的在通信过程中使用的通信介质(互联网,本地文件系统等)。我们把这个参数称为域。 在linux文档中介绍了AF_UNIX/AF_LOCAL,AF_INET,AF_INET6 等10多种域。而其中以下面三种种最为常见:

  • AF_UNIX/AF_LOCAL:UNIX文件系统域,即以本地文件系统为通信介质。
  • AF_INET/AF_INET6:IPv4/IPv6互联网域,即以IPv4/IPv6为通信介质。

套接字类型(type)

套接字类型是指创建套接字的应用程序要使用的通信服务类型。主要有下面三种:流套接字(SOCK_STREAM),数据报套接字(SOCK_DGRAM),原始套接字(SOCK_RAW)。

  • 流套接字(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操作,可以指定套接字为非阻塞套接字


套接字协议(protocol)

套接字协议来用来指定三种套接字类型所对应的底层通信协议。对于流套接字(SOCK_STREAM)和数据报套接字(SOCK_DGRAM),一般都只是使用默认值。即流套接字对应TCP/IP,数据报套接字对应UDP。当使用原始套接字时,可以使用特定的套接字协议来进行开发。

创建套接字描述符

#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);
通过该系统调用,可以创建一个匿名套接字描述符,三个参数分别对应了上面三种特性。

套接字命名

通过上面的系统调用,创建的是一个匿名套接字描述符。但是要想让创建的套接字可以被其他进程使用,服务器必须给套接字进行命名。在socket编程中,通过套接字地址来给套接命名。而套接字地址和套接字描述符的域相关,下面针对AF_UNIX和AF_INET两种域定义他们的套接字地址。
AF_UNIX套接字地址:该地址通过本地文件系统的路径来标识,地址结构是在sys/un.h中定义的struct sockaddr_un:
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。


接受连接


对于一个已经创建监听队列的命名套接字,对于每一个请求连接的客户端的信息保存到上面的监听队列中。此时我们可以通过accept系统调用从该队列中取出第一个未处理的连接,如果该队列为空,将会阻塞该系统调用。
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
因此调用accept的套接字,必须已命名和创建监听队列。调用将会创建并返回一个新的套接字和客户端进行连接,并且由addr指针返回客户端的地址信息。

请求连接


客户端套接字可以通过系统调用connect与已经创建监听队列的命名套接字建立连接。
#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

根据主机IP查找主机信息

#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 查询过程。

本机主机信息

每台主机都有一个主机名,通过gethostname可以得到本主机的主机名,通过sethostname来设置主机名:
#include <unistd.h>

int gethostname(char *name, size_t len);
int sethostname(const char *name, size_t len);


服务信息

在主机中,通过在文件/etc/services中为每一个端口号分配一个名字,使得用户可以使用符号化的服务名而不是端口号的数字。因此可以根据服务名或端口号加上协议类型,来获得相应的服务信息。

根据服务名查找服务信息

#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);

在主机中,通过在文件/etc/services中为每一个端口号分配一个名字,使得用户可以使用符号化的服务名而不是端口号的数字。
总结:通过服务名得到服务端口信息,通过主机名得到主机信息和连接地址,从而可以连接到指定的服务上。比如ftp


你可能感兴趣的:(linux基础编程:进程通信之套接字)