目录
计算机网络背景

在没有网络之前,计算机B需要计算机A的数据时,是计算机A将数据保存在软盘中,通过软盘拷贝给计算机B。这是十分缓慢的。并且,此时各个计算机之间是互相独立的。

可以再弄一台计算机充当服务器,当计算机A处理完数据后,将这些数据推送到服务器上,计算机B和计算机C就可以从服务器中获取数据,处理完后的数据再推送到服务器上。此时就是将多台计算机连接到了一起,实现了数据共享。没有了人的参与,效率就高了。

随着互联网产生,有了更多的通信需求,就诞生了很多通信公司,研发出了很多通信设备,像路由器、交换机就是通信设备。在一个局域网内部,可以使用交换机连接;两个局域网可以通过路由器连接,使两个局域网之间能够通信。
刚开始的只是局域网。局域网是有问题的,因为只允许在一个局域内通信,太远的距离是无法通信的,所以就诞生了广域网。局域网也叫私网,广域网也叫公网。

结论:网络的诞生,使计算机之间能够互相协作。计算机是服务于人的,而人是互相协作的,所以,计算机之间互相协作是必然的。
初识协议
网络协议
在初识协议部分只是对协议做一个简单的介绍,让你对协议有一个初步的认知。

两台计算机要进行通信,就必须有一套约定。这个约定分为很多的层级,有些计算机在识别光电信号时,使用的是光电信号的强弱,有些使用的是有无,等等。就是根据光电信号的强弱、有无等,弄成一个0和1的二进制。像计算机、手机、平板等具有计算机属性的设备,在硬件层面上都必须先做好约定。所以,一个计算机体系要基于网络搭建起来,就需要先保证在硬件层面上,双方使用的是同一套约定。
但是,只有硬件上是不够的,计算机上会有非常多的软件,这些软件也必须是一套约定。如大端计算机和小端计算机通信,就需要事先约定;两台计算机的显卡、网卡等供应商不同,OS的版本也不同,如何保证它们遵守一样的规则?如一台收到的数据从低字节开始放,一台从高字节开始放,一台做4字节对齐,一台做8字节对齐。
所以,两台设备要进行通信时,需要非常多的约定,包括硬件和软件,不是只规定好01即可,像数据的编码格式、类型大小、存储方式等,都需要有约定。此时,就要求计算机内部存在非常多的协议。每个协议,都会有非常细致的规则,协议必须由个人、组织或公司来专门定制这个邻域的协议
如何让这些不同厂商之间生产的计算机能够相互顺畅的通信?京就需要有人站出来,约定一个共同的标准,大家都来遵守,这个标准就是网络协议。
实际上,在计算机内部,CPU和内存存在协议,内存与磁盘存在协议,磁盘也有自己的协议。以磁盘为例,我们之前说过,我们要从磁盘读取,或者向磁盘写入时,交给磁盘一个LBA地址,磁盘就能找到对应的扇区,从而进行对应的操作。磁盘是如何找到发过来的二进制序列,意思是什么?如是读,还是写?是读到内存,还是读到哪里?等等。在计算机中,往往需要由工程师为磁盘提供一个驱动程序,这个驱动程序就是使用磁盘的协议的。驱动程序接收到OS的信息后,会将这些信息整合成一个固定大小的二进制序列交给磁盘,如这个二进制序列可能有64个0和1,前2个位代表读或写,中间32个位表示若是读,要读到哪一一个地址,剩下的位就代表的是LBA地址。这就是协议。所以,不谈网络,单纯在计算机体系当中,协议也是无处不在的。
所以,网络协议只是计算机众多协议中的一个。网络协议就是专门为网络通信设计的约定。网络协议是一个统称,是由多个协议组成的。
协议分层
之前在学习OS时,我们知道,计算机的整个体系结构就是分层的。而协议本质也是软件,在设计上为了更好的进行模块化,解耦合,也是被设计成为层状结构的。软件是分层的。一个软件,横向是成模块的,纵向就是分层的。在面向过程的程序中,将所有的代码都放在main中,这个软件就是没有分层的;现在将main中的部分弄成函数,main函数去调用这些函数,main函数就是顶层调用,这就是软件分层。基本上所有的计算机语言都有封装、继承、多态,封装、继承、多态就是支持软件分层的方式之一。另外,现代计算机语言都会提供容器,并且是面向对象的,因为要先描述,再组织。在计算机世界中,任何问题都可以通过新增一层软件层来解决。
软件分层的好处

当两个人直接使用汉语进行对话时,遵守的是汉语协议。当两个人通过电话进行对话时,并不是两个人直接对话,此时是分两层的。一个人说的话是说给电话的,电话中会有录音设备,并会根据通信协议将采集到的声音编码、加密、打包,完成后,再通过电话线发送给另一台电话,这一台电话就会根据通信协议,进行解包、解密、解码,将声音从听筒中放出。站在打电话的人的角度,人会认为是自己直接与对方通信的,因为会忽略底层给我们的支持。
学习网络时,会有两个视角:
- 小白视角:同层协议,直接通信
- 工程师视角:同层协议,没有直接通信,是各自使用下层提供的结构能力,完成的通信
高内聚是在同一层,关联度特别高;低耦合是层与层之间的关联度特别低。分层是解耦合的有效方式,程序的耦合度低了,可维护性就提高了。所谓可维护性,就是增强了代码的可读性,并且当代码出错时,更容易排查和修改。类就是高内聚的表现,类与类之间通过接口互相调用就是低耦合的表现。当层与层之间只有接口的调用和被调用关系,此时是最好的。像上面的图,通信设备层改变了,并不影响语言层;语言层改变了,同样不影响通信设备层。
在这个例子中,我们的"协议"只有两层:语言层、通信设备层。但是实际的网络通信协议,设计的会更加复杂,需要分更多的层。但是通过上面的简单例子,我们是能理解,分层可以实现解耦合,让软件维护的成本更低。
当两个人在打电话时,会有一个习惯,接通电话后,在说正式的内容前,会"喂",以确保对方是否能够听得到,并且对方听到"喂"后,也会"喂"一声,双方都能听到后,就开始正式的通信了。这两声"喂"实际上也是通信的约定,因为双方都懂这个"喂"的含义。这也是一种协议,这两声"喂",在通信层面,可以称之为通信握手。只有握手成功之后,双方再进行通信。
结论:
- 协议是一种约定
- 有了协议之后,软件在设计上就是分层的
OSI七层模型

OSI模型一共有7层。但是,它既复杂又不实用,所以实际工程中一般只实现其中的4层;所以我们按照TCP/IP四层模型来讲解。
为什么标准有七层,而真正实现只有四层呢?
定义标准的人和未来写代码实现的人不一定是同一批人。所以,实现未必与标准完全相同。当OSI标准定义出来后,并公开让别人都可以免费使用,Windows、Linux、华为的手机等都想接入互联网,Windows就会组织内部的工程师依据OSI标准去写代码,在Windows内将网络实现,Linux等也是一样。虽然Windows使用的是Windows的网络,Linux使用的是Linux的网络,但是它们遵守的都是同一套协议,这就是Windows与Linux,Windows与手机之间能够互相通信的原因。这些工程师在实现网络的时候,发现这个OSI标准太复杂了,并且不是很实用,所以,只设计了4层。
TCP/IP五层(或四层)模型
刚刚说过,在计算机邻域,是有海量的协议的,网络协议是由非常多的协议构成的 。TCP/IP协议是一组协议的代名词,其中包括了非常多的协议,组成了TCP/IP协议族。TCP/IP通讯协议采用了5层的层级结构(实际实现时只有4层),每一层都呼叫它的下一层所提供的网络来完成自己的需求
物理层:负责光/电信号的传递方式.比如现在以太网通用的网线(双绞线)、早期以太网采用的的同轴电缆(现在主要用于有线电视)、光纤,现在的wifi无线网使用电磁波等都属于物理层的概念。物理层的能力决定了最大传输速率、传输距离、抗干扰性等。集线器(Hub)、网卡工作在物理层。
集线器:在物理层,数据是以光电信号的形式传递的,以前是通过电缆传递光电信号,现在是光纤。在物理设备上传播时,光电信号是会衰减的,因为会衰减,就必然导致了没办法进行远距离传输,为了进行远距离传输,就设计出了集线器,用于放大信号。
数据链路层:负责设备之间的数据帧的传送和识别.例如网卡设备的驱动、帧同步(就是说从网线上检测到什么信号算作新帧的开始)、冲突检测(如果检测到冲突就自动重发)、数据差错校验等工作.有以太网、令牌环网,无线LAN等标准。交换机(Switch)工作在数据链路层。主要是用来进行局域网通信的。在家里,我们可以通过路由器获得局域网,家里可能会有几部手机、几台电脑,当一个局域网中主机数过多的话,就需要有一个设备交换机。后序再介绍交换机的原理。
网络层 :负责地址管理和路由选择.例如在IP协议中,通过IP地址来标识一台主机,并通过路由表的方式规划出两台主机之间的数据传输的线路(路由).路由器(Router)工作在网路层。在网络层,有一个最典型的设备就是路由器。网络层主要是将数据通过网络从一端送到另一端,需要配合IP地址,就可以支持数据包进行长距离转化了。也就是说,链路层主要解决局域网转化的问题;网络层主要解决长距离转化的问题。
传输层:负责两台主机之间的数据传输.如传输控制协议(TCP),能够确保数据可靠的从源主机发送到目标主机。从传输层开始,就不需要有设备与之对应了。而是由两端的主机、OS与之对应。
应用层:负责应用程序间沟通,如简单电子邮件传输(SMTP)、文件传输协议(FTP)、网络远程访问协议(Telnet)等.我们的网络编程主要就是针对应用层。
总结:TCP/IP模型将OSI模型的上三层压缩成了一层,应用层,下四层是一一对应的,导致7层变成了5层。我们写代码时,是在应用层写代码的。
再识协议
为什么要有TCP/IP协议?
首先,即便是单机,你的计算机内部,其实都是存在协议的。我们知道,每一个计算机都是一个冯诺依曼体系结构,里面会有输入设备、输出设备、CPU、内存等,这些设备是通过系统总线连接的,并且它们之间也有各自的协议,系统总线是线,网线也是线,所以,冯诺依曼体系结构就是网络。在单机情况下,我们从来不会说计算机内存有网络,因为在计算机内部,这些设备距离非常近,基本上只有几厘米的距离。但是,两台主机之间,相隔的可能就非常远了。当两台主机距离很近,通信是容易的,但是相隔很远时,此时就会有非常多的问题了。

有了问题,就需要解决问题,否则就无法进行远距离通信。第一个问题,主机A将数据发送到路由器上,是局域网的问题。第二个问题,要找到主机C,是目标主机定位的问题。第三个问题,数据丢失是丢包重查的问题。这三个问题,是不同性质、不同种类的。对于每一个问题,都应该有一个解决方案。实际上,一种解决方案对应的就是一种协议。因为问题的性质不一样,所以协议是需要分层的。第一个问题对应的是物理层和链路层,第二个问题对应的是网络层,第三个问题对应的是传输层。当我们解决了1、2、3这三个问题后,两台主机间就能够互相通信了。也就是主机C能够拿到主机A的数据了。但是发送数据只是手段,使用数据才是目的。主机C要如何使用数据呢?这就是应用层的问题了。
结论:存在网络的终极原因就是传输数据时,两个主机的距离变远了。因为数据传输距离变远了,就会导致一些问题。而TCP/IP协议就是这些问题的解决方案。
什么是TCP/IP协议?
TCP/IP协议的本质是一种解决方案。
TCP/IP协议能分层,前提是因为问题们本身能分层。
重谈协议
网络与OS的关系
注意:网络是有标准的,即网络协议。但是OS是没有标准的,只有一些共同的理论,Linux和Windows具体实现是不一样的。

网卡就是底层硬件;数据链路层属于驱动程序,所以数据链路层一般都在网卡驱动内集成着;网络层和传输层被实现在OS内核中;应用层在OS上层,由用户实现。
所以,Linux操作系统的实现者在实现时,需要根据TCP/IP协议或OSI模型去设计自己网络部分的代码,Windows也是一样。因为是基于一套标准的,所以一定能通过网络进行通信。上图中,两个操作系统是不同的,但是它们的网络协议栈是根据TCP/IP协议设计出来的。也正是因此,两台不同OS的主机才可以通信。所以,系统的层状结构几乎与网络协议栈的层状结构是对应的。
网络与OS的关系,结论:OS内需要实现网络相关的功能,可以理解成网络是OS的一个模块或一部分,因为这个模块所有OS都一样,所以我们不将其纳入到OS中讲解,而是单独拿出来讲解。
什么是协议?
应用层、链路层、网卡先不管。所以,往后我们说TCP/IP协议主要指的是传输层和网络层。传输层和网络层是实现在内核中的,这两层中都会有大量的协议,所以OS内是有大量协议的。所以,OS需要管理这些协议,管理方式就是先描述,再组织。所以,协议就是两个OS为了能够进行网络通信,而约定出来的数据结构 ,一般就是一个结构体。所谓协议,就是通信双方都认识的结构化的数据类型。

OS是C语言写的,而传输层、网络层是实现在OS内核中的,所以TCP/IP网络是C语言写的。Windows和Linux的源代码是不一样的,但是网络部分的代码一定是一样的 。既然网络部分的代码是一样的,那么Windows定义一个结构体,发送给Linux后,Linux系统一定是看得懂的。一端规定好每个字段的含义,另一端能够看得懂,这就是双方的约定 。所以,协议实际上就是结构体。未来我们在学习各种具体的协议时,就是在学各个结构体里有什么字段,这个字段的含义是什么。
以一个例子对上面两个问题进行总结。我们在网上购物时,假设买了一包泡面,我们买的东西是一包泡面,但是我们真正拿到快递时,是使用一个箱子装着的,并且箱子上还会有一张快递单,也就是说我们拿到的东西,会比我们买的东西多一些东西,重点就在这个快递单。这个快递单就是一个结构体,它是由发货的卖家填写的,并且卖家和买家都是可以看得懂的。这个结构体就是协议。买的是泡面,发的是快递单+泡面,这个过程叫做封装。这个快递单称为协议报头。在网络通信中也是一样的,现在要给对方发一个"hello",实际发送的东西会在"hello"前多带一个东西,这个东西就是一个结构体变量,称为协议报头。"hello"是实际数据内存,称为报文。这个协议报头发送方和接收方都是认识的,此时就可以进行报文的解析和读取的。
注意:报文并不放在报头中,而是报头在前,报文在后的。
网络传输基本流程
局域网传输流程
局域网分为多种,以太网就是其中一种。我们以以太网为例。
当两台主机都连接同一台路由器的网络或同一部手机的热点时,这两台主机就是处于同一个局域网之下的。处于同一个局域网之下的两台主机是可以直接通信的。前面主机A和主机C通信的第一步,就是主机A先将数据发送给路由器,而路由器和主机A就是处于同一个局域网之下的,所以处于同一个局域网之下的两台主机是可以直接通信的。有了这个认识,我们就可以知道,现在会有很多小的局域网,处于各自局域网之下的主机可以互相通信,要解决的问题就是如何让这些局域网互相通信呢?路由器就具有这个功能,当然,这个是后面说的。
局域网通信原理 ,我们以一个例子介绍。当老师和同学在一间教室上课时,老师点名让一个同学回答问题,老师说这句话时,班级里面所有的同学都能够听得见,听见之后会从这句话中提取出名字,然后跟自己的名字进行对比,发现不是就不响应,若是,则响应,也就是站起来回答问题,回答问题也是同样如此,这名同学是对着老师回答的,但是全班同学也都能够听到。在这个过程中,老师认为他是直接和同学通信的,被点名同学也认为他是直接和老师通信的。在这个例子中,整间教室就是一个大的局域网,老师就是主机A,被点名同学就是主机B。局域网通信的原理就是局域网中一台主机发出的信息,在这个局域网之下的所有主机都能够收到,只不过每台主机都需要有一个标识自身唯一性的地址,这个地址称为MAC地址。MAC地址是一个48比特位,即6字节,网卡出厂时就已经内置好的序列值。一台主机发送信息时,会将自己的MAC地址带上,同时会将目标主机的MAC地址带上,这两个MAC地址是放在协议报头中的。当主机收到信息后,会获取里面目标主机的MAC地址,并与自己的MAC地址比较,若不同就将这条信息丢弃掉,若相同,则接收。
对于MAC地址,后序谈到数据链路层时,会详谈MAC帧协议,此处只是知道有MAC地址这个东西即可。实际上,很多设备都有自己的唯一值,包括磁盘、内存,这个唯一值也称为序列号。但是
MAC地址比较特殊,因为网络通信时需要他。我们通过指令查看我们的主机的MAC地址。

当然,在Linux下查到的是云服务器的MAC地址。在Windows下是ipconfig /all


这里虽然看到的是主机A、B...,但是要知道其实是一个一个的网络协议栈在通信。这就与之前学习OS时,每一台计算机都看成冯诺依曼体系结构是一样的。因为当前是以太网通信,所以我们只考虑网络协议栈的下两层即可,即数据链路层和网卡。发消息时,一定是用户想让主机A发消息,是用户层的请求,这个请求一定会贯穿网络协议栈走到最下面,然后通过硬件发送出去。主机A将消息放到网络中后,其他主机收到消息一定是硬件先收到消息。其他主机收到消息后,就会在识别目标主机的MAC地址,这个识别工作是数据链路层协做的。若不是目标主机,直接丢弃,若是,则将这条信息向上传递,直到到达用户层。所以,主机识别到信息不是自己的,将信息丢弃的行为上层是不知道的。主机E给主机A回消息也是类似的。
回到刚刚老师上课点名的例子。假设老师提问A同学,同时,B同学对C同学说话,C同学对D同学说话..,导致的结果是每个人都无法正常通信。所以,局域网通信,任何时刻只允许存在一份有效的信息在局域网当中 。当主机A和主机B都发送消息,就会发生了数据碰撞 。为了避免数据碰撞的发生,任何一台主机在发送消息之后,都会进行碰撞检测和碰撞避免 。主机A发送了消息,自己也是可以收到的,当主机A发现发生了数据碰撞,主机A就会休眠一会,过一会将消息重发,主机A发现自己的消息发生了冲突,这个过程叫做碰撞检测。过一会再重发一次,这个过程叫做碰撞避免。当主机A和主机B发生数据碰撞时,两台主机都要进形碰撞检测和碰撞避免。两台主机都会休眠一会,过一会再重新发。此时局域网中的消息就变少了,其他主机就可以在它们休眠期间进行通信了。一个局域网也叫一个碰撞域。
在一个局域网中,主机怎么知道发生了碰撞呢?
主机获取数据是通过网卡不断地从局域网中获取数据。首先,我们要知道网卡怎么知道局域网上有数据。这是纯硬件的。发送数据就会有高电压。当没有发送数据时,所有网卡的电压和局域网的电压是一样的,一发送数据,局域网的电压就高了,网络设计时,就可以让网卡接收这个高电压。实际上,CPU和内存互相写数据时,也是一个道理,它们通过系统总线相连,系统总线中也是有高低电压的,电压永远都是从电压大的往电压低的跑。当发生数据碰撞了,电压就更高了。可以给网卡设置一个阈值,当超过阈值了,就代表发生碰撞了。
所有主机都可以访问局域网,所以局域网就是一个共享资源,发生了数据碰撞就是数据不一致问题。我们进行碰撞检测和碰撞避免本质就是在保证局域网唯一被使用。所以,我们可以将局域网当成一个临界资源。所有丰机是以互斥的形式访问局域网的,只是这个互斥比较特殊,什么都不管直接发,数据碰撞了再处理。在一个局域网之中,当主机数越多,发发生数据碰撞的概率越高。此时就会造成网速低的现象。
同层之间,都认为自己在和对方同层协议互相通信:

报文=报头+有效载荷。在应用层,报头是应用层报头,有效载荷是"你好";在传输层,报头是传输层报头,有效载荷是应用层报头+"你好"。在其中一层,就只能识别这一层的报头,所以拿到数据后,会将这一层的报头与有效载荷分离,这个分离的过程称为解包 。之前添加报头的过程称为封装。每一层都会有非常多的协议,数据链路层将数据链路报头分离之后,如何知道将剩下的有效载荷交给网络层的哪一个协议呢?
报头的共性:
- 报头与有效载荷分离的问题。也就是说报头一定需要有一些字段,能够帮助进行报头和有效载荷的分离。
- 报头内部,必须包含一个字段,叫做交给上层的谁的字段---分用。假设发送数据是从网络层的IP协议交付给数据链路层的,那么数据链路层的报头里面就需要有一个字段IP。当接收数据的主机分离出数据链路报头后,就会进行解析,识别到IP后,就会将其交给网络层中具体的IP这一层。这个过程称之为分用。
实际上,我们在每一层对报文的叫法是不同的

- 为什么数据在发送之前要进行封装?
我们要向网络中发送数据,必须通过网卡,而网卡是硬件。用户要将数据写到网卡中,就必须通过OS,因为OS是软硬件的管理者,而用户要访问OS,就必须通过系统调用。所以用户想要将数据写到网卡当中,就必须贯穿整个软件结构,从系统调用 -> OS内核 -> 驱动程序 -> 硬件,才能将数据写到网卡上。所以,之所以要进行封装,是由OS的结构决定的。
2.接收到信息的主机,信息自底向上交付的过程一定是通过软件来做的,也就是通过OS来做的。接收信息的主机的OS怎么知道自己的网卡上有数据了呢?中断。然后OS执行中断向量表中的内容,将网卡中的数据搬到OS中,然后进行解包、分用,并一层一层向上交付。
各层的主要协议:

两台计算机通过TCP/IP协议通讯的过程如下所示

跨网络传输流程
要想让处于不同局域网之下的两台主机互相通信,就需要将数据包进行跨网络传输了。先来两个预备工作:了解IP地址、了解以太网与令牌环网。
网络中的地址管理 - 认识IP地址
IP 协议有两个版本, IPv4 和IPv6。我们整个的文章,凡是提到IP 协议,没有特殊说明的,默认都是指IPv4
- IP地址是在IP协议中,用来标识网络中不同主机的地址;
- 对于IPv4来说,IP地址是一个4字节,32位的整数;
- 我们通常也使用"点分十进制"的字符串表示IP地址,例如192.168.0.1;用点分割的每一个数字表示一个字节,范围是0-255;
可以通过ifconfig来查看主机的IP地址,因为我们使用的是云服务器,实际上是云服务器的IP地址

IP地址会通过"."分成4个部分,每个部分的取值范围都是0-255。这种IP地址表示方案称为"点分十进制"。可是查询到的这个IP地址为什么与登录云服务器时的IP地址不同呢?登录云服务器的IP地址是公网IP,查询到的是内网/私有IP。后序会介绍。
主机间要跨网络传输数据,数据从一台主机,经过若干台路由器传输到另一台主机,就会涉及到非常多的IP地址。
MAC地址和IP地址都可以标识主机唯一性,它们有什么区别呢?
举一个例子帮助理解。我们都知道《西游记》。唐僧的终极目标是从东土大唐到西天。假设唐僧当前刚到达车迟国,那么就会去询问车迟国国王,想要到达西天,下一站应该去哪里,车迟国国王回答说女儿国,唐僧下一站就会前往女儿国。此时唐僧心里面会有2个地址,第一个地址是从东土大唐到西天,第二个地址是从车迟国到女儿国。唐僧到达女儿国之后,会去询问女儿国国王,想要到达西天,下一站应该去哪里,女儿国国王回答说祭赛国,唐僧下一站就会前往祭赛国。此时唐僧心里会有2个地址,第一个地址是从东土大唐到西天,第二个地址是从女儿国到祭赛国。可以看到,在唐僧的心中,永远有2套地址。唐僧从东土大唐到西天的过程当中,会经过非常多的中间站点,而第一个地址是不会发生变化的,而第二个地址是随着站点的变化而变化的。
- 东土大唐(源IP地址) -> 西天(目的IP地址) 不变
- 车迟国(源MAC地址) -> 女儿国(目的MAC地址) 每经过一座城池就改变一次
这一座一座的城池就是一个一个的路由器。城池的国王就是路由算法,询问车迟国国王要怎么走,这叫做查找路由表,执行路由算法。路由就是做路径选择。而路由的依据是目的IP地址。也就是说,下一次到达哪一个路由器,是看目的IP地址的。车迟国和女儿国离得近的意思是位于同一个局域网。所以,数据跨网络传输的本质就是将数据从一个局域网的一台设备,交给另一台设备,这另一台设备再交给和他相邻的其他设备,局域网或者子网之间消息不断做转发,无数个子网串联起来,最终构成了一个大的网络拓扑结构,数据长距离传输就是经过一个一个子网进行传输的。
唐僧当前位于车迟国,既然知道目的地是西天,为什么要询问车迟国国王下一站到哪里呢?因为局域网通信需要MAC地址。
唐僧每到达一个国家,这个国家的国王只会关心唐僧从哪里来,到哪里去,也就是从东土大唐来到西天去。不会关心上一站是哪里。意思是,一般而言,MAC地址只在局域网中有效。IP地址一般是不会变化的。
结论:MAC地址通常用来标识一台主机在局域网当中的唯一性;IP地址通常用来标识报文在网络当中的唯一性。
以太网与令牌环网

我们要知道,A主机所在的局域网中会有多台主机,B主机也是一样。局域网通信的标准中有两种,大部分情况下使用的都是以太网,所以,以太网就是一种局域网通信标准。上面我们介绍的有碰撞检测、碰撞避免算法的局域网就是以太网。在局域网中还存在另一个网络叫做令牌环网。以太网是主机想发送数据直接发,若碰撞了,就休眠,没有碰撞就发送成功了;令牌环网是只有拿到令牌的主机才能够发送数据,把一段特定格式的数据当成令牌,持有令牌的主机才能够发送数据。收消息是不需要令牌的。
A主机所处的局域网是以太网,B主机所处的局域网是令牌环网,它们的工作原理是不一样的,但是它们的网卡可能是相同或类似的。它们的驱动程序不一样,控制网卡发出去的消息不一样。A主机和B主机处于不同的局域网,它们要进行通信就必须要有路由器。路由器是工作在网络层的一个设备。在主机A看来,路由器就是一台主机,在主机B看来,路由器也是一台主机。因为主机A、B都有网络层,所以主机A、B也有路由功能。因为这个路由器横跨两个局域网,所以他要有两个驱动程序、两张网卡。主机A向网卡1发消息,主机B向网卡2发消息。所以,路由器要横跨两个网络,就必须要有两个局域网的接口,以及连接两端网络的驱动程序。
有了这两个概念铺垫后,来看看数据跨网络传输的流程。

主机A想将数据交给主机B,就必须先将数据交给路由器,交给路由器的过程实际上就是一个局域网通信的过程。
主机A为什么将数据交给路由器呢?主机A、B都会有自己的IP地址和MAC地址,路由器会有自己的IP地址,主机A是有网络层的,所以有路由功能。在同一个局域网当中,设备的IP地址的前缀是一样的。当数据到达主机A的网络层时,就会对检测目标IP地址,并与自己的IP地址比较,若前缀不同,就知道了目标主机与自己一定不属于同一个网段或者说局域网,此时就将报文交给自己所处局域网的路由器。所以,路由器就是局域网的出口。
主机A要将数据发给路由器,就需要知道路由器的MAC地址,主机A怎么知道路由器的MAC地址呢?主机A要连接局域网,就必须先访问路由器,所以,路由器的MAC地址、IP地址主机A是知道的。实际上,主机的IP地址都是路由器分配的。
在网络层只是决定要将数据发送给路由器,实际上是数据是网络层->数据链路层->主机A网卡->路由器网卡。所以,主机A向路由器发消息就是一次局域网通信。也就是同样会有封装和解包。

路由器拿到主机A发送过来的数据时,一定是网卡先拿到数据,然后会对数据进行解包和分用。路由器的网络层就可以拿到网络层的报头,从而知道了目标主机的IP地址,如果这台路由器认识这个IP地址,直接发送到这台主机;若不认识,可以继续发往下一台主机。在上图中,这台路由器连接了两个局域网,而目标主机就是另一个局域网当中的主机,所以路由器是认识的。路由器就会将数据向下封装,然后通过网卡发给主机B。主机B的网卡收到报文后,主机B的数据链路层就会判断MAC地址,发现就是发给自已的,向上分用、解包,到了网络层,发现目标IP地址就是自己,所以,继续向上解包、分用,就可以拿到数据了。
可以看到,在跨网络通信中,所有主机、路由器拿到的网络层报文是相同的,而链路层的报文是各不相同的。这正是MAC地址只在局域网中有效的证明。实际上,网络层之上的报文也是不变的。
是先有局域网,再有广域网。所以,局域网的通信方式一定是多样化的,如以太网、令牌环网、无线网。这些通信方式不同的局域网要如何进行通信呢?我们之前说过,计算机是层状结构的,任何软件问题都可以通过新增一层软件层来解决。虽然各个局域网在底层有各种差异,我们可以新增一个设备路由器,新增一层软件层叫网络层,只要不同局域网中的主机支持同一个网络层协议,就可以屏蔽掉底层网络的差异。全球内所有想进行跨网络通信的主机采用的网络统一称为IP网络,IP之下,有差别,IP之上,所有人都一样。所以,IP,即网络层是对底层网络进行屏蔽差异化的一种软件解决方案。既有MAC地址,又有IP地址,是历史发展的产物。

IP网络是基于IP协议构建的数据通信网络。可以认为数据是从A主机经过无数的网络层到达B主机
Socket编程预备
理解源IP地址与目的IP地址
往后说IP地址时就不再考虑数据链路层了,因为在我们看来,所有的网络都是IP网络。IP网络中,IP地址是用来标识主机唯一性的。实际上,IP是分为公网IP和私有IP的。现在说的是IP是公网IP。
但是这里要思考一个问题:数据传输到主机是目的吗?不是的。因为数据是给人用的。比如:聊天是人在聊天,下载是人在下载,浏览网页是人在浏览。
但是人是怎么看到聊天信息的呢?怎么执行下载任务呢?怎么浏览网页信息呢?通过启动的qq,迅雷,浏览器,而启动的qq,迅雷,浏览器都是进程。换句话说,进程是人在系统中的代表,只要把数据给进程,人就相当于就拿到了数据 。所以:数据传输到主机不是目的,而是手段。到达主机内部,在交给主机内的进程,才是目的。
但是系统中,同时会存在非常多的进程,当数据到达目标主机之后,怎么转发给目标进程?这就要在网络的背景下,在系统中,标识主机中进程的唯一性。这就需要看端口号了。
端口号
端口号(port)是传输层协议的内容。端口号是一个2字节16位的整数;端口号用来标识一个进程,告诉操作系统,当前的这个数据要交给哪一个进程来处理。当我们启动一个网络进程时,会从传输层申请一个端口号。所以,IP地址用来标识全网中主机的唯一性;端口号用来标识特定主机中进程的唯一性 。IP地址 + 端口号能够标识网络上的某一台主机的某一个进程。
网络通信的本质:进程间通信。网络就是这两个主机的共享资源,两个进程通过各自的OS来实现共享网络的效果。IP地址 + 端口号这个组合叫做Socket,套接字。所以,我们通过一台主机给目标主机发送消息,我们需要有目标主机的IP地址,以及目标进程的端口号,发送的主机是如何知道这两个的呢?我们要访问任何一一个网站都需要有域名,域名最终会通过域名解析拿到IP地址;一般客户端要访问的某一种应用层服务的端口号都是固定的,若不是固定的,则需要由用户内置到客户端当中。

Linux下一切皆文件,所以网卡就是文件,网络通信的接口也是被集成到文件系统当中的。是文件就需要被OS管理,先描述,再组织。只需要让struct task_struct的struct file底层指向的方法是网络接收、发送的方法。所以,应用层通信是使用文件描述符来通信的,类似于读写文件。SystemV之所以不重要,就是因为无法集成到网络当中。所以,网络也可以进行本地通信。所有进程间通信都可以使用网络,这样会有非常强的扩展性。本地通信时就正常通信,当要网络通信时,直接将进程跨平台移接到其他主机,代码就可以改成分布式的代码。
已经有了进程ID,为什么还要有端口号呢?进程ID属于是OS的进程管理的内容,如果网络直接使用,那么OS的进程管理与网络就是耦合的。所以,网络里一定要有他独立的一套机制。
端口号范围:0-1023:知名端口号,HTTP,FTP,SSH等这些广为使用的应用层协议,他们的端口号都是固定的;1024-65535:操作系统动态分配的端口号。客户端程序的端口号,就是由操作系统从这个范围分配的。
OS拿到报文时,是如何根据端口号将数据交给进程的呢?在OS看来,进程就是一个一个的PCB,传输层要将数据交给进程,就是在OS内查找这个PCB,所以,OS可以在OS内核中维护一张端口号的哈希表。简单一点,我们理解为哈希表下标就是端口号,存储的内容就是PCB的地址,这样传输层每拿到一个端口号,就能够找到对应的PCB了。当某一个进程启动时,就会创建PCB,并申请一个端口号,将PCB的地址填入到这个端口号对应的位置。实际上,OS内也有一张哈希表维护进程PCB与PID的关系。
进程在处理数据时,OS可能在不断地接收数据,会导致接收的一些数据没办法立刻交给进程,等线程处理完了数据,再从OS中获取数据。这就是一个生产者消费者模型。OS内会有非常多的进程,所以会积压非常多的数据,这些数据就是报文,OS就需要管理这些报文。就一定要有结构体描述,并组织起来。并由OS维护他,这就是生产者消费者模型的1。
简述整个过程:网卡接收到报文后,数据链路层就会检测MAC地址,发现是就继续向上解包、分用,到网络层会检测IP地址,发现是就向上解包、分用,到了传输层就会将报文放入一个链表中,未来进程就可以从这个链表中获取报文了。
传输层的典型代表
如果我们了解了系统,也了解了网络协议栈,我们就会清楚,传输层是属于内核的,那么我们要通过网络协议栈进行通信,必定调用的是传输层提供的系统调用,来进行的网络通信。传输层里用户最近,所以一般使用的都是传输层的接口。也是可以调用网络层的系统调用的,因为传输层、网络层都属于OS。往后我们使用到的各种网络库,实际上都是对系统调用的封装。

认识TCP协议与UDP协议
此处我们只是对这两个协议做一个简单的介绍。
这两个都是传输层的协议 。TCP在通信时要先建立连接,而UDP不需要 。TCP提供可靠传输,UDP提供不可靠传输。TCP会提供可靠传输,比方说数据丢包了,会重传,报文出现重复会去重,出现乱序会排序等。UDP只是给报文添加一个报头,并交给下一层就完了。可靠传输和不可靠传输不是这两个协议的优缺点,而是特点。可靠传输要做更多的事,说明TCP协议一定更加复杂,因为复杂,所以应用上就会有更多的约束,资源占用也会更多。选择TCP还是UDP是需要根据具体场景选择的。当需要可靠性特别高,比如说转账、下单等,使用TCP;当场景对数据丢包等不敏感,比如直播等,就可以选择UDP。
TCP是面向字节流的,UDP是面向数据报的。字节流:类似于自来水,自来水公司可能给家里提供了1吨,但是我们每次用水,可以只拿1升,1毫升,都可以。字节流内部消息的解释由用户自己来解释。像我们之前往文件中写入数据时,写入简单,但是读取就非常麻烦。文件写入就是字节流式写入,所有的信息都是字节流式的,没有格式、没有边界。怎么读与怎么发是没关系的。之前往管道写入时,可能写入了100次,但是1次就读完了,这就是字节流。数据报:我们收到快递时,快递是一个一个的,我们不能拿半个。怎么读,完全取决于怎么发的,这叫做面向数据报。
网络字节序
我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分,磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分,网络数据流同样有大端小端之分。那么如何定义网络数据流的地址呢?**网络规定:所有发送到网络上的数据,都必须是大端的!**大部分内容OS会做大小端转化,但是也有一些必须用户自己转化的,如IP地址、端口号,OS提供了一些系统调用来帮助完成转化。
cpp
#include<arpa/inet.h>
// 将 32 位(4 字节)无符号整数从主机字节序转换为网络字节序(大端序)
uint32_t htonl(uint32_t hostlong);
// 将 16 位(2 字节)无符号整数从主机字节序转换为网络字节序(大端序)
uint16_t htons(uint16_t hostshort);
// 将 16 位(2 字节)无符号整数从网络字节序转换回主机字节序
uint16_t ntohs(uint16_t netshort);
// 将 32 位(4 字节)无整数从网络字节序转换回主机字节序
uint32_t ntohl(uint32_t netlong);
h代表主机,n代表网络,是32位,s是16位。16位主要用于转端口号,32位主要用于转IP地址。
"192.168.34.45"是字符串风格的IP地址,点分十进制,可读性高。网络通信时不能使用字符串,因为字符串太长了,网络通信一定要是极简的。因为.之间的都是0-255,所以可以使用char表示,即总共只需要使用4个字节就可以表示一个地址。网络通信时就会使用这个地址。
端口号也是一样,网络传输时需要转为网络字节序。
Socket编程接口
cpp
// 创建 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);
可以看到,很多接口都需要传入struct sockaddr类型的参数。
在套接字编程中,套接字的种类有多个:
- 网络socket。既可以进行本地通信,也可以进行网络通信。所以,我们只需要学习网络socket即可。
- 本地socket(unix域间socket)。只能进行本地通信。就是一个网络版本的管道。这里不作介绍。
- 原始socket。这个不管。
为了进行网络通信,OS需要提供系统调用。而套接字的种类这么多,如果每种套接字都提供对应的系统调用,那么系统调用的数量就太多了,并且这些接口有很强的相似性。OS为了将接口设计成统一的,就定义处理struct sockaddr。也就是原本需要设计2、3套接口,现在只需要1套接口。

用户要使用网络通信时,需要告诉OS三个事情:源IP和目的IP、源端口号和目的端口号、本地通信还是网络通信。所以,就需要由用户向OS进行传参。本地通信或网络通信都可以使用struct sockaddr_in,本地通信使用struct sockaddr_un。为了统一接口,就设计了struct sockaddr。所有接口都统一使用sockaddr,而真正传入的可能是sockaddr_in或sockaddr_un,在用户层强转一下即可。
虽然接口只有1套,但是在接口内部,仍然是需要是网络通信,还是本地通信的。所以,这3个结构体的前16位都必须表示同一个内容,称为地址类型。根据这16位,就可以判断是做网络通信,还是本地通信。AF_INET和AF_UNIX就是宏。AF_INET表示网络通信,AF_UNIX表示本地通信。这就是多态。结构体大小不同是没事的,因为有类型,OS内部是这3个类型都有定义的。
其实会发现都可以不需要sockaddr,直接在参数中使用void*,因为void*就可以接受任意类型的指针,为什么不用void*呢?首先,void*语义不清楚,使用conststructsockaddr*语义更加清楚。其次,在设计这个接口时,C语言还不支持void*。