引言:
北京时间:2023/8/12/15:32,自前天晚上更新完文章,看了一下鹅厂新出的《扫毒3》摆烂至现在,不知道是长大了,还是近年港片就那样,给我的感觉不是很好,也可能是国内市场对港片不大友好,反正质量不怎么高。然后昨天大部分时间花在了追《我欲封天》这本小说,主要是情节比较耐人寻味,另人欲罢不能!耳根不愧是经典的代名词。再后来到了晚上刷了会视屏,先是看了《饥饿站台》的剪辑,然后又看了一下《饥饿游戏》的剪辑,这种视屏真的是划不走,也是令人欲罢不能,哎!然后刚午睡醒来,为了今天能够更文,现在必须要豁出去了,虽然娱乐了挺久,但是没有关系,就当是对前几天疯狂码字的奖励吧!今天就让我们重拾键盘来一个疯狂星期六吧!该篇博客让我们来学习一下有关套接字(socket)的相关知识吧!
深入学习网络传输基本流程
在上篇博客中,我们重点对网络协议栈中的网络层和数据链路层进行了详解,明白了为什么说路由器和交换机工作于网络层和数据链路层,并且对于IP地址和MAC地址以及数据传输有了一定的认识,所以此时我们再来谈谈相关知识。
回顾封装、解包和分用
在上篇博客中,我们重点讲解了有关网络传输的基本流程,明白当一个数据想要从主机A发送到主机B,因为网络协议栈每一层都有与其对应的协议和需要实现的功能,所以我们需要在数据经过网络协议栈时,将每一层特定的协议封装到数据中,其中就包括报头协议、识别协议和功能协议,所以只有对每一层特定的协议进行了封装,最后在数据向下传递或者向上传递时,才能正确的按照顺序传到每一层协议,并且正确的被每一层协议识别,然后完成在该层协议中对应需要实现的功能,当然这也就是解包和分用的概念,并且注意,当数据在传输层时我们称为报文,在网络层称为数据报,在数据链路层称为数据包,在物理层则称为数据帧。
深入理解MAC地址和IP地址
在上篇博客有关数据链路层和网络层中,我们简单理解了一下MAC地址和IP地址,明白MAC地址是当数据在局域网中传输,交换机对于某网卡的唯一标识符,而IP地址是对于广域网中进行数据传输,路由器对于某主机的唯一标识符。并且明白,因为主机的IP地址是通过路由器向网络中的DHCP协议申请,所以对应路由器的路由表中天生就记录了该主机的IP地址,也就是在广域网中当某个其它主机想要发送数据给目标主机时,只要让与其对应的路由器根据路由表中对应其它路由器的IP地址找到目标路由器就等于是找到了目标主机,当找到目标路由器并且将数据发送之后,注意:虽然目标路由器的路由表中就有对应主机的IP地址,但是IP地址只是起一种标识作用,并不能直接接收数据(只有网卡,也就是MAC地址才具有接收数据的功能),所以此时最关键的问题就是要让路由器获取到目标主机的MAC地址,那么问题就来了路由器如何获取到目标主机的MAC地址呢?首先明白,此时需要分为两种情况,一种是路由器与目标主机之间存在直接联系,另一种是目标主机和路由器之间不存在直接联系,什么意思呢?也就是需要判断该主机是否在路由器的子网中(类比学校主机和自己的笔记本电脑),当我们的笔记本电脑使用WIFI时,此时就表示该主机与路由器之间存在联系,反之学校机房的电脑与路由器不存在联系(是否获取到路由器本身提供的IP地址接口)。情况一:也就是存在联系,那么路由器和目标主机之间就可以直接通过路由器提供的IP地址接口进行交互(同理目标主机获取自己的IP地址),直接将自己的MAC地址告诉路由器,最后路由器直接根据自己的转发表和交互接口将需要传送的数据全部传给目标主机。情况二:反之,路由器和目标主机之间不存在联系,那么此时同理路由器需要先将数据交给交换机,然后通过交换机传送给目标主机,但是,此时同理有一个问题,也就是路由器如何找到交换机呢?同理路由器需要根据自己转发表中目标主机的MAC地址和交换机之间的关系来确定将数据传送给那一个交换机,也就是同理此时路由器需要获取到目标主机的MAC地址,问题也就来了,在路由器和目标主机不存在直接关系的情况下,路由器如何获取到MAC地址?答案是:此时涉及到一个叫ARP的网络协议,很好理解,也就是当某路由器获取到了目标主机的IP地址后,根据ARP网络协议,此时路由器会进行ARP广播,也就是将目标主机的IP地址通过交换机发送给所有与交换机连接的主机,当目标主机发现自己的IP地址与接收到的IP地址相同时,此时目标主机就会进行ARP响应操作,也就是将自己的MAC地址发送给路由器,此时路由器就成功的获取到了目标主机的MAC地址,这也就是ARP网络协议。当我们通过该方法让路由器获取到了目标主机的MAC地址,本质也就是让IP地址的标识作用变得更加实质性(IP地址->MAC地址)之后,此时路由器就可以根据转发表中MAC地址与交换机之间的关系,找到与目标主机关联的目标交换机,从而实现将数据从路由器交给交换机再给网卡。注意:虽然此时路由器也获取到了MAC地址,但是因为路由器和目标主机之间并不存在关系,所以不能直接传送数据,必须经过交换机,与上述情况一有一定区别。明白了这些之后,此时我们对MAC地址和IP地址以及数据传输过程中的细节知识就有了更深的理解,下面我们一起看副图来具体理解一下广域网中(IP地址)数据的传输过程(结合封装和分用理解),如下图所示:
如上图所示,与局域网中的数据传输过程不同,此时在广域网中IP地址是必不可少的部分,结合之前有关封装、解包和分用的知识以及数据链路层的知识,此时我们就可以更加深入的理解为什么数据链路层可以支持多种不同的协议,本质如上图就是因为在数据传输的过程中,每一层协议想要被识别就需要有对应的报头,当我们在发送数据时,在数据链路层是根据以太网协议来封装数据包,那么交换机想要再将数据传送给路由器,那么前提就是它根据以太网协议完成了以太网协议的解包过程,解包完成之后再根据协议进行分用,此时就可以成功的将数据包传送给路由器,此时路由器再经过一些列的查询路由表过程,找到含有目标IP地址的路由器,最后同理当我们想要将数据从路由器传送给目标交换机时,此时就一定需要对目标交换机使用的协议进行封装(以太网协议、令牌环协议、无线LAN协议),只有这样才能使交换机正常工作,所以依据这一原理(封装、分用、解包)就可以很好的解决数据链路层协议不一致问题,当然为什么不一致在上篇博客谈TCP/IP协议栈时,我们有重点强调。所以这也就是为什么网络层和传输层的IP协议以及TCP协议保持一致时,数据链路层可以不保持一致的原因。
注意: 明白上述知识,再加上上述对IP地址和MAC地址的深入理解,此时我们再明白最后一点之后,就可以说是无敌的存在了,也就是为什么说源IP地址和目标IP地址不变,而MAC地址在变,搞明白这个点,就会有一种守得云开见月明的感觉,在上述我们明白了一个很重要的概念:也就是IP地址就是一种标识,MAC地址才是实质性的,原因就在于MAC地址代表的是网卡,只有网卡才具有传输和接收数据的能力,也就是说在广域网中,路由器查询路由表寻找目标IP的过程本质是一个寻找MAC地址的过程,也就是当一个路由器想要将数据传输给下一个路由器(简称:下一跳),这个过程同理我们上述所说路由器如何获取MAC地址,也就是路由器无论是否找到目标路由器,它想要完成下一跳,在第一次时,它一定需要进行一次ARP广播(同理上述所说),然后某个路由器发现它的路由表中存在目标IP地址,那么它就进行ARP响应,将自己的MAC地址发送给对应路由器,最终同理路由器获取到了目标路由器的MAC地址,然后根据该目标MAC地址将数据传送给目标路由器(并且记录IP地址和MAC地址间的映射关系,记录之后下一次传输时,就可以直接找到MAC地址,也就是不需要再进行ARP广播),接下来的过程统统同理。明白这点之后,小小的数据传输过程如砍瓜切菜一般,概念我们就彻底搞定。如下图所示,就是我们云服务器上的IP地址和MAC地址:
什么是套接字(socket)
明白了上述知识,此时我们正式来看一看该篇博客的主题,有关套接字相关的知识,莫慌,我一直强调,想要理解一个陌生的东西,最好的方式就是类比学过的知识,首先明白套接字是在网络协议栈中传输层使用TCP/UDP协议实现的一种网络通信方式,也就是操作系统在传输层提供了一套接口(套接字)供给我们使用,让我们可以实现网络通信(系统调用)。然后因为网络通信的本质是进程间通信,所以套接字和消息队列、共享内存、管道一样,都是一种进程间通信方式,只不过前者不仅可以在系统内部实现进程间通信(本地套接字),也可以实现网络通信(网络套接字),而后者只能用于进程间通信,当然具体为什么网络通信的本质是进程间通信,下述我们详解讲解。
从网络数据传输看套接字
对于上述套接字的关键概念有了一定理解之后,此时我们从套接字是一种网络通信,而网络通信是一种进程间通信来深入理解一下有关套接字的知识。首先,此时我们对网络数据传输过程已经如火纯情,对于数据从主机A到主机B的过程了如指掌,那么明白数据从A到B不是我们的目的,我们的目的是让主机B在接收到数据之后可以去访问某种服务,然后将对应的服务返回给主机A,并且根据网络协议栈的知识,我们知道这个申请服务和访问服务的过程一定是在应用层实现,所以此时就有了客户端和服务端(服务器)的概念,也就是两个主机之间的网络间通信本质就是客户端(APP)和服务端(该APP的功能实现)之间的通信,明白了这点之后,此时对于网络通信就是进程间通信的概念可以说是更加明了了,但还不够,我们还可以知道,当我们需要通过某个客户端(APP)去获取某个功能时,这个操作是随时随地都能完成的,这也就说明我们访问的服务一定是被启动,并且一直处于运行状态的,所以明白了这两点,我们就可以把客户端和服务端理解成两个进程,当我们访问某个客户端(APP)就是在启动某个进程,而服务端进程是一个被启动的死循环进程。总而言之:网络通信就是进程间通信。
深入理解网络通信
明白了上述知识,对于套接字就是一种网络间通信,一种进程间通信的概念我们已然耳聪目明,呼之欲出,此时对于数据从主机A到主机B的过程我们就可以深入理解为主机A创建一个进程(应用层打开APP),然后该进程执行相应的代码(创建套接字等),再根据TCP/UDP协议以及套接字(与主机B建立联系),此时数据就被传送到主机B,然后主机B经过物理层、数据链路层、网络层到达传输层,在传输层上套接字就可以将对应的数据推送给上层指定的进程。
此时对于上述这段话我们还不能很好理解,当然有关数据在网络层封装IP地址以及路由器、交换机相关的知识我们不再强调,此时我们需要搞定的问题是:套接字如何将数据推送给上层指定的进程以及套接字的本质是什么?第一个问题:也就是在系统中有那么多的进程,套接字如何找到目标进程呢?明白,同理IP地址,当某主机想要实现网络通信,那么前提就一定是它获取到了目标主机的IP以及目标进程的标识,因为网络通信的本质就是进程间通信,所以此时只要对某主机和某进程进行的标识,那么在整个互联网中,我们就能找到那个唯一的服务端进程,从而顺利完成两个进程间的通信。明白这点,端口号的概念油然而生,端口号就是用来标识那个网络中唯一的进程,所以对于使用源IP、源端口号、目标IP、目标端口号实现的进程间通信方式,我们就称为套接字(socket)网络通信。所以也就是套接字可以直接根据端口号找到目标进程,然后将对应数据拷贝到该进程的缓冲区中,注意:操作系统内部存在许多进程标识符(PID)用于管理进程,但是在网络通信中想要识别一个进程我们只使用端口号,这样可以降低进程管理模块和网络模块之间的耦合。
深入理解接收和发送数据
搞定了上述知识,此时我们对网络通信有了更深的认识,明白网络通信就是进程间通信,明白进程间通信需要使用目标IP和目标端口号来标识两个唯一的进程,明白socket通信方式,当然具体socket如何实现这种通信方式,下述和下一篇博客我们详谈,本质就是各种socket接口的使用,所以socket网络通信本质其实也叫socket编程接口。搞定这些之后,此时我们来看看有关网络通信的最后一个知识点,进程接收和发送数据的具体过程: 首先是接收,在之前我们将网络协议栈与操作系统结合那块知识我们谈到过网络协议栈是和文件系统有关的,这块知识充分体现,也就是目标进程启动时,用户可以在该进程中打开一个文件,文件被打开之后,根据文件系统的知识,此时操作系统就会为其创建struct file结构体,其中就包含了该文件的inode和缓冲区等信息,然后当目标进程对应的套接字接收到数据之后,此时该套接字就会根据TCP协议和有效载荷(数据)找到目标进程的端口号,然后该套接字根据目标进程对应打开文件的文件描述符找到被打开文件,最终将自己缓冲区中的数据拷贝到该文件缓冲区中。其次是发送:同理,创建进程(启动APP),在该进程中打开文件,将数据写入到该文件的缓冲区中,再经过操作系统将该缓冲区中的数据拷贝到与该进程对应的套接字缓冲区中,然后套接字经过网络协议栈将数据发送给目标主机。通过上述方式,进程就可以很好的实现如何凭借文件进行网络数据的接收和发送。
认识TCP协议和UDP协议
明白了上述有关socket网络通信的知识,此时我们来简单认识一下TCP和UDP协议,因为这两种协议都是传输层协议,且套接字(socket)就是工作在传输层,简而言之也就是我们可以利用不同的协议来实现socket网络通信,下述就是有关TCP和UDP之间的特点,也就是不同,如下所述:
1.TCP
TCP协议是一种传输层协议,也叫作传输控制协议,是一种面向连接、面向字节流的可靠性协议,怎么理解呢?面向连接也就是说在数据发送之前,发送方和接收方需要提前建立一个联系,而面向字节流指的是该协议对于数据的读取/发送没有控制,想读/发多少就读多少,而可靠性表示的是使用TCP协议该协议就会保证对应的数据被接收方接收,如果中途失败,那么TCP协议就会进行重传等操作,反正就是通过各种手段来确保数据传输成功(数据分段、流量控制、拥塞机制等)。
2.UDP
同理UDP协议也是一种传输层协议,也叫作用户数据报协议,是一种无连接、面向数据报等待不可靠协议,同理TCP理解,此时对于UDP的理解就比较简单,UDP协议在发送数据之前不需要建立发送方和接收方的联系,并且在读取/发送数据时必须完整的读取/发送,而不可靠性表示的也就是发送方将数据发送之后,不再关心接收方是否接收到数据。
总结来说:TCP适用于对数据传输可靠性要求较高的场景,而UDP适用于对实时性要求较高、对数据传输可靠性要求较低的场景。【想要进一步理解可参考该链接:深入学习TCP/UDP】
网络字节序
认识了TCP和UDP之后,此时我们对于socket编程就又进了一步,以后我们肯定会对TCP和UDP协议各自的特点进行区分,然后实现不同的socket网络通信,也就是TCP网络通信和UDP网络通信,不过在此之前我们需要理解一下网络字节序相关的知识,该知识本质是为了解决有关数据使用大端存储还是小端存储的问题,如下图所示:
如上图所示,此时我们明白,在计算机中进行多字节存储的时候,由于体系结构的原因,此时不仅支持大端存储,也支持小端存储,所以如果一台大端存储的机器和一台小端存储的机器之间进行网络通信,那么此时就会出现数据被反向存储的问题,最终影响数据正确性,所以为了解决该问题,此时就提出了网络字节序的概念,规定网络字节序列必须是大端,所以在网络中获取到的数据就一定是大端存储,也就是如果发送方是一个小端存储机器,那么它在网络通信数据传输时,就需要将数据从小端转换成大端,同理接收方如果也是小端,那么它在接收数据时,就需要将数据从大端转换成小端,这也就是网络字节序的概念。
注意: 如果我们的主机也是小端,那么我们在用户层编写代码时,也需要完成小端转大端的工作,所以此时操作系统为我们提供了一些接口,其中包括:htonl、ntohl(32位)
/htons、ntohs(16位)
(主机->网络/网络->主机),具体使用以后详谈。
认识socket编程接口
功能 | 对应接口 |
---|---|
创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器) | int socket(int domain, int type, int protocol); |
绑定端口号 (TCP/UDP, 服务器) | int bind(int socket, const struct sockaddr *address, socklen_t address_len); |
开始监听socket (TCP, 服务器) | int listen(int socket, int backlog); |
接收请求 (TCP, 服务器) | int accept(int socket, struct sockaddr* address, socklen_t* address_len); |
建立连接 (TCP, 客户端) | int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); |
(上述接口下篇博客我们重点介绍以及使用)