[架构之路-62]:目标系统 - 平台软件 - 基础中间件 - Linux Socket网络进程间通信的基本原理与示例(AF_INETAF_UNIXAF_TIPC)
Posted 文火冰糖的硅基工坊
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[架构之路-62]:目标系统 - 平台软件 - 基础中间件 - Linux Socket网络进程间通信的基本原理与示例(AF_INETAF_UNIXAF_TIPC)相关的知识,希望对你有一定的参考价值。
目录
3.7 send() //适合connect之后的socket
3.8 recv() //适合connect之后的socket
前言:
Linux进程间原生的进程间通信机制只能解决同一个Linux操作系统管辖之下的多个Linux进程间通信,并不能解决处于不同Linux操作系统两个机器之间的进程间通信。 Linux Socket网络进程间通信正是解决此问题的机制。本文就是从宏观的讲解Linux Socket网络进程间通信的基本原理以及基本示例。关于TCP IP协议栈的工作原理,不在本文范围。
第1章 Linux Socket网络进程间通信概述
1.1 概述
进程通信的概念最初来源于单机系统。由于每个进程都在自己的地址范围内运行,为保证两个相互通信的进程之间既互不干扰又协调一致工作,操作系统为进程通信提供了相应的进程间通信的设施,如:
- UNIX BSD有:管道(pipe)、命名管道(named pipe)软中断信号(signal)
- 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进程间通信的主要方式