[架构之路-62]:目标系统 - 平台软件 - 基础中间件 - Linux Socket网络进程间通信的基本原理与示例(AF_INETAF_UNIXAF_TIPC)

Posted 文火冰糖的硅基工坊

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[架构之路-62]:目标系统 - 平台软件 - 基础中间件 - Linux Socket网络进程间通信的基本原理与示例(AF_INETAF_UNIXAF_TIPC)相关的知识,希望对你有一定的参考价值。

目录

前言:

第1章 Linux Socket网络进程间通信概述

1.1 概述

1.2 基本的网络架构

1.3 协议栈分层

1.4 网间进程标识的标识问题

1.5 创建socket与Socket标识

1.5 AF_INET、AF_UNIX、AF_TIPC区别

第2章 Socket的本质

2.1 socket套接字的进一步解读

2.2 套接字描述符

2.3 文件描述符和文件指针的区别

第3章 Linux Socket通信的流程

3.1 通信流程概述

3.2 socket()

3.3 bind()

3.4 listen()  //TCP only

3.5 connect()  

3.6 accept() //TCP only

3.7 send() //适合connect之后的socket

3.8 recv() //适合connect之后的socket

3.9  sendto()

3.10 recvfrom()

3.11 单方向关闭socket:shutdown()

3.12 全方向关闭socket:close()


前言:

Linux进程间原生的进程间通信机制只能解决同一个Linux操作系统管辖之下的多个Linux进程间通信,并不能解决处于不同Linux操作系统两个机器之间的进程间通信。 Linux Socket网络进程间通信正是解决此问题的机制。本文就是从宏观的讲解Linux Socket网络进程间通信的基本原理以及基本示例。关于TCP IP协议栈的工作原理,不在本文范围。

第1章 Linux Socket网络进程间通信概述

1.1 概述

进程通信的概念最初来源于单机系统。由于每个进程都在自己的地址范围内运行,为保证两个相互通信的进程之间既互不干扰又协调一致工作,操作系统为进程通信提供了相应的进程间通信的设施,如:

  1. UNIX BSD有:管道(pipe)、命名管道(named pipe)软中断信号(signal)
  2. UNIX system V有:消息(message)、共享存储区(shared memory)和信号量(semaphore)等

他们都仅限于用在本机进程之间通信,详细参看:

[架构之路-60]:目标系统 - 平台软件 - 基础中间件 - Linux进程间通信的主要方式_文火冰糖的硅基工坊的博客-CSDN博客

网间进程通信要解决的是不同主机进程间的相互通信问题(可把同机进程通信看作是其中的特例), Linux Socket网络进程间通信正是解决此问题的机制。

1.2 基本的网络架构

跨网络进程间通信有两个基本的模型:C/S架构与B/S架构。

(1)基本的C/S(Client/Server)架构

C/S架构的客户端应用进程是客户端应用程序,如QQ, 微信,腾讯视频等等。

应用程序底层的协议是多样的,可以直接基于TCP/IP socket,也可以基于更上层的协议。

(2)基本的B/S (Browser/Server)架构

C/S架构的客户端应用进程是客户端的浏览器,如IE, Firefox等。

应用程序底层的协议为Http或https协议。

1.3 协议栈分层

1.4 网间进程标识的标识问题

进程间通信,首先解决的就是跨进程间的统一标识问题。

(1)同一主机上:

  • 不同进程可用进程号(process ID)唯一标识。
  • 跨进程的通信机制(如队列)可以通过全局性性的key或本地全局文件名唯一标识。

(2)在网络环境下:

  • 进程的识别问题

各主机独立分配的进程号不能唯一标识该进程。例如,主机A赋于某进程号3,在B机中也可以存在3号进程,因此,“3号进程”这句话就没有意义了。

  • 多重协议的识别问题

其次,操作系统支持的网络协议众多,不同协议的工作方式不同,地址格式也不同。因此,网间进程通信还要解决多重协议的识别问题。

(3)TCP/IP协议栈的解决之道

TCP/IP协议族已经帮我们解决了这个问题:

  • 主机标识:网络层的“ip地址”可以唯一标识网络中的主机
  • 协议族标识:TCP/IP协议族、其他协议族等....
  • 协议标识:协议类型:TCP/UDP/SCTP.......
  • 应用程序标识:传输层的"协议族+协议标识+端口”可以唯一标识主机中的应用程序(进程),一个主机的一对"协议族+协议标识+端口”,只能绑定一个应用程序。

使用TCP/IP协议的应用程序通常采用应用编程接口:UNIX BSD的套接字(socket)和UNIX System V的TLI(已经被淘汰),来实现网络进程之间的通信。就目前而言,几乎所有的应用程序都是采用socket,而现在又是网络时代,网络中进程通信是无处不在,这就是我为什么说“一切皆socket”。

1.5 创建socket与Socket标识

int socket(int protofamily, int type, int protocol);   //返回sockfd

int socket(int domain,      int type, int protocol);      //返回sockfd

(1)domain

指明所使用的协议族通常为AF_INET,表示互联网协议族(如TCP/IP协议族)

  • AF_INET:因特网域,与AF_INET_IPv4等效。
  • AF_INET_IPv4:因特网域;
  • AF_INET6_IPv6因特网域;
  • AF_UNIX:Unix域;
  • AF_ROUTE路由套接字;
  • AF_KEY密钥套接字;
  • AF_UNSPEC:未指定;

(2)type

(3)protocol

(4)返回值:sockfd,进程空间内的socket标识。sockfd是socket标识

(5)port:端口号,是Linux内核分配给应用程序的标识号,它是本地应用程序的标识

1.5 AF_INET、AF_UNIX、AF_TIPC区别

(1)AF_UNIX

AF_UNIX用于Linux单机、本地的不同进程间,通过Socket进行通信,此时,传送的数据不需要经过TCP/IP协议栈的编码和解码。只需要借助于Linux内核空间的sk_buffer提供数据的共享与中转即可。

 

(2)AF_INET

AF_INET用于跨网络,不同的单机上不同进程之间的Socket通信 。

使用IP地址作为主机标识,使用IP +  domain + type + protocol + port标识全局唯一的应用程序。

(3)AF_TIPC

AF_TIPC不使用IP地址标识主机,也不使用主机名标识主机,而是使用一个全局的类似于Key的方式来标识主机。网络中的应用程序,不直接使用IP地址标识通信端,而是使用标识符来标识。Linux内核会维护一个主机标识到IP地址的映射关系,并把标识符转换成IP地址。

这种方式非常适用于由多个包含独立操作系统的子卡组成的小系统,在这个小系统中的业务类型、子卡的作用都是明确的,但IP地址可能是多变的情形。

第2章 Socket的本质

2.1 socket套接字的进一步解读

socket起源于Unix,而Unix/Linux基本哲学之一就是“一切皆文件”,都可以用“打开open –> 读写write/read –> 关闭close”模式来操作。

Socket就是该模式的一个实现,socket即是一种特殊的文件,通过特殊的文件标识 socket id操作socket,socket对象的基本操作函数就是对其进行的操作(读/写IO、打开、关闭)。

说白了,Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。

在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。

注意:其实socket也没有层的概念,它只是一个facade设计模式的应用,让编程变的更简单。是一个软件抽象层。

在网络编程中,我们大量用的都是通过socket实现的。

2.2 套接字描述符

其实就是一个整数,我们最熟悉的句柄是0、1、2三个,0是标准输入,1是标准输出,2是标准错误输出。0、1、2是整数表示的,对应的FILE *结构的表示就是stdin、stdout、stderr

套接字API最初是作为UNIX操作系统的一部分而开发的,所以套接字API与系统的其他I/O设备集成在一起。特别是,当应用程序要为因特网通信而创建一个套接字(socket)时,操作系统就返回一个小整数作为描述符(descriptor)来标识这个套接字。然后,应用程序以该描述符作为传递参数,通过调用函数来完成某种操作(例如通过网络传送数据或接收输入的数据)。

在许多操作系统中,套接字描述符和其他I/O描述符是集成在一起的,所以应用程序可以对文件进行套接字I/O或I/O读/写操作。

当应用程序要创建一个套接字时,操作系统就返回一个小整数作为描述符,应用程序则使用这个描述符来引用该套接字,需要I/O请求的应用程序请求操作系统打开一个文件。操作系统就创建一个文件描述符提供给应用程序访问文件。从应用程序的角度看,文件描述符是一个整数,应用程序可以用它来读写文件。下图显示,操作系统如何把文件描述符实现为一个指针数组,这些指针指向内部数据结构。

 对于每个进程,系统都有一张单独的表。精确地讲,系统为每个运行的进程维护一张单独的文件描述符表。当进程打开一个文件时,系统把一个指向此文件内部数据结构的指针写入文件描述符表,并把该表的索引值返回给调用者 。进程内的应用程序只需记住这个描述符,并在以后操作该文件时使用它。操作系统把该描述符作为索引访问进程描述符表,通过指针找到保存该文件所有的信息的数据结构。

套接字fd只适用于进程内,要实现跨网络的进程间通信,还需要一个跨进程的全网唯一的进程标识:IP地址+协议族+协议类型+端口号。

针对套接字的系统数据结构:

   1)套接字API里有个函数socket,它就是用来创建一个套接字。

套接字设计的总体思路是,单个系统调用就可以创建任何套接字,因为套接字是相当笼统的。

一旦套接字创建后,应用程序还需要调用其他函数来指定具体细节。

例如调用socket将创建一个新的描述符条目:

    2)其他字段

虽然套接字的内部数据结构包含很多字段,但是系统创建套接字后,大多数字字段没有填写。

应用程序创建套接字后在该套接字可以使用之前,必须调用其他的过程来填充这些字段。

2.3 文件描述符和文件指针的区别

文件描述符:在linux系统中打开文件就会获得文件描述符,它是个很小的正整数。每个进程在PCB(Process Control Block)中保存着一份文件描述符表,文件描述符就是这个表的索引,每个表项都有一个指向已打开文件的指针。

文件指针:C语言中使用文件指针做为I/O的句柄。文件指针指向进程用户区中的一个被称为FILE结构的数据结构。FILE结构包括一个缓冲区和一个文件描述符。而文件描述符是文件描述符表的一个索引,因此从某种意义上说文件指针就是句柄的句柄(在Windows系统上,文件描述符被称作文件句柄)。
 

第3章 Linux Socket通信的流程

3.1 通信流程概述

Linux系统中常用的socket网络编程接口有:

socket()、bind()、listen()、accept()、connect()、send()、recv()、close(),

  • socket()与close()则由服务器与客户端共用
  • connect()与send()为客户端专用接口
  • bind()、listen()、accept()及recv()为服务器端专用接口

3.2 socket()

int socket(int domain, int type, int protocol);//返回sockfd

  • domain参数指明所使用的协议族,通常为AF_INET,表示互联网协议族(TCP/IP协议族);AF_INET_IPv4:因特网域;AF_INET6_IPv6因特网域;AF_UNIX:Unix域;AF_ROUTE路由套接字;AF_KEY密钥套接字;AF_UNSPEC:未指定;
  • type参数:指定socket的类型: SOCK_STREAM 或SOCK_DGRAM,Socket接口还定义了原始Socket(SOCK_RAW),允许程序使用低层协议;SOCK_STREAM:流式套接字提供可靠的、面向连接的通信流:它使用TCP协议,从而保证了数据传输的正确性和顺序性(TCP:可靠的、重传、有连接的,一般用于控制命令);SOCK_DGRAM:数据报套接字定义了一种无连接的服,数据通过相互独立的报文进行传输,是无序的,并且不保证是可靠、无差错的。它使用数据报协议(UDP:不可靠的、无连接,大数据传输,数据可能会丢失)
  • protocol:通常赋值"0"。 0选择type类型对应的默认协议;IPPROTO_TCP:TCP传输协议;IPPROTO_UDP:UDP传输协议;IPPROTO_SCTP:SCTP传输协议;IPPROTO_TIPC:TIPC传输协议
  • 返回值:Socket()调用返回一个整型socket描述符。

备注:socket创建后,内核并没有为socket分配一个端口号,直到调用bind函数。

3.3 bind()

int bind (int sockfd, const struct sockaddr * addr, socklen_t addrlen);

通过socket调用返回一个socket描述符后,在使用socket进行网络传输以前,必须配置该socket。

bind函数将socket与本地网络接口进行绑定,这样socket就可以通过网络进行收发数据。

  • sockfd:是调用socket函数返回的socket描述符。
  • my_addr:是一个指向包含有本机IP地址及端口号等信息的sockaddr类型的指针;
  • addrlen:常被设置为sizeof(struct sockaddr)
// sockaddr:通用数据结构
struct sockaddr 
    unsigned short sa_family; /* 地址族, AF_xxx */
    char sa_data[14]; /* 14 字节的协议地址,为支持的所有协议族保留了最大的地址空间 */
;


// sockaddr:internet数据结构
struct sockaddr_in 
    sa_family_t sin_family;
    in_port_t sin_port;
    struct in_addr sin_addr;
    unsigned char sin_zero[8]; 

sin_port:指定socket绑定的端口号,通过将my_addr.sin_port置为0,函数会自动为你选择一个未占用的端口来使用,也可以自己来指定端口号。1--1023 系统保留端口号,用户应用程序一般不能使用。

sin_addr.s_addr:指定socket绑定的IP网络接口,通过将my_addr.sin_addr.s_addr置为INADDR_ANY,表示设备任意的IP网络接口,当发送数据包时,内核根据路由表选择的接口,自动填充发送接口的IP地址。

备注:

在使用bind函数是需要将sin_port和sin_addr转换成为网络字节优先顺序;

计算机数据存储有两种字节优先顺序:高位字节优先和低位字节优先。Internet上数据以高位字节优先顺序在网络上传输,所以对于在内部是以低位字节优先方式存储数据的机器,在Internet上传输数据时就需要进行转换,否则就会出现数据不一致。下面有几个字节顺序转换函数:

htonl() //把32位值从主机字节序转换成网络字节序
htons() //把16位值从主机字节序转换成网络字节序
ntohl() //把32位值从网络字节序转换成主机字节序
ntohs() //把16位值从网络字节序转换成主机字节序

3.4 listen()  //TCP only

int listen(int sockfd, int backlog);

如果作为一个服务器,在调用socket()、bind()之后就会调用listen()来监听这个socket,如果客户端这时调用connect()发出连接请求,服务器端就会接收到这个请求。socket()函数创建的socket默认是一个主动类型的,listen函数将socket变为被动类型的,等待客户的连接请求

sockfd:第一个参数即为要监听的socket描述字。

backlog:第二个参数为相应socket可以排队的最大连接个数。

返回:0表示成功,-1表示失败。

为了理解其中的backlog参数,我们必须认识到内核为任何一个给定的监听套接字维护两个队列

(1)未完成连接队列,每个这样的SYN分节对应其中的一项:已由某个客户发出并到达服务器,而服务器正在等待完成相应的TCP三路握手过程。这些套接字处于SYN_RCVD状态,如上图。   

(2)已完成连接队列,每个已完成TCP三路握手过程的客户对应其中一项。这些套接字处于ESTABLISHED状态。

下图描绘了监听套接字的这两个队列:

 

3.5 connect()  

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

客户端通过调用connect函数来建立与TCP服务器的连接。

  • sockfd:第一个参数即为客户端的socket描述字,
  • struct sockaddr *addr:第二参数为服务器的socket地址
  • addrlen:第三个参数为socket地址的长度。

备注:

  • 该函数对TCP是必须的,对UDP是可选的。

3.6 accept() //TCP only

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); //返回连接connect_fd

TCP服务器端依次调用socket()、bind()、listen()之后,就会监听指定的socket地址了。

TCP客户端依次调用socket()、connect()之后就向TCP服务器发送了一个连接请求。

TCP服务器监听到这个请求之后,就会调用accept()函数取接收请求,创建一个新socket,用于TCP通信,这样连接就建立好了。

之后就可以开始网络I/O操作了,即类同于普通文件的读写I/O操作。

  • 参数sockfd

参数sockfd就是上面解释中的监听套接字,这个套接字用来监听一个端口,当有一个客户与服务器连接时,它使用这个一个端口号,而此时这个端口号正与这个套接字关联。当然客户不知道套接字这些细节,它只知道一个地址和一个端口号。

  • 参数addr

这是一个结果参数,它用来接受一个返回值,通过这返回值获取客户端的地址,当然这个地址是通过某个地址结构来描述的,用户应该知道这一个什么样的地址结构。如果对客户的地址不感兴趣,那么可以把这个值设置为NULL。

  • 参数len

如同大家所认为的,它也是结果的参数,用来接受上述addr的结构的大小的,它指明addr结构所占有的字节个数。同样的,它也可以被设置为NULL。

如果accept成功返回,则服务器与客户已经正确建立连接了,此时服务器通过accept返回的套接字来完成与客户的通信。

  • 返回:连接connect_fd

3.7 send() //适合connect之后的socket

int send(int sockfd, const void *msg, int len, int flags);

  • sockfd:是你想用来传输数据的socket描述符;
  • msg:是一个指向要发送数据的指针;
  • Len:是以字节为单位的数据的长度;
  • flags:一般情况下置为0(关于该参数的用法可参照man手册)。
  • 返回:Send()函数返回实际上发送出的字节数,可能会少于你希望发送的数据。在程序中应该将send()的返回值与欲发送的字节数进行比较。当send()返回值与len不匹配时,应该对这种情况进行处理。

备注:

适合connect之后的socket,因此,不需要指明对端的地址信息。

3.8 recv() //适合connect之后的socket

int recv(int sockfd,void *buf,int len,unsigned int flags);

  • sockfd:是接受数据的socket描述符;
  • buf: 是存放接收数据的缓冲区;
  • len:是缓冲的长度。
  • Flags:也被置为0。
  • 返回:Recv()返回实际上接收的字节数,当出现错误时,返回-1并置相应的errno值。

适合connect之后的socket,因此,不需要指明对端的地址信息。

3.9  sendto()

int sendto(int sockfd, const void *msg,int len,unsigned int flags,const struct sockaddr *to, int tolen);

该函数比send()函数多了两个参数:

  • to:表示目地主机的IP地址和端口号信息。
  • tolen:常常被赋值为sizeof (struct sockaddr)。

备注:

sendto()和recvfrom()用于在无连接的数据报socket方式下进行数据传输。由于本地socket并没有与远端机器建立连接,所以在发送数据时应指明目的地址。

3.10 recvfrom()

int recvfrom(int sockfd,void *buf,int len,unsigned int flags,struct sockaddr *from,int *fromlen);

该函数比recv多了两个参数:

  • from:表示源主机的IP地址和端口号信息。指明该socket只接收哪些主机的数据。
  • fromlen:常常被赋值为sizeof (struct sockaddr)。

备注:

sendto()和recvfrom()用于在无连接的数据报socket方式下进行数据传输。由于本地socket并没有与远端机器建立连接,所以在发送数据时应指明目的地址。

3.11 单方向关闭socket:shutdown()

int shutdown(int sockfd,int how);

可以调用shutdown()函数来关闭该socket。

该函数允许你只停止在某个方向上的数据传输,而一个方向上的数据传输继续进行。

如你可以关闭某socket的写操作而允许继续在该socket上接受数据,直至读入所有数据。

3.12 全方向关闭socket:close()

close(sockfd);

当所有的数据操作结束以后,你可以调用close()函数来释放该socket,从而停止在该socket上的任何数据操作:

以上是关于[架构之路-62]:目标系统 - 平台软件 - 基础中间件 - Linux Socket网络进程间通信的基本原理与示例(AF_INETAF_UNIXAF_TIPC)的主要内容,如果未能解决你的问题,请参考以下文章

[架构之路-58]:目标系统 - 平台软件 - 中间件软件(嵌入式)与中间件平台(中台)

[架构之路-61]:目标系统 - 平台软件 - 基础中间件 - 远程过程(函数)调用RPC原理与其网络架构

[架构之路-57]:目标系统 - 平台软件 - 用户空间驱动与硬件抽象层HAL

[架构之路-60]:目标系统 - 平台软件 - 基础中间件 - Linux进程间通信的主要方式

[架构之路-59]:目标系统 - 平台软件 - 基础中间件 - POSIX(可移植操作系统接口)与标准库函数libc

[架构之路-63]:目标系统 - 平台软件 - 基础中间件 - TIPC透明进程间通信与AF_TIPC Socket