网络基础(一)

网络基础(一)

你不用担心说,怎么听不懂,一点儿头绪都没有,你要记住感觉,不要现在有些听不懂,你后面能听懂了,觉得没进步。第一个是,网络基础(一)主要谈的是网络框架;第二个呢,网络套接字;第三个,网络原理;第四个,叫做高级IO。 高级IO我们主要谈,多路转接,5种IO模型以及reactor模式。
》第一个说说网络发展背景;第二个呢,局域网和广域网;第三个呢,理解网络协议,然后重新理解一下tcp、ip5种模型;第四个,网络传输的基本常识,理解封装和分用的关系。对我们来讲呢,网络部分比系统学习的成本更加低一些,第一次写代码呢会写的生份一点,写多了就是固定套路。

网络背景

》截止到目前为止我们写的所有代码都是单机的,也就是说你可以创建进程、线程、什么信号通信你全部都可以搞起来,但是依旧只是单机的。什么叫做单机呢,就是在一台机器上,早些年人们在进行编程的时候或者写代码处理某些事情的时候,大部分都是单机的,可是呢总架不住我们需要协同工作。假设一台电脑叫做独立模式。假设有三个3电脑即3个终端,其中呢我们要把不同的数据进行处理,比如有一份数据是同学们的名单,第一个人要做什么呢,要把同学们的名单中错别字挑出来;第二个人呢,然后呢把挑出来的错别字全部改成;第三个人他要继续基于我们的名单呢继续对我们名单排版进行调整,然后就完成了对名单的处理工作。假设有3台电脑,A、B、C,所以三个人用不同的电脑做不同的工作,这个没有什么问题,但是早些年呢,机器和机器,计算机和计算机是独立的,其实说白了,就跟你现在拿计算器按来按去一个道理,做计算,左处理嘛,所以此时呢,A把数据全部挑出来了 ,他要把东西交给B,怎么交给呢,在那个没有网络和网线的年代,怎么把数据从终端A到终端B呢,有人说用U盘拷 ,那时候没有。那就只能人工交给B了。那么其中呢,我们主机A生产完的数据在交给主机B,中间就要经过人工处理,记住只要有人参与的地方,一定是效率很低的地方,所以后来随着时代的发展,对应的各种实验室,他们有强烈的诉求,不同的人做着不同的事情,大家的电脑如果不连通,一个人电脑处理完想交给另外一个人就只能通过软盘,恰好实验室的人懂技术觉得这样不妥,所以此时呢就有人尝试把多台计算机连接起来,用线路连接起来,此时就有了多台计算机互相连接来完成对应的数据共享。说白了就是理解成多台计算机用网线把我们的所有主机连接起来,各主机之间就可以彼此之间互相通信了,我们就有了网络的初步的形态。
》我们先抛出一个话题,就是我们如何重新看待计算机结构—理解计算机通过网络互联的可能性!!我们重新把之前计算机上面各种体系结构和硬件呢给大家讲个故事,让大家重新认识计算机体系结构,然后再重新看看,你说拿根网线连起来就可以,那这个原理该怎么去理解呢,不能说连起来就是网络了,太肤浅了。所以让同学们在体系结构层面上重新换一个视角去理解一下,他这个连起来之后,主机和主机之间通信,我们应该以怎么样去看待这个现象。我们接下来重新理解一下计算机体系结构。
》我们之前在讲的时候说过一个概念,我们有输入设备、输出设备、内存、CPU,然后呢在我们CPU内部呢又有运算器和控制器。我们曾经一直在说,输入设备,比如磁盘、外设,对应的一定要将自己的数据从外设般至内存当我们正在被访问时。然后CPU呢对数据做处理加载回内存,然后再刷新到我们的显示器上或者外设,这都没问题,这叫做冯诺伊曼。但是你曾经讲输入设备把数据从外设搬入到内存里,然后输出设备呢就是从内存搬出去。可是我有这么一个问题,那么其中输入、内存、CPU、输出他们都是硬件,这些硬件是如何进行数据交互的??什么意思,就是你说把数据从外设搬到内存里, 这不是你拿嘴说的,我们确实外设可以生产数据,然后拷贝至我们的内存里,但是呢,这是两个设备,是两个独立的硬件设备,那么这两个独立的硬件设备是如何交互的呢?同学们,两个设备交互,前提条件不是隔空去交互的,前提条件是,这几个设备之间必须得用我们称做的线连接起来。这个应该是一个常识,两个设备互相独立,用线连起来,他们两个才有可能通信,要不然他们两个怎么通信?一般这个线呢,你内存有自己的内存槽,然后你把内存插刀内存槽里面,设备呢要接入到我们的主板当中,CPU呢也是内嵌到我们的主板当中的,你是可以拿下来的。但是呢,有一个什么问题呢,我们所有的电路,它这个线呢,其实就是有的你能看到的线,有的呢就是直接在我们的主板当中焊接好的硬件电路,总之呢,要有线,不然拿什么完呢。只不过,因为我们对应的这些硬件设备是在一台机器上。所以一台机器,那么线就比较短。所以通信的时候,它的数据各方面的问题呢,比如通信时,因为不涉及长距离传输,所以它这个线呢,传输的可靠性还是很强的。
》但是呢,如果我们是两台机器呢。假设有网卡设备,两台计算机都有网卡设备,两台计算机在通信的时候,一定是要能够,每台电脑内部是有线的,我们两台计算机也要能互相通信,那么这两台计算机也得用线连接起来,你现在就理解成网线,当还有我们对应的无线Lan。但是呢,无论你通过什么方式去连接,它也是线,所以计算机内它本质上我们可以称之为,叫做
计算机体系结构本质也可以被看作成为一个小型的网络
。也就是说在体系结构里面呢,它们各自独立的硬件设备用线连接起来,所有不同的设备都有不同能力,所以它本质就是一个小型的网络,所有计算机内部之间各个硬件设备也是用线连接起来的。所以我们所谓的计算机体系结构,我们把它就看做成一个小型网络,本来就是用线连接起来的,只不过是短而已。
》如果我们有两台计算机连接起来的时候,本质上是不是相当于我们对应的两台,我们可以称之为,叫做两台计算机,与其说在体系结构层面上是两个计算机通信,倒不如说两台计算机的网卡之间又通过网线连接起来了,无外乎就是线长了一些。换而言之,我们的多主机连接本质上,其实也是通过“线”连接起来。两台计算机也是线连接起来的,体系结构内的线短而已,仅此而已。那么我们可以这么去理解,我们现阶段认为两台主机之间 互相连接通信,本质上是可以理解成它是体系结构上的延展,也是通过线连接起来的。所以我们回过头看一下,我们从单主机到多主机拉根线就完了,我们就应该想到他是具有可行性的。因为只要是计算机里面,硬件设备和硬件设备用线连接起来就可以通信,那么我们也可以把两台计算机的网卡用线连接起来,只不过线长了点,只不是计算机体系结构的延展,当然就能够通信了。
网络基础(一)_第1张图片

》当我们有了这么一个理解,我们就听说过,大型公司呢,有自己的存储集群,然后有自己的计算集群,缓存集群。所谓的集群说白了就是有特别多的主机,在主机的内部呢用内网连接起来。对每一个内部的集群呢也照样充满了很多很多的主机,大型互联网公司有自己集群,所以要被计算集群计算之后,所以可以把任务放到我们的缓存集群里面,然后呢以任务队列的方式让我们的计算集群拿到,计算集群拿到之后返回到缓存集群里面 ,再把数据写到存储集群里面。我们是不是照样可以使用多主机互相联通,构建宏观的冯诺伊曼呢。所以,我们再重新回过头看我们的计算机体系结构,你就会发现,其中体系结构上呢有网络, 网络呢也可以构建体系结构,它们两个其实是不分的。所以当你有这样的宏观认识,此时再看我们刚刚所说的概念时你也一瞬间就懂了。
》那么其中我们在一台计算机里面,我们把外设和内存之间连接起来的这根线,我们一般叫做,IO总线。外 和内存人家还有新的设备,比如南桥。CPU和内存之间连接的线呢,我们一般叫做系统总线,我们一般听说的32位、64位就是指的CPU和内存之间线的个数,这是硬件决定的。所以我们知道了一台计算机内部其实也是一个小型的网络结构。我们两台小型网络结构通过网线,长距离连接构成了更大的网络结构其实很正常,多个网络结构构成,我们也可以把他们搞成 不同的主机集群,然后我们可以构建冯诺伊曼体系,此时我们叫做大型互联网计算和存储平台。其实是一个道理。 这就是你换成不同的视角看到不一样的结果。
》当我们理解到这一层之后,你不得不面面对一个问题,主机内,“线”比较短, 跨主机,“线”比较长。同学们记住了,短有短的好处,长有长的难处。短一定面临的一定是数据和数据之间信号相互干扰的问题。有的时候我们把总线设计的很密集,线路线路大家都知道,当电子在流动的时候会形成磁场,磁场和磁场之间会互相干扰,所以一旦太近了就容易互相干扰。所以一台机器里面,设备把数据交到我们的内存里面,它往往也要定协议的,他不是单纯的把数据放到内存里,他也有自己对应的协议,只不过呢我们在硬件和硬件一台机器里面,他们互相交互的时候,做得最多的就是纠错,防止比特位翻转,所以放数据的时候会有一些校验信息,来校验这部分数据曾经是否在传输过程中出现问题。所以不要简单的认为,计算机里面没有任何处理,它是做了的,也有自己的校验的。对我们来讲,线路一长也会带来问题,物理结构变化一定会带来新的问题。线一旦长了,第一个就是,可靠性的问题;第二个,就是效率的问题;第三个,就是寻找到对方的问题。 所有的网络研究的问题,本质最根本的道理就是机器与机器之间传输的线路太长。大家也知道,数据经过长距离传输,短距离不影响,短距离信息密集可能干扰的话我们校验一下,重做的话很快就能完成。但是长距离面临和短距离面临的问题是不一样的。比如说你丢包了怎么办,数据在传输过程中会发生我们信号衰减的,越来越弱了,长距离传输很容易出现我们丢包、比特位翻转各种其他问题。还有就是传输的时候你怎么尽快的保证把数据交给对方,距离太长了,所以效率问题怎么保证。第三个呢,你今天是网络一台主机,同时和多个主机连接,可是不像你在一台机器里面,比如说我的磁盘要找到内存,那我们对应的就可以通过我们连接的线和我们内存的线在主板的什么位置,大致是确定的,所以一个设备找到另一个设备,但是长距离的时候你要找到那个主机可能和你距离相隔千里之外。所以我们学网络知识的本质都是因为我们的线边长了
网络基础(一)_第2张图片
网络基础(一)_第3张图片
》一开始主机之间不能通信,也就是不联通,然后我们发展,在硬件级别上,我们可以把所有主机用线连接起来,当然话说到这里,你单单用线把主机连接起来也是不行的,就跟你平时打电话一样,把线拉到你家里面,你不用,也没办法。所以网络除了把线连接起来,其实还要进行我们软件上的配合,这就叫做协议,后面说。所以对我们来讲,各个主机之间呢, 它互相使用我们对应的叫做,同学们可以理解成网络将我们的所有主机连接起来。后来我们在有了网络之后呢,连接的主机越来越多了。越来多的时候呢,此时我们一个网络,可以称做局域网,小型的局域网呢,比如说呢,清华大学有一个局域网,内部实验室用的是一个网络,然后呢北京大学也有一个局域网…后来呢,不仅在公司内部有交流,在学校有交流,我们可能还要跨学校,所以我们有了各种新的设备诞生,其中局域网为了能够更好的通信呢,就引入了,交换机的概念,每一个局域网都引入了交换机的概念。关于交换机呢,我们后面再讲Mac帧的时候再说。在下面呢,还有我们说的路由器进行局域网和局域网内进行数据包转发,所以我们最后接入对应的网络变得越来越大,我们其中有一个局域网LAN,说白就是一个局域网,在局域网通信的范围之内我们可以直接去通信,然后呢各个局域网内部可以去通信,但是两个机器通过跨网络传输,因为线长了,那我们得有很多的策略把数据安全可靠的送到对方,那么其中就衍生出来了很多的设备,这个道理呢举一个小例子来理解。就好比古代的时候,我住在城里面, 我要吃饭就去城里面找一家馆子,这叫距离近。比如说我们是在北京城里,然后呢想去南京城里面去吃饭,此时单纯从北京走到北京城里的某一家馆子吃饭,这叫做路上走,那么我从北京到南京也是在路上走,本质上是路变长了,它不仅仅是路变长了,他一定也要衍生出新的东西来,比如说路上会出现驿站等新的东西出来。所以一旦距离变长了呢,我们为了支持在长距离传输等过程中,包括数据衰减了怎么办,包括我们为了更好进行在网络中传输,我们数据该怎么去编码这个问题,然后再下来呢,我们数据包互相转发的时候,凭什么找到对方主机,那么我就要有路由器这么一个东西。所以长距离传输带来新的问题,就一定要有新的设备去解决它。当我们的网络变大的时候呢,中间就诞生了各种各样的设备,其中关于设备呢我们后面都会介绍,但是同学们, 常识告诉我们路由器他一定是用来连接不同的网络的,所以左侧是局域网,右侧也是局域网,中间通过路由器来进行连接。
网络基础(一)_第4张图片
所以就有了局域网概念。后面慢慢的随着一个地方的网络结构变得越来越复杂,最后我们就有了广域网,所谓广域网就是将我们所有内部主机组成局域网,然后各个局域网将我们的数据通过路由器在公网内转发我们的数据包到不同的城市。所以从我们上面来看呢,课件里面说了一句话,局域网和广域网或者城域网,本质上是相对的概念,如果你非得要明确的概念,我给你一个概念,所谓的局域网网络的内部没有路由器,可以有其他的设备,但是没有路由器,这叫做局域网。但如果是广域网的话,一定是要涉及到多种路由器,还要接入公网和私网或者内网,这些网的概念在讲我们的IP协议的时候我们会说。所以现阶段网络来讲其中大家要理解的就是,可以通过不同的 ,我们构建不同的网络呢可以让所有的主机跨网络进行传输,这就叫做局域网和广域网。我们说的局域网和广域网呢,其实就是想对的概念。比如说你宿舍就是一个子网,当然在学校里面,也可以将学校看成局域网,我们国家呢也可以看成大的广域网。

认识“协议”

因为长距离传输了,我们刚刚讲的局域网和广域网这么一个概念帮助大家去理解,其实在设备之间呢进行连接,那么他们两个就构成了新的体系结构,其实说白了,他就是计算机体系结构,只不过距离长了一点,长有长的难度,长了的话,我们双方就必须得定协议。关于协议的例子呢课件上有,但是我不想用它的。
》什么叫协议呢,关于协议呢,我们先讲什么是协议,然后再是如何去看待协议。给大家讲一个小故事,在以前的年代,打一次电话很费钱,你也知道是三大运营商给我们提供的通话服务。假设呢有一个学生是张三,张三收拾包袱准备去上学了,张三家是云南的,考上了北京要去上学了,他有一个担心,就是他的家庭条件不是很好,它每一个月要给家里报平安,第二个给家里要钱,第三比如说 有其他事情要给家里要说。所以张三呢心里就盘算着,怎么更好能够帮家里面省了钱,还能把我要表达的信息传递到呢。所以张三就想到了一个招跟他爸爸说,我要去北京上大学了,我后面给家里打电话的时候, 你只要看到是北京打得电话一定是我打的,如果我今天打了电话,电话响一声这是给家里报平安,如果想了两声这是我这个月没钱了,如果电话响了三声,你在接电话我们来沟通。所以张三和自己父亲约定好规则后呢,就奔赴北京上大学了。后面张三去北京上了大学,然后给家里打了电话只响了一声说明自己是平安的,父亲也知道;又过了一个月响了两声,父亲知道孩子没钱了。我们把父亲与儿子之间的约定,我们把它称之为“协议”。
》所以什么叫做协议呢,所谓的协议是通信双方,很明显张三是要发消息,老爹呢是要收消息,通信的双方做的一项大家心知肚明的约定,约定好了之后呢,他们就遵照约定来进行后续的工作,那么其中这就叫做协议。刚刚是我们告诉大家的,可以称做我们日常当中的约定。其实呢,关于协议的内容呢,我之前已经有意无意的写过了代码,代码里面呢也已经体现了协议定制的问题,虽然很小,但是也能说明问题。记不记得我们曾经在讲系统的时候,父进程和子进程直接通过管道通信,我们写了一个简单的进程池。当时我们通过管道让父进程向子进程写入4字节的数据,我们还有隐形的规定,我们写入的4字节数据必须得是整数。作为父进程和子进程,子进程怎么知道一次要从管道里面读几字节呢,它怎么知道我把读上来的若干个字节当做什么类型来看待呢?全都是我们约定好的。所以我们在写的时候使用uint32_t,读的时候我们也是用的uint32_t去读,我们隐形的至少做过两个约定。第一个就是,记住我每次只写4个字节,你读的时候也只能读4个,不能少也不能多,你必须4个4个读;第二,还有一个约定就是,我给你发的是整数,你必须将其转成整数再做处理。这叫做计算机上的约定,我们所做的就是约定,这叫做协议。我们在讲网络的时候,一定会涉及到各种各样的协议场景,这些场景呢慢慢说,
》如上就是要讲的协议,所以我们两台计算机之间想要互相通信的时候,我们也知道计算机只认识二进制,不光计算机,网络也是只认识二进制。在我们计算机里面呢,我们表示二进制在不同的设备上表现形式是不一样的,有的是通过信号的有无来表示信息的,比如说我们的CPU和内存互相传数据,有的是用强弱,比如说网路,有的可能是用频率等,包括我们之前讲的磁盘,用南极北极来表示二进制。所以我们想要传递不同的信息的话,我们只要双方约定好数据的格式,此时我们就叫做把协议定好了。光光将协议定制好,两台计算机就能通信吗?给大家举一个例子,那么其中只有协议够不够呢, 就能足以支撑两台计算机通信吗?答案是:还不够。同学们,对我们来讲呢,协议对我们来讲呢是一个软性层面上,我们多台主机规定好,我们可以遵守同样的通信协议,但是我们的计算机对应的生产厂商可是非常多的,不同的厂商对应的操作系统有很多,即便是操作系统一样,在硬件上呢也有很多大家都不一样。我们可以遵照协议,但是呢大家硬件和软件上实现的不同,极有可能带来的结果就是大家看到协议的时候,可能以同样的方式去解释时大家就听不懂。给大家举例子,比如说我们玩某某蹲的游戏,说到哪一个东西要蹲下来的话,那大家都得蹲下来,这叫约定好了游戏规则,约定好了就是协议定制好了,所以在给大家说话的时候,其实就是广播协议的时候,喊某个种类的蹲的时候,所有人按照游戏规则正常规定。但是突然里面有一个李四,他呢是一个博学多才,玩游戏的时候就是不跟你好好玩,他用法语说话,那我是不是也遵守了游戏规则,只不过用的法语说的,但是没有人能够听懂,所以也没有人能够正常的跟他玩下去。如果你把游戏的协议规则定好了,但是大家玩这个游戏时所采用的语言的不同,就无法沟通。就好比所有的主机都遵守网络协议,但是有的厂商有无来表示二进制,有的根据波峰波谷来区分0/1等,大家都可以用1010表示我们多个主机之间发送某些信息,但是呢我们不同的厂商解释的不一样所以互相之间也不能正常通信,所以除了要把协议定制好后,我们的主机与主机之间输入二进制上也必须得有标准。说白了就是,你不光要把约定做好,你还必须得保证不同的厂商之间人家会遵守你的约定,但是必须得保证他们是一样的,那么这里就有难度了,什么意思呢,比如说硬件设备通信的时候,他用的硬件层面上的传递的0/1信号的方式和对应主机传递0/1信号的方式必须得是一样的,这样大家对协议的解释方式是一样的,那么提取协议的时候也是一样的,这样才能正常通信。也就是说协议本身是双方沟通好的,就跟大家玩游戏,我们都用中文说,你非得一个人搞法语,你一个人在那里玩的很开心,最后你也遵守了游戏规则,确实你话说完没有人听得懂,你遵守了游戏规则呀,但是你照样玩不下去,这叫做大家底层硬件不一样,你照样玩不转,怎么办呢,此时我们不但要把游戏规则统一,我们还要能够把我们的表达方式统一,就好比我们要把协议统一,同时我们还要要求对应的厂商制作操作系统或者硬件的时候也必须采用统一的标准。我们为什么说比较复杂呢,这涉及到一个标准定制的问题,标准谁定呢,各个厂商都说我来定,那听谁的呢?谁的技术能力强就听谁的。
》我们刚刚只说了一个概念就是协议,顺便让大家理解我们有协议,也会有标准,除了软件上,硬件上也得有标准,如果你软硬件都不一样,你也写代码,有时候一个字节写错了程序就编译不过。更别说计算机需要大家都配合的,这么精密的仪器,出现任何问题你都是通不了信,所以大家都得遵守协议。下面呢我们要讲协议的层状结构。

协议分层

问同学们一个问题,你是如何理解软件分层的?我们在讲系统的时候是说过,什么叫做软件分层呢,给大家举例子,你以前一开始写代码的时候大概率就是把所有代码一股脑写在main()函数里面,后来你学会了要把功能写到一个个函数里面,然后我们在对应的main()函数里面你去调用对应的函数,你需要什么功能就去写什么功能,这叫做面向过程,然后呢你把所有的功能都封装成函数,那么此时呢我们可以认为你的代码是分层的,第一层是你的main()函数在顶层叫做调用逻辑;第二层就是你调用的函数。后来学了C++,我们学了继承,有了基类和子类,我们可以采用多态的方式来实现让我们基类指针指向不同的子类,从而体现出调用不同的方法。这也是一种分层是面向对象,这一层面是是分了两层。给你抽象出来基类,然后使用基类去指向子类去实现多态,这也是一种软件分层。再到后面呢,我们讲过一个非常重要的概念,就是linux下一皆文件,它通过操作系统层面上为每一个被打开的文件创建struct file,然后1.填充上所有属性,2.文件操作的函数指针,通过函数指针的方式指向底层不同设备所具有的方法,所以我们此时就有了,上层呢,就是struct file那层我们称之为虚拟文件层, 底层就是具体的设备对应的读写,这也是一种软件分层。所以我们必须得承认软件是可以分层的。虽然我们目前呢软件分层我们接触还不是很多,但是不影响,软件是可以分层的。也就是说呢,我们知道可以分了,那为什么呢?
为什么要分层?从目前来看,我们所说的所有软件层面的解耦或分层,最终大家明显能够感觉到一点就是越分层代码的逻辑结构越清楚。而且对应的软件层状结构当中,对应的文件系统,上层调用下层的方法,通过函数指针指向不同的方法就能够实现C语言式的面相对象和多态,这就是分层带来的好 处。所以除此之外呢,我们未来在排出如那件问题的时候,如果我们发现struct file访问其他设备好着呢,访问另一个设备就不行,那么大概率不是struct file那层出问题了,而是我们的下层对应的设备读写逻辑出问题。所以分层有什么好处呢,1.软件在分层的同时,也把问题归类的,哪一层出问题其实我们是可以通过排查对应的哪一层出问题我们就解决哪一层的问题。因为层与层之间呢是调用和被调用的关系,所以对我们来讲呢,我们只要把某一层的问题解决了,并不影响另一层,所以呢方便我们排查对应的问题,这是其一;其二呢,2.分层的本质,就是软件上解耦。分层之后呢,层与层之间只有调用和被调用的关系,就和我们虚拟文件系统和底层设备每一个具体的读写,它底层对应的是磁盘,它就读的是磁盘的文件系统里面的具体文件系统的读写方法,它若指向我们的显示器,它对应的就是显示器的读写方法,如果是我们对应的键盘,那么就是键盘底层的读写方法,所以它呢把我们操作系统层面上维护文件的逻辑,和我们设备上具体维护的方法进行解耦,这就是分层带来的好处。与此同时呢,由于问题归类了,软件上也互相解耦了,那么其中我们一定带来的最终的结果就是,3.便于软件工程师进行软件维护,其实说白了就是好找问题和好改代码,什么叫做好维护,说白了就是出现问题好排查,第二个就是想改代码好改,我们可以改动的时候呢并不影响其他层,这就是为什么要分层的原因。所以基于一,软件是可以分层的,第二,软件的分层有这么多的好处,我们所对应的网络,网络所对应的协议栈本身呢,**网络本身的代码,就是层状结构!**我说的东西应该是能理解的。因为当时我们在讲硬件驱动,操作系统再到系统调用接口再到我们的库,再到我们的用户,我们其实自底向上在讲软硬件上,它就是层状结构,所以对我们来讲呢,层状结构是我们软件领域非常重要的一种结构,。所以软件行业里面有一句话,没有任何一个问题不能通过加一层来解决,也就是说呢,我们软件设计上了出现了不同的概念,我们可以通过加一层来解决,加一层什么呢,叫做软件层。话说到这,我们曾经讲的虚拟地址空间,没有虚拟地址空间的时候,我们的所有的进程是实模式下访问内存的,也就是直接能看到内存并对内存进行读写,这有问题吗?有,比如说是越界访问,可能自己有意或无意的对内存做修改,不安全,也不便于操作系统进行进程管理问题,其实他有很多问题,其实这就是问题。本着只要有软件问题,我们都可以通过添加一层软件层来解决,所以虚拟地址空间便诞生了,它本质上不就是加了一层软件层叫做虚拟地址空间吗,让页表进行映射,此时我们就可以在我们虚拟地址空间那一层对我们所谓的非法请求进行侦测拦截,操作系统识别对目标进程发信号,终止目标进程,这就叫做用层状来解决问题,当我们理解了这一点之后呢,网络协议栈也是这样设计的。
》所以软件本质上是分层的结构,关于分层的好处呢,给大家举一个例子,有A、C互相打电话,通过座机,双方通过打电话约定好吃什么了。那么我们可以认为我们打电话其实是分为两层的,第一层叫 做我们对应的语言层,我们双方能够语言来进行通话的;第二层呢就是通信设备层,我们认为我们是通过电话来沟通的。那么其中,如果有一天发生变化了,我们两个人继续通话,但是不再用我们的座机而是我们的手机。在语言层,该怎么说还是怎么说,没有改变,我们只是把底层设备换了,但是并不影响我们上层通信,只要你提供的功能是我所需要的通信功能就可以了。再说,底层没有变就是网络通信层没有变,而上层的两个人由汉语沟通,变成了英语沟通,也并不影响两部电话的通信,这就叫做软件分层。我相信能理解,因为我们当时在讲虚拟地址空间的时候,一个struct file函数指针,指向底层不同的设备,指向不同的方法就会有不同的表现,这个没有问题,我们都能知道,他们是可以直接进行我们对应的通信的,对我们同学来讲呢,我相信这个问题也能理解。
网络基础(一)_第5张图片

》我们再来谈下一个问题,我们层与层之间,它们之间又是怎么去通信的呢,比如说A、C两个人打电话,张三和李四,两个人打电话的时候,它们约定吃什么在哪里吃,当我们在打电话的时候,我们认不认为张三和李四两个人是直接通话呢?答案是:是的。是认为张三和李四是直接沟通的,但是呢,在实际上真正的直接沟通并不存在,为什么呢,就比如直播因为并不是在和同学们直接沟通,而是我们把说的话、声音等采集好,采集好之后,传到给平台,然后平台再帮我们做转发到我们每一个同学的桌面上。这是什么呢,这是站在普通老百姓用户的角度我在认为和你直接沟通,但是呢我们实际上呢,底层是通过不同的设备帮我们做转发的,比如说电话,电话实际上采集声音,然后对声音做编码,然后压缩,压缩完之后通过无线网,把数据推广到离我最近的基站,然后基站内部呢再做转发到对应的最近的基站然后再转到手机上,然后再解压和解码,然后调用我们喇叭的程序把我们的声音播放出来。其实我们正常沟通的时候底层帮我们做了很多很多的事情,但是不好意思我们并不关心。第二个呢,我认为我们两个在直接通信,那么这两个电话怎么想,这两个电话有没有认为在直接沟通呢?是的,它们也认为他们两个在直接沟通,可是呢,可能我在中国,你在美国,我们两个电话他们认为自己是在通信实际上数据是经过底层大量的转发,所以,这里呢我想告诉大家一个结论叫做,**层状结构下的网络协议,我们认为,同层协议,都可以认为自己在和对方直接通信,忽略底层细节。**我们可以认为每一层都在和自己同层的程序在直接通信,底层呢给他赋予的这样的功能呢,它完全不关心,这就叫做层状结构,所以呢,我们一般在定协议的时候,所以对我们来讲,每一层都认为自己在和对方直接通信,网络是层状的的,所以,同层之间一定都要有自己的协议。比如说,给大家举一个例子,比如说我们人之间的协议是什么呢,人之间协议就是打电话,打电话的时候我们两个把电话接起来,永远你把电话接起来的第一句话,永远是,喂?然后另一方也是,喂?此时我们就叫做语言层我们建立了通信。我们两个人之间呢,用的是人类自然语言定制的协议沟通。比如我说什么了,你就不能说,你在说什么,我就不能说。我们两个以半双工的方式进行通信,当然两台设备之间也是有自己的通信协议,所以同层之间,你们都如果认为自己在直接通信的话,那直接带来的结果就是,同层之间每一层都要有自己的协议。所以对我们来讲,分完层之后,层跟层之间最大的好处是什么呢,最大的好处就可以是,上层只需要调用下层提供的接口或者方法就可以了,你具体怎么做,我们不关心,我们不管,反正我调用一下你,最后你把功能直接给我,我要的结果直接拿到,我的目的就达成了,你具体底层怎么办和我没关系。就跟你调用printf,你从来都不关心底层实现,就跟你调用fork一样,你从来都不关心它会做什么,read、write一样也不会知道他们要做什么,我们要的是拿到的结果。因为网络是分层的结构,我们知道他是层状的,那下面就是它有哪些层呢?
》既然网络是分层的,网络的代码也是用软件写的,所以对于网络来讲,它是层状的,那它有哪些层呢,最常见的,我们当时定网络协议的时候,有个组织叫做OSI,承担了网络协议栈的标准的定制,定标准的和写代码的有可能不是同一批人。所以OSI这个机构呢定制的协议,当时把网络划分成了七层模型,自底向上,分别是物理层、数据链路层、网络层、传输层、会话层、表示层、应用层。那么它其中呢,把我们忘网络划分成了若干层,它的这些每一层都负责有不同的功能,你现在肯定不太理解,因为还没有讲,站在一个很懂网络的视角呢,OSI他们的这一套标准定的时候,是定的特别好的,而且每一层该做什么工作,其实做的非常好,只不过呢,实际上在实施的时候,我们会发现实施的时候你所定义的协议呢挺好的,但是呢呢有很多东西不可能在标准情况下定好的。尤其是上三层,会话层、应用层、表示层是不好处理的,所以呢,实际上做出来的时候,我们的协议其实是五层,其实更准确的来说是四层协议,不过大部分教材写的时候是五层,因为我们谈的是网络协议栈,纯软件的有四层,所以我们就有了对应的,应用层、传输层、网络层、数据链路层、物理层。其中呢我们实际上制作就变成了五层协议。我们也承认许多教材里面的叫法,其中也把物理层也纳入某一层,其实也挺好,因为物理层本来就不属于软件范畴,但是我们后面在讲的时候,我们是按四层去讲的,因为我们重点讲的是软件层,也就是说应用、网络、传输、数据链路四层。这里呢,就不得不给大家谈一谈,产品经理和开发者故事,架构师把代码架构的再好,实际上在执行的时候也会发生变化,这个很正常,其实很多软件在设计的时候,曾经的设想和现在已经形成的其实差别很大,主要是受到实际客户的需求影响,其中,OSI中的上三层,应用、表示、会话,为什么实际上在制作的时候发现是不太行的,原因是,你看会话层解释有一句话:何时建立连接、何时断开连接以及保持多久的连接?实际上工程师子啊设计的时候发现,我们底层的协议呢是没办法去支撑它想要的功能,因为对应的工作是由软件去帮我们维护的,就比如说有的软件就不需要你,链接建立好完了,比如说http通信建立好了,很快就将连接断开,但有的软件就需要你维持长链接,就比如说我们的聊天系统,我们的QQ或者做一些其他软件,这东西就是要我们把链接保持请求。同时呢,如果你长时间不通信,会自动给你断链接。其实你会发现,这个东西是操作系统万全没办法做的,为什么呢,因为操作系统无法决定上层软件何时断开连接,所以对我们来讲实际上施工的时候无法达成。OSI做的挺好,但是把其写到操作系统内部是不太合理的。表示层在做的时候也是有问题的,就是你可以理解成,文字我们需要什么格式,不同的软件要求是不一样的,比如说微信上发的消息就可能是你的消息加你的头像,其他软件呢,可能是用来推送声音的,什么声音,分贝多少,这些其实都不一样 。应用场景更不用说了,每一个应用的场景不一样,所以你作为一个协议的定制着你想把这部分内容坐到内核里是很困难困难的,因为应用层的协议实在是太多了,有的是超文本传输的,有的是文件传输的,有的是域名解析的,有的是远程登录的,还有很多网络服务的,不同网络服务呢,协议不一样,你作为操作系统,你能把协议全部归纳到内核里面吗,做不到,实际上我们在做的时候,工程师在内核当中实现的是下面:传输、网络层,然后数据链路层是由我们的驱动程序去提供的。然后表示层、会话层、应用层由我们的用户自己去完成,后来我们会越来越感同身受,其实当时定这个标准是定的很好,但是在实际施工的时候发现做不到。如上就是网络分层有哪些层。
》·OSI(Open System Interconnection,开放系统互连)七层网络模型称为开放式系统互联参考模型,
是一个逻辑上的定义和规范;(你要记住了,你像网络通信协议这种,你不遵守就拿不到数据,你通不了信,这样的协议呢,除了定制协议的组织或个人或者公司本身权威之外,也倒逼着公司或者个人必须遵守我的标准,若不这样你就做不了。但是呢也有一些规范和标准也不能促使其它人或公司去做。OSI显然就是不被严格遵守的规范,它将网络分了七层,事实上我们网络也就是四层。)
·把网络从逻辑上分为了7层. 每一层都有相关、相对应的物理设备,比如路由器,交换机;
·OSI 七层模型是一种框架性的设计方法,其最主要的功能使就是帮助不同类型的主机实现数据传输;
·它的最大优点是将服务、接口和协议这三个概念明确地区分开来,概念清楚,理论也比较完整. 通过七
个层次化的结构模型使不同的系统不同的网络之间实现可靠的通讯;
·但是, 它既复杂又不实用; 所以我们按照TCP/IP四层模型来讲解

TCP/IP五层(或四层)模型

我们自底向上给大家说一下,比如我们说说硬件物理层,主要是解决光电信号问题,比如说我们通信的时候用的什么网线(双绞线),什么是双绞线呢,就是家里面的蓝色网线,现在大部分家庭用的是光纤,光纤在家里面入网口和你家里的猫的设备连接着的,从墙里面出来的。第二个就是无线WIFI就是路由器,它呢实际上就是用的电磁波作为他的物理层,物理层呢本质上就是,有可能用的是双绞线,光纤、无线WIFI,他们通信标准,因为物理结构不一样嘛,那么通信标准也肯定不一样。物理层一般考虑的就是设备和设备之间传输速率、传输距离,包括抗干扰性等等,其中呢集线器就工作物理层 。集线器是一个什么设备呢,大家都知道我们的数据在经过长距离传输的时候,信号是会衰减的,信号一旦衰减了,就直接决定了 ,无论你以哪种物理层通信作为我们基本的通信方式,当你实际上在做通信的时候,长距离传输一定会衰减,衰减了就决定了这个信号传不远,可事实上呢,当今的互联网是全球性的,你坐在家里,几秒钟你的信息就直接发送到了美国,这就是信息能穿多远呢,其实有一个设备就叫做集线器,集线器最主要的工作就是信号放大,也就是当你的信号很弱的时候,当你经过中间的某一个集线器的时候,它会把你的信号放大,相当于你不强了,就给你放大一下。
》还有呢就是数据链路层:硬件呢负责光电信号,数据链路层呢主要负责设备和设备之间数据帧的传送。后面在讲的时候,不同层的数据有不同的叫法,大家都懂,这就是大家都是二进制,只不过在不同层有不同的名称,数据链路层呢,我们一般都用数据帧,它利用帧的转发与识别。比如说,我们在网卡的驱动层面上,它能帮我们去编写数据链路层的代码,包括我们在局域网当中进行帧同步,负责局域网当中帧发送,数据帧的碰撞检测等,包括冲突了就会给我们重发,这东西其实就是我们数据链路层这个软件配合网卡做的。网卡这个硬件设备就属于物理层,而我们控制网卡的驱动程序它是软件层,对应的就是数据链路层。当然数据链路层是一种叫法,因为你也知道,不同的硬件也就决定了不同的驱动,有的网卡叫做有线,有的网卡叫做无线,所以不同的网卡对应的,不同的数据链路层代码逻辑是不一样的,这就是底层的差异嘛。所以呢,像我们底层的那些网络知识呢,局域网当中,以太网、令牌环网、无线LAN各种各样的标准。其中有一个设备叫做交换机工作在数据链路层的。交换机呢主要是用来局域网当中数据帧转发的,这里呢还有很多的问题,我们后面讲的时候再说
》再下来就是网络层:网络层呢,主要负责我们对应的一个叫做地址管理,还有路由选择的。我们刚刚说了,如果长距离传输的时候,我们面临的一个是效率的问题、一个是可靠性的问题,还有一个就是你怎么找到对方,所以我们在软件层面上呢,定了一套我们IP地址的方式,然后呢,在我们的路由器当中形成路由表,通过路由转发的方式,帮我们把数据转发,找到局域网中的主机。同学们,记住了,唐三藏从长安城出发,在地球上能找到西天并且见到如来佛祖,一个人从千里之外找到另外一个人,就决定了,一个数据包他也一定能过经过大量的数据转发找到目标主机,这是能做到的,只不过确实比较复杂。所以我们对应的路由器就工作在这一层,像家里连网的时候, 连接你家里的路由器,这东西就是帮你上外网的。
》再下来呢还有传输层:传输层通常是在两台主机之间,比如说A主机和B主机,他们通过某些协议来长距离传输,传输层呢就重点帮我们去解决可靠性问题,比如说效率的问题,实在传输层帮我们解决的;
》再下来呢就是我们的应用层:应用层呢是负责程序见沟通, 比如我们的电子邮件、FTP等协议,我们后面写的代码其实都是应用层的代码。
》这些呢就是我们的网络层状结构,这些与你们相关的,肉眼可见,在家里知道还是不知道,有两个设备你要知道,有一个设备叫做,猫,也叫做调制解调器,一般在家里面呢可能是根路由器落在一起,也有可能是在你们家中的网络入口处,若你家里事无线网的话就会有,猫,这么一个设备;第个设备呢就是路由器,路由器好理解,同学们上网连手机是要拿路由器的。,猫,这个设备做什么的呢,他其实是工作在物理层的,它跟那个集线器还不一样,它是为了支持长距离传输,负责信号的发大的。而调制解调器,它主要是用来进行我们的物理层的数据的一个转化的。我们通信领域呢,一般在网络里传输和家里传输,二进制的编码格式是不一样的,有一种信号叫做数字信号,有一种信号叫做模拟型号,所以我们的猫呢主要是信号转模拟,模拟转信号。一个是有利于我们在局域网内通信的,一个是比较利于长距离转发的。所以当你从外网进入的时候呢,对应的信息是要经过转发,经过转发到你的路由器被你所识别到。

我们要讲三层概念,第一层封装、解包概念,另外一层呢,数据包以及网络协议栈整体的包括它的角色定位,数据包它怎么去走的,宏观的轮廓给大家一说。第三点呢,我们要认识两个地址,一个是MAC地址,还有一个就是IP地址。我们临近下课的时候讲了网络分层的时候,我们TCP/IP分层,分为物理层、数据链路层、网络层、传输层、应用层,每一层呢都有自己对应的协议,当然OSI也规定了,他自己定出来的七种模型,这七种模型确实划分的很好,但是呢,让我们实际上落地的时候呢,是不太OK的,原因是有些划分的层次在操作系统层面上想完成是很难去做的,包括你用户层想去把这些全写了也是非常费事的,实际上我们在讲的时候呢,是按四层来说的。其中呢OSI模型是左边的,TCP/IP是右侧的。最底层呢,我们都叫做物理层,再上面呢我们叫做数据链路层,其实呢也有人叫做网卡层,我们暂时不管。再下来呢,我们叫做IP层等对应的就是网络层,还有呢就是传输层对应的就是传输层;而上面上会话、表示、应用,而我们TCP/IP实现里面统称为应用层。而应用层的协议非常多,包括http、https,包括我们FTPS等很多,你可以理解成应用层协议和上层用户等基本使用情况有很大的关系。我们的硬件和我们的数据链路层其实是属于我们的驱动和网络网卡这样的硬件设备之间的关系,再下面呢,我们把TCP/IP这两层协议其实是属于操作系统帮我们去完成的。再下来我们经常听说的http、https等属于应用层对应的协议,所以对我们来讲呢,这一点我们要搞清楚。还有呢,我们要想清楚,一般呢,在我们的实际应用当中呢,物理层我们实际上考虑的比较少,所以一般我们把网络协议层称为TCP/IP四层协议。
》一般而言
对于一台主机, 它的操作系统内核实现了从传输层到物理层的内容;(其实准确一点的说法呢,操作系统实现了,从我们的传输层到IP层之间的内容,然后呢,下两层硬件就不说了。数据链路层呢帮我们解决驱动硬件在局域网当中通信的一个功能。)
对于一台路由器, 它实现了从网络层到物理层;(我们路由器不仅是IP转发的功能了,IP甚至有了自己组建局域网,还能够帮我们提供应用层的服务都有可能哈。所以我们每一层硬件呢对应的,在它1本层当中都有自己的核心功能哈。)
对于一台交换机, 它实现了从数据链路层到物理层;
对于集线器, 它只实现了物理层;

网络分层和数据包封装和解包概念

一般呢,在我们的网络当中,记住了,我们首先回答同学们一个问题,首先回答网络和我们操作系统之间的关系。我们经常会说网络如何如何,那它和我们操作系统有什么关系呢?下面嗯给大家画一张图,首先呢,我们现在知道网络其实是一个层状结构,我们把物理层也带上吧,其中最下面呢就是物理层,再上面数据链路层;在上面就是网络层;在上面呢就是传输层;在上面就是应用层。那么其中对我们来讲呢,你给我说的这些软件分层的这些概念,和我们的操作系统是什么关系呢?
》我们操作系统也有自己的层状结构,一层是驱动程序,然后最下面一层呢叫做硬件。我们真正在内核当中实现的是传输层和网络层,而数据链路层是属于驱动程序的范畴,而物理层其实是属于硬件级别的。我们曾经也说过,操作系统他对上呢,对于任何人来讲要使用我们对应的,你想想,最终来回发数据不就是用网卡嘛,网卡不就是一个硬件嘛,你要访问硬件你就必须得贯穿操作系统,必须得贯穿操作系统什么意思呢,意思很简单,你必须得贯穿操作系统,没有所谓的操作系统,对不起,你不能直接访问硬件,因为操作系统是硬件的管理者,所以你必须贯穿操作系统,那么其中我们操作系统必须对上提供一个概念,叫做系统调用,所以呢,以前我们学习各种创建进程、线程、信号,包括我们的基础IO还有一些其他的接口呢,实际上最终都叫做系统调用,用来在系统层面上做相关工作。而同样的吗我们对应的有一层叫做应用层,应用层本来就是一层软件,它本质上一定是在我们的系统调用之上做的系统调用来完成对应的工作。所以呢,一个问题,网络和操作系统,严格意义上来讲,TCP/IP协议和我们操作系统之间的关系是:操作系统内部有一个模块,就叫做TCP/IP协议。那么换句话说呢,网络协议栈是隶属于我们操作系统的,这一点我们要注意。
》我们曾经也讲过,在操作系统在操作系统再往上还有什么呢, 是不是有一些库、编写的程序、shell外壳程序等,所以再往上是谁呢,那就是用户了。所以经常在系统部分说,我们有各种库、程序、指令、shell,再上面就是我们的用户了,用户可以使用这些功能来完成对操作系统的访问。同样的,我们应用层本质上也是和我们曾经的所谓的lib、指令、shell一样,也是需要被普通用户所使用的。所以用户是用来,使用应用层,来完成他自己的数据的转发功能,这就是我们操作系统和网络协议栈之间对应的关系。
》那么其中对我们来讲呢,我们更重要的是,网络协议隶属于操作系统,同时,它也是一套标准,它的这一套标准必须要我们每一个操作系统都必须遵守,包括你的手机。所以对我们来讲,未来可能是windows、linux等,但无论你是什么操作系统,你的底层一定要用你自己的方式来将我们的网络协议栈来实现起来。所以我们在这里如果有多台主机的话,他们要想通信,比如说一个是Windows,另一个是Linux,两台主机完成通信,一定是由用户发起,然后呢使用我们的应用层协议,这个应用层协议可以自己写,也可以用现成的,然后贯穿我们整个网络协议栈自顶向下哈,到我们的物理层,然后呢,再经过网络路由转发,被对方主机的物理层收到,交到对方的链路层, 依次上面的每一层提交,最后就达到了,对方的用户。所以此时呢,我们一定要清楚的是,网络的最终流向呢,一定是自顶向下,或者自底向上的。为什么要自底向下呢,因为我们的计算机最终发数据也要通过硬件去转发,用户层的数据无法直接抵达硬件,必须得贯穿操作系统,而你的网络协议栈隶属于操作系统,所以你必须得自顶向下交付给硬件,这是其一;其二,我为什么又要自底向上交付呢,原因很简单,因为发数据的时候,大部分的数据都是用户给用户的,所以你要给用户,你底层的硬件也没办法直接给用户,你必须贯穿我们的操作系统,以及对应的协议栈来讲数据进行交付。所以同学们,那**我们的体系结构直接决定,数据包在主机内部进行流动的时候,一定要进行自顶向下,或者自底向上流动的!**这一点我们并不陌生,为什么呢?因为,以前的所有的IO过程都是这样的!比如说你写磁盘,你要把数据写到磁盘的时候,不也是从用户层,再通过系统调用接口把数据交给操作系统,操作系统再通过自己对数据进行缓存,定期把数据经过我们对应的操作系统内部,把数据刷新到外设,刷新到磁盘上,那你读取文件的时候,本质上也是把磁盘上的数据再拿到操作系统内部,再通过系统调用接口在内部让用户拿到,所以我们以前所有的行为,其实全部都遵守这里的所谓的从上到下,自底向上这样的规则,只不过我们以前从来没有提过罢了。这是我们今天要讲的第一点。所以,同学们,我们要对他进行重新想象,这个过程我们并不陌生,以前我们经常说,同学你把数据写到显示器上,算不算你访问硬件呢,你们说算,那我们访问硬件的话,其中我要不要自己访问呢,是不是自己写到硬件上呢,同学们说不是,那我们怎么访问呢,我们说,操作系统是硬件的管理者,我们必须得自顶向下的把数据贯穿我们的操作系统,同学们,那时候我们都已经在讲自顶向下了,当我们读取的时候也是一样的。所以网络当中他要自顶向下也是很正常的,自底向上更加正常,因为这就是计算机它本身的工作方式,这是第一点。第二点,上次我们谈过一个问题,问题是这样的,我们是说过,网络协议栈是分层的,比如说我今天给同学们打电话的时候,给你打电话,比如说我给手机说话,你从你手机听到说话,那我们是不是问了一个问题,同学们,你们认不认为是在给你直接通信呢,同学们,说是的,你在给我直接通信,然后我说,实际上我们并没有直接通信,而是我把我说的话给了手机,我的手机把对应的数据交给了你的手机,你的手机把数据转化之后才听到我的声音的。但是呢,我们上面阐述了一个结论,这些细节作为平民老百姓压根就不关心,什么意思呢?我们上面产生的第二个结论就是,同层协议都认为自己在和对方直接通信。什么意思呢,意思就是说这里对应的用户,自己认为在和对方的用户直接通信,那这里的应用层协议呢,它呢,认为自己在和对方的应用层在直接通信,这里的传输层呢,认为在和对方的传输层通信;这里的网络层呢,认为自己在和对方的网络层通信,以此类推。所以同层协议,都认为自己在和对方直接通信。我们对应的呢,可以认为,这里的应用层呢完全不关心底层怎么做,好像我们打电话的时候,从来不关心电话是怎么把消息发到你那里的。上层协议呢永远只是知道我调用什么接口,能拿到什么结果。就好比我知道,我呢给手机按下号码,这叫做我们称之为给手机传某些参数,然后再点击发送,那么就可以去拨打电话了,它里面的工作细节我们从来不关心,我们知道跟我们没关系,每一层呢,都认为自己在和对方直接通信。
网络基础(一)_第6张图片

》第三点,我们要来进行重谈协议了,你说的挺好的,反正就是第二点我还得慢慢体会一下,其实第一点绝对能理解,但第二点不一定,第二点告诉你同层协议都认为自己在和对方直接通信,这个呢是要需要一定程度的想象力的, 也就是说,同层协议是忽略底层协议的一些细节的,我呢知道调用你底层的一些方法,我就能得到什么样的结果,但是你的工作细节我不关心,但是我呢,比如说我是传输层,我的任务就是把我的数据层层交给对方的传输层 ,应用层呢就是把它的数据再交给对方的应用层,就跟快递员一样,我们以快递员为例,你在网上跟对方的店家,比如说店家是卖电脑配件的,你买了一个鼠标,你此时在店家下了单,你在通信时是跟店家沟通的,你沟通完之后呢,底下的细节你是不关心的,因为你知道你沟通完,你把钱付了,对应的卖家一定会把东西发过来的,然后呢,卖家跟你沟通了, 然后此时调用了底层的接口,就是快递员填了一个单子,把东西邮给你,邮给你之后呢,其中你是不关心底层快递员是怎么过来的,你不关心,你只关系什么时候给我送到,你商品出现任何问题,你永远都不会找快递员而是直接会去找卖家,因为卖家这个人和你是同层的,这点要注意,第二点需要大家慢慢体会。我们写写代码就能理解了。
》第二个我们说协议的时候,说过一句话,协议是一种约定,什么是约定呢,就是双方约定好,某一种行为代表什么含义,比如说电话响一次代表的是报平安,电话响两次是没钱了, 响三次的话你再接,这叫做约定。同学们,那这里就有一个问题,所谓的约定,那么有没有成本呢?也就是说,**在计算机的视角如何看待协议呢?**也就是,你两个人之间的约定有没有成本呢?两台计算机之间的约定有没有成本呢?以及两台计算机你说约定好就约定好,那么两台计算机的约定是谁来约定的?最后这个约定是怎么体现出来的呢,你不能光拿嘴说是由约定的,你得有东西来证明它。
》下面呢给大家举一个例子,同学们,有没有可能你会忘记约定呢?我们得有一个人,后来忘了,生活当中处处存在忘事的人,当然明天答应给人送报告但是忘了等等,你做的约定有可能忘嘛?答案是:有可能。还有什么,约定既然能够被人遗忘,说明约定一定要被人记住,它是有成本的,什么意思呢,你自己就得把曾经双方的约定记下来,同学们,约定什么了,时间、地点、人物、事情等等,本质上人类世界里的约定占你的大脑的存储空间的,不管是永久的还是临时的,反正会占用你的空间,那么我们双发在大脑里面 构建了一个约定的数据结构来进行我们约定的处理,只有我们双方脑子里都记下来我们曾经的约定的细节,我们最后才能把事情做成,这是人,计算机呢,它如何去看待所谓的协议呢?记住了,协议呢实际上在计算机当中,我们双方能够根据协议去办事,就一定是,1. 体现在代码逻辑上;2.体现在数据上
》给大家举一个例子,你们经常呢,有网购的经历,比如说呢,今天就买了一个键盘,买了一个键盘,快递员会不会千里迢迢到你的楼下给你打电话,然后呢当东西到了给你打电话,你下了楼之后,快递员给你把键盘给你,会不会呢,你就是买了一个键盘,快递员就拿了一个键盘给你,现实生活当中是不是这样子的呢?并不是!实际上当你收到了东西的时候,你是收到了一个包裹,**你和卖家沟通好要买一个键盘,实际上快递员不是光光的给你一个键盘,而是一个包裹,包裹里面是有键盘的。**那么同学们,对我们来讲呢,我们就要一个键盘,但它实际上给我的是一个包裹。说明我要一个键盘,实际上,它多给了我一些东西,我要的是键盘,但实际上当我拿出来的时候,**多给我了一些东西,多给我们什么呢?多给了一个盒子,当然今天的盒子不重要,因为所有的包裹都有盒子,但是所有的包裹上总是会贴一个东西,叫做快递单。那么换句话说呢,我们就想要一个键盘,但实际上当我们收到东西的时候,会比我期望的要的东西会多一些,多出来的东西呢就是我们的快递单。那么这个快递单上呢,其中快递单上呢填的是一些数据,这些数据有什么用呢?这些数据写着,发货人是谁、发货人的联系方式等等,这个数据呢,这个快递单呢,实际上,当你收到这个快递时,你压根就不关心,但是为什么还要贴上纸呢?答案是:快递单子不是给你用的,是给快递公司用的,这个单子呢其实给我们起的是路由的功能,或者呢是帮我们定位你这个人的功能 。其中我们把快递单子多出来的数据,那么它就叫做快递公司和快递点以及快递小哥之间的协议!**换句话说呢,我们的快递单呢其中是让快递公司来进行,和快递点之间和快递小哥之间定的协议。包裹当中的各种细节呢不是给你看的,而是给别人看的。所以对我们来讲呢,其中上面我们对应的快递单子呢其实是包裹在我自己的一个键盘盒子上多出来的一些东西。那么这件事情说明什么呢?说明我们告诉大家,实际上你在网上买东西的时候,你就想买一个键盘,但是你会多出来一部分东西,多出来的这部分东西是一部分数据,这部分数据就是我们为了维护或者干脆就叫做快递公司定制的协议字段。每个字段是什么意思,同学们,一个不认识字的快递小哥,能做快递员吗?答案是:不能,它上面的字都不认识,上面单子上派发给谁,客户叫什么名字呀,它可能都做不到。所以,同学们,这里一定要注意的点叫做,我们快递单上的数据呢,他就是快递公司和快递点,快递点和快递小哥之间定的协议。
》所以这里我们想说一个,为了维护协议,一定要在被传输的数据上,新增其他数据,这个新增的其他数据,叫做协议数据。我们要注意,任何一个这里对应的,只要他有协议,必须得新增自己的协议数据。也就是说,我们可以看到,协议一定会体现在某种意义的数据上,这是第一个。第二个,快递公司根据你的协议,它一定是要能够,根据数据有不同的动作的,什么叫做不同的动作呢,就是根据不同的协议,要能够派发我们的包裹,将包裹从北京邮递到上海等等,不管怎样,它一定要根据包裹上的字段完成对应的动作。这个呢其实体现在代码逻辑上,举个例子,同样的快递单,大家比如说,采用顺丰或者菜鸟,他们采用的是同一套物流系统,所以呢,不管是谁填的单子,中间哪一个物流公司它都能物流,其中对我们来讲,为什么呢,因为他们能够根据协议上的数据来执行公司内部的物流策略,然后完成数据包的转发,这就叫做代码逻辑上。当然代码逻辑上的体现,暂时还没有这个感觉,你可以简单理解成,快递小哥拿着你这个快递,他一定给你打电弧,而不是给其他人打电话,为什么呢,因为快递小哥,根据单子上的数据,决定了他的动作就是给你打电话。所以同学们,我想告诉大家的最终结论呢,就说,我们的协议呢,一定在传输的数据上新增其他数据。这个多出来的,就叫做协议数据。
网络基础(一)_第7张图片

》同学们,再继续,你刚刚说的,1.同层协议都在认为直接和对方通信;2.体系结构决定,数据在流动时,一定是自顶向下,或者自底向上流动。好的,既然同层协议都认为自己在和对方通信,那么这就直接决定了,每一层都要有自己的协议。就相当于什么呢,同层协议呢,都认为自己在和对方通信,我们两个,不管你是在网络层还是什么的,我的网络层和对方网络层通信,两个网络层开口说话了,然后呢,一个网络层说,我给你发了消息,你注意一下。其中我这个网络层会不会关心我上层是什么,下层是什么呢?不会关心,我只能为我在和对方的网络层通信。所以,同学们,同层协议呢,要直接通信不是拿嘴来说的,而是同层的协议呢,每一层都要有自己的一套协议规范。那么我们可以得到的结论有三个了,1.数据要进行流动,方向是自顶向下或者自底向上,这是其一;2.同层协议呢,都认为自己在和对方协议,直接通信,所以要有自己的协议;3.一定要在被传输的数据上新增其他数据。
》我又不懂,我根据你这3个结论又能得出什么结论呢?所以我们的数据呢,它在进行自顶向下,自底向上进行转发的时候,这里可以是向下交付,也可以向上交付。现在,我们有两套套完整的协议,就相当于是两台主机了。假如,我们有一个数据叫做“你好”,当我们要发送一个 你好 的时候,首先,从上面的结论我们知道,这个 你好 的信息必须要被对方,必须得自顶向下交付到自己的底层,这是其一;其二呢,交付给底层,要一层一层过,而每一层都认为自己在和对方通信,每一层都有自己的协议,而协议呢是要在自己要传输的数据上,新增协议数据,所以,‘你好’在从用户这里产生,到应用层的时候一定要添加应用层的一部分协议数据,然后当我们这一部分协议数据呢,经过路由转发,到了我们对方的应用层的时候,对方应用层也就能看到对方的应用层添加的协议报头。就好比,我们作为人来讲,是不可能跟一条狗或者一条猪做约定的,因为我们是属于不同的层级,大家所属的层级不同呢,语言上也就没有共识,它也听不懂我们在说什么,所以无法建立我们的协议字段。当我们属于同层的时候,我把我的数据交给对方,其中呢,因为我是把我的数据交给了你,我不仅仅是把数据交给了你,我们为了要让数据被你所识别,被你所处理,我们还要在被传输的“你好”数据上一定要新增一些其他数据,这个数据,我们到了应用层加进来,应用层要继续调用系统接口继续向下交付,为什么一定要继续向下交付呢?原因很简单,因为体积结构规定了他必须得自顶向下交付,每一层协议都要有自己的协议,都要保证自己在和同层进行通信,所以每一层都要维护自己的协议,都要添加自己的协议的信息。所以到了我们的传输层之后,传输层说,知道了,我要给对方发消息,我要添加我的一个协议有效数据,添加完之后呢,传输层他也不管,它只知道我把数据呢调用接口交给下一层,下一层就能够直接收到,数据被对方是怎么转发或接收的我不管,我传输层在认为自己和对方直接通信,所以我自己填了自己的协议字段之后,同层协议就能通过,要收到的这部分数据呢,多出来的协议字段来决定对方传输层收到的是什么信息。同样的传输层还得继续向下交付,此时我们又必须得添加网络层的多出来的字段,然后又向下交付给数据链路层,添加对应的字段,此时有交付到了硬件,硬件我们不考虑,然后就到了对方的物理层,被对方收到,对方收到,然后将数据向上交付到数据链路层,到了数据链路层呢,当他收到数据时他收到的数据其实是上面几层要看的协议数据,也就是你实际收到的数据比你期望收到的数据要多,就好比你要收到的键盘里面会在快递包裹里面多一张快递信息。你数据链路层此时收到了数据,你将上面几层要的协议数据提出来,这是对方给我发的消息,就好比,当你拿到快递单子的时候,快递单的细节你其实不关心,但是呢,你得确定这个快递单子是不是发给你的,所以你首先要读取快递单子,然后发现上面写的是你的名字,那这就是我的快递,你也要处理这里多出来的协议字段,处理完之后,此时你要做的就是把数据向上交付,为什么你要向上交付呢,原因是体系结构规定的,因为我们现在正在完成的是用户和用户之间的通信,所以你从上到下把数据给我,传送到了对方,对方必须把数据向上交付,因为对方必须得吧数据交给用户,这是由体系结构决定的。所以交付给网络层后,他要确定是不是发给我的,所以他收到的信息是上面几层要的协议数据,他要的是自己对方网络层发的协议报头,然后他会去掉自己要的报头,就好比你发现包裹是你的,然后你就把包裹拆了,拿出自己要的,拿出来之后呢,收到的数据继续向上交付,传输层拿到对方传输层添加的协议字段,我知道了,然后再把自己的报头拎出来,然后将其余数据继续向上交付,此时应用层知道了,我是直接和对方应用层通信的,因为我们是同层都认为自己在和对方通信,所以我们每一层都要有自己的协议,两个应用层之间定好的协议数据拿出来自己要,多出来的数据自底向上继续交付。所以用户发的是“你好”,对方收到的就是你“你好”。用户发的“你好”,然后交给应用层,添加自己的协议数据,然后对方应用层收到的就是同层的协议数据以及要交付的数据,以此类推。
》那么其中,我们把多出来的,新增的协议数据,在我们每一层协议交给下一层之前,添加上自己当前协议数据的这部分内容,即我们把每一层要交付给下一层数据,给他添加上本层“多出来的协议数据”(不太严谨的说法)拼接到原始数据的开头,那么此时我们把这部分多出来的数据,我们可以称之为“报头”,所以我们协议当中,报头就是用来描述某一层协议的相关字段的,那么其中每一层都要有自己的协议,每一层都有属于他当前层的报头结构,就好比同学们,你收到的快递单子,单子上面写的东西,他其实就是对应的报头,每一层协议都有自己的报头,所以我们在自顶向下交付的时候,每一层协议都要添加自己的报头字段,那么其中我们把这个动作叫做封装的过程。也就是说我们封装。也就是添加报头的过程,就是对数据包进行封装的过程,然么其中当我们的数据到了对方层,在进行把他解出来,自底向上交付的过程,我们同层都是把自己的报头去掉交给上层,我们这个就叫做解包的过程。
网络基础(一)_第8张图片
》我们就根据计算机的体系结构帮助大家理解,所以能不能理解一定要自顶向下交付呢和自底向上交呢,原因是体系结构决定的,你要用网络通信,你必须得把应用层交给底层,因为只有硬件才能收发数据,这是一定的,这是第一。第二呢,每一层都认为自己在和对方层直接通信,通信呢,我们的本质是,通信的双方进行添加报头,所以每一层都要有自己的协议,背后的含义就是每一层协议都有自己的报头,其中自顶向下交付的时候一定要不断添加报头,然后到了对方的时候,要不断的自底向上不断的把报头再拆出来,这就是我们的一个 “封装” 和 “解包” 的概念。

网络基础(一)_第9张图片
下面呢有两个协议栈,以后当你看到有两个协议栈的时候,你首先要想到的什么,这两个协议栈,一套操作系统有一套协议栈,这是两台主机,你要想到这一点,这是其一;其二呢,我们就是要考虑到一点呢,就是双方进行我们所对应的通信的时候,我们左侧和右侧是两台主机,这两台主机之间呢,通过协议栈来进行我们所对应的通信,那么其中我们把下三层(传输层、网络层、数据链路层)是隶属于操作系统的,而我们应用层称之为用户进程,它呢是属于用户的,我们称之为应用层,至于处理通信细节我们会每一层每一层去讲。那么其中对我们来讲,我们就看到有两台主机之间通过局域网联通,那么此时又有一个小问题需要和大家确认一下,这个问题问一下吧,如果两台主机属于同一个局域网,那么这两台主机能够直接通信吗??告诉大家:同一个局域网内的两台主机是可以直接通信的。给大家讲一个故事,以前写完一个代码在一间房间里,代码的测试就可以在局域网一起测试了,这是其一;其二不知道大家以前有没有打过竞技类游戏,有些游戏需要联网,比如CS,只要同一个网段里面你就可以直接在一起玩了。其实我们在同一个局域网就能够直接通信了,学校不让上网怎么办呢,没关系一栋楼的少年们有自己的娱乐方式,电脑一连,反正大家都在局域网,互相就可以通信了。首先我们要告诉大家,如果在同一个局域网当中,我们两台的主机是可以通信的。第二个,我们其中最常见的所谓的局域网呢,有一种局域网称之为叫做以太网,它是一种局域网的标准,当然它里面的一些工作细节和工作原理呢后面会说,但现在我就告诉大家,它名字叫做以太网。你要清楚一点的就是,在局域网当中所有的主机能通信,本质上也是要有局域网的一套标准的,这个最常见的标准就是以太网,还有呢就是令牌环网或者无线LAN,我们拿手机在局域网当中就属于无线LAN,不重要,以太网呢是我们局域网当中的一套标准。
》这个以太网呢,给大家今天不谈它的一个就是内容,严格上他是怎么工作的我们还谈不了,稍后会给大家谈一点点。但是呢我想给大家说的是,以太网这个东西呢,它的名字就叫做以太网,有人说令牌环网这个东西我也不太懂,然后无线LAN就更不懂了,但是以太网呢我们首先不是想他是谁怎么通信的,我总是觉得他的名字特别奇怪,就是说他对我这个网为什么,叫做以太网呢。其实是有一个故事在露面的。在20世纪初,在物理学界有一个特别重要的争论,争论是什么呢,任何事物的传播是要有介质的,就好比我们能听到对方的声音,本质上是空气帮我们传播了声音。所以在任何地方呢,声音的传播是需要有介质的,所以很多物理学家呢就想,既然所有的传播都需要介质,那么光的传播是更加需要有介质的,所以这个时候呢,科学家呢有一个问题,太阳和地球之间有段是真空的,那么是怎么走到地球的呢,根据正常思维,不是说任何事物的传播都是需要有介质嘛,所以有科学家猜想说,我们看到的宇宙是真空的,但是不一定是真空的,只不过填充了某些物质没有被我们发现,只是我们认为看不到罢了。所以当时就有人猜想,宇宙当中是充满一种物质的,个这个物质呢取名叫做 “以太”。最后有科学家想要论证是否存在以太这个物质的,本来要做几个月,结果做了两三天就做不下去了,因为所有现象表示这个物质并不存在。所以这个事情相当于被证伪了,也就是并不存在。后来就称为物理学界的一个笑话,因为当时科学家对此观点保持肯定,认为肯定存在以太,而且一旦以太被证实,我们物理学所有的理论全部都落地了。反正物理一直存在的笑话就是以太的存在。在五六十年代的时候,就有一大批网络工程师开始局域网通信了,当开始对应的通信的时候,构建了一个局域网能够通信,可是这些工程师工程师在想给他取一个什么名字呢,不是物理学界有一个笑话嘛,那么我们就致敬一下吧,所以命名程以太网叫法。所以以太网名字由来还是值得一提的。
》所以以太网是一种局域网的通信标准,这是其一;其二呢,这里有两台主机可以通过以太网来进行通信,以太网的工作方式我们待会儿给大家提一下。但是我们今天不在于细节上,而是能不能通信。所以我们数据在流动的时候,根据我们刚刚所讲,通过体系结构的学习呢,数据要发送到对方的主机,一定要自顶向下交付,那么数据被收到的时候,一定要自底向上进行交付,那么其中同层协议之间要有协议字段,协议就是在原始发的数据之上多出来的一部分数据,一定要进行封装,到达对方之后,一定要进行我们对应的解包,那么其中对我们来讲呢,这就叫做封装和解包这样的概念。那么其中呢,我们这就叫做局域网通信。在我们局域网通信呢,就要稍微谈一谈
局域网通信的原理
。我们下面来谈一谈一个原理,稍微说一下。
》我们要引入一个概念,局域网通信,尤其是以太网局域网通信,它的通信原理是什么呢,比如说,老师和同学在一间教室上课,突然在班上挑一个人,叫张三站起来,你为什么上次的作业为什么没完成?让张三这名学生回答问题的时候,你们听到了吗?听到了,问题是,你们为什么不站起来呢,有人说,叫的又不是我,我站起来干什么呢。所以张三颤颤巍巍的站起来,解释说作业完了,那么此时张三和老师说话的时候,请问你听到了吗?答案是:你也听到了。其中所有的谈话你都听到了,但是你为什么不处理呢?原因很简单,因为他们的谈话不是发给你的,但是当老师根张三进行一次互相通信的时候,老师和张三认不认为他们在单独通信呢?也就是说老师把张三叫起来,两个人在不断说话的时候,张三也在回复,他们两个在通信的时候认不认为在教室里面单独通信呢?答案是:认为。但是他们两在单独沟通的时候,旁边有一大群的吃瓜群众,其实也能够听到这个声音,这就叫做局域网通信的原理。换句话说。在刚刚讲的例子当中呢,其实有几个细节要一个个说一下,第一个细节叫做,在教室里面呢,一定要有名字,要不然没有唯一性的标识的话,怎么能让张三站起来呢,同时其他的同学怎么能证明,这个老师叫的时候不是叫的自己呢。所以同学们正如我们在教室里面每一个人都有一个唯一的名字一般,即每一台主机都要有唯一的标识,那么我们把每台主机上唯一的标识叫做该主机上对应的MAC地址。也就是说每一台主机上呢,对应的至少要有一个MAC地址,这个MAC地址呢实际上是叫我们的网卡地址,也就是我们对应的机器电脑在出产的时候配套的装有网卡,网卡上面呢已经内嵌了该网卡所对应的MAC地址,这个MAC地址主要工作在局域网,用来在局域网当中饭标定主机的唯一性,这是第一;第二,第二天又来教室和同学们愉快的上课,在正在上课的时候,突然喊,张三昨天叫你做的作业,今天怎么又不做,当叫张三的同时,李四王五都在大声的沟通,赵六和钱七打起来了等等,我们的课堂秩序非常的混乱,此时叫起张三的时候,张三此时压根就听不到老师说的话,那么其中我们在封闭的环境当中呢,任何一个人在说一句话的时候,比如说,张三在说一句话的时候,被同时其他的同学也能听到,那么其他同学不说话的时候,其他同学能正常听到老师讲的话,但是所有人都在说话的时候,一个接收到的信息是会互相干扰的,所以我们一般在局域网当中任何一台主机在任何时刻都可以说话,就好比在课间休息的时候,在教室里面,任何时刻,它都可以说话。那么其中任何一个人,在任何时刻,都可以随时发消息。就好比在教室里面,每一个同学都可以给对方给任何一个人说话 ,那么此时我们把这种局域网称作碰撞领域的概念。也就是局域网内的信息可能会互相干扰,有碰撞域的话就无法准确的听到对应的消息。就好比上课期间,秩序良好的时候,老师和张三沟通的时候,同学们是不说话的,张三在跟我说的时候,其他同学也是不说话的,那么我跟张三认为是成功沟通,可以想像一下小学、初中应该会有一个早上6、7点你们会晨读,当你在早读的时候,你在读的时候,旁边同学也在读,你在外面听到的声音就是乌拉乌拉一大片,压根就不知道在念什么,这叫做信息发生了碰撞,那么其中对我们来讲呢,在任何一个局域网当中,任何一台主机都可以直接向对方发消息,一旦发消息此时信息会互相干扰,我们就称之为碰撞域,一旦有了碰撞域,任何人都会发,这个时候就出问题了, 同一个局域网里,任何主机想要通信,那是不可能的,因为我已发对方也发,信息就干扰了,怎么办呢,没关系,所以呢我们就有一个叫做,同学们可以理解成是什么呢,对我们来讲呢,我们的工作方案就是这种,基于概率去碰撞,然后向局域网去发消息。但是呢每一台主机呢要能够识别到局域网当中是碰撞的,发生了碰撞就好比我今天在跟张三说话,张三说的时候也正在给王五说,王五也在跟赵六说等等,当我发现想说的时候,突然发现别人也在说话,那我就不说话,让别人说完,每一个人都执行别人的原则,那么其中我们识别发生了碰撞(碰撞检测),我呢看到别人说话,我就不说话了,那么此时我们做的动作就叫做碰撞避免,听到没有人说话的时候,再说。我们任何时刻都只能有一个人在说,两个以上的人说话,信息干扰了,那么就要执行信号碰撞检测和碰撞避免,这就是局域网通信的原理,。首先我们要清楚,在局域网当中两台主机是可以直接通信的,所以每一台主机在局域网当中有唯一的MAC地址,唯一的MAC地址呢,所有的主机就可以根据局域网向指定主机发消息了,这就叫做局域网通信的概念。刚刚讲的这个呢,我们把原理一说,到后面再给大家讲MAC帧的细节的时候再谈,总之只要能理解将来以太网是能够通信的就可以了。
网络基础(一)_第10张图片

》所以两台主机之间要进行互相通信,第一它要贯穿我们所谓的协议栈,这个是由体系结构决定的,所有的数据都必须得从上到下,从下到上,对于磁盘读取是这样的,任何硬件都是这样的,不仅仅是网络。其二呢,我们为了能够正常通信,我们每一层都要封装它的一个报头,为什么每一层要报头呢,因为每一层协议都和对方的同层协议定了协议,定了协议就得在沟通的时候多携带一些我们多出来的数据,这是第二个;第三个呢,两台在局域网当中通信的主机是可以直接通信的,当进行通信的时候,自顶向下交付,要完成封装,在局域网当中可以把数据交付给对方,在局域网当中呢,一、可以直接交付;二、工作原理呢,同上课一样,在一间教室上课呢,A给B说话能够听到,B给A说话能够听到,只要大家不同时说就可以了。
》给大家再延伸一步,我们再换一个视角。为什么两台主机不能同时向网络里面发呢?其实很简单,你再发的同时,别人也在发,这在硬件上都是光电信号,光电信号之间相互干扰了,互相干扰的时候呢,其中,你在发的时候别人也在发,同学们,你在发的时候,别人也能发,你在写的时候别人也能写,你在访问的时候,别人也能访问,所以此处这里的以太网,我们如果换一种视角,站在系统角度,两台主机你想发我也想发,我们两能够干扰本质是我们访问的是同一个以太网,所以此时在局域网当中,这种公共能够被大家同时访问的资源,我们叫做什么资源呢?叫做临界资源。所以本质上在通信的时候,要保证信息不互相干扰本质上是保护临界资源当中数据的一致性,再比如说,当我们实际上访问的时候发生了碰撞,发生碰撞之后,然后我们执行碰撞避免,这叫做让我们访问以太网时,串型访问的其他策略。所以我们为什么说碰撞避免,碰撞了我们就要实行碰撞避免,然后呢,没有人发的时候我再发,也就意味着,没有人在使用以太网时我们再使用,这叫做什么呢,这就叫做我们站在系统角度去重新看待它。所以以太网本质上在局域网当中,可以把以太网看作成两台主机之间的临界资源,访问临界资源的代码呢我们就叫做临界区。那么任何人想要访问临界资源,如果出现冲突,那么我们把数据作废,我们双方延迟一下再重新发送,这就叫做碰撞检测和碰撞避免,同学们跨主机之间保证临界资源,数据一致性,大家可以看到和同一台主机内做法就不一样了,有人说两台主机之间加一把锁,有没有呢,有的!只不过,主机A和主机B 两个之间呢,要互相进行我们对应的进行竞争这里的以太网资源,那么因为你要加锁,前提条件先要让这两台主机看到同一把锁,现在两台主机没有同一把锁怎么办呢,那么你也就没办法加锁,但是我们可以在主机内呢,可以有一个,不知道同学们还记不记得我们讲互斥锁的时候,我们说互斥锁就是拿CPU内的寄存器和我们内存当中做交换,那么如果我们规定一种特定的数据格式,那么拿到特定的数据格式的主机才能互相访问发消息,那个东西其实就相当于一把锁,对应的通信标准呢就是令牌环网。所谓的令牌环网当中,只有拿到令牌的人才能向网络里面发消息,没有令牌的人不可以。同学们,拿着令牌的人不就是申请锁成功了吗。所以我们正式也引出了MAC地址。

认识MAC地址

·MAC地址用来识别数据链路层中相连的节点(是用来识别主机的地址);
·长度为48位, 及6个字节. 一般用16进制数字加上冒号的形式来表示(例如: 08:00:27:03:fb:19)
·在网卡出厂时就确定了, 不能修改. mac地址通常是唯一的(也是全球唯一的)(虚拟机中的mac地址不是真实的mac地址, 可能会冲突,但是不重要,重要的是我们知道MAC地址在一定程度上要保证他的全球唯一性。知道同学们有一个困惑,MAC地址不是局域网内用的吗?你怎么搞个全球都唯一呀,这个说来话长,首先MAC地址必须唯一,每一个厂商不能生产出一样的,虽然是在局域网工作,但是你必须这样做; 也有些网卡支持用户配置mac地址)

IP地址和MAC地址

下面我们再进入下一个,如果两个主机之间通过,我们可以称之为叫做跨网络呢,那么数据帧又是怎么流向呢?一会儿把这个一说,把IP地址一引入、封装解包概念和分用,那么我们网络基础一就结束了。
》刚刚给大家提了一种地址就是MAC地址,它呢在我们的机器当中呢是我们网卡硬件的地址,如果我们想查看我们的MAC地址怎么去查呢?云服务器输入#ifconfig。
》下面我们再来谈下一个概念,如果我们两个主机通信通过协议栈叫做跨网络传输呢,跨网络传输呢,如果有两个局域网,两个局域网通过路由器连接起来。那么其中对我们来讲,数据呢从上到下进行通信,自底向上进行交付,大家知道左边一个协议栈,右边一个协议栈说明是两台主机,这两台主机呢是属于不同的局域网的,不同的局域网呢其中我们用路由器连接起来,图中虚线框框里面的就是路由器,同学们,只要它是一台路由器,这台路由器只要能够在两个网络内,跨网络传送数据的时候,这个时候,这个路由器一定是至少具有两个网络接口,甚至是两张网,这两张网卡一定是连接左侧的一个网络,还连接另一个右侧的网络。所以此时要把数据帧转发到对方,前期是你得把数据帧自顶向下转到路由器上,到了路由器,再经过路由器转发到另一台机器上。所以对我们来讲呢,我们是可以把我们讲的路由器呢,当作左侧的一台主机,在另一个局域网当中也当作它的主机,所以路由器它是横跨两个局域网的,只有你横跨两个局域网,身处两个局域网当中,才有可能完成我们数据的路由转发功能。今天我们不谈细节,但是我们常识应该知道,比如说上层用户发了一个“你好”这样的信息,它呢经过我们的用户发下去,交给了我们的FTP层,FTP层呢要添加我们所对应的报头,添加好报头之后,它也一定要向下交付给我们的TCP层,TCP呢他也要添加自己的报头,然后继续向下交付,到了IP层呢也要添加自己的报头,继续向下交付给我们的以太网驱动程序 ,它当然也要添加自己的报头,添加好之后呢,此时所有的数据转发到我们的路由器当中,转发到路由器,一定是通过我们的以太网,因为这个路由器属于这个局域网当中的一台主机,通过以太网转发到所对应的路由器当中的网卡,至此我们就把数据交给了路由器。
》这个时候必须得引入另外一个概念了,就叫做IP地址。下面为了让大家对IP地址有更好的理解呢,给大家讲一个故事,我们呢可以发现一个问题,假设我是一个北京人,假设我的朋友在云南,比如张三和李四,张三说,李四赖我云南玩呀,所以此时我就特别想去云南玩,当我想去云南玩的时候,我此时并不能直接从我们的北京到我们的云南,有人说坐飞机不就完了,暂时不考虑,假设我们就要坐高铁或者火车去玩,我们去云南首先要做的就是从河北到山西,从山西到伤陕西,然后从陕西到云南。那么我的问题是我在北京的时候想去云南,下一站去的是山西呢?为什么不是黑龙江呢。这个问题呢,得把根给大家说清楚,答案很简单,当我们到了山西之后,山西一个朋友说,你从哪儿来,你说你从河北来,他说你怎么来山西呢,你说,我要去云南。换句话说呢,我们从河北到山西到时候,别人问我从哪儿来,然后告诉别人说我们从河北来的,然后别人问我你要到哪儿去呀,为什么要来山西呀,你回答说是要去云南。所以当我们从山西到了陕西之后呢,又碰到了一个陕西的老铁,陕西老铁聊的很开,你哪儿人,从哪里来的,你说你是北京人,从河北过来的,陕西老铁说,你为什么来我陕西呢?然后你说,因为我要去云南。同时呢,我们陕西老乡呢有多问了一句,你上一站从哪儿来的呀,你说,我上一站是从山西来的,然后这一站就到了陕西了,那么其中当我继续到四川再到云南的时候,那么其中我们到达了我的目的地,其中这个过程就叫做我的路径选择,这是一个生活当中的常识。下面呢,结合这个常识再举一个例子 ,我们看过西游记,西游记里面呢,唐僧每到一个地方,别人都会问他两套话,第一套话,和尚你从哪儿来到哪儿去,然后他会说,我从东土大唐而来,我要去西天取经;第二套话呢就是,和尚你上一站从哪儿来,下一站到哪儿去?上一站我从A,下一站我要去B。所以呢,我想告诉大家的是,常识告诉我们,我们一般在进行路线选择的时候,我们身上一般有两套地址,**1.从哪里来,到哪里去。2.上一站从哪里来,下一站去哪里。**就好比呢,我在河北,然后呢,我从哪儿来呢,我从河北来,到哪儿去呢,我要去云南;那你上一站从哪儿来呢,我上一站从河北来,下一站计划去山西。那么请问,我下一站去哪里由谁来决定?是由我们要到哪里去决定。换而言之,我们把2这一套,上一站从哪儿来,下一站到哪儿去,这种地址我们称之为,MAC地址。而我们的从哪里来到哪里去,称之为IP地址。所以我们的地址从粗略程度划分上有两套,一套是IP,另一套就是MAC了。我们细细的去划分呢,可以划分为原IP地址,和目的IP。其中呢,我们得MAC地址细细划分呢,划分为,原MAC地址,和目标MAC地址。其中呢,当我们自己在进行我们的,假设我自己就是数据包,其中我要去云南的时候,我为什么要去山西呢?原因很简单,我们的目的IP是云南,所以我必须得把我下一个MAC地址按照目标来设定,下一站我就应该去山西。
》所以对我们来讲呢,我们今天很简单的道理,你今天要把你的数据交给对方,对方没有直接和你相连,你要把数据交给他,所以他的IP地址,假设叫做IP A,接收方呢叫做IP B。所以对我们来讲呢,讲清楚非常关键的一个点,我们标识一台主机是IP A,另一台主机呢叫做IP B。所以给一个定义,什么定义呢,MAC地址,用来在局域网当中用来标定主机的唯一性IP地址,用来在广域网(公网),标定主机的唯一性(不太严谨,我们暂时先不管)。也就是说呢, IP地址解决的是我的数 据包从哪儿来到哪儿去的问题,所以当我有了一个数据包,经过网络,你要把数据包交给对方主机,那么前提条件是你得把主机先交给和这个主机同样所在的局域网里面的路由器,你不可能直接交给对方。我们的数据呢,其实是进行转发,必须得先交给路由器,再由路由器交给对方。你不能跨越我们的路由器直接把数据交给对方,是做不到的。所以对我们来讲呢,我们其中要在我自己的IP层,我要决定我的数据包该怎么走。该怎么走呢,我发现我要去的主机是IP B,所以此时,我知道我必须得先把我的数据转发交给我们的路由器,其中呢,我们在将数据进行向下交付的时候,在IP层就做好了路由选择,就好比你出发前和你朋友商量,你说你已经到了陕西了,下一站到哪里呢,然后你朋友说去四川,此时你继续向南走。其中呢,就好比我们的主机呢,当前数据包交付到IP层了,识别到自己要去的是IP B的主机,所以识别到不在同一个网段,然后一定是跨网络的,所以要将数据交给路由器,所以呢就通过以太网直接交到路由器。记住了,当我们把数据交给了以太网,一定是以太网底层的数据链路层先拿到,就好比当我们任何时刻,自顶向下发数据,收发的时候,对方收到数据的,一定是对方最底层的物理层先收到,收到之后要做什么呢,路由器是工作在网络层的,所以它收到数据的时候也要向上交付。因为是以太网,他们都是用的以太网驱动程序,所以最终呢,数据帧呢被以太网驱动程序层收到,收到之后就要解包,解包之后向上交付给IP层,对于他来讲,他也要路由呀,也要识别下一站我要去哪里,此时路由器是被到,你要去的IP呢就是IP B,和我的路由器是直接连着的呀,所以此时这个路由器就要把数据帧继续向上交付吗?不是的,是交付给令牌环驱动程序的,添加令牌环驱动程序的报头,因为可能用的是不同的网络标准,没关系,此时报头加完之后,就将数据发送对方,对方收到之后呢,此时确认了这个报文就是发给我这个主机的,怎么发呢,是不是就是向上交付了呀,怎么交付呢,那么一定是去掉同层的报头,进行解包,然后再交给上层,交给上层之后,上层识别到,你这个报文当时写的时候,填充的IP呢是要去的主机IP B,此时就拿数据里面的IP B和我的IP做对比,确实是发给我的,继续向上交付麦,当然也要把自己的协议数据给去掉对不对。交付到TCP后,也要识别自己的报头,再进行解包,然后再向上交付,交付给应用层,应用层再解包,交到了用户。所以你会发现,经过这样的转发之后呢,同层协议,尤其是IP层,这一层要发的数据包,就是对方收到的数据包,其中我们这个呢就称作,我们可以跨网络传输。很细节的一个点就是,当我们的数据包呢,发到局域网当中,被我们路由器的某一个网口,即以太网驱动程序收到,收到之后向上交付给路由器的时候,路由器是要解包的,路由器再重新进行路径选择,发到对方主机时,要重新封包的,交付给令牌环网驱动程序(不是说就是令牌环网,看用的什么网络标准),封包之后,对方收到的,就是我要发的,然后此时,我们就完成了数据的转发。其中最关键的是,我们看到了一个细节在IP层,即往上看,也就是在发送方的IP层、路由器的IP层、接收方的IP层,在各个IP层看到的数据的样子都是一样的,并且在IP层往上的层都是一样的,所以我们发现一个现象 ,叫做所有的,IP往上的协议层,发送和接收主机看到的数据是一摸一样的 !那么什么意思呢,当我们的自顶向下交付,封装的时候,然后我们在IP层进行路径选择,我们发现你要去的目标主机是IP B,是它怎么办呢,你必须得经过路由器这么一个IP层,所以你要把数据交给路由器,交给我们的路由器,此时我封装的我的MAC帧写的一定是路由器的MAC帧,把数据交给以太网驱动程序层,交给他,它也没闲着,它不是直接就把数据交给路由器IP层,而是要先解包,解包之后就是我发送方IP层交给它的样子,交给了路由器,然后路由器IP层交下交给令牌环网驱动程序层,令牌环网驱动程序层再进行封装自己对应的报头,这叫做入乡随俗。然后到了对方主机的时候,重新向上交付给对方的令牌环网驱动程序,它进行解包交付给接收方的IP层就是发送时的数据的样子。所以我们在IP这一层看到的所有的数据都是一样的!所以我们的IP协议呢,有一个非常大的亮点,就是IP层往上,只要站在IP层往上,我们可以认为互联网当中,只有一层协议就是IP协议,也不敢这么说,或者说,站在IP层往上,所有的主机看到的IP报文都是一样的。所以我们一般把我们看到的网络叫做IP网络。所以为什么我们叫做IP网络呢?原因很简单,因为在IP层往上,所有的每一层都,包括我的发送端、中间任何路由器IP层、目标主机的IP层收到的数据包都是一样的,所以
IP这一层
呢,是不是就叫做,**屏蔽了底层网络的差异!**我们底层的网络呢,有以太网、令牌环网,但是在IP层往上呢,我们看到的所有的报文都是一样的,所以大家协议定制的时候,无论你是什么主机,无论你在哪一个局域网当中,都不重要了,只要在IP层往上,我们就可以使用同样的一套的协议标准,全网的主机都可以了,这个呢就是IP带来的意义,屏蔽了底层网络的差异。
网络基础(一)_第11张图片

》那么他是怎么做到的呢?其实是当他走到局域网时,套上对应的局域网的报头,当经过路由器又会去掉对应的局域网报头,然后路由器内重新转到下一个路由器的时候,会重新封装MAC地址,所以当一个IP报文不断流动的时候,它的目的,尤其是目的IP一直都不变,但是原MAC地址和目的MAC地址一定是在变的,但是IP地址不变,相当于IP层在网络内将所有的主机设置了一个虚拟的软件层,也叫做IP协议往上,所有人看到的报文呢,在任何设备上,只要你是一个IP层往上的设备,曾经对方发的和你收到的,在哪一个中间设备上看到的数据都是一样的,这就是IP的意义。同学们,你想到了什么呢?是不是经过我们IP协议的设定,那么我们是不是屏蔽了底层局域网当中网络的差异之后,上层的协议在全网内其实都是统一的,因为我们收到的原始报文都是IP报文,底层我就不再管了,这就是一种软件虚拟化技术。在我们Linux学的时候,叫做一切皆文件,创建了一层软件层,在操作系统层面上看,所有的文件都是普通文件,struct file结构。大家讲进程的时候,我们给进程和内存之间构建了一个虚拟地址空间,让所有进程看到内存时,都以4GB的空间来看待内存。在网络这里有了IP的存在,我们全网内所有的机器在IP层往上大家看到的所有的网络报文都可以从IP报文为起始,底层的差异就不用管了,同学们,这叫做什么,这叫做任何问题都可以添加一层软件层来解决。如果没有IP协议呢,路径路由成为了困难;第二我们光处理不同网络之间的差别那你就要花很长时间。
网络基础(一)_第12张图片

》再继续,在我们讲的过程中呢,我们每经过一次路由器, 我们一定要重新解包,再向上交付(比如图中的以太网程驱动程序解包交给路由器,然后路由器再向令牌环网传的时候,会在令牌环网驱动程序 那里进行封包),再向下重新封包,一解一封就给他换了一个底层的MAC报头。其中这个概念呢,就跟我们在生活当中一样,你要去云南旅游,无论你到哪个城市,你从哪儿来到哪儿去,永远都是不变的,但是上一站你从哪儿来,下一站你到哪儿去,这个是一直在变化。同学们,当你从河北到了山西的时候,你上一站是河北,下一站是山西;到达山西之后呢,你接下来正准备从山西要进入陕西的时候,那么其中对你来讲呢,此时你上一站就成了山西,下一站你就是要进陕西了。但是不管你在哪一座城市,你进入到了哪一个省份,其中你从哪儿来到哪儿去,这个地址是不变的,但是上一站和下一站一直都在变化,所以对我们来讲这就是IP和对应的MAC地址的概念。所以最后我们强调了一个非常重要的概念,叫做IP地址和MAC地址。

下面呢,当我们实际在理解了这一点之后,我来告诉大家,我们一般网络通信的时候,它的一个基本轮廓是什么样子的呢?给大家画一张图。
》我们肯定会经过很多很多的局域网,一个协议栈代表了一台主机。所以我们必须得先承认一件事实,叫做,所有的数据必须在“网线”上跑,也就是说呢,所有的数据你再怎么网络传输,你必须得在网络里面跑,所以网线在哪儿呢?比如说,一个协议栈和路由器之间就是有一条网线,所有我们必须得承认,你再怎么数据转发,你必须得考虑所有的数据都得在网线上跑。给大家讲一个故事,你辅导员呢在A栋宿舍4楼(FTP客户层),其中它呢把你叫过去说,张三,你帮我去找一下叫做F栋,然后在4楼帮我们把文件袋放到4楼的桌子上,此时你呢就拿着辅导员的文件袋 ,你是不是直接飞过去到F栋4呢?不是的,你听到要求,肯定是先下楼(封装向下交付),下楼之后呢,然后你走到了一个对应的位置(以太网驱动程序层),你不知道路,你只知道你要去F栋4楼,你不知道路怎么办呢,你导员跟你说,你在路上不认识了,你在路上找一下两层的房间,2楼(路由器层)有保安大爷,你问一下保安大爷,所以当你此时不认识路的时候,你进入到二楼,然后问保安大爷你怎么走,保安大爷告诉你从哪里出去,然后你就下来了(路由器往下倒令牌环网驱动程序),一定要记住你在路上走,路上走之后呢,你又不认识路了,你再去自底向上到2楼找到保安大爷,然后你再下楼循环往复,我们要记住了,我们在进行我们数据转发的时候,因为所有的数据必须在网线上跑,就跟你一样,从一个地方到另一个地方必须得脚踏实地走在路上,所以每一个设备呢,出来的数据必须得到达它所在局域网,然后呢,在局域网内,自底向上交付,经过路由器,在以太网驱动程序这里封包,在令牌环网这里进行解包,重新再转发,最后经过路由器的路径选择,最后到达目标主机,所以我们整个数据的流向是这么流,其中你要记住了,因为一个局域网内的所有主机是可以直接通信的,所以某一个协议栈即一台主机认为自己是和一个路由器是在同一个局域网内的,同理,其他主机和路由器都认为咱两是同一个局域网的。记住一句话,我们的所有主机都认为自己在局域网当中可以和另一台主机之间通信,所以同一个局域网的一个主机和路由器,这两个被当成一台主机,而路由器一定横跨两台主机,也一定横跨两个局域网(子网),一旦这个局域网可以通信,另一个局域网可以通信,而路由器可以即在这个局域网里通信,又在那一个局域网通信,局域网内可以直接通信,就决定了两个局域网内的数据也一定能够互相通信,只不过做法会更加复杂一些。
》当我们真正的意识到这一点的时候呢,我们的同学才慢慢的体会出,数据是要自顶向下交付的时候要添加每一层的报头,其中我们把添加的报头过程称之为,封装的过程!那么其中对我们来讲,我们再把它反过来,数据反过来向上交付的时候,反过来看的时候,他要不断的去掉同层报头后,再交给上层,这个过程叫做解包的过程。这就是封装和解包的过程,经过我们刚刚呢会发现,实际当中,当一个报文发出的时候,他一定会经过一次完整的封装,在路上会进行一定程度的解包和封装,到了目标主机再彻底的解包。所以我们网络传播的本质就是,数据在网络当中不断的被封装和解包的过程,同时配合着查找我们对应的IP层,对应的各种路由表来进行,来进行路径选择,这就是我们数据转发的整个过程。
网络基础(一)_第13张图片

最后一个问题,我们再把了图拎出来,再来给大家一个概念,这个概念呢很好理解。当一个数据包呢,被收到了,被以太网驱动程序收到,收到之后呢,它当然要向上交付咯,我们要知道,每一种协议向上交付的时候,以太网上面可不止一个IP层协议,还有arp/rarp等协议,只不过课件的图简化了,TCP层呢还有UDP协议、ICMP等协议,应用层呢不仅是FTP客户层,还有有http、https、dns等等各种协议。当我们对应的底层收到了数据帧的时候,接下来他要向上进行交付,可是当他向上进行交付的时候,假设从IP层向上交付为例子更好理解,它怎么知道数据帧里面有它去掉的报头呢?当他要把去掉的报头后的数据帧继续向上交付的时候,它怎么知道是向上交付给TCP、UDP还是TCMP等协议层呢?IP层脑袋上可是顶着多种协议的 。所以我们要记住一个点,叫做,数据包添加报头的时候,也要考虑未来解包的时候,将自己的有效载荷交付给我们上层的哪一个协议!是什么意思呢,意思就是说,我们每一个数据帧/包,当以太网层序层它识别到自己的报头的时候,剩下的我们称之为,有效载荷,此时他要向上交付,它怎么知道他要向上交付的是IP协议还是其他协议呢?所以这里呢就要有一个问题,我们一般在封装报头的时候,添加报头字段的时候,它的字段里面呢,也要考虑解包的时候,将有效载荷交付给上层的哪一个协议。其中我们把报头添加的决定将有效载荷交付给哪一个协议呢,这个字段是必须得有的,而且我们把决定向上交付给哪一层协议过程,我们称作有效载荷分用的过程。什么意思呢,意思就是说,当我们以太网驱动程序收到了数据帧,他要向上交付的时候,它怎么知道把有效载荷交给脑袋上的哪个层呢?那么以太网驱动程序的报头必须考虑,考虑什么呢,就是把有效载荷交给哪一层协议。我们再解包的时候,决定把我们的有效载荷交付给哪一个协议,就是分用的过程。
》最后基于我们上面所说的所有点,要给大家产出2个基本结论,我们在封装的时候,一般而言,任何报头,都必须解决一个问题,因为它能够封包,那之后是不是还得把它解出来,你如果解不出来的话,那是不是出现问题了,所以不管是同层的MAC帧还是同层的IP,原IP层还是目的IP层,还是在其他层,一般而言,任何报头的属性里面一定要存在的一些字段要支持,我们进行封装和解包。也就是你说,你给我封了,报头是什么,报头就是添加的数据,这数据里面肯定有不同的字段,一定要有一个字段来决定哪些属于报头,哪些属于有效载荷,必须得把这个说清楚,要不让对方没办法通过我们对报头的解析,将报头和有效载荷进行分离。怎么分离呢,报头里面必须得添加属性。第二个,任何的报头的字段里面一定也要有一些字段支持,支持什么呢,就是支持分用。也就是,一般而言,任何报头的属性里面一定要存在的一些字段要支持,我们进行分用。也就是说呢,它必须得有一个来表明,我这个报文的有效载荷将来是要交给上层的哪一个协议的。上面的这个结论呢,是我们未来要学习协议的公共属性。也就是说,我们未来学习的时候,每一个报头都有各种的字段,但是在这么多字段里面呢,我们首先把共性都抽出来,就可以大大减少我们的记忆和理解成本。就好比什么呢,就好比我们封装应用层协议的时候,它的报头里面就必须得决定,第一个我未来怎么把报头字段和有效载荷分开,比如说,它的报头字段表示的是,整个报文是多长,报头字段是多长,整个报文-报头,剩下的就是有效载荷,它得有这样的字段。第二个呢,它未来得决定将有效载荷交付给上层的哪一个协议,上层的协议很多,所以它里面一定要有。同样的,当在TCP层协议封装的时候也得有将自己的报头和有效载荷分离的字段,比如说,整个数据段长度是多少,然后报头是多少,此时呢,就可以将报头和有效载荷分开,分开之后,它里面还得有字段表明,我要把我的有效载荷交付给哪一层协议。其中这两个结论一定要记住,至此,我们的网络原理一我们全部讲完。
网络基础(一)_第14张图片
》我们在网络基础一呢,学到了一些零散的知识,比如知道了,局域网和广域网,理解两台主机通过网线连接是可以通信的;第二个,我们知道了协议,这是约定,计算机和计算机之间的协议呢,对应的也是要通过数据来表达他们定的协议的;第三个零散知识呢,就是MAC地址,MAC的基本通信原理,在局域网当中,反正能够通信就可以了,再下来就是IP,IP和MAC地址的区别我们也知道了,他其实相当于MAC地址标定的是局域网中计算机的唯一性,IP地址表示的是公网当中的唯一性,那么其中对我们来讲呢,IP地址和MAC地址的作用是不一样的,就跟我们原地址和目的地址,上一站地址和下一站地址的差别,这是第四个零散知识。第五个,我们知道有封装和解包。
》总体结构性重要知识,我认为是三个。第一个呢,我们的网络协议栈是层状结构;第二个呢叫做,我们的网络协议栈的层状结构和操作系统之间的关系属于操作系统内的一部分;下一个就是我们数据包转发的时候,一定是从上到下,然后经过路由器不要断的去转发,最后到达了目标主机,在转发过程中,数据必须得在我们对应的具体的某一个局域网当中被进行转发。
》最后我们得出了两个结论,就是,一般而言,任何报头字段里面,能够支持我们进行解包和分用。你想想,你要能够解包和分包,必须考虑解包,你说解包就解包,凭什么呢?所以你的报头里面必须得包含哪几个字节属于报头,哪几个字节属于我们的有效载荷,所有报头里面都要有这个属性。第二个呢,我们的有效载荷交付给哪一层协议呢?上层的协议那么多,交付给哪一个呢?其中我们就要考虑每一个报头的属性里面,他一定会有我们对应的,支持我们分用的功能,这就是我们网络基础一。

网络基础2

我们是从自顶向下去讲,大部分教材是自底向上去讲的,都是先讲最底层协议,再讲最顶层协议,我们自顶向下去讲。因为我们已经有了应用层的应用了,后面会说我们的socket究竟是在做什么。
》其二呢,我们先遇到的第一层协议是应用层。当然第一层协议叫做应用层没有错,但是呢,接下来你可以理解成,应用层协议太多了,它不像UDP、TCP、IP协议、MAC帧等,我们重点是想贯穿把重点的讲讲,尤其是应用层,它的协议太多了,我们主要是以http为主线然后把应用层全部搞定。
》我想先问一个问题,我们曾经用到的套接字接口都是什么接口呢?我们一起用的socket、bind、liset等,这些都是系统调用接口。
》我们说过,网络协议栈是层状结构的。最上层,我们称之为应用层,接下来就是传输层,再下面就是网络层、数据链路层。最底层的硬件设备是网卡。中间的传输层和网络层它其实是属于我们操作系统的,最下面的数据链路层其实是属于我们的设备驱动层。最后衍生出来一个问题。
》**请问,我们之前写的各种socket代码,是在干什么呢??**我们首先用的是socket、listen、bind这些接口,我们也要明确我们之前写的所有代码都是隶属于应用层。也就是传输层、网络层、数据链路层和你半毛钱关系都没有, 你只是用人家的接口,另外,你用的这批接口是谁给你提供的接口呢?说白了就是传输层给你提供的接口,有些tcp给你提供,有些事udp给你提供的。而我们所做的工作,其实都是在原生的编写应用层代码。我们之前写的代码,其实已经是在做应用层的工作了。
》其中对我们来讲呢,应用层的应用呢,有很多的场景。我们写的网络套接字可以完成一些任务,但是我们少了一份工作,我们没有定制协议。有人说,我们不是定制了吗,我们发的就是字符串呀。是的,如果你是这样理解的话,那确实是没有问题的。但实际上呢,你发过来的字符串是什么呢?当我们聊天的时候,我们认为我们发过来的是字符串,这就是我们的一种约定嘛。但是呢,这个协议定制的并不明显,这是其一。其二呢,应用层有很多的其他概念。
为了满足不同的应用场景,早已经有很多的前辈,早就给我们写好了应用层协议!http、https、DNS等。人家早就写好了,我们之前做的工作,可以叫做,造轮子或者学习网络通信,或者呢,未来在公司里面叫做,定制化服务,也就是你自己从原生的套接字往上去写,这叫做定制化服务。目前在主流的服务器里面,早有一大批的开源项目帮我们去解决。比如说,应用层,你想用htpp,那就有http框架,你想基于远程调用,就有rpc等 。虽有很多的框架已经帮我们准备好了,但是我们必须得学会这些轮子,因为这一个经历你少不了。如果你没有写轮子的经历的话,人家做的事情,你压根就看不懂,学到现在,应该是能理解这句话的。
》对我们来讲,人家早就写好的,人家已经开始用了,比如说java等。记住了,所有的网络服务器,如果你要说,网络服务器是怎么写的,都是由C、C++来写的,其中虽然有python、java也有写,但是不要忘了,python底层也适用的我们学的东西来写的。你听到过的所有服务器,都是用C、C++来写的。 另外,在有些特殊领域,需要我们不要用现成的协议,需要我们定制化,那么就需要自己定制一大堆。我们C、C++既可以用来做服务器,也可以用在各种嵌入式设备。搭建网站其实是最简单的应用,手写服务器才是最牛逼的存在。
网络基础(一)_第15张图片

应用层

再谈“协议”

所谓的协议就是一种约定,人和人之间可以约定,我们所对应的程序和程序之间也是可以做约定的,我们曾经算不算定过协议呢?算的,只不过,我们的协议太简单了,比如给你发一个消息,就是人类可识别的字符串,服务器也这么认为;我给你发一个命令,你也认为我在给你发命令。我在给你发对应的命令,意味着什么呢?意味着,当前我们最终认为所有的字符串都是命令,客户端发送完之后,服务器立马就可以知道字符串不需要被转发,应该是被我们的open()执行,为什么呢?因为双方有共识,我们今天要干的,就不仅仅是它了。
》协议既然是一种约定,那我们在编写的时候,socket api的接口,在读写数据时,都是按字符串的方式来发送接收的。那如果要传送一写结构化的数据怎么办呢?
》这个可以给大家举一个例子,可以想想,我们传过去的呢,都是“字符串”,其实说是字符串是不太准确的,更准确的说法,应该是,我们在用TCP通信时,注意,从现在开始,我们主讲的都是以TCP为主,因为TCP是最难的,UDP呢,没什么好说的。我们都知道,实际上我们在进行网络发送时,你发送的数据都是TCP,而TCP都是面向字节流的,而这里的读写是按字符串的说法是不太准确的,应该是按字节流的方式来发送。只不过流式概念呢,大家对其还有一些障碍,那么我们就用字符串的话术来给大家说了,。
》如果我们要传一些结构化数据,那么又有问题了,什么叫做结构化数据呢?比如说,在C语言里,我想传一个结构体怎么办呢,一个结构体,它有整数,也有字符串、浮点数等等数据,那么我们该怎办呢?答案是:你不能怎么办,你也暂时不能传。那为什么不能传呢?这个结构化数据不也是看做二进制数据,看成二进制数据,我直接给对方发过去不就完了嘛。同学们,你想的挺简单,也想的挺美的,但是,结构化的数据在C语言里面,比如说,就是一个结构体,或者结构体定义出来的变量;在C++当中就是一个类,类内成员。你把一个结构化的数据发送给对方,你此时想把它按照字节的发送。说实话,理论上是可行的。
》比如说,我们接下来要进行的一个操作,叫做,网络版本计算器。我们想写一个网络版本计算器,想让它能够帮我们做某种计算。所以,我们一定是,首先定义一个,我们的数据请求,第一个struct data{int x;int y;char op;}这个就相当于结构化数据。我们将我们的结构化数据,发送到网络里面。我们用data定义一个数据data d = {10,20,“+”};我直接把d对象发送给对方。有人说,发过去有什么不好呢,可以呀,此时这不就是字节流嘛,那你把它当作二进制,直接给对方发。对方收到了,对方直接做一个这样的操作不就好了嘛,比如,对方收到了,对方收到的消息叫做buffer,我把buffer强转成我们的data*,这样是不是可以直接访问x、y、op了。这样其实你给我发过来一个结构化数据,这个结构化数据是12个字节,这12个字节对方也收到了,对方将收到的数据进行强转,强转呢,就能得到x、y、op,再进行对应的操作不就完了嘛。这就是我定义的结构化数据,同学们,这样可以吗?
》大部分情况下,是可以的。为什么呢?因为,网络协议就是这么定制的。实际上你可以去看看,网络协议,我们曾经不就讲过嘛,讲IP的时候,网络协议的定制,就是这么定的,其实就是按照字节来定的。 一旦网络协议定制的时候,定义出来就是一个结构体,然后结构体:位段等等。虽然这样是可以,这只是网络协议可以,但你不要这么干。而且你也绝对不能这么干。为什么呢?
》第一个问题,比如大小端问题。再比如说,大家都学过结构体对齐,那么结构体对齐的时候,我们也学过,一个结构体,它在不同的对齐规则下,它的大小是不一样的。我们就以结构体对齐为例。其实,在网络通信的时候,它已经帮我们把大小端给考虑好了,这个我们可以不用关心。但是呢,一个结构体的大小,在不同的对齐规则下是不一样的大小的。有人说,这有什么不一样呢?我客户端和服务器编译的时候,是一块编译的呀。同学们,你怎么这么天真呢?我们今天写的服务器和客户端是在linux下统一编的,未来服务器是Linux下编的,客户端是在Windows下编的,或者甚至是在手机上编的。你怎么保证 在Linux、手机、Windows下编译的结构体大小都是12个字节呢?
》所以,再怎么写代码,这种直接传结构体方案,在我们了,Linux、Windows下编,肯定是可以的。为什么呢?因为,大家的字节数都是一样,但是,在我们实际应用当中,绝对不可以这么干。你也不要想着,网络协议是这么干的,我也这么干。你能,但是不要这么做。收下你的锋芒,人家网络协议是踩着几十年的坑,所以不要去传结构体,除非你自己是在做本地传送,否则还是不要,甚至本地都不要这么做。我们不能够把数据直接二进制的传送给对方。有人说,我就是在Linux和Windows下通信都不行吗?你能保证你自己,把服务器写好了,可是代码还要维护,十几年之后,你的服务器不知道被迭代了多少个版本了,你的编译器也优化了多少个版本了。一旦客户端把你写的客户端下载过去了,客户很懒,就是不想升级,或者你的服务器升级了,用的更新版的服务器。但是,客户端已经把软件下载过去了,你已经鞭长莫及了,客户就是不更新。
》所以,你就用结构体来通信,未来服务端收到消息的时候,收到了还好,大小没变化还好,即便是同样一个软件,在不同的版本下,结构体大小就会有不一样。所以,如果出现你把服务器升级,那么所有的老客户,你不就管了吗。所以,直接传送结构体的方案,是绝对不可以的。
第一个问题:直接发送同样的结构体对象,是不可取的,虽然在某些情况下,它是可以的。
》如果,我不用你说的这样的做法,不进行发结构体了,那我该怎么办呢?所以,我们一般在想发送结构体之前,把结构体转化成“字符串”,这个字符串只是一种手法,有的甚至编译成二进制,都有可能。但是呢,我今天告诉大家,我们把它转成字符串。
》转字符串相当于什么呢?就比如说,我的结构体就变成了这么一个样子了10:20:+,我把它变成字符串的样子了,只是举一个例子。然后怎么办呢?然后,我将字符串发送给对方,对方它就会收到这个字符串。因为两方有约定:一共三个区域,用:分割。所以对方收到这个字符串,紧接着要根据自己的算法呢,把我们所对应的字符串按照“:”作为分隔符,把我们发送的10、20、+进行提取,将其提取成对应的数据,而且我们协议定制还得加上,前两个是int,后一个是char类型字符。这叫做什么呢,这叫做,我们把它全部给我们提取划分成10、20、+三个子串,我把前两个10、20转化成我们叫做,结构体。所以我再在它本地构建一个data对象,data d = {x,y,op}此时,就在重新形成一个属于它本地的结构化对象,此时再将它向上交付,那么上层就可以用了。其中,我们把这一个动作称作,把一个结构化数据,转化成字符串,或者叫做字节流的序列,我们叫做序列化!我们把对应的,你发过来的字符串的工作,按照我们的要求,把它进行转成我们的服务器所需要用的对象,叫做反序列化。这也就是我们定制的协议。
网络基础(一)_第16张图片
》给大家举一个例子:比如说你在软件上给我回复消息。所以在网络通信里面,其实你将来要送的消息message,其实是这个样子的struct message{std::string name;std::string_image;std::string message…}这就是结构化数据。这里有三个字符串,我们要做的就是,将这3个字符串转成我们对应的1个字符串,那我们怎么转呢?那么我们就可以定制协议哈,比如说:nick_name\3image\3message\3。此时就把3个字符串转成了一个字符串。转完之后,再发送给对方。对方收到之后,就会把你转成的1个字符串,它呢,也会有struct message msg = {part1,part2,part3},此时就有了格式化,反序列化的数据。
》我们将字符串3变1的过程叫做序列化。我们把网络里面传送好之后,收到之后就要进行反序列化。但是,你怎么知道是3部分呢?你又怎么知道是用\3作为标识的呢,这叫做协议定制。
协议定制:有几个字段,每个字段什么含义?
》有了序列化和反序列化的过程,我们未来要对软件做扩展,然后你在你的结构体里面做新增。你的老客户端没有更新,还是只是原来的变量,没关系。我收到之后,是可以判断你的新老客户端。如果你只有原来的变量,那么你就是老客户端,我就按老的一套给你处理,如果你是新版客户端,那么我就按照新的处理方法。
换而言之呢,有了这个,序列化和反序列化这么一层。这就相当于,在用户发送 和 网络之间,加了一层软件层!!可以进行各种操作。我们以前也说过,任何问题都可以通过加一层软件层来解决。以前我们说,进程在访问内存的时候,它可能会出现越界访问内存的情况,进而导致软件不可用,或者操作系统挂掉,这样我们就加了一层虚拟地址空间。后来又是,我们读写文件的时候,文件底层的差异太大,我们每一个硬件直接暴露给用户,那么就会有成百上千个接口需要用户全部熟悉。打开网卡、磁盘、显示器等等的接口完全不一样,对于用户来讲是不友好的,那么我们就加了一层虚拟文件系统,那么一切皆文件。一切的一切都在告诉大家,有什么问题,都能够添加一层软件层来解决。
》所以,序列化和反序列化就是充当了这么一层软件层,这个软件层,你可以在这里面做各种各样的处理。比如说,我们在进行序列化和反序列化的时候,我可不可以带上我序列化和反序列化的对应的定制化的版本,那么协议版本,如果你是老版本,那么此时我就知道,我就提取3个数据,如果你是新版本,我就提取若干个字段就可以了。所以,我们可以在用户和网络之间添加一层软件层,方便我们服务器进行各种任务。
》这也就是未来进入工作了,如果你是做的网络方向,你会发现底层的协议,比如TCP、UDP,它在通信时,就是我说的,用的结构体,挺好的。但是,一旦到公司层面上,在进行软件通信的时候,永远都要自己用一些组件或者框架等其他方式来完成序列化和反序列化的工作。为什么呢?最根本的就是,如果你愿意的话,你也可以用结构体来定义自己的协议,但太麻烦了,然后你要永远记住,网络底层协议为什么敢用结构体,它的大小端等等都考虑好了,为什么呢?第一,网络要求效率,它不允许你有任何浪费数据的存在;第二,网络协议的底层变化不大,它不变,不变的话,那让产品经理怎么提需求,提不了需求,那我们就考了效率呗。那么所有字段能给你压缩就压缩,用几个bit位就是几个bit位,暂时不用考虑扩展的问题。所以用的就是结构体通信,而且是二进制的。但是,告诉你们,应用层不一样,应用层你面临的用户,有可能是老版本用户,可能给你服务端发来的有5个版本,所以,作为服务器来讲,它要考虑的不仅是最新的,还要考虑老版本。所以,怎么让自己的协议在软件通信的时候,更好的适配各种的应用场景呢,这里就必须得增加灵活度,考虑的是灵活度优先而不是效率优先,所以,我们必须得添加软件层,也必须得有序列化和反序列化的过程。所以,这也就是网络服务器必须得有这么一步。
》你所说的约定我懂了,你所说的结构化数据,不管是C语言的结构体,还是C++ 当中的类,这些东西都是我们将来定制协议的字段,我们要将其转成对应的字符串。怎么转呢?有很多的做法,可以自己去写,去定制,也可以用其他的组件。现在的问题是,就这样完了吗?还差一点,
》比如说,我传的是一个结构体,但是你想过没有,如果今天将其序列化了,就按你说的用特殊字符将其分割。可是每一个人的name、message等字段的长度不一样,头像大小不一样,发的消息也不一样。消息呢,有大有小,就直接决定了,字符串整体的长度,你是怎么知道的?意思就是说,你的格式我是知道的,有几个字段,每一个字段的含义我都知道,但是我在网络读取的时候,我怎么知道字符串是多长呢?
》有人会问,需要知道吗?答案是:非常需要。因为TCP是面向字节流的。你发过去的长度,你以为你发了1024个字节,可是对方收的时候,就根本没有收。可能上层应用层在忙着,没时间去read。那么可能你发消息特别快。就跟我们曾经讲的管道一样,客户端可能往管道里面塞了一段数据,那么对方就读一条数据。客户端发的特别快,一下子发了10条信息,那么你读端可能一次就将10个消息都读上去了。
》但问题是,你怎么知道这个字符串的整体长度是多长呢?因为你要单独的去区分一个字符串嘛。就是,字符串发过去了,我们之前写的服务没有什么问题,是因为我们的场景比较简单,再加上我们发消息比较短。如果发的消息特别长,读取特别快,而且还有混着发的。我虽然知道你的格式和字段,我解析他,反序列化,是不是得保证我把字符串读全了,既不能多读,也不能少读,那怎么办呢?所以,你怎么知道字符串的长度呢?对不起,你并不知道。那怎么办呢?你不是玩我吗,我不知道,我怎么去通信呢?
》所以,我们还有第二个问题。我们在对应第一个问题上,我们要对它进行,我们对应的,它确实不知道我们数据结构体对象,我们这里需要做什么呢?我们需要进行序列化和反序列化。
》第二个问题,你怎么知道字符串的长度是多长呢?很简单,我们需要定制协议的时候,序列化之后,我们需要将长度,我们设置为4字节,将长度放入序列化之后的字符串的开始,之前!
》说白了,什么意思呢,意思就是说, 我在发送的时候,我规定好,我前4个字节,我发送的50这个数据,然后后面跟着的是字符串。所以对方在读的时候,他可以先只读取4字节,把长度读取出来,然后根据长度,再决定读取多少个字符。这种定制的协议,我们叫做自描述长度的协议
》所以,我们终于解决了,我们在网络通信的所有问题,当然,为什么要这么干,然后还有其他做法吗?后面在讲TCP字节流的时候,我们再来给大家把原理讲完之后,我们再深刻的去体会一下他。今天呢,我们就先进行网络服务器的最后一块拼图。我们一般给我们添加的数据,添加字节数的动作呢,叫做,encode,对方在读时候呢,那就叫做decode。当然encode,不仅仅会有长度,其他协议里面有可能会涉及加密和解密,我们不考虑这个。
》下面,我们进入造轮子的环节。

网络版本计算器—序列化和反序列化

例如, 我们需要实现一个服务器版的加法器. 我们需要客户端把要计算的两个加数发过去, 然后由服务器进行计算, 最后再把结果返回给客户端。
》我们接下来要做的工作,是属于哪一层呢?应用层。我们前面写过了TCP服务器和客户端了!所以直接复用。
》我们在SeverTCP.cc里面添加 void netCal()函数。所以Task t定义出来的对象,初始化的时候调用的方法直接改成netCal()函数,即 Task t(serviceSock, peerIp, peerPort, netCal);我们还差一口气,我们再来一个文件Protocol.hpp,这叫做什么呢,接下来我们要在里面进行自己的协议定制。换句话说呢,我们要将我们协议相关的内容写到里面,我们自己来定制对应的协议。我们对应的服务器和客户端都要包含对应的协议头文件Protocol.hpp,协议肯定是双方都要知道。
》接下来就是,我们怎么去定制协议呢?我们要做的是,网络版本的计算器。定义一个class Request{}请求类,也还要定义一个class Responce{}响应类。那么请求和响应,我们都有了。在请求Request里面,我们得有int x;int y;char op的成员变量。我们接下来就是要在Responce里面,有int exitCode_;int result_;这里面的result很明显是我们定义的协议当中的运算结果,那么exitCode_,是什么呢?你能保证你的计算结果是正确的吗?你可能在进行计算的时候,对应错误,你怎么保证你的计算是正确的呢?你只有是正确的,那么你的结果才是会有意义的。如果你在服务端除0了,那怎么办呢?那么此时,我们服务器应该是不给你计算,给你返回退出状态,所以我们用0表示运算结果合法,非0表示运算结构是非法的。
》我们将协议字段定好还不够。你不是定协议嘛,我们要求的协议标准就是,我们将来是x op_ y,你凭什么让x作为被除数,y作为除数呢?那么我们就要服务器和客户端约定好,x就是左边,y就是右边。我们规定好op的操作就是±*/%运算,如果不是,那不好意思,那么当前的数据直接就是违法的,你就是非法请求,可以吗?可以呀,这就叫做约定。我们约定好,客户端和服务器都必须遵守,这就叫做协议定制。整体的结构就叫做结构化数据。
》那么我们的Request一定要提供对应的构造方法Requset()和~Request()析构方法。Responce少不了构造和析构函数。除此之外,Request在发送的时候,需要序列化,在收到的时候,需要反序列化,所以就需要有一个序列化和反序列化接口。比如说,你这个请求,我序列化完成肯定是变成字符串了,我要发给对方,对方是不是要进行反序列化,就要有序列化serialize()函数和deserialize()反序列化函数。
》序列化呢,我们是需要把3个计算的成员变量进行转化成对应的字符串,void serialize(string& out),反序列化deserialize(string& in),这就有了序列化和反序列化。对于我们今天所讲的Request,因为Requset是客户端请求,那么客户端就需要序列化,服务端获取,就要进行反序列化。
》对于我们的Responce,服务器要进行响应,是不是要序列化成字符串发出去,客户端是不是还要反序列化,把字段提出来呀。
》作为请求,还需要添加长度,我们再来一个对序列化的字符串进行添加长度encode()函数,对我们的string in字符串,进行添加uint32_t len的长度,即string encode(const string& in, uint32_t len),另外,还需要对序列化之后的字符进行提取长度string decode(const string& in,uint32_t len);我们Responce需要添加字节数和解析字节数,我们的Request也是需要的。
》我们将来的协议是这个样子的,我们想让整个发送过来的字符串,前面加上长度,即strlenxxxxxx,这个长度呢,有两种方案,一种是,就单纯的是4字节,定长的4字节,然后直接被转成二进制,在将信息扔给对方,对方只要读取,对方必定先读取前4个字节,必须得把前4字节全部读上来,读完之后,转化成我的有效字符串是多长,然后再从网络里面读取报头长度,len长度所标定的对应的字符长度,才算是将完整的字符串读完。当然,你发的时候,肯定也是按照协议来发的。
》第二种方案呢,就是采用字符串风格的“strlen”,即“strlen\r\n”用\r\n作区分,紧接着把有效字符串带上,即“strlen\r\nxxxxxx\rn”。我不喜欢采用第一种方案, 因为它的可读性不好,如果中间出了问题想要调试,是非常难的。如果想要打印一下读取的内容,可能都不全。但是,如果转成字符串,大家也都知道,我们把字节数当作字符串,然后再去处理有效字符的内容的话。我们把长度获得之后,把字符串转成整数,再去读取有效字符串,那么也就把序列化之后的数据读到了。所以,我们采用第二种方案。
》可能大家还有疑惑,第一个疑惑:你现在有一个整数,假如我的有效载荷长度是1000个字符,那么你前面的strlen的长度就是1 0 0 0 ,可是你的长度也是变化的呀,不像第一种方案,用标准的4个字节,就固定死,报头长度是4个字节,你爱读不读,所以,用4个字节的方案可以吗?可以的,但是可读性不好。用第二种方案呢,有一个问题,你对应的字符串长度呢,它本身也是变长的,有可能是10、20个字节,那么你的strlen也是变长的,我在读取的时候,我怎么知道,strlen长度就是整个字符串中的长度字符串风格全部读完呢?我怎么保证呢?很简单,我只要读完\r\n就可以了。
》有人说,你读到\r\n可以的,你是说,只要读到\r\n了,我就已经将长度大小读到了,那我为什么还要用你呢?我直接xxxxxx\r\n不就好了吗?何必还要在前面加上strlen长度,即"strlen\r\nxxxxx\r\n"呢。这里就有一个问题了,你怎么保证,序列之后的字符串本身就不包括\r\n呢?假如,对方就是发的\r\n呢,那么你这个协议定制的时候,就特别的不好处理。那又有人说,你就能保证你加上长度字符串之后,就能保证长度字符串里面没有\r\n呢?是能够保证的! 因为,长度都是由整数组成的,而且可以称作是一个无符号数,一定是一个数字,0~9,它不可能出现\r\n,所以,我们只要将\r\n,按照正常的去读,读到\n之后,我们就认为长度的字符串读完了,将读到的长度字符串转出来,然后再去读有效字符串。这都是细节,一定要搞清楚。
》同样的,如果此时你用标准的4字节来标定长度,是可以的,因为你是定长报头。但是呢,这种方案的可读性不好。这种方案反而好写,无非就是在内存空间里面,把一个整数长度按4字节拷贝到空间里面,把字符串也拷进去然后再发出去,这种方案,反而还好写。虽然第二种方案不太好写,但是这种的可读性很好,而且我们就采用前面是长度,后面是字符串。“strlen\r\n”是报头,然后“xxxxxx”是有效载荷,我定的就是应用层协议。只不过我们的报头比较简陋。我们可以将我们的有效信息都放到xxxx里面哈。
》“strlen\r\nxxxxx\r\n”这种样子的字符串可读性比较好,一旦读上来是这个,但实际上,我们的代码里面要用的是xxxxx的内容,所以,我们可以调用decode()函数进行解码,发送的时候进行加码encode(),也就是在我们的序列化和反序列化之后,添加报头字段。说一下,我们今天是从0开始写的,像别的java语言等,当然,我们也可以不从0开始写,但是我们得有这么一个过程,不然的话,别人写的你也不理解。现在最重要的问题是什么呢,实际上encode和decode可以做添加字节数,也能做加密和解密的工作。
》Request如此,我们的Responce也是同样的逻辑,我们把所有的功能写好,双方之间就可以用Request或者Responce来通信了。现在讲的整个协议定制,涉及到x、y、op这是你的结构化数据,x是什么,y是什么、op是什么。op为什么是± /%呢,这是协议的规定,为什么是x-y、x%y等等呢,这也是协议规定。我们Responce响应的时候,exitCode_,result又是什么意思,为什么exitCode_表示状态呢,result表示结果呢,这是协议规定好的。以后呢,exitCode_0表示/0,1表示%0等等这也叫做我们所对应的协议定制。所以在上面的一批字段呢,exitCode_和result是和我们的业务强相关,也就是和我们要写的网络版本计算器逻辑是强相关的。对我们来讲呢,我们网络通信之中还要做序列化和反序列化,还要对整个报文定制我们网络传送的解决方案,你前面为什么是长度然后是\r\n,为什么不是\3\5等等呢,不好意思,这是因为协议定制,这才叫做整体的协议定制。
》你如果不用长度,而是就用\r\n来分割,这样可以吗?答案是,这样是有问题的。你字符串本身就有可能包含\r\n的,你用特殊字符再怎么定,有可能你字符串本身就包含了你设置用来作为分割的有效字符。如果存在了,就有可能会影响你协议的运行。这种其实是一种二进制不安全的方案,所以我们要做的,加长度的方法是最安全的,也是目前最合适的。
》接下来就要编写网络版本计算器,协议雏形已经定制好了。后面会写大量的序列化和反序列化的代码。我会写两种方案,第一种:全部手写;第二种:部分采用别人方案–序列化和反序列化的方案。

首先别人给我发来的一定是序列化后的字符串,我们先来写服务端。你能保证一次读取就能把别人序列化好之后的字符串全部读上来呢?我们的字符串里面报头表示的长度不会将\r\n包含进去的。你怎么保证,你一次能够把发送过来的字符串一次全部读完呢,能做到吗?因为TCP是面向字节流的,它在发送的时候呢,有自己的一套机制,所以,对方不一定将全部内容一次就发送到你这里了,有可能只发送一部分,然后再给你发送后面的部分。所以对我们来讲,对方不一定一次全部发完,而我们心里清楚,我们要的是什么。我们要的是,必须前面得有长度,再带我们的报文长度。这样的才是一个完整的报文,才方便我们后续处理。所以在读取对方发送来的序列化的字符串有相当多的细节来处理一下。
》所以,我无法保证对方,一次将数据全部发送过来,那怎么办呢?所以此时,只能是没读完的时候,就让它继续循环的读。没有读完一个完整的报文的时候,就只能让他继续读,否者的话,我们没法处理。所以我们得加上inbuffer,我们在读的时候,可以这么去处理。char buff[128],我们说字符串,但是我们也不来考虑\0,你读多少就是多少,所以read()读到buff里面,读多少呢?读sizeof(bufff),接下来,读上来,此时s > 0,说明我们读取成功了,s == 0,说明对方是关闭的, s < 0说明就是读取失败了。
》网络版本计算器这个任务呢,主进程将其排发给线程。读的时候呢,对方把链接直接关了,关了之后,即便是做计算,我把结果返回给他,所以当s = =0的时候,我们什么也做不了,我们打印日志logmessage()。s < 0读取失败,我们也打印日志。
》当我们s > 0,读取成功的话, 因为我们是要把数据单独放在一个buffer里面。我们从read()里面读上来放在buff里面的数据,是只包含了序列化后上来的一部分字符,所以到底有没有读完,是需要自己做检测的。所以读了之后呢,buff是需要拼接到buffer里面的。然后在inbuffer里面做检测哈。那该怎么写呢?
》因为要做拼接,那么我们还是sizeof(buff)-1把。然后buff[s] = 0,inbuffer += buff;所以此时我们就把对应的缓冲区buff里面的数据添加到我们的buffer里面了。
》下面我们就需要检查我们的inbuffer是不是具有了一个,当然,别人在给你发的时候,也有可能发过来就是给你发了2个报文,是不是也有可能。因为TCP是基于流式,现在对方发过来的字符串里面呢,一次给我塞过来两个字符串,可能这次read呢,一次读多了,我要的是把一个报文拿全就可以了。那我们就要检查是不是已经具有了一个完整的报文了。如果具备了,我们就要提炼出来,进行后续的反序列化工作,如果没有具备,那么我们就需要让他继续读取。
》别人发来的,一定是一个Request,而一旦是一个Request呢,我们Request要执行的第一个步骤就是decode,就要根据它传入的字符串进行提取长度,那么返回的就是有效的字符串。就是,我们的decode,1.必须具有完成的长度;2.必须具有和长度相符合的有效载荷。只有当这两个条件满足了,我们才返回有效载荷和对应的len。否则,decode就是一个检测函数。
》换句话说呢,decode就是身兼两职,它对字符串做扫描,它里面必须满足,长度它是有了,第二,长度识别出来之后呢,它的有效载荷和长度是匹配的,而且必须是一个完整的报文。此时我们才将其解析出来返回的是报文,然后len长度是一个输出型参数,是字符串本身含的长度。
》如果就是,长度没有,比如我的有效载荷有10个长度,可是你呢,有效载荷只有9个,那不好意思,我直接给你的len就是0,那就不做字符串返回了。这就是相当于decode是一个检测函数。否则满足条件的话,decode就相当于是一个提取函数了。
》所以我们要构建一个请求的话,是需要定义一个Requset对象,即Request req;这个req呢,就充当我们的客户端请求的。所以,我们read()成功后,就需要req.decode(),将inbuffer传入,并将报文长度要传入&packageLen,即req.decode(inbuffer,&packagLen);长度是不可能为0,别人发过来的请求是不可能为0的,它至少得是一个3吧,即1 + 1。如果packgelen = =0,那么这次decode,我们没有解出完整的报文,那我们得continue,让你回到read()继续读取之后放到buff里面,然后再次拼接到inbuffer里面,这也是为什么inbuffer被放在while循环外面定义的原因。
》如果decode给我们输出的参数是一个大于0的,那么就是获得了一个完整的报文了,也就是我们把xxxxx的内容读了。接下来,我们要做的就是,反序列化的工作了。因为是经过别人给我们发过来的消息是经过序列化的,那我们就需要反序列化,把字符串里面的字段反序列化到我们的Request的内部。
》也就是说,我们接下来要进行反序列化,即req.deseialize()函数,将我们的package获得的报文进行反序列化。如果反序列化成功,那是不是字段已经在Request里面了,x、y、op我们全都有了,然后就是我们处理逻辑了。
》处理逻辑怎么处理呢,是不是就是我们真正的网络计算了呀。网络计算你需要什么,网络计算,你输入的是一个request请求,得到的是一个responce响应。我们来进行一下封装,用一个calculator()函数进行封装。计算的时候呢,我们传进来的是一个req,就是把刚刚已经反序列化完成之后对象传进去,里面的x、y、op都有了。那我想要得到什么呢,我想要得到的是一个responce,那么就是Responce resp = calculator(req);那么就要单独编写一下calculator()函数,那么如何编写呢?
》这个calculator()函数不需要暴露给外面,也能够将其定义层static函数。为了方便,我们就直接将Request里面的成员变量公开,即public,方便我们使用。另外,我们的Responce里面的成员变量也public,也方便我们使用变量。我们在calcutor里面定义一个Responce对象,即Responce resp;那我们怎么计算呢?
》我们switch(res.op_),进行选择,然后根据不同运算符进行将结果放入resp里面的result和exitcode里面。这样就把我们的请求的计算完成了,然后我们就是return resp,就完成对应的计算了。
》那么我们现在已经得到了一个responce了,那我们接下来应该干什么呢?因为responce是一个结构化的数据。有人说,就两个字段也是结构化数据嘛?是的!哪怕只是一个字段也是结构化数据。所以我们接下来要做的事情是不是要将我们的响应responce转成我们对应的序列化对应的数据,你一定能够理解。
》那么我们就要进行序列化。怎么序列化呢?我么你的responce类是提供序列化的方法的,直接调用类内函数,resp.serialize()。我们希望是拿到一个序列化完之后的结果,那么我们定义一个string respPacka,作为响应报文。我们将serialize()函数传入一个输出型参数,则resp.serialize(&respPackage),然后呢?后面就和你的resp就没关系了,就只和respPackage有关系了。可是光光有这个序列化后的字符串你能不能将其发出去呢?是不能的,因为我们还有协议呀,对方怎么知道我序列化之后的字符串是多长呢?所以我们还得对报文进行encode,添加上我们的报头长度呀。后续呢,encode和decode我们是可以单独拎出来,一会儿写完会发现Responce和Request的encode和decode其实写法是一样的,与类无关,它只是对字符串相关的做提取。
》我们现在要添加报头长度,就要需要调用Responce的encode()函数了,它所需要的参数呢,第一个是你要添加报头的字符串和你想添加长度的大小,完成之后,返回的就是你添加长度之后的报头+有效载荷,即resp.encode(resPackage, resPackage.size()),这个只是添加报头长度,是一定能够成功的。然后呢,返回之后的字符串就用我们的resPackage字符串来接收算了。
》然后我们接下来是不是就可以发送了呀,发送也是有问题的。你调用的write()函数,你再仔细的考虑一下,write()的返回值是你实际上发了多少字节。比如说你要发1000字节,最后呢返回的是500字节,也就是说没有按要求一下子全部将1000字节发出去,会有这种情况吗?会有的,那么就需要将你的发送的缓冲区全部记录下来,当我们发送的时候,你也要判断这个pakcage有没有发完,所以这块是需要做处理的。我们今天简单一点处理,我们将关注点聚焦在读取上,保证读取上来的报文是完整的,对于写入呢,我们在后面统一解决,后面会给出解决方案,其实说白了就是将读取那部分的代码也再类似的写一写。就相当于读取的时候inbuffer是用+=,它写入呢就使用-=哈。发多少字节,就将发出去的去掉,然后下次循环再发,写起来会将代码写的非常乱,这里呢就简单进行发送—后续再处理。虽然我们考虑到了这个问题,但其实我们现在所写的所有工作,大部分情况下是不会出错的,因为,我们现在的报文量少,且很短,所以不会出现很严重,你说的问题,但是也得考虑上,后面会处理发送的问题。
》接下来发,往哪里发呢,你读的时候read()是从socket里面读,发的时候也是往socket里面发呀。从我们的respPackage里面发,发resPackage.size()的大小,即write(socket,resPackage,resPackage.size());
》稍后有一些地方我们要单独做处理,第一个呢,strunf package = req.decode(inbuffer,&packageLen)这里要做一下处理。因为,后面我们在写的时候会发现encode和decode,因为是协议嘛,客户端遵守这样的规则,服务器也遵守这样的规则,所以encode和decode方法其实可以共用。而decode和encode其实只需要给我们提供所对应的序列化和反序列化功能就可以了,稍后再说,只有写出来才知道。所以encode()我们也会稍微改一下。
》至此,我们将逻辑大概的写出来了,接下来,我们再一步一步的完善函数内部的实现方法。
》我们先来完善Request里面的decode。首先你给decode()函数传进去了一个字符串,我们ServerTcp传进去的是inbuffer。我们在decode的时候呢,一方面,它要对我们的长度做检查,对后续的有效载荷也要做检查处理,只有条件满足才会将我们的有效载荷做返回,否则什么都不会做。那么我们该怎么去写呢?
》首先用assert(len);因为对方发过来的消息是满足协议要求的,所以前面呢,需要在我们的inbuffer里面查找我们表示长度的字符串,即一开始的\r\n之前的是表示有效载荷的长度,要先进行对它进行查找。当然,因为在查的时候呢,涉及到\r\n的问题,所以为了方便,我们宏定义一下,#define CRLF “\r\n”,我们还需要\r\n的长度,那么后续在切割长度的时候,就不需要再做了, #define CRLF_LEN strlen(CRLF),但是这里千万不要用sizeof(CRLF),因为sizeof会将\0也会计算进去,那么此时你提取字符串的时候会出问题。所以呢,我们现在就有了,中间的分隔符,它对应的宏和他的长度,在提取对应的字数的时候,就不用手写\r\n,这么麻烦了。
》我们先调用find()函数,即在我们的传入的inbuffer里面找一下有没有CRLF,即ssize_t pos = in.find(CRLF);如果pos = =npos了的话 ,那就是说明没有\r\n,也就是,你不是以\r\n开始,那我就不玩了,那我就没有办法去提取长度,那么有效载荷我也没办法去提取,那么,我们返回一个空串,光光这样还不行。我们在给decode传入参数的时候,是传入了packageLen的,并且它是一个输出型参数,并且用它来判断是否decode已经给我们返回了有效载荷的字符串,如果不是,会继续从socket里面读取内容。所以在decode()里面做解析之前,先将len设为0,即len = 0;如果提前返回不符合decode的条件的话,那么len就是0。
》其中,我们也知道,string的find()函数,如果找到了指定的字符,会返回\r的位置,所以pos是指向\r的。所以,如果pos!=npos,那就说明,我们是可以提取长度字符串。所以就可以提取长度,但是,在提取长度的时候,不要对传入的字符串inbuffer原始字符串做任何修改,暂时还不需要。
》那在提取长度的时候怎么做呢?我们定义string inLen;将长度字符串单独提取出来,那怎么做呢?是不是可以调用substr()函数呀,in.substr(),从0位置开始,提取的长度刚好是find()返回的位置pos,即string inLen = in.substr(0,pos)。
》这里有一个常识,一般在循环的时候,for(int i =3 ,i < 9;i++),这个属于[),这样有一个好处就是,9 - 3刚好是你得到的元素个数,所以我们find()返回的是\r的位置,“1234\r\n”,那么pos刚好是前面有多少个字符,这就是[)的写法。
》然后呢,我们inLen已经拿到了子串,那我们就需要把字符串转化成整数呀,调用atoi()函数,int inLen = atoi(inLen.c_str());我们必须具有完整长度这一个条件已经有了。
》第二步,得保证,in字符串剩下的有效载荷里面的长度必须能够>=inLen的长度,因为只有>=inLen了,才能得到一个完整的报文。
》所以接下来要做的第三件事情,就是确认in字符串中有效载荷也是符合要求的,如何确认呢?很简单,我们已经有了find()后返回的pos,pos指向的\r,我们要拿到后面的有效载荷的长度必须能够>=inLen。所以我们计算一下,去掉前面长度字符串,即第一组\r\n前面表示有效载荷长度的字符串,然后剩下的长度是多长,我们算出来。因为,现在in的总长度,我们是可以知道的,即in.size();现在pos指向的是\r位置,其中第一组\r\n我们也是不需要的,第二组\r\n我们也是不需要的,当然如果包含第二个报文的内容,那更好,即123\r\nxxxxx\r\n12\r\n…,先将前面2组的\r\n长度也不计算在内,所以int surplus = in.size() - 2
CRLF_LEN-pos;其中这里的pos就是表示长度字符串的个数哈。那么相减后,剩下的大小surplus就是至少要包含一个完整有效载荷的内容长度哈。
》所以就需要判断一下,if(surplus >= inLen),说明至少包含一个完整的有效载荷,如果不满足if(surplus < inLen),那么就返回空串,输出型参数len = 0不会被修改。
》如果有完整的报文,那么前半部分就是我们说的decode有充当检测的功能的。确认有完整的报文结构,接下来就是,我们现在长度已经有了,那么我们传进来的输出型参数len就可以被赋值了,len = inLen;当然除了给len赋值,我们还得把有效载荷给提出来。
》我们一定也是string package = in.substr(),从我们的\r往后+2就是我们的有效载荷的开头了呀,所以其实位置就是pos + CRLF_LEN,剩下的报文长度,我是不想要\r\n,只想要xxxx部分,那么xxx部分是多大还用算吗?不需要了,因为已经前面算了有多大了,就是inLen大小,即string package = in.substr(pos + CRLF_LEN,inLen),此时,我们的报文是不是就提取出来了呀。
》这就完了吗?还没有,我的报头长度和报文都拿到了,还需要玩什么呢?没必要再往后面走了呀。想多了,你还要再考虑一个问题。我们在用read()读取的时候呢,我们是把读取的数据全部都拼接到inbuffer里面的。但是当我们将其decode出一个完整的报文的时候,那我是不是还要再处理下一次报文呀,那么被提取出来的报文不应该再留在inbuffer里面了。那是不是还要再做一个工作,将我们当前的完整报文从我们的in中移除!不然的话,我们inbuffer里面的第一个报文,永远都是已经被提取出来过的。
》这也算是报文和报文之间要进行协议处理,报文之间不能互相影响。那么接下来的工作,就是将已经提取出来的报文从inbuffer里面移除。
》如果要移除的话,我们就要确认我们的报文长度是多长,那我们应该怎么去移除呢?那我是不是要知道整个报文的长度呀,怎么移除呢?我们int removeLen;你可以自己再算,但是也可以不用再算了,因为移除的长度,无非就是inLen长度,它是不是我们提取出来的报头的长度呀,再加上我们整个提取出来的报文长度package.size(),然后加上2组\r\n呀,即int removeLen = inLen.size() + package.size() + 2
CRLF_LEN;此时我就将要移除的数据的长度是多少就算出来了,然后就是调用in.erase()函数,怎么进行移除呢?需要传入要从哪里删除,然后要删除多少,这两个参数,即in.erase(0,removeLen);
》移除工作做完了,我们才进行正常返回,return package;就行了。此时我们就完成了对应的decode()函数,该函数记完成了检测工作,还兼容了对缓冲区的移除。我们只是一次提取一个报文,你自己也可以循环的进行提取多个报文,然后定义一个vector,将package放入vector里面。
》下面我们要对报文进行的就是deserialize反序列化,我们在decode的时候是一整个字符串,当走到deserialize()反序列化,我们是传入的已经被提取出来的一个完整报文了,即“100 + 200”这个样子的了。
》就是你传入的pacakage的完整报文,你必须得是严格按照我的格式要求“x op y”才行。所以我从头到尾,先找到第一个空格,然后再找到第二个空格。找到2个空格之后,就是从0开始到第一个空格,第一个空格到第二个空格,第二个空格到末尾,分3次将数据提取出来。所以我们,#define " " SPACE size_t spaceOne = in.find(SPACE),然后就是判断pos 和 npos,即if(spaceOne = =string::npos)那就说明你要格式不存在,那就说明有问题,那我直接return false;如果第一个空格符合格式,我们再找第二个空格,我们可以从后向前着,那么就可以调用rfind()函数了,即size_t sapceTwo = in.rfind(SPACE),同样判断一下if(npos = =spaceTwo),说明不符合要的格式,就return false;如果第二个空格也符合,那么,我们就可以一个个的将数据提取出来了。
》string dataOne = in.substr(0,spaceOne);string dataTwo = in.substr(sapceTwo + 1),因为我觉得+1可读性不高,所以我们提前宏定义一下#define SPACE_LEN strlen(SPACE),记住用strlen()而不是用sizeof()不然会多出大小。所以 string dataTwo = in.substr(spaceTwo + SPACE_LEN);这样第二个数据就得到了。操作符string oper = in.substr(spaceOne + SPACE_LEN,spaceTwo-(spaceOne + SPACE_LEN));spaceTwo-(spaceOne + SPACE_LEN)意思就是我要截取多少个的大小哈。然后我们必须得保证if(oper.size() = =1)操作符必须得是等于1的,如果操作符不合规,那么就return false;
》接下来就是将数据转化成类内成员,因为我们为了方便,是将成员变量是公开public的,所以x_ = atoi(dataOne.c_str());y_ = atoi(dataTwo.c_str());op_ = oper[0];这就完成了反序列化。
》反序列化完了,我们一定知道,已经将我们的数据都提取出来了,为了后面方便调试,我们给我们的Request类里面加一个方法,void debug(),打印我们的数据。
》我们calculator()函数处理逻辑是传入了我们已经反序列化完的Request类的req对象,然后该函数最后会返回一个Responce对象resp,resp是一个结构化的数据,那么我们得对resp进行序列化,要得出来的是一个respPackage字符串。也就是我们将两个结果数据转成一个字符串,该怎么转呢?
》我们要的字符串就是按照我们的要求,把结果处理好就可以。我们的serialize()函数得是Responce类的成员函数,我们要的呢,就是将result_、exitCode_转成字符串。在形成字符串的时候呢,因为字符串是有长度。我们想要转化成什么样子呢,“exitCode_ result_”就是两个整数,中间用空格隔开,因为我们还会调用encode()函数给他们添加长度,那么对方收到就能知道一次响应的结果有多长,然后再以空格为分隔符,将我们退出码和结果提取出来就可以。
》所以开始将我们的整数转成字符串了,string ec = to_string(exitCode_);string res = to_string(result_);现在已经有了ec、res两个字符串。我们给seralize()传入的是我们需要序列化后的字符串out哈,所以我们将转成的字符串拼接到out上面。out = ec;out += SPACE;out += res;这样序列化就完成了。
》光光序列化还是不够的,序列化呢,你只是将你的结构化数据转成了一个字符串。为了对方收到之后方便提取,你还得添加长度,所以还得到用encode()函数。encode呢,我们需要传入已经序列化后的字符串和该字符串的长度。
》我们知道,我们传入的序列化后的字符串是这个样子的“exitCode_ result_”的格式,我们现在要对其进行encode,那要改成什么样子呢?是“len\r\nexitCode_ result_\r\n”这种样式。
》我们先得到长度字符串,string encodein = to_string(len);然后就是encodein +=CRLF;encodein += in;encodein +=CRLF;然后就是return encodein;这样我们就可以将其发给对方了。所以我们就完成了,网络版本的计算器,服务端的一个基本编写。
》你会发现还是不够,因为Request的序列化你怎么没写呢,encode()你怎么也没写呢。Responce中的反序列化和decode你怎么没写呢?这个是因为,我们刚写的是单独一方的服务端,其实按照我们的主逻辑,是将我们的服务端要用到的接口全部写完了。但是你别忘了,你还有客户端。
》我们客户端ClinetTcp.cc里面,你客户输入了表达式,我们是要将数据构建成一个Request请求的,即Request req;然后调用req.serialize()对结构化的数据进行转成字符串,然后就是要req.encode(),对序列化后的字符串添加字节数。所以我们定义一个string package;我们将其传给serialize()函数,使其得到序列化后的数据。我们还得package = req.encode(package,package.size()),就得到了一个完整的字符串报文,将其发送出去。
》我们发送出去,是不是会得到对方服务器的响应之后发过来的数据呀,那是不是得将服务器发送过来的数据进行读取。读取的操作,我们也可以像服务器那样,同样的处理方式。我们定义一个char buff[1024],调用read()读取的数据放到buff里面,即ssize_t s = read(socket,buff,sizeof(buff) - 1);if(s > 0)说明读取成功,buff[s] = 0;然后将buff拼接到,响应字符串echoPackage += buff;按道理是要和服务器一样这样处理。但是为了偷懒,我们就string echoPackage = buff了。
》然后接下来就要对echoPackage进行解码。解码,按道理我们是不是对响应Responce类进行解码呀。那么我们定义一个Responce对象,Responce resp;然后调用resp.decode()函数。我们decode解码是不是取决于对应的编码,而无论是服务器编码还是客户端编码,我们不考虑他们有效载荷的区别,因为有效载荷肯定是不一样的,Request的有效载荷是3个成员变量,Responce的有效载荷是2个成员变量,所以序列化和反序列化不一样。但是我们解码decode的时候,你是怎么解的?是不是先确认是不是一个薄汗len的有效字符串,然后提取长度进行判断,然后就是确认有效载荷是符合要求的,进行操作,然后确认有完整的报文结构再进行操作,接着将当前报文完整的in中全部移除掉,最后就是正常返回。其实是和我们上面的服务器的编写decode的步骤是一样的。
》所以decode()函数不应该是分别属于Request、Responce类内方法,我们将这个decode()函数单独拎出来。我们请求和响应的报文的格式是一样的,我说的一样,是不牵扯有效载荷,序列化和反序列化肯定是不一样的,但是无论是响应还是请求,都是字符串长度+有效载荷的格式。那么其中ecode()方法对于Request和Responce来说,也是一样的,所以encode()函数也单独拎出来。
》所以继续客户端里面编写,int len = 0,echoPackage = decode(echoPackage,&len);我们有了一个完整的报文之后,我们就对echoPackage进行反序列化,resp.deserialize(echoPackage);反序列化成功之后,我们所要的结果是不是就有了,resp.reust_和resp.exitCode_;
》下面我们就得完成,Resquest的序列化seralize(),序列化的话,一定是x、y、op都已经填好了,和我们上面序列化Responce的一样的,Resquest的序列化完成之后呢,我们还得完成Responce的反序列化工作,对于反序列化呢,也很好写,就是“exitCoide_ result_”。
》我们来编写Request的序列化,在此之前,我们先来看看Request的反序列化是怎么做的。我们当时反序列化是这么写的:我们期望对方发过来的报文是“100 + 200”的格式,你必须严格按照我这种要求来做,所以,提取的时候呢,就是按照这种规定好的格式去挨个进行提取,如果你不规范的话,我方法里面也有逻辑检查。下一个问题,就是序列化我们又该怎么去写呢?
》因为你的格式必须是“100 + 200”的样子,这是其一,其二,记住了,我们现在是假设了一个前提,我们说了,我们ClinetTcp.cc里面,是先定义出了Request的对象req,我们要对req进行序列化,前提条件是我们认为req里面的字段已经被填充了,也就是说,我们Request类里面的3个字段x、y、op已经有了,所以,我们呢已经认为我们的结构化字段中的内容已经被填充,我们不考虑内容是多少,而是他们三个应该怎么序列化。我们可以看到,再定义Request req前面,是我们客户输入的内容,我们要从标准输入里面获得输入内容,然后我们要做的是,根据用户输入的内容构建Request对象req,这时候是真正的填充x、y、op字段的,所以我们还有一段代码还没有写,这个是要注意的。
》我们接下来要做的基本工作呢就是,已经有了,我该怎么去序列化呢?首先我得因为你的参数呢是整数的,所以我得将整数转化成字符串呀,string xstr = to_string(x_);string ystr = to_string(y_);string opstr = to_string(op_);字段都被我们转成字符串了,我们现在就是将它们拼接到我们传入的out字符串里面,out = xstr;out += SPACE;out += opstr;out += SPACE;out += ystr;
》我们clinetTcp.cc读取的时候肯定是能一次读取成功的,因为我们的报文并不复杂,长度也不长,虽然有bug,但是读的话也是能读到完整的报文的。读取后面的decode()函数其实是做了3件事情,第一件事情就是对我们的合法性报文做检查,包括是不是完整的报文检查,再下来呢就是提取一条完整的报文,第三件事情就是要移除提取了的报文。
》我们下面再来写Response的反序列化deserialize()函数,还是要先看Responce的序列化serialize(),序列化很简单,就是两个整数,然后用空格将它们隔开的。然后,我们就要进行提取,我们这个提取比之前要简单的多。我们要做的就是将字符串转化成两子串,然后将其填充到成员变量字段里面。
》所以,我们先进行定点寻找分隔符,size_t pos = in.find(SPACE);紧接着就是判断if(npos = =pos),没找到就只能return false;找到之后呢,根据我们格式的要求,就是“exitCode_ result_”所以pos指向的是空格,exitCode_后面一位。那么string codestr = in.substr(0,pos);string resstr = in.substr(pos + SPACE_LEN);因为,我们说了,我们clinetTcp.cc的读取为了简便,就是没有将收到的buff拼接到inbuffer里面,认为读取一次就能将服务器发来的字符串读完整。在取res的时候就只是给了从哪个位置开始取子串,没有给出取多长,默认就是npos大小。
》既然将一条完整的字符串,分出了我们想要的结构化数据,下面就是将反序列化的结果写入到内部成员中,形成真正的结构化数据。所以我们就exitCoide_ = atoi(code.c_str()) ;result_ = atoi(resstr.c_str());
》我们现在的Request的对象req已经定义有了,用户输入的字符串也有了,我们前面不是说了嘛,当用户输入完了,我们其实就应该将用户输入的数据填充到Request对象里面的成员变量里面的。本来是说,用户必须得是“1 + 1”要带空格输入的,但是算了用户输入就“1+1”这个样子就行了。
》那么,我们现在得再来一个接口makeRequest(),就是用户输入了一段字符串,我们要的是,将你输入的字符串,将其构建成一个请求,这个请求呢,最终就是x是啥、y是啥、op是啥。请求构建完成了之后,再进行我们的后续的序列化和反序列化等一系列工作。所以我们现在来编一些makeRequest()接口,bool makeRequest(string& str,Request
req);“1+1”构建成一个结构化数据,我们怎么处理呢?
》也是一样,可以对字符串呢,做相关的提取。那么也是要根据特定的分隔符来进行提取子串。我们呢就用C语言对应的strtok()函数来进行切割,切割完成之后,哪一个字符,我们将其操作码提取出来,所以我们可以直接先#define BUFFER_SIZE 102,再char strtmp[BUFFER_SIZE];我们调用snprintf()格式化输入,snprintf(strtmp,sizeof(strtmp),“%s”,str.c_str());现在就已经有了用户输入的数据了,然后我要提取它的数据。因为用户输入可能是1+1、1
1等,所以操作码不确定。那么我们分隔符在提取的时候怎么去分割它们呢?我们定义一个char
left = strtok(),strtok()可以指定分割符,是可以传入多个的哈。第一个参数,我们要分隔的字符串是strtmp, 第二个参数传入我们要分割的字符是哪些,我们将要分割的字符宏定义一下,#define OPS ±/%,即char
left = strtok(strtmp,OPS);对我们来讲,用户输入的字符里面必须得是左右两边必须得有数据的,如果你left为空NULL了,if(!left)return false;如果获取子串成功,这是第一次提取,我们也不处理。接着,char
right = strstok(nullptr,OPS);第一次是提取左边的操作数,第二次提取操作数的右边,如果你right为空NULL,那么也是不正确,那么直接返回false了。
》那么接下来要提取操作符是什么了。我们是可以去扫描,因为用户输入的字符串,操作符的左半部分一定是0~9字符或者负数就有“-”这个符号,不会出现除此之外的符号,所以可以从左向右遍历,因为可能用户输入的是负数,所以我们最好是从第二个字符开始遍历,所以就不需要单独考虑负数的符号“-”了,然后遍历的时候直到碰到不是数字的时候就跳过,这是一种做法。当然,你已经把left都得到了,那么远是字符串紧跟着不就是操作符打头的字符串嘛(要去了解一下strtok()函数!)其实严格来讲,用户可能就是喜欢在输入的时候添加空格“1 + 1”,那么我们其实还得在用户输入完之后,加一个清理函数trimStr()。
》所谓的清理什么意思呢,就是,用户可能输入“1+1”是我们要的标准格式,但也有可能“1 + 1”带有空格,或者“1 +1”,等等各种穿插空格的情况。那么我们得将其中的所有特殊字符都要请理掉。说麻烦也不麻烦,就是遍历,就是碰到特殊字符,就将其erase()掉就行了,或者干脆,重新定义一个字符串,只要不是其他的特殊字符,就将其+=到新字符串里面,否则一概不要。这个呢,我们就不处理了吧。 所以算了,我们就不搞trimStr()函数了。
》我们继续来接着我们分割的思路。我们也清楚strtok()函数,它就是将你分隔符设置为"\0",那么前面就是一个子串,后面就是一个子串。所以呢,我现在要提取这个操作符,我们在此之前是调用了snprintf()函数,将我们的str临时保存在我们的strtmp里面,也就相当于做了一个副本,因为strtok()会对传入的字符串做修改,因为strtok()函数将我们的分隔符全部改成"\0"了,我们做了副本之后,我们的str是没有变的。那我们已经提取了操作符的左边数据和右边数据,我们现在就可以再根据我们原来的字符串来进行找分割符,char mid = str[strlen(left)];这个mid指向的就是我们原字符串操作符位置,那么也就拿到了操作符。现在所有的字段,我们都拿到了,就可以构建我们的Request了。
》req->x_ = atoi(left);req->y_ = atoi(right);req->op_ = mid;至此,我们就完成了构建Request对象这么一个结构体了。也就是,用户输入数据后,我们makeRequest()函数构建了已经填充好的Request对象,那么makeRequest()走到最后成功了,就返回return true;也就相当于,用户你输入完之后,我紧接着就构建好了一个Request请求对象。其实我们的makeRequest()也是一个反序列化的工作,它是不是将我们用户输入的字符串,转成对应的结构化数据呀!所以,说白了,这也是一个反序列化工作。
》所以,要记住一点,反序列化呢,不仅仅是在网络中应用,本地也是可以直接使用的。就比如说,我们今天是将用户输入的内容,我们自己闲着没事干,将其反序列化,将输入的数据构建成了一个结构化数据,这是从标准输入里面去做的。比如说,我现在想写一个通讯录,它是不是就有结构体呀,实际上呢,我们可以做持久化,来进行保存。你们以前是怎么写的呢,是直接把结构体的二进制文件直接写到了文件里面。今天,你也知道了,二进制的文件大小,直接以二进制写,万一你的软件升级了呢?比如你现在有一个一年之前保存好的通讯录数据,一年之后,你将代码改了,结构体什么的,甚至编译器都变了, 对齐规则和大小都变了,你直接写到对应的文本文件的二进制内容,你再读就读不出来了。也就是说,你只要软件变了, 那么文件的内容你也就读不出来了,所以你不应该那样去做。当然,不是不行,但是你必须得定制协议。也就是说,你得将你通讯录当中的每一个字段,你也要按照特定的协议,然后给他写到文件里,因为你用的是协议,所以你后面软件在改的时候,也不会影响,因为协议你很少改,你软件再怎么修改,我们读取的时候也能够很好的做兼容。结论就是,不要觉得序列化和反序列化只存在网络当中,网络也是文件呀,Linux下一切皆文件!我也可以将内容序列化到文件当中,然后再从文件里面将其反序列化出来,这就完成了数据保存和数据恢复的工作。其实,序列化和反序列化在哪里都是可以的。
》我们的Request对象构建好了,下面的其他接口也都完善好了,就可以开始我们的测试了。我们客户端给服务端发送1+1的时候,我们在客户端的输出窗口中得到的是 1 43 1,这是为什么呢?因为➕号的Acicall码值就是43。然后给我们打印的debug->encode显示的是6,6是1、空格、43、空格、1加起来是6个有效载荷长度。现在关键是没有响应,肯定是我们的代码当中有一点问题。
》为什么是显示的➕变成了43呢,是因为string opstr = to_string(op_)的时候,op_是char类型,char呢从属于int类型,所以就转成了43,那么就将整数的43转成了字符串“43”了!因为我们的string既支持+=string,也是支持+=char的。所以改成
out += op_。
网络基础(一)_第17张图片
至此我们完成我们代码的第一个阶段,叫做,手写自定义协议。但是总觉得用自己定制的协议比较low。协议呢,添加报文长度是必须的,我必须得保证我读取到的是一个完整的报文,但是我觉得序列化和反序列化那里写的比较低端。我们也看到了,我们手写序列化和反序列化的过程,格式化、空格等,挺麻烦的,所以呢,我们可以把有一部分工作交给比较成熟的解决方案来处理,就是序列化和反序列化的部分。序列化和反序列化部分,我们在写的时候,要注意,encode和decode()是你必须自己去做的,这个呢是没办法的,因为你必须保证读取IO没有问题。关键在于,你把它解决出来之后,我们序列化和反序列化写了很多。倒不是说它有问题,而是稍微有一点点low,那怎么办呢?所以,我们可以不用自己花那么多时间去序列化和反序列化。
》我们可以部分采用别人的方案,这个解决方案,目前比较主流的有xml、 json、protobuf等,有很多的解决方案,不过我们呢, 用最好玩的,可读性最强的josn。所以,我们压根就不需要自己手写序列化和反序列化,我们直接用josn就可以搞定了。那我们应该怎么弄呢?
》你要用josn,你得保证,因为josn是一个独立的第三方库,所以你得保证你在你的云服务器上安装了对应的库,那怎么安装呢?------sudo yum install -y jsoncpp-devel
》我们安装好库之后,我们可以宏定义一下,#define My_SELF 1;就是为了给我们选择是用我们自己写的协议还是用json给我们提供的库文件。在我们的protocol.hpp中添加条件编译,#ifdef MY_SELF #else #endif,意思就是说,如果我们定义了MY_SELF,那么序列化的方案,我们就采用自定义方案,否则的话,我们就调用#else下面json写的。同样的,在反序列化deserialize()里面也添加条件编译。
》下面我们把json带进来,json的版本比较多,如果想多了解的话,可以去jsonCPP的官网或者查找一下jsonCpp相关的资料去学习。我们想要使用的话呢,我们的json安装之后,头文件是在,/usr/include/jsoncpp/json;库文件是在/usr/lib64/libjson。
》所以我们在protocol.hpp里面这样包含:#include,头文件包好就行了。Responce序列化呢,很简单,就两个字段,那怎么进行序列化呢?要想使用json呢,得首先定义json::Value root;(这个Value呢,是一个类,是可以装任意类型的,有一大堆方法,我们不管。什么叫可以承装任意类型呢?json本身就是一个KV式的方案,我们在进行序列化的时候呢,1.必须得先有Value对象,它是一个万能对象,它可以接受几乎任意类型。它是C++写的,所以C++的string等类型都能够接受,所以呢,我们就随便往里面放,放到时候呢,你不光光要考虑序列化,还要考虑反序列化。2.json是基于KV,你想要把int x_字段序列化,你得给它取一个名字,保证这个名字是唯一的,取得时候根据名字来取内容,所以基于KV是这个意思。3.json有两套操作方法,它是跟版本有关系的,我们就用简单的。)我们root[“x”] = x_;其中“x”是key,x_是value,我们的Value对象会自动去识别x_的类型,其实识别不识别已经不重要了,4.其实json在进行我们的序列化的时候,会将我们的所有数据内容,转化成字符串。换句话说呢,其中你写了一个x_,Value内部会对符号“x”做相关的重载,所以对应的你就直接赋值x_,那么就可以用key的形式来提取x_了,x_已经自动给我们转成字符串了,你就不用管了。我们接着,root[“y”] = y_;root[“op”] = op_;我们就已经把对象呢,全部都填充进来了。
》填充好了之后呢,我们下一步要做的就是序列化,那我们就又需要定义json::FastWriter fw对象。这个对象有点像什么呢?你可以将填充的字段,如root[“x”] = x_等看成一个unorder_map,现在要做的呢就是,用FastWriter对象想把法将root对象写成字符串,所以怎么写呢?fw.write()方法,给write传入的参数呢,就是你刚刚填充好的root对象,即fw.write(&root);返回值,就是序列化后的字符串了,我们直接out = fw.write(&root);就这么一点代码,我们就完成了序列化的工作。一会儿,我们打出来看看,一看就懂了,只要像我们刚刚那样写,就能帮我们完成序列化的工作。
》因为,我们曾经有过序列化的经历,虽然,不懂它,但是我们是能够理解他的。我反正知道,不管你怎么做,你将来得让我序列化成一个字符串,完了之后,还得方便我提取,对不对,所以下面继续deseralize(),进行反序列化。
》deseralize()反序列化该怎么写呢?我们一样的,得要有一个中间类型json::Readr rd;然后rd里面有一个parse()方法,第一个参数呢,就是你要进行反序列化,那么你就得告诉我,你想进行反序列化的字符串是谁,这个字符串呢,将来一定是经过decode之后提取到的json字符串,所以,我们要进行反序列化的话,数据源就直接从in里面拿就行了。你像我们上面自己写的字符串in呢,就是通过我们对应的in,从它里面提取出来的,所以,我们在填充参数的时候呢,第一个参数就是in字符串;第二个参数,就是一个输出型参数,我要把字符串结果再把你转成json:;Value,那么直接就是传对应的root;即rd.parse(in,root);至此我们就完成了反序列化。反序列化之后的结果在哪里呢?结果是在我们的Value对象的root当中,可是我们要填充的是x_、y_、op_呀,怎么办呢?我们直接反序列化完成之后,我们叫做x_ = root[“x”];因为你曾经在设置root的时候,你是以KV的形式设置的,那你接下来提取参数,是不是得告诉我,你要提哪一个参数,对不对。因为你提取的时候。root[“x”]全都是字符串,这个字符串可能是把曾经的float、double等类型转成了字符串,当你要提取的时候,root[“x”],提取出来了, Value是字符串,那是不是还得转回我们原先初始化的类型呀!所以x_ = root[“x”].asInt();这样我们就简单的,将x提取出来了,那么y_怎么提取呢,一样,y_ = root[“y”].asInt();op_ = root[“op”].asInt;至此,我们也就完成了反序列化。
》我们接着来写Response的序列化。json::Value root;root[“exitCode”] = exitCode_;root[“result”] = result_;我们依旧是out = Json::FastWriter fw;fw.write(root);就这样序列化就搞定了。
》反序列化呢,Json::Value root;Json::Reader rd;rd.parse(in,root);exitCode_ = root[“exitCode”].asInt();result_ = root[“result”].asInt();至此完成了反序列化工作。
》我们在进行测试的时候,发现编译的时候链接不通过,那是因为,我们用到了第三方库,你得在Makefile里面这样写g++ -o $@ $^ -std=c++11 -l jsoncpp,才可以的哈。一下图片就是我们用json之后的序列化的打印。
网络基础(一)_第18张图片
我们上面用的是json的FastWriter对象,为什么用FastWriter对象呢,因为它给我们序列化之后的字符串是一行,比较好看。然后呢,但实际上可读性还有一种就是我们的Json::StyledWriter,使用上呢,没有任何区别,只不过显示的话呢,是以更加人性的方式去显示,它会给你将KV字段以行为显示。
网络基础(一)_第19张图片
我们protocol在写的时候,encode和decode是和Request和Responce无关的,它直接就是根据传入的字符串,以前传入的是你自定义协议之后的字符串,现在又变成了,你用json协议之后的字符串。只要我们一开始用自己协议之后的字符串传入能够成功,那么json协议之后的字符串也是一定能够成功的。
》很明显,我们想要切换我们底层的序列化和反序列化方案,我们就只要改宏就可以进行切换了。最好的就是,我们想要改成哪一个方案,就很容易变成想要的方案,这是什么呢?这其实就是一种软件解耦。就是你上层的逻辑,和我下层的解决方案,虽然在数据上面有耦合,但是在方案层面上,我们是分开的,这是最好的。
》我们除了在代码里面修改我们的宏来选择方案,我们也可以在我们的Makefile里面进行处理。我们在Makefile里面添加Method=-DMY_SELF(—D选项是命令行式的定义宏的意思),然后在我们的方法指令上面稍微修改一下:g++ -o $@ $^ $(Method) -std=c++11 -ljsoncpp。现在呢,我们已经在命令行上将我们的宏已经定义好了, 我们在实际上编译的时候,它默认的就是采用MY_SELF方案,如果我不想用自己的方案了,我们只要打开Makefile,将我们的Method=-DMY_SELF中的-DMY_SELF注释掉,即Method=#-DMY_SELF,这样Method就是空了,那么就没有这个选项了,此时就变成了json的方案。
》所以,我想说的是呢,以后我们可以选择这样的方式去自由选择我们底层的方案,不需要再改源代码了,直接在Makefile当中添加宏就可以了。 那么-D就相当于是可以在命令行上面的定义宏的方式。我闲着没事干,我为什么要将宏定义放在Makefile里面呢, 我自己改源代码也是可以的呀。但是,你应该会发现一个问题,当我们去编译一个软件的时候,它会自动根据我们当前的系统,对我们的代码做自动化裁剪。比如说,我们要进行编写项目,项目代码既在windos下面跑,又在Linux下面跑,所以,它底层就是拿条件编译完成的,那它怎么知道,我们是windows还是Linux,是x86还是64呢? 所以,实际上在他的源代码充斥着大量的条件编译,但是呢,实际上在编译的时候,不能把宏定义放在源代码里面,而是应该在Makefile这里,它里面也有一些功能识别你的平台是什么,是多少位的,然后设置对应的选项,然后再把参数设置好之后,再用gcc、g++使用定义好的宏,再进行代码裁剪,所以,我们这种方案,就是一整套的裁剪的整体解决方案。我只是为了让大家能够在理解别人的项目,经常看到别人也有条件编译,它是怎么做的呢,实际上呢,他将条件编译写好,写好之后,它把宏定义不放在源代码里面,而是放在编译gcc、g++的选项里面,最后呢,就可以自由的裁剪内容了。
》至此,我们的全部的序列化和反序列化全部讲完了哈。再总结一下。为什么要,手写一次,再去采用别人的方案呢,为什么呢?第一、手写之后能够感觉到,摒弃掉我们对序列化和反序列化的恐惧感这是第一;第二呢、我们能够感受到它的不容易,我们今天的场景仅仅是一个计算器,那如果场景更加复杂呢?这个序列化和反序列化,那如果有大量字符串提取操作呢?那是不是也太麻烦了,所以,序列化和反序列化并不简单。我们也通过手写的痛苦经历,能够感受到,站在巨人肩膀上的快感,也就是用别人的方案呢,我们很快就能做出东西,这就是在对比之下,能够感受到谁优谁劣哈。
》我们代码讲完了,就要进入下一个阶段了。这份代码很有意义,能够帮组大家承上启下,下面呢,我们进入到下一个阶段了。我们再回过头看看协议,什么叫做约定呢,这你就理解了。从我们的通信角度呢,你怎知道你的报文能完整地被对方收到,你怎么知道出现部分报文的时候你还得再继续读,保证你读取的是完整的报文?那是不是我们自己定义的协议,我们的协议格式就是“长度\r\n有效载荷r\n”这是第一;第二就是,我们在业务上也有协议的字段,有x、y、op、exitCode、result,这些字段,我们都叫做协议,双方通信的时候,是都有的。现在我们再理解协议,协议就是一种约定,各位同学不能再是脑子一懵,不知道在说啥了哈。现在应该已经知道,我们就是在逻辑上做好约定就可以了。

SeverTcp.cc
static Response calculator(const Request &req)
{
    Response resp;
    switch (req.op_)
    {
    case '+':
        resp.result_ = req.x_ + req.y_;
        break;  
    case '-':
        resp.result_ = req.x_ - req.y_; 
        break;
    case '*':
        resp.result_ = req.x_ * req.y_;
        break;
    case '/':
        { // x_ / y_
            if (req.y_ == 0) resp.exitCode_ = -1; // -1. 除0
            else resp.result_ = req.x_ / req.y_;
        }
    break;
    case '%':
        { // x_ / y_
            if (req.y_ == 0) resp.exitCode_ = -2; // -2. 模0
            else resp.result_ = req.x_ % req.y_;
        }
    break;
    default:
        resp.exitCode_ = -3; // -3: 非法操作符
        break;
    }

    return resp;
}

// 1. 全部手写 -- done
// 2. 部分采用别人的方案--序列化和反序列化的问题 -- xml,json,protobuf
void netCal(int sock, const std::string &clientIp, uint16_t clientPort)
{
    assert(sock >= 0);
    assert(!clientIp.empty());
    assert(clientPort >= 1024);

    // 9\r\n100 + 200\r\n    9\r\n112 / 200\r\n
    std::string inbuffer;
    while (true)
    {
        Request req;
        char buff[128];
        ssize_t s = read(sock, buff, sizeof(buff) - 1);
        if (s == 0)
        {
            logMessage(NOTICE, "client[%s:%d] close sock, service done", clientIp.c_str(), clientPort);
            break;
        }
        else if (s < 0)
        {
            logMessage(WARINING, "read client[%s:%d] error, errorcode: %d, errormessage: %s",
                       clientIp.c_str(), clientPort, errno, strerror(errno));
            break;
        }

        // read success
        buff[s] = 0;
        inbuffer += buff;
        std::cout << "inbuffer: " << inbuffer << std::endl;
        // 1. 检查inbuffer是不是已经具有了一个strPackage
        uint32_t packageLen = 0;
        std::string package = decode(inbuffer, &packageLen); 
        if (packageLen == 0) continue; // 无法提取一个完整的报文,继续努力读取吧
        std::cout << "package: " << package << std::endl;
        // 2. 已经获得一个完整的package
        if (req.deserialize(package))
        {
            req.debug();
            // 3. 处理逻辑, 输入的是一个req,得到一个resp
            Response resp = calculator(req); //resp是一个结构化的数据
            // 4. 对resp进行序列化
            std::string respPackage;
            resp.serialize(&respPackage);
            // 5. 对报文进行encode -- 
            respPackage = encode(respPackage, respPackage.size());
            // 6. 简单进行发送 -- 后续处理
            write(sock, respPackage.c_str(), respPackage.size());
        }
    }
}
protocol.hpp:

#pragma once

#include 
#include 
#include 
#include 
#include "util.hpp"

// 我们要在这里进行我们自己的协议定制!
// 网络版本的计算器

#define CRLF "\r\n"
#define CRLF_LEN strlen(CRLF) // 坑:sizeof(CRLF)
#define SPACE " "
#define SPACE_LEN strlen(SPACE)

#define OPS "+-*/%"

// #define MY_SELF 1

// decode,整个序列化之后的字符串进行提取长度
// 1. 必须具有完整的长度
// 2. 必须具有和len相符合的有效载荷
// 我们才返回有效载荷和len
// 否则,我们就是一个检测函数!
// 9\r\n100 + 200\r\n    9\r\n112 / 200\r\n
std::string decode(std::string &in, uint32_t *len)
{
    assert(len);
    // 1. 确认是否是一个包含len的有效字符串
    *len = 0;
    std::size_t pos = in.find(CRLF);
    if (pos == std::string::npos)
        return ""; // 1234\r\nYYYYY for(int i = 3; i < 9 ;i++) [)
    // 2. 提取长度
    std::string inLen = in.substr(0, pos);
    int intLen = atoi(inLen.c_str());
    // 3. 确认有效载荷也是符合要求的
    int surplus = in.size() - 2 * CRLF_LEN - pos;
    if (surplus < intLen)
        return "";
    // 4. 确认有完整的报文结构
    std::string package = in.substr(pos + CRLF_LEN, intLen);
    *len = intLen;
    // 5. 将当前报文完整的从in中全部移除掉
    int removeLen = inLen.size() + package.size() + 2 * CRLF_LEN;
    in.erase(0, removeLen);
    // 6. 正常返回
    return package;
}

// encode, 整个序列化之后的字符串进行添加长度
std::string encode(const std::string &in, uint32_t len)
{
    // "exitCode_ result_"
    // "len\r\n""exitCode_ result_\r\n"
    std::string encodein = std::to_string(len);
    encodein += CRLF;
    encodein += in;
    encodein += CRLF;
    return encodein;
}

// 定制的请求 x_ op y_
class Request
{
public:
    Request()
    {
    }
    ~Request()
    {
    }
    // 序列化 -- 结构化的数据 -> 字符串
    // 认为结构化字段中的内容已经被填充
    void serialize(std::string *out)
    {
#ifdef MY_SELF
        std::string xstr = std::to_string(x_);
        std::string ystr = std::to_string(y_);
        // std::string opstr = std::to_string(op_); // op_ -> char -> int -> 43 ->

        *out = xstr;
        *out += SPACE;
        *out += op_;
        *out += SPACE;
        *out += ystr;
#else
        //json
        // 1. Value对象,万能对象
        // 2. json是基于KV
        // 3. json有两套操作方法
        // 4. 序列化的时候,会将所有的数据内容,转换成为字符串
        Json::Value root;
        root["x"] = x_;
        root["y"] = y_;
        root["op"] = op_;

        Json::FastWriter fw;
        // Json::StyledWriter fw;
        *out = fw.write(root);
#endif
    }

    // 反序列化 -- 字符串 -> 结构化的数据
    bool deserialize(std::string &in)
    {
#ifdef MY_SELF
        // 100 + 200
        std::size_t spaceOne = in.find(SPACE);
        if (std::string::npos == spaceOne)
            return false;
        std::size_t spaceTwo = in.rfind(SPACE);
        if (std::string::npos == spaceTwo)
            return false;

        std::string dataOne = in.substr(0, spaceOne);
        std::string dataTwo = in.substr(spaceTwo + SPACE_LEN);
        std::string oper = in.substr(spaceOne + SPACE_LEN, spaceTwo - (spaceOne + SPACE_LEN));
        if (oper.size() != 1)
            return false;

        // 转成内部成员
        x_ = atoi(dataOne.c_str());
        y_ = atoi(dataTwo.c_str());
        op_ = oper[0];
        return true;
#else
        //json
        Json::Value root;
        Json::Reader rd;
        rd.parse(in, root);
        x_ = root["x"].asInt();
        y_ = root["y"].asInt();
        op_ = root["op"].asInt();
        return true;
#endif
    }

    void debug()
    {
        std::cout << "#################################" << std::endl;
        std::cout << "x_: " << x_ << std::endl;
        std::cout << "op_: " << op_ << std::endl;
        std::cout << "y_: " << y_ << std::endl;
        std::cout << "#################################" << std::endl;
    }

public:
    // 需要计算的数据
    int x_;
    int y_;
    // 需要进行的计算种类
    char op_; // + - * / %
};

// 定制的响应
class Response
{
public:
    Response() : exitCode_(0), result_(0)
    {
    }
    ~Response()
    {
    }
    // 序列化 -- 不仅仅是在网络中应用,本地也是可以直接使用的!
    void serialize(std::string *out)
    {
#ifdef MY_SELF
        // "exitCode_ result_"
        std::string ec = std::to_string(exitCode_);
        std::string res = std::to_string(result_);

        *out = ec;
        *out += SPACE;
        *out += res;
#else
        //json
        Json::Value root;
        root["exitcode"] = exitCode_;
        root["result"] = result_;
        Json::FastWriter fw;
        // Json::StyledWriter fw;
        *out = fw.write(root);
#endif
    }
    // 反序列化
    bool deserialize(std::string &in)
    {
#ifdef MY_SELF
        // "0 100"
        std::size_t pos = in.find(SPACE);
        if (std::string::npos == pos)
            return false;
        std::string codestr = in.substr(0, pos);
        std::string reststr = in.substr(pos + SPACE_LEN);

        // 将反序列化的结果写入到内部成员中,形成结构化数据
        exitCode_ = atoi(codestr.c_str());
        result_ = atoi(reststr.c_str());
        return true;
#else
        //json
        Json::Value root;
        Json::Reader rd;
        rd.parse(in, root);
        exitCode_ = root["exitcode"].asInt();
        result_ = root["result"].asInt();
        return true;
#endif
    }
    void debug()
    {
        std::cout << "#################################" << std::endl;
        std::cout << "exitCode_: " << exitCode_ << std::endl;
        std::cout << "result_: " << result_ << std::endl;
        std::cout << "#################################" << std::endl;
    }

public:
    // 退出状态,0标识运算结果合法,非0标识运行结果是非法的,!0是几就表示是什么原因错了!
    int exitCode_;
    // 运算结果
    int result_;
};

bool makeReuquest(const std::string &str, Request *req)
{
    // 123+1  1*1 1/1
    char strtmp[BUFFER_SIZE];
    snprintf(strtmp, sizeof strtmp, "%s", str.c_str());
    char *left = strtok(strtmp, OPS);
    if (!left)
        return false;
    char *right = strtok(nullptr, OPS);
    if (!right)
        return false;
    char mid = str[strlen(left)];

    req->x_ = atoi(left);
    req->y_ = atoi(right);
    req->op_ = mid;
    return true;
}
Makefile:

.PHONY:all
all:clientTcp serverTcpd
Method=#-DMY_SELF

clientTcp: clientTcp.cc
	g++ -o $@ $^ $(Method) -std=c++11 -ljsoncpp
serverTcpd:serverTcp.cc
	g++ -o $@ $^ $(Method) -std=c++11 -ljsoncpp -lpthread

.PHONY:clean
clean:
	rm -f serverTcpd clientTcp

我们刚刚写的网络版本计算器,你可以理解成,我们写的是应用层。有三件事情,我们是做了,1.基本系统的socket接口使用;2.定制协议;3.编写我们对应的业务。
》那么有没有已经非常成熟的场景,全世界人民都要用的场景呢?有程序员自定义协议,并且写的还不错,被所有人接受,最后这套协议就成了一套标准,成了应用层的特定协议的标准。就拿我们刚刚写的网络版本计算器,假设我们写的还不错,后来还有很多的人要用,可以的,但是你必须得按照我的标准,比如说我的协议必须得是整数\r\n有效载荷\r\n,序列化和反序列化的字段必须得是x、y、op,结果必须得是exitCode、result,你必须得遵守我的协议,那么遵守我们的标准,就诞生出了很多写好的应用层协议了,就能被别人直接使用了。
》比如说http、https等等协议都是被用的比较多的。所以,我们必须得清楚,我们下面呢,要学习应用层协议,学的就是别人定制应用层协议的细节。就好比,你今天把你的代码写完了, 给别人了,你说,你能不能学一下我写的定制协议的过程,那么别人不看你的源代码,它只看你的protocl.hpp这样的头文件,然后把你的方法看懂,那么协议就能看懂了。所以呢,我们就先来讲应用层协议http、https为主,这一块非常重要。
》个人觉得应用层、TCP层,像IP层和MAC帧层,那其实就是让我们理解原理,对于我们来讲,最重要的就是应用层和传输层这两层协议,能够做出东西才是最重要的!所以呢,我们自顶向下,先来讲http协议。
在这里插入图片描述

HTTP协议

http更多的是在我们网页的获取上面,当然,为什么应用层那么多协议,我们就挑http来讲呢?因为http的用途太广了 。有人说,我现在,在电脑上访问,我经常可以随便访问一个网站,你可以发现,比如说,访问百度、qq它们的网站,全部都是http,有人说,不是后面带了s嘛。但是大概在2008年之前,我们的互联网处于草莽阶段,除了像银行会采用单例模式,其他的互联网公司基本上都是http,但是现在,基本上都是https ,所以,我们http会讲,我们https也一定会讲。
》所以,我们先讲http有一个概念。有人说,我们怎在手机上面没有见过,那是因为手机上的网络请求或者链接,全部都给你隐藏了。你打开哪个app就是帮你连接了它的服务器了,是不会出现链接的。当然,也有分享内容,转发的是什么呢?其实,说白了就是链接,只不过app给你进行了图片化处理,让你看起来是一个图片,它底层全都是https。所以,我们整个互联网的应用层世界,都是用由https构建的,这是其一;
》其二,我们也有一些特殊应用,它是自定义协议,我这么告诉你,在自定义写协议当中,在应用层有相当大的一批协议呢,是模仿http定制的协议的。所以呢,你说http协议重要不重要呢。应用层http+https几乎是应用层最具代表性,没有之一。
》我们认识http,就得先来认识一下URL。

认识URL

网络基础(一)_第20张图片
一般呢,我们的网址包含的就是http、https,然后就是://+登录信息,但是从来没见过呀,也没有登录过呀,我看到的全都是手机扫一扫或者账号登录了。你记住了,现在所有的应用端,让我们很方便去登陆,底层全部都是http,只不过会给你做相关的转化罢了,所以登录信息会被省略,我们全都是用http网页方式,让用户去输入账号密码,然后再去登陆,这个登录信息,已经是以特定的方式,比如说是在请求报文当中,发送给我们对应的服务端的,所以对我们来讲呢,登录信息这个字段我们不用考虑。
》再下来呢,就是我们的域名,比如www.com。再下来呢,就是服务器端口号,端口号呢,现在肯定不陌生了,一个服务呢,要绑定端口号,我们写的所有服务器内容都是需要端口号的,端口号呢,也是需要在URL当中出现的。那么为什么得必须要有端口号呢?你一定要记住这句话,域名,比如说www.com,域名就是我们的www.com这种,就是我们的域名,当域名在进行我们对应的访问网站的时候,它必须被转化成为IP地址。访问网络服务,服务端必须具有端口号port!
》你信誓旦旦的给我说这个结论,是凭什么呢?因为我们现在懂了,网络通信了里面,我们都是套接字通信,所以网络通信里本质就是:套接字socket通信,就是IP + Port。所以呢,域名必须得被转化成IP地址。当然转化过程,你不用管,也是浏览器+一个服务,叫做DNF服务来给我们做域名解析。然后端口号这个字段呢,我们有一个问题。那么,你告诉我,一完整的URL当中呢,必须得包含我们对应的端口号,但是,我怎么没在网址上见到呢?你的www.baidu.com怎么没有带端口号呢?比如说,我搜索中国:https://www.baidu.com/s?ie=utf-8&f=8&rsv_bp=1&tn=84053098_3_dg&wd=%E4%B8%AD%E5%9B%BD&oq=%25E4%25B8%25AD%25E5%259B%25BD&rsv_pq=b49cce810000a212&rsv_t=562c2WPRH%2FlDVBJT0Us0fDSgdzmFgQPQSlBtcPL8XnjCfjbcmORnNzmuiowrWj2vYpOLqg&rqlang=cn&rsv_enter=1&rsv_dl=tb&rsv_sug3=11&rsv_sug1=4&rsv_sug7=101&rsv_sug2=0&rsv_btype=t&inputT=1478&rsv_sug4=1767。也没有见到什么端口号呀。你说域名必须会被转化成IP地址,我先暂且接受了,具体后面再谈,现在问题就是,域名对应IP地址,我们的网络服务必须得具有端口号,但是,我并没有看到端口号呀。
不见端口号的原因是,在使用确定协议(是我们常见的,http、https、smtp等)的时候,我们一般显示的时候,会缺省我们所对应的端口号,说白了,就是把你的端口号忽略了。它所谓的缺省是在URL上面,包括你分享的链接上面,它不用再带上端口号了,并不代表,它发送请求的时候没有,是有的!也必须得有。什么意思呢,就是说,我们在给别人分享链接呢,是没有端口号的,但是,当任何一个浏览器,或者手机app收到了一个链接,在点击的时候,就有浏览器或者你的app必须得给你自动添加上端口号,只不过没有在我们的URL上面体现出来。
》说白了,就是我们自己肉眼看到的URL,大部分用的是没有端口号,我们在请求的时候,我们的浏览器它是自动给我们添加的,域名转成IP地址之后,再加上端口号,那么IP地址 + 端口号Port,然后才能发起connet,没有这个工作的话你别想通信,那是不行的。
》这也就衍生出第二个问题,第二个问题就是,浏览器它怎么知道我的端口号是多少呢?你浏览器要自动添加端口号,你怎么知道我的端口号是多少呢?所以呢,浏览器在访问指定的URL(统一资源定位组)的时候,我们确实看到URL本身没有带端口号,但是浏览器必须得给我们自动添加Port,如果没有Port,当然域名对应的IP得有,没有Ip、Port,你这个客户端是没有办法去访问服务器的。我们也写了这么多的服务器了,我们写的都是命令行的,但是呢,你现在看的是图形化界面或者app,但是它底层必须得有这个东西,没有这个东西它走不起来的。所以,它得自动添加报头,那么现在的问题是,浏览器如何得知,我们对应的URL匹配的Port是谁呢?你为什么给我加特定的端口呢?这个我们回忆一下,这个很好解决,因为特定的纵所周知的服务,端口号必须得是特定的!也就是说http对应的server必须得帮我们绑定端口号->80,https绑定的端口号是->443,这就是服务和端口了必须得是绑定的、确定的。
》有人说,不对呀,你给我演示的端口号,换的很勤快呀,一会儿8081,一会儿8080等。那是因为,我们在做测试,一旦你服务上线,端口号是必须确定的。我们曾经讲过,我们用户自己写的网络服务bind端口的时候,你只能bind 1024之后的端口,因为1024之前的是给特定的服务使用的!你把别人占了,就会导致别人启动不了了,所以,有时候,这些端口是不会让你去绑定的。
》它们两者之间的关系呢,一个特定的服务和特定的端口之间的关系,就好比110和警察的关系,119和我们的火警关系等关系一样,它们是强绑定的。所以你怎么去标定一个协议的成熟呢?你就看操作系统和整个网络的生态,有没有给你的服务留端口,如果没有给你留端口,那么就说明你的应用不算特别成熟。有人又说,我们经常用的mysyql3306、redis3379这些端口也是1024之后的呀,这个确实用的是1024之后的,但是呢,这些端口号也是被全世界人民都知道了,一般这些端口,我们也是不会轻易去使用的。只不过,我们用的1024之前的端口呢,太成熟了,相当于被使用的最广泛最基础的服务,整个操作系统或者云服务器说,你要用这个端口不行,我不能让你使用。当然可以试试在虚拟机里面能不能绑定。所以,我们URL里面的端口号也是可以被省略的!因为,我们前面有协议字
网络基础(一)_第21张图片
段,所以我们端口号是可以被省略的。
》有了IP地址可以确认主机,有了端口号就能确定那个服务,那么http是做什么的,我们重新理解一下http是干什么的。我们只清楚了URL的前半部分,我们要来搞一搞
http是干什么的

》一般在我们的浏览器或者手机App上,你一般都是干什么?以浏览器为例,你可不可以查阅文档呢?比如说,我看了一个网上的教程,或者在CSDN上看了一篇文章,这是查阅文档。我们还可以观看音视频,就相当于有些听音乐、视频等。
》它们的呈现方式都是以网页的形式,说白了网页就是一个最基本的.html文件。无论你是什么,当你浏览器发起请求的时候,我们的服务端有一个文件,比如说网页文件,你浏览器请求的时候,我把网页返回给你。网页文件有内容,那它是不是也是数据,所以本质上,我们请求一个服务时,我们本质上也是进程间通信,说比啊了就是套接字嘛。所以,它把文件内容,打开文件,然后把内容读取到内存里面再给我们发过来,然后我们的浏览器就能收到它了。换句话说呢,我们就可以通过获取网页的方式来得到我所想要得到的东西。
》所以,最终,我们永远清楚一件事情,**http–是获取网页资源的,视频、音频等也都是文件!!所以我们浏览器可以请求的资源呢,目前像文件、图片、视频、音频等所有的资源呢,我们可以称之为http是向我们指定的服务器申请特定的资源的。**像我们今天,把所谓的网页文件、图片文件、视频文件等我们统称为资源。http呢,核心目标是申请特定的资源,把资源获取到本地,本地可能是你的app、浏览器、迅雷等等,拉取到本地进行展示或者使用的,这就是http协议。
》人家官方叫法,叫做超文本传输协议,那么他为什么叫做,超文本传输协议呢?网页就是文本的,为什么叫做超文本呢? 超在哪里呢?超就超在,他可以传图片、音频等等。按照今天,我们曾经对网络代码的编写,对于一个数据如何发给客户端,我们肯定能过理解,一个文件发给客户端,你们也能理解!说白了就是,在我们的服务端,你要访问哪一个文件,我把文件给你打开就行了,然后把内容给你读取出来,然后把内容发给你就行了,这一定是能过理解的。
》但是忽略了一个问题,无论是网页、图片、音频、视频,**如果我们clinet没有获取的时候,资源在哪里呢?**比如说,你想去腾讯视频看电影,你没看的时候,资源在哪里呢?还能在哪里呢,你自己部署的服务,在你的服务器上,那么资源也就只能在这个服务器上呀,你不在服务器上,我怎么去获取和打开呢,所以,这个虽然是一个常识性的东西,但往往会被我们所忽略。所以,很简单,**资源就在你的网络服务器所在的服务器上(硬件,计算机上)。**比如说,优酷经常上一批新的电影,它没有上线之前呢,它的电影在版权商上面, 买过来了呢,就将版权给了优酷了,优酷是不是就可以将视频文件上传到服务器上,部署对应的网络服务,那么就可以将资源打开之后发给你了,我们就这么理解。
》那么,**服务器都是什么系统呢?Linux系统!那么请问,在我们Linux系统上,这些资源是文件呢? 有的资源呢,有图片、视频、音频、网页等,这些是文件吗?全部都是文件,那是文件就能够被打开,无非是文本打开,还是二进制打开。那么,既然是文件,我们就得出一个结论,叫做,资源文件在我们的Linux服务器上,那么当我们后续有人请求的时候,我们的服务呢,想把文件打开,你是不是要把文件打开 ,比如说,自己写了一个网络服务,bind、listen等都搞定,搞定之后呢,别人想访问html,它要得到html,那么我服务端是不是要将文件打开,打开之后,用read读到我们的内存里面,然后再write给别人写回去,所以资源文件在服务器上,我是不是得打开。所以,我作为服务器,我得先打开这个文件。
》所以
要打开资源文件,读取,发送给我们的客户端,一切一切的前提是,软件服务器必须得先找到这个文件。**你连文件都找不到,谈何打开文件呢,是不是!请问Linux下要找到这个文件,如何找到呢?根据路径!所以,我们可以通过在机器上使用路径找到这个文件呀,找到之后就能访问这个文件呀!
》当我们知道了,在访问我们服务器上的某一个资源,它一定要有路径的,所以我们URL后面的部分会跟上/dir/index.html等其他相关的信息。www,example是机器,80是机器上的服务,/dir/index.html这个是我想在该机器上在该路径下,用该服务,将这个资源给别人返回去,所以这个叫做资源路径。这就是URL后面会带上这个的原因。所以你有没有发现,有带上“/”呀,随便翻其他网页都是会有的。因为“/”是Linux下的路径分割符,这个也证明服务是部署在Linux服务器上的!
》那么路径的第一个"/“是不是就是根目录呢?答案是:不一定!因为你也懂,访问某些文件是可以用绝对路径访问,也可以用相对路径访问,所以最开始的”/"呢,可能是以根目录开始,也可能从某个特定路径开始,怎么做的后面会说。
》http人家官方说法叫做超文本传输协议,重点是可以获得各种各样的资源。我们呢,给资源下了一个定义,我们在Linux当中呢,以后站在网络视角上,Linux服务器上的所有东西都称之为资源,无论是图片、音频、视频、网页等,我们都叫做资源。所以http说白了就是从本地向服务端获取资源的,这就叫做http协议。虽然我们http一行代码没写,其他理论基本也没讲,但是我们心里其实也已经很清楚了。他这个资源呢,通过网络服务器呢,收到之后就可以在本地,根据它传来的,要访问什么资源,既然路径已经给我了,那么我直接打开之后,然后再发给他,这就叫做http。
网络基础(一)_第22张图片
实际上,在我们进行网络请求的时候,因为我们是中国人,用的是汉字。如果,我想通过http协议去传输URL你本身的一些字段呢。比如说,你URL里面的特殊符号,比如?等,我也想要在URL当中传输其他符号,一些特殊符号本来就是在URL当中有特殊用途的,那么我该怎么去处理呢?总不能再加上特殊符号,把我的特殊符号以字面值的方式放在后面,那这样解析URL的时候就出问题了,我们用的是一些汉字,另外有些汉字也是需要给你重新编码的,所以我们就有了urlencode和urldecode。

urlencode和urldecode

关于urlencode和urldecode这两个概念呢,我们之前给大家讲的时候,主要是给大家用来做协议处理的,我们也说过,协议处理的字段比较简单,有时候要传输的字段呢会包含一些特殊符号,这些特舒服好在进行我们URl传送的时候,我们需要把这些特殊符号进行一定程度上的编码,这些编码呢体现在哪儿呢?
》假设我今天要搜索C++,然后对应的会跳出网页会有对应的URL,那么我们复制出来,我们可以看到,https://www.baidu.com/s?wd=C%2B%2B&rsv_spt=1&rsv_iqid=0xea23843c0007f2f0&issp=1&f=8&rsv_bp=1&rsv_idx=2&ie=utf-8&tn=baiduhome_pg&rsv_dl=tb&rsv_enter=1&rsv_sug3=14&rsv_sug1=5&rsv_sug7=101&rsv_sug2=0&rsv_btype=i&inputT=3759&rsv_sug4=5154。前面https是协议,然后www.xxx.com是域名,然后加上我想访问资源的路径,后面还跟着一些以&符号隔开的信息字段,其中这些就是通过URL的方式来向服务端传参,其中就包括有KV式。我们再搜索一下CPP,再粘贴URL,其实大部分字段是差不多的,唯一差别会在下面的图中用方框括起来。
》那么百度怎么知道你要搜索什么东西呢?除了你URL一大堆的属性信息,kV值传给了我们的百度,最重要的就是wd,wd就是word的简写,你要搜索的关键字是什么,然后C%2B%2B,另一个是CPP,两者有一些区别。原因是,一个我们搜索的是C++,一个我们搜索的是CPP。什么意思呢,意思就是说,我们实际上发起我们http请求的时候,有些字段是需要给我们进行编码的,记住,编码不是加密,而是将我们有些字符进行编码。因为有些符号呢会在URL里面出现特殊符号,+号就是一种特殊符号,所以你C++发出去的时候,就会给你重新编码了,那么CPP呢不是特殊符号就给你保留了。
》我们搜索好人的时候,可以看到URL里面的wd值也是一串特舒符号哈。后面回血mysyql,会告诉大家,一个汉字呢,它通常被编码的时候会采用3个字节来进行编码的。所以,你要首先理解的就是,实际上进行我们编码是为了便于我们传输,有些内容,尤其是http上面,它的有些字段呢,是会给我们进行编码的,所以呢,这些信息是被你本地编码完之后,再交给服务器端,服务器就要能够对编码进行解码说白了就是将%2B解码成+号,那么就需要urlencode和urldecode了。
网络基础(一)_第23张图片
》urlencode和urldecode核心工作,就如它的名字一样帮我们进行编码和解码。当然这里的编码和解码呢,和我们前面代码进行对接就相当于,如果我要做这件事情,那么我们在我们的encode那里除了将报文解析出来,还要做的工作就是将报文进行encode和decode,你可以在序列化之前做,也可以在序列化之后做。我们推荐的是一般在序列化之后做。当然,不同的自定义协议,它是不一样的。所以就可以理解成,urlencode和urldecode对出现的特殊字符进行重新编码,到服务端进行重新解码。
》那你这个编码的工作是谁做的呢?编码的工作就是由你的浏览器来做,那么解码是由谁来解呢?那么就是由我们对应的服务端去做的。那么为什么,我们前面写的socket没有做这份工作呢?那是因为你写的代码是不需要做的,因为客户端也是你写的,但并不代表在http里面不需要哈。http协议的特殊性就决定了。
》那么如果,我想知道哪些符号被编码之后的值是怎么样子的呢?我们可以搜索在线进行编码和解码平台。

http协议格式

http的请求格式

说白了,我们现在对应用层也清楚了,我们前面写了一个基于自定义或者jsoncpp方案的一个序列化和反序列化的代码的关系了。现在的问题是什么呢?那么现在对我们来讲呢,我们能够序列化和反序列化,然后有自定义协议的经历了。那么http也是曾经另一批人呢,也在做这样的事情,另一批人呢,在应用层做这件事情的时候呢,说白了它底层就使用的套接字接口 ,它也进行读写,但是他在应用层要自己定制协议,它们的协议格式呢,非常简单。我们下面先来看看http请求格式。
》http reqeust我们简化一下方便理解。http请求很多教材按3部分,但是我们按照4部分来说。
》第一部分以行为单位\r\n,这就是我们为什么在写自定义写网络计算器的时候,非要给大家用\r\n这个东西,如果不加也是可以的,那么就是为了让大家能够理解。那么http协议的第一行,我们称之为叫做,请求行。要记住,http是基于行的一个协议。其实说白了,就和我们上面写网络版本计算器也是基于行的 ,我们第一行是长度\r\n,再下来就是正文,必须得根据长度,再去读取报文,报头和有效载荷用\r\n区分开。请求行一般会被分为3部分。第一部分叫做请求方法,请求方法,常见的有get方法、post方法,当然还有其他方法,delete方法,当然我们最常见的就2中get、post方法。然后就是以“空格”作为分割符,URL,其中URL一般是去掉端口号的,只包含路径的URL,但并不排除有时候URL是一个字段全的URL,包括了你的所有的http请求,你要请求的服务器是谁,你请求的资源是谁。说白了,这里的URL只有从路径开始“/”往后的部分。但是,有时候web服务器充当代理的时候,它的URL就是整个URL,这样才方便进行二次代理。
》我们今天呢,能看到的就是,你想请求服务器的资源路径是什么。第三个就是发起请求的客户端的http协议的版本,常见的有http1.0、1.1版本,现在主流的就是1.1版本,其中呢以\r\n作为结尾。我们的协议第一行就是下面图片这个样子,来进行我们对应的处理。
网络基础(一)_第24张图片
》第二个部分是包含了有多行,但是我想将其看成一个整体,但实际上呢,http第二个部分是有多行内容,所以每一行都要包含一堆的\r\n。虽然将多行看成一个整体,但依然是多行内容哈。 其中这么多行当中呢,主体就很多http请求的报头属性,我们可可以把http第二个部分整体叫做请求报头。但是请求报头的属性非常多。最常见的可能是给大家讲的5、6个,但是所有字段呢,都是key:value,也就是说http请求当中,包含的所有的字段里面包含的都是KV形式的。有个小细节就是“K : 空格 value”的形式,这就是http请求的第二个部分,它是按行为单位的。
网络基础(一)_第25张图片
完成之后,紧接着,在第三部分,本身就是\r\n开头。说白了就是,我这个http第二个部分请求报头它是\r\n结尾就会跳到下一行,然后第三部分是一个\r\n开头的,那么又会跳到下一行。所以我们说的第三部分表现的最典型的特征呢就是空行。它这一个空行带来的一个比较好的地方呢就是,我们前面的内容能够保证,前面的内容不会有空行,是必须得有字段,有字段的话,按照我http协议读取的时候,我们可以按行去读,读一行、两行…一直读,直到读到空行,我们就能证明我们把报头和请求行全部读完了。因为,我们把我们说的第三部分之前的全部读了,我只要知道,第一行是我们对应的请求行内容,那么剩下的就是请求报头。
》那么我们说的第三部分是一个空行,就相当于用空行来做分割符,来将自己的报头,全部让别人解析出去。
网络基础(一)_第26张图片
所以对我们来讲呢,http请求呢,当我们看到目前的结构就能够理解,用特殊符号来表示报头部分。不知道大家还记不记,曾经问过大家一个问题,任何协议,都要面临如何将报头和有效载荷分离的问题,当时我们在讲网络基础一的时候讲过。我们在我们的代码中也践行了这个规则。我们在写我们的网络版本计算器,其中我们就是先用我们的报文长度,然后就是读到\r\n,证明我已经将长度读完了,然后解析长度,将有效载荷提取出来。
》现在的问题就是,当我们实际在按照我们所对应的协议去定的话呢,就是用特殊字符\r\n,今天呢,我们http它也是这个样子,它也是通过\r\n能够把报头,不管你是请求行还是请求报头,都可以称之为前半部分,协议的本身字段。后半部分以空行作为分割符,然后再下来的一部分内容,我们称它为叫做,有效载荷。说白了就是用户你自己定的哈,有点像我们前面写的网络版本计算器,我们的有效载荷呢,就是数字+操作符+数字这样子的,或者就是状态嘛+结果这样的。所以这里最后的部分用网络术语就叫做有效载荷,用http术语称它为请求正文。
》一般请求正文就是,登录账号和密码、个人信息、音频、视频等,都属于客户资源。所以内容呢,就是在有效载荷或者叫做请求正文里面了。
网络基础(一)_第27张图片
》所以http请求就被分为了4部分,其中请求行和请求报头是前2部分,空行独立成一部分,然后有效载荷时第四部分,而我们前两部分是一体,后两部分是一体,或者干脆就分两部分,请求行、请求报头和空行是http协议等报头,然后有效载荷就是报文。这就是http协议等请求!
》所以http就可以通过浏览器构建了一个http请求,发起一次http Request,说白了就是把我们所对应的字符串呢,会按照行为单位,把我们全部的发送过去。然后最终,服务器是不是就收到了,就和我们前面自己写的协议一样。我们自己构建了一个序列化和反序列化,定义了一个protocol,然后客户端向服务端发起我们对应的请求,客户端向服务端发起请求的格式就像我们这个样子。发的格式是这个样子,我们在解析的时候可以按行去读。读取呢,解析请求方法,你想要请求什么资源,协议的版本是什么, 然后你请求的相关报头属性 ,包括你的空行和有效载荷。
》那么我们接下来就要做什么呢?作为http的处理方,也就是服务器方,那我是不是要收到http,我就要对他的请求做解析,就好比,我们上节课收到计算器的request,我们要对Request做解析,进行解码和序列化和反序列化相关工作,然后进行逻辑计算,得到结果,那么就能够构建Responce,那么http响应分几步呢?也叫做http的Responce。

http的响应格式

http的responce分为几个部分呢?也是分为4部分,这也就是http设计的比较优雅的地方。其中http响应的第一部分,它响应也是按行为单位的,即\r\n。它的响应行呢,也包含3部分。第一个叫做状态码,这个状态码不应该感受到陌生了,我们前面的计算器是不是也就有状态码呀,用不同的数字表示不同的状态。状态码呢说白了就是http这次响应是怎么个样子,最典型的就是,404,那么404表示的意思就是,你这个请求的资源不存在,这就是状态码。状态码一般是数字,为了能够支持更好地理解这个意思,所以会在后面跟上另一个部分叫做,状态码描述。那么状态码描述呢,说白了就是解释状态码是什么意思,比如说,404对应的就是Not Find这样的报错。第一行呢,除了有状态码、状态码描述,还有一个就是http协议的版本。大家应该是有这样的一个感觉,我们能够知道,我们现在的网络呢是有客户端和服务器这样的概念的。有时候客户端是一个软件,它定期会更新各种版本,比如说你现在有微信、抖音、快手等,这些app都有自己的版本,那么服务器有咩有自己的版本呢?服务器也是有版本的。
》那么这里就有一个问题,如果我拿一个老客户端,不小心访问了新的服务器,那么按照照道理来讲,我们要保证老客户端正常工作;还有,要保证客户端没有升级就不应该看到新的功能。所以,我们一般在进行我们企业级或商业级呢,会定制自己的版本,说白了就是,你将来在双方第一次通信的时候, 有的人说app协议发来发去有什么意思,其中这个呢,就是表明客户端给服务器说,我的版本是1.1的,服务器响应的时候,也说,我的版本也是1.1的。当然,客户端给服务器响应我的版本是1.0的,那么此时你客户端和服务器通信的时候,就以版本最低的协议来进行通信。所以双方在长时间没有维护的协议或者软件或者版本的话,那么双方就会在建立通信的时候,有可能会有交换协议版本的过程。通过协议版本来保证双方都能够正常工作,也保证了很好的软件向前向后的兼容。
网络基础(一)_第28张图片

》所以出现两次http协议版本呢,大家也不用觉得奇怪,一个是表明客户端,一个是表明服务端。
》那么,http响应的第二部分,和我们的http请求是一模一样的。响应的第二部分也是以KV的方式组织的,只不过它的这个字段的报头呢,我们称之为响应报头。响应报头也有很多,它是以KV方式帮我们去组织好了,以换行符的方式,可以按行进行呈列。这一点呢,和我们的请求也是一样的。
》再下来呢,也是和我们的Request一样的是,\r\n开头,说白了也是空行。换句话说,http协议的响应和请求很像,它也是4部分,然后以空行为分割符,然后把前半部分内容响应给客户。
网络基础(一)_第29张图片
》最后一部分当然是,有效载荷了,这个有效载荷,是我们通常称作的响应正文。你Request有请求正文,同样的,我Responce也应该有自己的响应正文。响应正文是什么呢?不要忘了,你一般请求什么都是在你的URL当中,URL里面是不是包含了,你在我这台机器上请求了什么资源对不对。所以,在进行响应的时候,对应的响应正文部分涵盖的就是客户请求资源。一般,资源包含什么?像我们听过的html、css、js、图片、视频/音频等等,说白了这些都是我们前面说到的一个概念–资源。换而言之,就相当于你的请求呢,服务端识别到你是要请求什么资源,然后再把对应的资源给你打开,然后通过网络的方式返回给你,返回给你之后就拿到了对应的资源。
网络基础(一)_第30张图片
》这就是http整体的框架结构。
网络基础(一)_第31张图片

实验和解释GET、POST方法

下面呢,我们要做一些实验。你说的请求是这个样子,那我是不是得见一见呀,一个是见到请求,一个见到响应。我们不废话,我们先尝试获取到客户端发过来的请求。我们尝试了一下,在浏览器输入我们服务器的IP地址+服务端的端口号,我们就收到了浏览器发来的请求,我们可以看到:Get是请求方法,请求资源呢,是“/”,http版本呢,是1.1这是对应我们说的请求行。然后接下来就是请求报头:Host行;Connection行,它表示我们请求时所采用的链接方式,keep-alive后面会说它的,表示长链接;Cache-Control行;Upgrade行,我们也暂时不考虑。重要的是User-Agent代表的就是我们所用的浏览器对应的版本;还有很多行,就不说了。下面图片就是完整的http请求。
网络基础(一)_第32张图片
网络基础(一)_第33张图片
浏览器本来就是客户端软件,请求失败,会发送多次请求,所以你收到的请求可能是多条也是正常的。目前可以看到请求,如果还想看到响应的话,我们可以在命令行上获取某一个特定服务器的响应。图片在下面。
网络基础(一)_第34张图片
我们现在请求和响应都见到了,我们接下来要做什么呢?那么我们是不是要慢慢的学习它里面的一个个字段呀。所以我们从请求开始。
》我们第一个学的就是请求方法字段,这个我们会在待会儿边写代码边给大家验证的。我们已经通过handlerHttpRequest(int sock)函数读取出请求了,我们先对请求不做处理,我们给它一点响应。我们定义一个string responce;我们学过http响应的格式,所以responce = “HTTP/1.0 200 OK\r\n”;然后还要继续+=,目前响应报头的字段我们还不懂,那么我们先不写,那就直接跳到空行那一部分,即responce += \r\n;那么空行下来是不是就是响应正文了呀,可是正文我们也不懂呀,那么没关系,我们也responce += “hello bit”;我们除了可以read()和write()之外呢,我们还可以调用send()函数,它可以向特定的套接字,发送特定的缓冲区数据,并且可以指定希望发多少,然后还有一个flag可以设置为0,那么send()就和write()等价。所以我们就调用send()函数了,即send(sock,responce.c_str(),responce.size(),0);到底能不能工作呢?
》我们将我们的服务器启动起来,然后再开一窗口,我们输入#telnet 127.0.0.1 8080,只要显示“^]”符号表示我们登录成功了,紧接着#contrl + ^],再回车键一下,我们就可以发起请求了,输入#Get/http/1.0,回车键,就能收到服务器的响应了,如下图。
网络基础(一)_第35张图片
我们再用我们的浏览器来向我们的服务器发起请求,可以看到,我们在网页上面看到一个hello bit。
网络基础(一)_第36张图片
我们现在就知道,我们一般用的浏览器请求的服务,说白了,是一定有人写过服务,然后部署在Linux下,然后你才可以请求。当然,我们今天可不仅仅想这样写,我们也想简单的进行响应的时候,在响应正文当中,响应的是一个网页,这个网页其中的基本字段,我们该怎么设置呢?我们不懂,没有学过,怎么办呢?没关系,我们可以搜搜html的教程就可以了,我们将它给的代码片段复制一下就可以了,或者模仿一下它的格式。responce += “

hello bit

\r\n”我们再用浏览器去访问试试,我们可以看到,得到了比较大的hello bit,很显然,浏览器根据我服务器给他返回的网页,做了一定程度渲染才得到的结果。
网络基础(一)_第37张图片
可是这样还没完,虽然现在的主流浏览器已经做的非常好了,它知道我给它响应的是什么东西,但是呢,我还是想告诉它,我给它的是什么东西。所以呢,就好比,我们刚刚请求的时候,是那样子请求的,服务器给我返回的响应也是我们预期的效果。那么,我怎么知道响应的正文是什么类型呢?多长呢?
》一个完整的响应呢,我就得告诉别人,我给你的是什么类型。我们在讲http响应的格式也说了,响应的正文包含了很多的类型,如html、音频等等,所以,换句话说呢,现在就是,怎么让别人知道,我给它的是一个网页呢?所以我们继续在返回的响应responce里面+=一个字段,那就是“Content-Type:”Content代表内容,Type代表类型。意味着,我这次给你的响应的类型是什么。Contnet-Yupe字段究竟有哪些类型呢,有非常多的类型,作为后端开发,我们只需要了解一下就可以了。换而言之Content-Type标定了,我们这个响应正文的类型是什么,因为我们这个是一个网页,所以responce += “Content-Type:text/html\r\n”。我们再来命令行发起请求,就会看到响应正文有它的身影了。
》下面还有一个问题,我的一次请求和响应当中,现在呢,就要回答两个问题,任何协议的Request 和 Responce是包含两个部分的,那就是 报头 ➕ 有效载荷。那么现在的问题呢,就是,第一个,**http是如何保证自己的报头和有效载荷被全部读取呢?**无论是请求还是响应。也就是无论是请求还是响应,http是如何保证你能够读到一个完整的报文呢?
》那么,我们是不是可以很好的去理解。1.读取完整报头:按行读取,直到读取到空行!(那么你如何按行读取,你怎么保证你读到空行,这个是关于IO处理,我们就暂时不处理它)现在呢,我们原理上是按行进行把一行读取,这个工作,我们在前面写计算器就做过了哈。所以,我们容易读到报头,当然更重要的不是它哈。
2.你如何保证,你能读取到完整的正文呢?就是,我现在能够将报头读完了,那你怎么知道正文有多长呢?就好比,我们前面写的网络版本计算器,我们说了,我们能保证报头读完,那么我们怎么知道报文有多长呢?没关系,我们读取到报头就是长度,我们再将字符串转整数,然后就知道报文有多长了。现在问题就是,你能够将http完整的报头读到,这没问题,可是正文部分如何保证呢?
》我们有办法,其实和我们的计算器的思路类似。我们读取报头的时候,报头里面有那么多的属性呢,那么我们是能够
首先保证报头属性读取完毕
。只要你的请求是合法的,或者你没有出现关的情况,那么正常情况,你发过来了,那么我肯定能把报头读完。不管是请求还是响应,你不是都有一堆的报头属性嘛。所以, 请求或者响应的属性中,一定要包含正文的长度。(但是,客户端并不是我们自己写了,可能人家客户端有自己的很聪明的做法,不需要长度哈。)
》那么,我们在响应报头当中带上正文的长度字段哈。那么问题是,我们怎么到正文有多长呢?我们先string html = “

hello bit

\r\n”保存一份。那么我们现在给responce +=( “Content-Length:” + to_string(html.size()) + “\r\n”);
》我们再来进行测试一下,我们是能够看到响应中会带上长度的字段是34。
网络基础(一)_第38张图片
你说的挺好,没有什么问题。现在有一个问题,什么问题呢?我自己写的服务器,网页有什么内容,难道还要我关心嘛?作为一款服务器,我只负责请求,将信息给你推过去就可以了,难道还要我服务器关注一大堆html的东西吗?那是不是太麻烦了。所以,实际上我们在服务端这里,我们C++写端服务器呢,比如一些做网站开发的,我们和网站开发没什么关系哈,我们的重点是服务器 。现在就相当于,现在有一个做前端的同学,它可能写了一堆的网页,包括其他图片等一些资源,现在的问题是,难道还要我一个服务器去关心你刚才说的那些东西吗?
》我们就不需要string html = “

hello bit

\r\n”保存一份了,我们直接string html = readfile()从文件里面把内容拿上来,然后再将html拼接到responce里面。如果从文件里面拿的话,这里会衍生出两个问题, 第一个问题:文件在哪里?第二个问题:如何取呢?
》第一个问题,我们重来没有讨论过,什么意思呢?意思就是说,你想访问什么文件呢,你想访问什么资源呢?不知道还记不记得,我们在前面讲,你在请求html的时候,即URL的前半部分是域名,端口号忽略,然后后面就会跟着你要访问文件的路径,这也是Linux下的文件路径。第二个,我怎么知道它要访问什么文件呢?
》我们来做一件事:#control ➕^],然后回车键,输入#GET方法,即GET /a/b/c.html http/1.0,然后就发起请求了,可以看到,服务器收到了我的请求行当中要请求的东西,如下图。
网络基础(一)_第39张图片
换而言之,第一个问题我们就能回答了,文件在哪里呢?**文件在请求行中,请求行的第二个字段就是你要访问的文件! 想一想,它想要访问文件,它会带路径,如果他访问的这个路径是一个不存在的路径,那么他访问的资源是不是也势必不存在呀,如果不存在,我给它返回的响应不就是“404”嘛,如果存在了,且各种权限都没问题,那是不是我就将文件打开,返回文件内容给你就行了,对不对。
》现在的问题就是,我们前面也说过,在URL格式中,紧跟域名和端口号后面的是要访问文件的路径,其中“/”是路径分割符,第一个“/”是什么东西呢?就是你请求的东西是从“/”开始的,那么这个“/”是什么呢?之前没有谈过,但是现在是要有一个感觉,我们前面也特意说了
,放在开始的“/”不是根目录,但是可以为根目录,它是web根目录,可以设置成为根目录。**这句话的意思就是说,从概念上讲,他不是Linux根目录,它是web根目录,如果你愿意的话,你可以让根目录充当web根目录。那么,我们今天让它成为根目录是最简单的,直接去访问他就可以,但是我不想让“/”成为根目录,我想把我的资源都放在特定的路径下,所以,我们创建一个文件夹wwwroot,所以这个文件夹保存的是我们想要的文件信息。所以我们在该wwwroot文件夹下再建立一个index.html。一般特定的网址当中包含的首页信息就在index.html里面了。换而言之,文件在哪里呢?文件就在请求行的第二个字段里面(即URL)。
网络基础(一)_第40张图片
》比如说,我们将来读取到,解析请求的时候,读到了,它要访问的资源在path里面,所以我的path是这样子的,path = “/a/b/cindex.html”,这个样子的话,我再继续往后面读取的话,那么我们得先做一件事情,你想要的资源在哪里呢?所以,我得给你拼接一个resource = “./wwwroot”;这个就是我们的web根目录,然后呢?resource += path;这样就构建出了一个“./wwwroot/a/b/index.html”。
》换而言之,对于外部想使用我服务器的人呢,它在请求的时候,只要知道“/a/b/index.html”就可以了,而在我们服务器内部呢,我知道资源实际上是在我当前目录下的wwwroot目录下的,所以我给你的路径path加上了前缀“./wwwroot”,就限定了在用户请求的路径下呢有特定的资源。此时呢,我们服务器就可以去打开特定路径下的文件就可以了。
》所以,有没有清楚,当别人请求“/a/b/index.html”时候的首符号“/”可以不是我们的根目录!当然你服务器内部不加前缀,那么就是根目录了,但是我们一般还是建议大家加上前缀。所以当你来了所有的请求,我在你所有的请求当中前面都加上前缀wwwroot文件夹,那么当别人想访问的时候,就直接访问就可以了。我们目前是没有做请求处理的,如果想处理一下的话,我们也可以做一点处理。
》我们可以写一个getPath()的方法,将我们从sock读到的buffer内容当作参数传进去,即string = getPath(buffer),也就是我就处理一下路径。一旦我确定了路径,我就可以加上我想加的前缀,最后呢,已经确定好要访问哪个资源了,那我再用readFile()函数给你打开并读取到,读取到之后,将其作为响应给你发回去。
》我们先来写从我们的请求当中获取路径字段,即string getPath(string http_request)。对于我们来讲呢,我们已经得到了一次http请求,我们也就不做判断了,我们就认为一次就将请求全部读上来了。所以,我们要考虑的就是,先要提取一行对不对。我们先找"\r\n",size_t pos = http_request.find(CRLF。)我认为我已经读到了一个完整的请求行了,反正闭着眼睛我们都知道,我们拿到的是我们想要的第一行。然后string request _line = http_request.substr(0,pos);然后我们再要找的就是空格,size_t first = request_line.find(SPACE);因为第一行的每一个字段都是以空格来隔开的。我们也说了,请求行有3个字段,我们还得再找一个空格,size_t second = http_request.rfind(SPACE);
》所以string path = request_line.subst(first + SPACE_LEN,second - (first + SPACE_LEN));此时我们就将请求行的URL读到了嘛,那么直接return path。
》这里呢,有一个问题,如果对方访问我的时候,路径就只有一个/的话,那不就是要访问我的web根目录嘛,说白了就是将我web根目录下的所有内容都返回,你想的是不是有点太美了,怎么可能将web根目录下的所有资源都给你呢,如果我web根目录下有几百上千部电影,难道资源全给你嘛?当然不行。
》所以,如果用户请求的是一个“\”的话,那么我们就得拼接上,我们默认的首页字段,那怎么加呢?所以,我们就要在分割出path之后,返回retrun之前要做一个判断,if(path.size() == 1 && path[0] = = “/”)那么我就要给你加上index.html,即path += “index.html”或者我们宏定义之后,path += HOME_PAGE;
》有了路径之后,我想要干什么呢,不能乱读,你要请求的资源,我现在有。我现在要把你请求的资源呢,进行给你返回。那我们就还需要再处理一下,要给你的路径继续拼接上我的web根目录。我们现在,在命令行发起请求用的GET方法。我们来拼接,string resource = ROOR_PATH;resource += path;所以我们就给提取到的路径path拼接上了我们的根目录。我们的重点是在原理上,不是在代码上。
》接下来就完成下一个工作就是读取文件readFile()函数。int fd = open(),打开路径就是recource.c_str(),打开的方式呢,毫无疑问,就算该路径下的文件不存在,你也不能给我创建,因为你必须存在,那么就使用的O_RDONLY,算了,我们用C++接口打开吧。所以,ifstream in(recource);如果成功打开了,那么我们继续往后走,没成功,if(!in.is_open()),没有成功,正常情况下,我们外面获取返回值,然后进行判断,将状态码什么的都改一下,我们这里就简单一点,返回return “404”;如果打开文件成功的话,那当然是对文件的内容进行读取。string content;string line;while(getline(in,line))content += line;读取完之后就是in.close();将我们读取的内容进行返回return content;
》反正就是,你要访问什么文件我知道,我将文件打开并读取内容,将其返回给你。我们下面就是要来编写首页index.html,我们就网上随便搜索并复制代码。我们简单的将index.html首页代码写完了,那么一会儿,我们的服务器启动,任何人想要访问我的网站,默认先输入的就是域名或者iP,加上端口号,默认发过来的就是/,你发过来的是/,那么我再服务器内部提取你的文件路径,然后再对你做路径的拼接,拼接之后,你就会在我们的wwwroot目录下去找index.html,所以会将index.html给你们返回出去,那么你们就可以浏览这个首页,可以跟树状结构继续往下浏览,当然我们就是单独一个首页,就不提供继续点击其他链接往后读了。
》我们来进行测试,在命令行启动telnet输入之后,输入#GET / http/1.0,方法是GET,路径是/,http版本1.0。所以,经过测试,我们的请求和响应都得到了成功。

我们下面要来学习GET和POST方法:
为什么,刚刚要写一个服务器,将我们的网页返回呢?原因就在于我们要研究一个表单的东西。表单是什么呢?说白了,平时在登录的时候,不管是登录还是注册,只要你想把你自己的个人信息上传上去,你就必须得有表单这个东西。
》我们先说一个概念:我们的网络行为无非就是两种,1.我想把远端资源拿到你的本地,即GET /index.html http/1.1;2.我们想把我们的属性字段,提交到远端。换句话说就是,我们搜索引擎,提交搜索关键字;登录的时候,有登录的字段,包括你注册的时候,会加上注册的字段。说白了其实就是这两种行为。
》其中很显然,GET方法就是要将我们的远端的资源拿到我们的本地。然后,我们的GET和POST方法,还有一个行为就是想把我们的属性字段,提交到远端。现在我们听到一些网站说是“静态网站”,说白了就是,只能够将东西拿到;还有一些网站呢,是“动态网站”,说白了就是能够交互的,能够交互的话,就能够提交一些什么东西。比如说,一个被阉割的论坛,只能让你看帖子,不让你发评论,那么这就是一个静态网站,如果允许你发评论,那么就是能够交互。
》现在呢,我们交互的时候呢,我们将数据提交到远端呢有两种方法,一种是GET,还有一种就是POST。GET和POST相比,肯定是有区别的,什么区别么,后面再说。我们现在对GET和POST的提交没有办法处理,因为,GET和POST在提交上面,有相当多的细节,我们有一个项目http实现 ,其中它呢是用到了一些第三方库。
》其实最难的并不是把东西拿下来,把东西拿下来,我们目前学到的是够用的。但是,你要将东西提交上去,提交之后,你还要进行解析字段,包括你提交上来的东西要做什么呢,比如说,你提交上来的数据是要注册,那么服务端还要写一大堆的注册逻辑,你要搜索的话,还要有一大堆的搜索逻辑。那么用户提交上来的数据是第一步,再往后面你是要有业务的 ,所以处理肯定还是比较复杂的。
》我们下面要做的就是构建一个表单,那么表单该怎么做呢?不懂没关系,我们直接网上搜索一下html表单就行了。

》我们进行了测试,我们有一个用户输入框和密码输入框,我们输入好之后,然后点击submit提交按钮,就发送到服务端了。我们现在来用用抓包软件,抓一下我们的请求,还有看看我们写的表单form和提交之后浏览器得到的是404响应。
网络基础(一)_第41张图片
》我们可以看到,抓包软件确实抓到了我们的请求,其中请求行的第一行就有我们发送的用户名和密码,所以我们在http中用GET方法,它是以明文方式将我们的对应的参数信息,拼接到URL当中。我们可以看一下我们的服务器得到的请求是怎么样子的。
》我们可以看到,会将我们http对应的请求,把我们输入的参数以http请求格式提交到服务器,那它是如何将参数给服务器的呢?就是通过http请求行当中GET方法字段后面,紧跟URL字段,其中URL中,开头就是想要访问的资源的路径,后面紧跟的就是,你想要访问资源所传递的参数,用户名、密码。其中user、passwd都是我们网页html写的内容,这是开发者自己定好的,这是要让后端的人知道,那么后端的人就可以根据名字、密码提取对应的内容。如果你想要登录,就会将内容拿到,去和数据库对比,这就是GET方法。
网络基础(一)_第42张图片
所以我们http中GET方法最大特点,就是将参数以明文的方式,帮我们拼接到URL的后面,这是最重要的参。如果我今天还想再做实验,把方法由GET改成POST,就是将我们的表单当中的第一行的method后面的参数将GET改成POST,其他的我都不变,我们再来进行操作。
》我们输入好相关用户名和密码后,可以看到浏览器上面的URL好像不和刚开始用GET一样了,它没有显示我们输入的数据,即参数。也就是URL当中并没有拼接任何提交参数,这是其一;其二,我们用抓包软件可以看到,我们提交的参数可以在空行的后面看到我们提交的参数user、passwd。还有,我们看到我们服务端收到的请求和我们抓包软件看到的基本上是一样的。
网络基础(一)_第43张图片
我们能够看到的就是,POST和GET都能够提交参数到你的服务端,只不过呢,我们对应的POST方法,提交参数的时候,会将参数以明文的方式,拼接到http的请求正文中来进行提交!

POST VS GET:
我们实验看到了,它们两个方法的提参数的位置确实不太一样,那么它们两个的区别到底是什么呢,包括我们后面如何选择使用它们两方法呢?**1.GET通过URL传参。2.POST通过正文传参数。**他们两个特点是这个样子,下面我们再谈GET方法有一个问题。GET在提参的时候呢,有时候会将我们的参数以明文的方式回显到了浏览器中的URL的后面,我们这里要说一下,3.GET方法传参不私密。(说一下,无论是GET还是POST传参都是不安全的,因为他们都是明文传送的,我们后面会讲,我们可能会碰到中间人攻击,所以别人就将你的信息盗走了。所以,我们用http传送数据,能有功能方面的满足,但是却是在网络当中裸奔,所以数据无论如何都是不安全的,所以不能说用GET传参不安全的说法。)4.POST通过正文传参,所以,相对比较私密一些。 5.GET通过URL传参,POST通过正文传参,所以一般一些比较大的内容都是通过POST方式传参的。 URL没有类型字段,正文是有类型字段的,如Content-Type等,所以也就决定了,正文能够传递更丰富的字段类型。比如说,你经常会在网站上,上传你的简历和视频,其中呢,我们基本上都是用POST的方法来传,因为POST有详细的字段类型来处理。URL呢,其实就是一个文本类的,虽然会有转码,但是如果传一些二进制内容的话,那会特别大,是挺不合适的。我们一般在传一些对私密要求,并且可能体积比较大的,一般建议直接用POST,否则你想上传的东西无关紧要,比如你在做搜索引擎,你在搜索的时候,你提交的数据字段又不多,所以默认的就用GET方法来传参数,所以会发现本来URL很短,但是一提取内容就特别长了,因为用的是GET方法来帮我们传参数,因为它也比较简单。如上就是我们的POST和GET。
网络基础(一)_第44张图片
除了有GET和POST方法,还有其他方法,但是用的特别少,在大部分web服务器上,大部分很多方法都是被注释掉或者禁掉。算了,其他方法就不说了,想了解可以去自己查查来了解。

server.hpp 

#define CRLF "\r\n"
 21	#define SPACE " "
 22	#define SPACE_LEN strlen(SPACE)
 23	#define HOME_PAGE "index.html"
 24	#define ROOT_PATH "wwwroot"
 25	
 26	using namespace std;
 27	
 28	std::string getPath(std::string http_request)
 29	{
 30	    std::size_t pos = http_request.find(CRLF);
 31	    if(pos == std::string::npos) return "";
 32	    std::string request_line = http_request.substr(0, pos);
 33	    //GET /a/b/c http/1.1
 34	    std::size_t first = request_line.find(SPACE);
 35	    if(pos == std::string::npos) return "";
 36	    std::size_t second = request_line.rfind(SPACE);
 37	    if(pos == std::string::npos) return "";
 38	
 39	    std::string path = request_line.substr(first+SPACE_LEN, second - (first+SPACE_LEN));
 40	    if(path.size() == 1 && path[0] == '/') path += HOME_PAGE;
 41	    return path;
 42	}
 43	
 44	std::string readFile(const std::string &recource)
 45	{
 46	    std::ifstream in(recource, std::ifstream::binary);
 47	    if(!in.is_open()) return "404";
 48	    std::string content;
 49	    std::string line;
 50	    while(std::getline(in, line)) content += line;
 51	    in.close();
 52	    return content;
 53	}
 54	void handlerHttpRequest(int sock)
 55	{
 56	    char buffer[10240];
 57	    ssize_t s = read(sock, buffer, sizeof buffer);
 58	    if(s > 0) cout << buffer;
 59	
 60	    // // std::string response = "HTTP/1.1 302 Temporarily Moved\r\n";
 61	    // std::string response = "HTTP/1.1 301 Permanently Moved\r\n";
 62	    // response += "Location: https://www.qq.com/\r\n"; 
 63	    // response += "\r\n";
 64	    // send(sock, response.c_str(), response.size(), 0);
 65	
 66	
 67	    std::string path = getPath(buffer);
 68	    // path = "/a/b/index.html";
 69	    // recource = "./wwwroot"; // 我们的web根目录
 70	    // recource += path; // ./wwwroot/a/b/index.html
 71	    // 1. 文件在哪里? 在请求的请求行中,第二个字段就是你要访问的文件
 72	    // 2. 如何读取
 73	    std::string recource = ROOT_PATH;
 74	    recource += path;
 75	    std::cout << recource << std::endl;
 76	
 77	    std::string html = readFile(recource);
 78	    std::size_t pos = recource.rfind(".");
 79	    std::string suffix = recource.substr(pos);
 80	    cout << suffix << endl;
 81	
 82	    //开始响应
 83	    std::string response;
 84	    response = "HTTP/1.0 200 OK\r\n";
 85	    if(suffix == ".jpg") response += "Content-Type: image/jpeg\r\n";
 86	    else response += "Content-Type: text/html\r\n";
 87	    response += ("Content-Length: " + std::to_string(html.size()) + "\r\n");
 88	    response += "Set-Cookie: this is my cookie content;\r\n";
 89	    response += "\r\n";
 90	    response += html;
 91	
 92	    send(sock, response.c_str(), response.size(), 0);
 93	}
 94	
 95	class ServerTcp
 96	{
 97	public:
 98	    ServerTcp(uint16_t port, const std::string &ip = "")
 99	        : port_(port),
 100	          ip_(ip),
 101	          listenSock_(-1)
 102	    {
 103	        quit_ = false;
 104	    }
 105	    ~ServerTcp()
 106	    {
 107	        if (listenSock_ >= 0)
 108	            close(listenSock_);
 109	    }
 110	
 111	public:
 112	    void init()
 113	    {
 114	        // 1. 创建socket
 115	        listenSock_ = socket(PF_INET, SOCK_STREAM, 0);
 116	        if (listenSock_ < 0)
 117	        {
 118	            exit(1);
 119	        }
 120	        // 2. bind绑定
 121	        // 2.1 填充服务器信息
 122	        struct sockaddr_in local; // 用户栈
 123	        memset(&local, 0, sizeof local);
 124	        local.sin_family = PF_INET;
 125	        local.sin_port = htons(port_);
 126	        ip_.empty() ? (local.sin_addr.s_addr = INADDR_ANY) : (inet_aton(ip_.c_str(), &local.sin_addr));
 127	        // 2.2 本地socket信息,写入sock_对应的内核区域
 128	        if (bind(listenSock_, (const struct sockaddr *)&local, sizeof local) < 0)
 129	        {
 130	            exit(2);
 131	        }
 132	
 133	        // 3. 监听socket,为何要监听呢?tcp是面向连接的!
 134	        if (listen(listenSock_, 5 /*后面再说*/) < 0)
 135	        {
 136	            exit(3);
 137	        }
 138	        // 运行别人来连接你了
 139	    }
 140	    void loop()
 141	    {
 142	        signal(SIGCHLD, SIG_IGN); // only Linux
 143	        while (!quit_)
 144	        {
 145	            struct sockaddr_in peer;
 146	            socklen_t len = sizeof(peer);
 147	
 148	            int serviceSock = accept(listenSock_, (struct sockaddr *)&peer, &len);
 149	            if (quit_)
 150	                break;
 151	            if (serviceSock < 0)
 152	            {
 153	                // 获取链接失败
 154	                cerr << "accept error ...." << endl;
 155	                continue;
 156	            }
 157	            // 5.1 v1 版本 -- 多进程版本 -- 父进程打开的文件会被子进程继承吗?会的
 158	            pid_t id = fork();
 159	            assert(id != -1);
 160	            if(id == 0)
 161	            {
 162	                close(listenSock_); //建议
 163	                if(fork() > 0) exit(0);
 164	                //孙子进程
 165	                handlerHttpRequest(serviceSock);
 166	                exit(0); // 进入僵尸
 167	            }
 168	            close(serviceSock);
 169	            wait(nullptr);
 170	        }
 171	    }
 172	
 173	    bool quitServer()
 174	    {
 175	        quit_ = true;
 176	        return true;
 177	    }
 178	
 179	private:
 180	    // sock
 181	    int listenSock_;
 182	    // port
 183	    uint16_t port_;
 184	    // ip
 185	    std::string ip_;
 186	    // 安全退出
 187	    bool quit_;
 188	};
index.html

<!DOCTYPE html>
 2	<html>
 3	
 4	<head>
 5	    <meta charset="utf-8">
 6	    <title>104 期测试</title>
 7	</head>
 8	
 9	<body>
 10	    <h3>hello my server!</h3>
 11	    <p>我终于测试完了我的代码</p>
 12	    <form action="/a/b/c.html" method="post">
 13	        Username: <input type="text" name="user"><br>
 14	        Password: <input type="password" name="passwd"><br>
 15	        <input type="submit" value="Submit">
 16	    </form>
 17	    <!-- <img border="0" src="https://img1.baidu.com/it/u=1691233364,820181697&fm=253&fmt=auto&app=138&f=JPEG?w=889&h=500" alt="Pulpit rock" width="304" height="228"> -->
 18	</body>
 19	
 20	</html>

HTTP的状态码

状态码在响应当中的响应行的第二个字段,我们服务器在进行发送响应的时候,我们是 response = “HTTP/1.0 200 OK\r\n”;出现200,200代表的意思就是,告诉客户端,你的请求是成功的,但是状态码也可以是其他的,代表不同的意思。
》1开头的意思就是,比如说,对方请求了一下,但是我现在长时间没有给他响应,所以,我可以给他构建一个以1开头的状态码的响应报文,告诉他,你不要着急,我正在给你的请求进行处理,叫做,信息性状态码,一般是很少见的。因为,不管是从网络上还是对方服务器上,速度都是很快的,是很少见的,但是给我们提供了思路,我们在发起请求的时候,我们可以给别人设置一些功能,比如查询当前自己的任务进行到什么程度了。
》2开头的状态码呢,基本都代表正常处理完毕
》3开头主体呢就是重定向状态码,301、302被支持的比较多。
》4开头一般都是客户端出错,比如找不到某种资源,就会返回404。
》5开头代表服务端出错,如果今天请求一个网络资源,服务端不存在,给我们返回的应该是4开头还是5开头呢?换句话说,这是属于客户端报错还是服务端报错呢?我们说过了,你请求的资源不存在,此时服务器就会给你返回4开头的状态码,记住了,你向服务器请求的资源不存在,是你的问题,不是服务器的问题。因为,你客户端的需求是无限的,服务器不可能全部都给你满足。那什么时候服务端会显示错误码呢?我们服务器为了给客户提供服务,它会给我们进行各种数据分析,创建子进程、管道、多进程等,比如说创建子进程给你提供服务,如果fork失败了,是服务器挂了,没办法给你提供服务,那怎么办呢?只能给你返回响应以5开头的状态码。

http常见的Header

》但我们进行http请求还是响应,大家是可以发现,它们都会携带报头的,报头里面的Content-Yype表示的就是正文部分的数据类型;Content-Lenth就是有效载荷的长度,这个也挺重要的,就是你可以按空行为分割去读,将我们的报头读完,可是正文有多长,我怎么知道,没关系,我们的报头里面会有正文长度报头属性的。还有等等的报头属性。
网络基础(一)_第45张图片
User-Agent是能够根据你使用的操作系统来自动给你在响应的时候选择不同的信息返回给你。比如下载某些软件的时候,你Mac、Windows、ios会自动给你推送到对应版本的下载地址。
》referer:当我们从A网站跳转到B网站,有时候在跳转请求的时候呢,请求就会带有referer字段,它代表的就是,我们当前要访问你的页面是从哪一个页面跳转过来,就是上一个页面是什么。页面的跳转本质上就是目录的跳转,Linux网页跳转,本质上就是Linux目录的跳转,其中referer代表的就是你上一个目录是谁。
》有时候需要进行重定向,那么重定向就需要我们使用一些重定向状态码,来完成服务端的重定向功能,这就叫做重定向location。
》因为http协议,本身是无状态的,那么无状态是如何进行会话保持,包括我们诸如用户级别的链接管理,那么我们就可以搞一个cookie的概念。
》下面我们重点来谈,3开头的状态码,即重定向。另外一个就是cookie的字段。

一般我们状态码是**301的话,称作永久重定向;302,称作临时重定向。**那什么叫做重定向呢?我们客户端在进行发起请求的时候呢,我们服务端呢,不会给你这次请求实际进行我们对应的响应或者http请求做处理,而是给你返回一个301或302的状态码,我这样说,肯定不是返回301、302这么一个数字,肯定是一个http Responce。其中在给你返回的时候呢,光返回301、302还不够,它的Responce的报头里面呢,还会携带一个我们对应的http的响应属性,location。说白了,对方在响应的时候,给我们填的是301或者302,代表的意思呢,就是要进行重定向。那么,光光告诉我是重定向还不够,它还会告诉我一个new URL,即一个新的网址,然后们的浏览器会自动的去进行我们对应的,跳转到另外一个服务端,去访问你这个new URL指明的服务,这个过程呢,我们称作重定向。
》说白了就是,你在向我服务器发起请求,我服务器说,不好意思,不要来访问我,我给你处理不了,我给你去另外一个服务器请求吧,我办不了,我告诉你去location指明的地址去做吧。不废话,我们直接来进行做实验。
》我服务器不是收到你的请求嘛,我不给你做处理,我直接构建http Responc返回给你。我们定义一个string responce = “HTTP/1.1 302 Temporarily Moved\r\n”;responce += “Location:www.qq.com\r\n”,你要请求对不对,我直接让你重定向到qq.com;然后responce += \r\n;没有响应正文,只有响应报头。然后我们在进行send(sock,responce.c_str(),responce.size(),0);所以send()之后呢,就是你浏览器自己做的事情,反正我不管。
》我们来测试一下。我们确实在命令行启动telnet的输入,然后#GET / http/1.1然后,可以看到给我们响应的报头里面是有Location:www.qq.com的字段的。我们用浏览器进行测试,可以看到,他说我们重定向多次,但是我们能够在浏览器中的URL看到,是显示了www.qq.com的。
》我们的服务器代码里面的重定向的方式不对,我们Locatio:后面填的一定是一个网址,这个网址,包括协议各种字段你一定要写全了,所以,你不能只写一个别人的域名,你得https://www.qq.com/\r\n,即“Location:https://www.qq.com/\r\n”,写成这个样子,才能识别成你是网址。如果你写成刚刚“Location:www.qq.com/\r\n”这个样子,他这个重定向,你可以理解成是站内重定向,说白了就是,在你自己的网站内部做重定向。
》我们再来测试,我们在浏览器输入我们服务器的IP+端口号,发现给我们直接跳转到qq的官网了。因为浏览器收到了你的302,以及浏览器讲你的Location字段提取出来,会自动帮你发起二次请求,就到了另一个网站。
》我们改成将代码里面的302改成301,发现也是一样的。
》下一个问题就是,301是永久重定向,302是临时重定向,那通常是做什么的呢?就比如说,我们的临时重定向,可能是我们当前服务器帮我们访问某一个网站,假设我们网站里面的内容局部要升级,但是我又不想让用户访问到我们现在的服务。所以凡事访问我这个服务的用户呢,我呢,为了能够更加平滑的去升级,我就在搞一批机器,它们提供同样的服务,只要用户访问到我了,比如说域名什么的都是写好的,凡事访问我呢,我就临时重定向到备份的服务当中,当我将要升级的完成了,再把这个临时重定向去掉,那么再去进行访问我的时候,就能正常提供服务,访问我该服务器提供的服务了。
》所以临时重定向最典型的表现呢,它就是一个临时的方案,比如我们的某些资源要进行升级或者进一步处理,我们就可以用临时重定向,后面不需要了,去掉就可以。
》还有一种就是永久重定向,是什么意思呢,给大家举一个例子:就比如说,我们有时候访问一个网站,比如说,网站的域名是www.e.com,这个网站的用户量越来越大,这个网站的域名当时是拍脑门随便起的,觉得域名不好,所以后来,凡是想访问我www.e.com的网站的用户,我都让他访问我的新网站www.2.com。比如我的用户有百万,可是它们老是记住我域名是www.e.com,所以,如何让他将来永远的访问我新网站呢?我将我的新网站推出,这是其一;其二,老网站我也不关,我将老网站的所有服务全下线,只提供一个功能,就是老网站被用户请求的时候,给用户重定向就可以了。所以,不管是我的新客户还是老客户去访问www.e.com都会去访问www.2.com,那么我们是不是可以保证客户不改变自己的行为,他继续访问老网站,但是最终呢,我们可以让他跳转到我们的新网站中,当然跳转的时候,我可以设置一个倒计时,给用户一个公告,叫他记住新域名,所以,像这种一直要存在的重定向的方式,我们叫做永久重定向。永久重定向对于我们客户来讲,和临时重定向的差别呢,一般就是,比如说临时重定向 ,比如我访问www.e.com,给我弹出临时重定向,让我到了www.2.com,对我客户没有任何影响,客户根本不关心www.2.com网站是谁,我只要访问我的内容就可以,将来我访问www.e.com网站就是访问这个网站,因为在我看来,你只是临时重定向到www.2.com,所以将来用的依旧是www.e.com,这是临时。永久重定向呢,对客户和浏览器带来的影响就是,客户访问的是e网站,然后告诉你说重定向,就到了2网站,甚至告诉客户,什么时间e网站就要停止服务了,那么客户就要将2网站保存起来,以后客户尽量不要访问1网站了,直接去访问2网站。
》所以永久和临时呢,在服务端没什么差别,主要是客户端这里,要不要记住重定向的目标位置。如果我不需要关心目标网址,只是临时重定向一下,那就临时;如果你是新网址,希望用户不要再用老网址,那你就永久重定向。

我们再来讲一下cookie:
在谈cookie之前呢,我们首先来谈一下http协议的特点之一:无状态。那么无状态是什么意思呢?意思就是说,你一秒前访问的网页,由浏览器的http请求向服务器得到http响应,服务器把响应给你返回了。比如,一秒前刚请求的A网页,那么此时你今天一小时后再访问该网页,http并不知道你1秒前请求过该网页。说白了,用户的所有各种资源请求的行为,我们对应的http协议,它本身就不给我们做任何记录。就是你要啥我就给你啥,我就是一个傻瓜式的,你要100次,我就给你100次。你要100次,就给你100次,你要什么我就给你什么,这就是我,我就是http。这就叫做http协议无状态。
》有同学说,不对呀,我明显发现,我访问某些东西的时候,是会帮我记录下来的呀。比如,我访问b站、爱奇艺等登录之后,我浏览痕迹不都有吗,再加上我是一个VIP,我在网站内任意跳转不同的视频都能让我看,它是怎么知道我是VIP并且能自动让我以VIP的身份来进行我们对应资源访问呢?我们说了http协议是无状态,但不代表http协议不会采用其他手段来进行维护我们的状态。http本质最小的精神内核呢,它是一个网络通信协议,所以在网络通信的时候,是不关心你的状态,但并不代表,http没有它的周边。它可能http里面要维护我们的浏览痕迹,维护让用户进行保持,它是需要有周边的能力的,其中一种周边能力就是cookie。 下面来给大家解释一下。
》首先http是无状态的,和我们平常的使用是相违背的,比如,我们举一个常见的例子。我们今天登录的时候,也是提交了一个表单嘛,输入我们的用户名和密码,或者干脆扫二维码,说白了都是我们对应的http请求。登录成功之后呢,我们的网站记录下来,有些资源,我们不能够进入的,现在我随便可以访问,我们在网页之间来回跳转,访问各种网页,每一次访问,不就是一种http请求吗,我在访问第100张网页,它怎么还记得我第一次登录的时候我这个人呢?
》问题是,如果就严格按照不记录你,让http裸的用它的无状态,那么会出现什么问题呢?所以考虑技术问题的时候,不能纯工程师思维,一定要想清楚这个协议是谁在用,也要考虑这个协议为什么这样设定,这是和用户有关的。比如说,我今天http无状态,我登录之后,是无状态登录,我在里面点进去是一个新的网页,要发一次http请求,可是因为http无状态,我刚刚登录了,下次请求的时候就不认识我,所以这个VIP视频你想看的话,对不起,看不了了,那是不是变成了,这个网站有100部VIP电影,我每想看一部电影,我都得进行一次用户认证,那么你还会访问这个网站吗?我刚刚都登录了,扫过码了,我看另外一个电影的时候,又不认识我了,那用户是不是就抓狂了呀,那对用户来讲,这个基本就没法用。所以,我们用户需要一个功能,叫做会话保持。
》说白了,我用户登录了,我再去访问其他网页时,你别老是让我再去登陆,我已经认证过了,我再去访问网页的时候,应该是以我刚刚登录的信息来身份认证。比如说,我是普通用户,即便登录了,VIP的东西也看不了,它怎么知道我看不了呢?因为你不是VIP,如果我是VIP我就能看,原因是,网站和用户端共同配合,帮这个用户会话保持。说白了就是,这个用户是谁,登录了没?已经登陆了,那么从此往后,在一段时间内,不需要再重新登录了,就自动的进行认证了,这是第二个。
》第三个,你也会发现,如果你一旦登录,只要不关闭浏览器,你随便去访问各种资源,有时候我甚至将浏览器关掉了,我再去访问,也是直接能够登录的。有的网站在进行登录的时候,会提示你,是否允许7天登录有效,或者你用什么软件,你不用输入密码,你点击就直接登录了,我们在有些app上,你不退出,它永远都会在线。比如你访问b站,你扫码之后,你不关浏览器,就将b站网页关掉,当你再第二次访问b站的时候,那么他直接就是默认的登录状态。所以,换一句话说,一旦登录,会有各种会话保持的策略。
》我们打开cookie,将b站有关的都移除,我们刷新一下网页,就发现不认识我了,需要我重新登录。认不认识我,不是由http决定的,而是会话保持的周边策略,其中有一种策略就是cookie策略。
》cookie相当于什么呢?我们用户在登录的时候,我们的http协议呢,本身为了支持会话保持功能,新增了一下策略,这些策略呢其实就是相当于cookie,说白了,当我们客户端向服务器首次请求时,你会得到一个网页,它会要求你输入一些内容,输入我们的用户和密码,服务端收到你的用户名和密码之后,会在后台验证,你要登录的话,前提是你注册过,注册的本质就是将个人信息写在服务后端的数据库里面,当你登录的时候,它会拿你的用户名和密码去后端对比,如果通过验证,并且知道你是VIP,那么它就会给你进行返回响应,会在你的浏览器本地写入cookie内容,这个cookie呢,稍后会来谈它究竟是一个什么东西。服务器认证完你的用户名和密码没问题之后,就会将你的用户名和密码写到cookie里面,从此往后你的浏览器在请求的时候,在任何网页请求,你的浏览器会自动携带你浏览器访问该网站对应的cookie文件的内容。说白了就相当于,在你写入一个cookie之后呢,当你在进行后续的访问时,你的浏览器http请求都会自动帮我们携带上,我们曾经写入到我们浏览器cookie的信息。比如说,我曾经将你的用户名和密码写入到了cookie文件里面,它会自动的将我们cookie文件提交到浏览器,再去帮你用户提交请求的时候,服务器会自动地拿着你提交上来的http请求,自动帮你认证,用户就不需要再输入用户名和密码了,这样就不需要再输入用户名和密码,它就可以始终让你以登录状态访问。
》下面得认证两点,你是如何将cookie信息写给客户的;第二点,你得证明,当发起第二次请求的时候,会携带cookie文件内容。怎么证明呢?我们代码里面的响应内容里面是包括了响应类型、响应正文的长度等字段,我们还得再添加一些东西。我们想给我们的客户端进行写入cookie的话呢,我们采用什么策略呢,我们需要使用一条命令,叫做Set-cookie,我们是有Set-cookie字段的,你直接,在你的http响应中,把对应的字段填进去,即responce += “Set-cookie:”Set-cookie它也有自己的格式,诸如内容、过期时间等概念,用分号隔开,我们今天就不考虑时间问题了。所以responce += “Set-cookie:this is my cookie content;\r\n”相当于我们自己服务器在响应的时候,在它的报头里面除了会有类型、长度,我也设置了cookie。设置了cookie,我们应该看到两现象,就是服务一旦启动,你一请求,你的浏览器就会收到服务器给你的写的cookie。第二个就是,当你再去向服务器发起请求的时候,此时,你一定要携带cookie,那么到底行不行呢,我们来打开浏览器试一试。我们确实能够在我们浏览器中查看到写入的cookie文件信息。我们随便给服务器给我们的网页上提交我们的用户名和密码,我们可以在服务器上看到,提交的信息里面会自动携带,一开始服务器向我们浏览器写入的cookie信息。
》既然,我们已经验证了,我们接下来要讨论的就是,cookie放什么和其他相关细节。
》第一个,cookie是什么呢?说白了cookie就是浏览器帮我们维护的一个文件。给大家说一下,浏览器帮我们维护的这个文件呢,可以落在磁盘上,这也就是为什么,你有时候将你的电脑关了,然后重启后再去访问一些网站,这个网站依旧是记得你的,它不需要你再登录,直接就是处于登录状态了,原因就在于,本地就将你的私密信息保存起来了,当你再去访问网站的时候,浏览器会自动携带cookie文件里面内容发起请求,这也就是会话保持。当然我们今天理解这个文件,不能简简单单的这样去理解,我想告诉大家的是,这个文件也分为内存级文件和磁盘级文件。所以,同学们可以理解成,这个cookie他呢存在呢,会以各种存在形式。
》第一种呢,就是真正存在我们的磁盘上。一般cookie文件呢,其实就相当于在我们浏览器特定的目录保存,而大部浏览器呢,主流一般不会这么干了;第二种就是,我们文件呢,是一种内存级文件,内存级相当于什么呢,只要你当前的浏览器处于运行的状态,只要不关闭浏览器,你把网页关了,再打开对应的网页,其中cookie内容依旧还是在的,但是你把浏览器整个都叉掉了,因为进程退出了,所以内部的数据被释放了,所以这个cookie文件就没有了,就需要你重新登录了, 所以有时候会遇到,我登录一个网站,只要我浏览器不关,网站关了没影响,我还是可以继续访问,但是浏览器整个叉掉了,再打开浏览器和对应的网页就又需要我们登录了。所以我们一定要记住
浏览器维护的文件会在正真正的磁盘上或者内存级别。

》第二个问题是涉及到安全的问题。有的同学会说,如果按照你现在给我讲的这个策略,我作为一个用户,我将我的私密信息、密码什么乱七八糟的私密信息保存在cookie里面,如果我是一个小白用户,不小心点了一些网站,这些网站呢,有一些恶意程序,比如说中了木马病毒,它会伪装成一个正常的程序,它会在我们用户进行浏览器上网的时候,它可能就把我们的cookie找到了。如果它将我们的cookie文件找到了,甚至它将我们的cookie文件通过后门的方式,说白了就是拿着我的cookie文件信息通过套接字传到非法客户的服务器上,将我们的cookie内容盗取了,那么黑客从收集到的cookie当中,把里面的内容拿到,把你的cookie放到黑客的浏览器当中,黑客正常的去访问你正常访问过的网站,那黑客是不是就绕过了登录,直接让服务端误认为当前的客户是一个合法的客户了呀。相当于cookie文件保存太私密的信息,那么这个用户的密码一旦泄露了,会对用户的数据安全造成很大的威胁,同时,黑客拿着用户的cookie,即便不看用户名和密码,拿着用户的cookie放在黑客自己的浏览器下,再去访问用户访问过的网站,就直接登录上网站了,这就是为什么又是qq账户会被盗取的原因。
网络基础(一)_第46张图片
》这个技术呢,不仅仅是浏览器可以使用这个技术,qq类似的软件,它底层是http协议,它也要做会话保持,也就是类似的原理。 我们一旦我们的cookie文件被人家盗取了,当然有人问,怎么盗取呢?具体不讲哈,你可能安装别人的程序,因为大部分小白在哪儿安装也都能找到,反正就是能去你的磁盘文件里面寻找。
》因为这样的方式太不安全了,所以现在主流的方式是cookie + seesion的方案。什么意思呢?你可以理解成我们的用户,基本的私密信息只保留在用户本地。因为一个小白的防御能力和意思都比较弱。所以cookie + session的原理又是什么呢?
》既然我们客户端和服务端将客户信息保存在cookie文件里面风险太大了,所以实际上,当我们第一次请求我们的服务器的时候没有问题,服务器给你返回让你去登陆或者注册,前面是固定套路,登录的话呢,你输入你的用户名和密码,让后发送给服务器了,服务器做认证呢也是和前面一样的。认证通过之后,服务端不着急给你返回,服务端做什么呢?服务端还要再做一件事情,在服务端形成session文件,此时呢,用户的临时私密信息,保存在这个文件里面,这是其一。其二呢,还有一个很重要的点,我们的seesion是服务端的一个文件,这个文件的文件名,我们服w务端会自动形成文件和文件名。这个文件名呢,通常具备在当前我的网站当中,具备唯一性。其实有很多算法是能够帮我们形成固定串大小,这个串呢,本身就具有很强的唯一性,就比如我们以前说的git提交的版本号,这个版本号呢,就是一长串的16进制数,这个16进制数就是唯一的,同时呢,我们也可以使用MD5算法形成固定大小的文件名,也是具有唯一性。还不理解的话,我们服务端会有随机数再加上微秒级和纳秒级的时间戳形成唯一的数,反正就是能够形成一个唯一的文件名。
》构成文件名,然后呢?1.用户的信息此时就不再去客户端的cookie文件里面维护了,而是在服务端呢给客户所有的私密信息维护起来,这是其一。其二呢,因为客户刚刚是登录,服务器给客户呢照样调用Set-Cookie,给用户写一条信息seesion_id。说白了,我们这里的唯一文件名,我们一般呢都把他叫做,session_id,说白了就是,你当前这个用户登录成功了,你这个私密信息给你放到服务端,给你指返回一个id值就可以了。那么浏览器收到这个session_id之后呢,只要将seesion_id写入到本地的 cookie中,那么以后用户再去访问该网站的时候,只要在请求中携带Cookie:session_id这个字段就可以了。那么我们服务器怎么判断这个用户曾经登录过呢,很简单,只要根据用户的session_id然后在我们的文件表里面找到这个文件的session,这个session包含了文件的私密信息,包含了这个用户是谁,这个用户的浏览痕迹是什么,当前用户最近的一次访问时间是什么,这个session有没有过期,反正就是知道,拿着这个session_id找到这个文件,就证明这个客户处于登录状态,然后就允许客户访问特定的资源,这样从此往后,客户再去访问时,都会携带session_id,从而保持这个客户处于在线状态。
》你说不对,这也有安全问题呀,我黑客照样能将你的session_id拿去访问你所访问网站,也是直接自动认证。你说的这个问题没有解决呀,目前是这样,同学们,首先这个问题就是没有彻底解决,也解决不了,你为了要保证我们客户的安全呢,当然也不是解决不了,但是带来的成本大于收益的,像腾讯这么大的公司,照样有qq被盗取,你说不安全,确实是存在的。但是这种方案,虽然也会造成cookie的丢失,只要你在本地保留,客户又没有什么防范心理,作为一个出色的黑客想拿你的信息,其实也就是板上钉钉的。session存在的意义就是,1.私密信息不会泄漏了, 因为私密信息是在服务端,你个人的cookie即便丢了,你也只是丢一个session_id,你没有丢其他信息,所以对用户来讲,他的账号是安全的,这是第一个。有人说,你把信息保存在服务端,那么服务端不会收到攻击吗?。服务端是会收到攻击,公司会有能力和动力去保护服务端,基本不会出问题。
》我们知道客户端损失小,最多只是丢一个session_id,还有,服务端有充足的防范措施。第二,服务端还有很多很多的其他防范措施,会根据你的坐标地址来判断你的账号是否有风险。

我们再来谈下一个,在我们对应的请求和响应当中呢,大家会发现还有一个字段就是connection:keep-alive。我们现在知道http协议是无状态;还有一个特点就是在http1.0当中,他是基于短链接。我们写的代码,相当于别人请求过来了,我给他发送对应的响应,我处理完之后呢,我也就退出了,说白了,最终我也就会close掉文件描诉符。这种一请求,客户端喊一声,服务端就给你响应作应答,完毕了之后,服务端关闭链接,这种呢就是短链接。
》短链接一次只是处理一个http请求,那么为什么在一开始的时候用的是短链接呢?所以,话也说到这里了,有很多新的技术,并不代表他最合理的样子就是这个,它是发展的产物。随着发展,传送的数据量越来越大,一张网页呈现在你面前的时候,它已经经历了十几次http请求或者上百次http请求,即用户所看到的完整的网页内容,背后可能是无数次的http请求。我每一次http请求难道都要发起一次http吗?当然的,因为你是http请求,但是http底层主流采用的就是tcp协议,tcp在正常通信的时候,你要发起http请求,将你的http报文发出去,前提是得先完成3次握手,在底层把链接建立好,底层链接建立好,然后你的应用层http才将你的数据整体打个包发给对方。说白了,就跟我们当时写那个计算器一样,我们得先让服务器起来,先connet(),connet()不就是会所的3次握手,建立好链接,然后链接建立成功了,accept()成功返回,然后我们后续才进行正常read()和write()。
》如果我们采用http请求,给一个处理瓦努一个后,我们服务端立马关闭,那么此时一个主流的网页都特别大,不像以前,一个网页就是一大段文字,就只是一个资源,但现在不一样了,我们现在对应的http请求呢,一个网页背后有若干个http请求,每一次请求都采用短链接的话,它对应的最大的成本就是,在底层要进行不断的3次握手,甚至是4次挥手。比如说,对该网页要进行20次http请求,对应的握手和释放链接就要有很多次,这样的话就大大的降低了我们网页获取的速度,就不适合了,但是依然 值得去学习。
》现在就有了http1.1,Connection:keep-alive,就是长链接。长链接说白了就是,你这次http请求,你这次要呈现给用户一个完整的网页,我http建立一个链接,建立一个链接呢,此时我客户端和服务端,双方要进行http版本协商,所以都要携带上自己http的版本,也就是我们http请求中的请求行会携带HTTP/1.1类似,同时会携带Connection:keep-alive字段。如果双方都是1.1版本,并且都携带Connection:keep-alive字段,意味着,在进行我们底层链接协商的时候,双方都同意采用长链接方案,那么其中对我们同学来讲呢,我们就可以不用再请求一个网页的无数次http请求的时候,一张网页有若干个元素构成,我们在底层发起请求,那么这个链接就暂时不断开,不断开怎么办呢,我们就可以给它发起若干次http请求,同时将我们若干次http请求呢,全部都给我们进行发送到我们对应的服务器,服务器从一个链接里,读到的不止是一个请求,它可以读到很多的请求,然后再按照顺序,将若干个响应全部都返回给客户端,当客户端看到,拿到了完整网页的时候,这个链接才会断开,也就是说我们一次呢,获取若干个元素的时候,这些元素用一条链接来请求和响应,不用再重复的建立http底层的3次握手和4次挥手,这样就能大大的提高效率,其中,我们底层支持长链接的时候,这个Connection:keep-alive的时候,我发的请求里面带了,我发的响应里面带了,就说明我们双方协商成功,我们双方都认可采用keep-alive叫做链接保活的这样的策略。
》而我们早些年的http协议呢,它的Connection字段呢,当然他也不需要,后来因为有了Connection:keep-alive,为了进行区分,就有了Connection:closed,就表示只支持短链接。这就是长短链接。
》再补充两个点:第一个点,一个tcp链接里面会有多个http请求,同学们面临的第一个不是去理解它,因为理解它,并不难理解,重要的是,你怎么保证现在一个链接有多个请求了,你怎么保证你每一次服务端读取到的时候读取到的都是完整的链接呢?这个工作,我们做过吗?我们在一条链接呢,发起多个请求,那么这个请求在服务端怎么保证它读到的完整的请求呢?并且一个请求既不能多读,也不能少读,只有完整的报文才能够处理,你是如何保证的呢?
》因为你的tcp不是流式的吗,流式的报文在一起,是二进制序列,文本序列,你读吧,你怎么读呢?很简单,这个工作,我们曾经就做过了,我们在讲网络版本计算器的时候,故意写了一个长链接,也就是我发起一个请求报文,建立好链接之后,我们再去发送1+1、1+2的字符串的时候,我们服务端是怎么处理的呢?我们服务端是不是对读上来的字符串进行decode呀,我们是不是根据我们收上来的二进制数据,根据协议,把对应的字段提出来,同样的http也一样。我不就是按行读呗,读到空行,我就能保证自己已经将报头读完了,报头里面不就是有Content-Lenth嘛,我不多读,也不少读,我就读Content-Lenth读大小字节,弱水三千,我只取一瓢。我读上来,我一定能保证我读上来的是一个完整的报文,我也不会多读其他的报文。所以我们经过报头和有效载荷合理的分离,经过Content-Lenth就能够做到将报文和报文和报头之间完成对应的分离,那么多个http请求也就具备了,能够向我们的服务端同时发起多个的天然土壤,这是其一。其二呢,http协议也有一个特点叫做,无链接。你刚才和我讲过,http是基于tcp的,tcp是面向链接的,你现在上来就告诉我,http是无链接的,好吧,你来解释一下吧。
》比如说,你有个亲戚特别有钱,你也是有钱人吗?当然不是,人家是人家的,最多是借一下人家的,最多用一下别人的能力。同样的,http协议和tcp协议是两层协议,两层协议是毫无关系的,tcp是所谓的面向链接的,和我http有什么关系呢?所以,我们的http是无链接的,它呢只是用了一下tcp的能力,把信道建立好,然后用tcp完成自己要做的工作,比如说发起请求,但是http通信的时候并不会建立链接。所以http最核心的特点就是,超文本协议,是一个无链接,无状态的一个应用层协议,这就是一个http的定义。
》最后一个点,比如说,我们今天发起了一个http请求,建立一个链接,链接建立好之后,在这个链接上,我按照顺序发起了http请求1、请求2…,什么意思呢, 就是对方请求我的内容的时候呢,我这里读到的就是请求1、请求2…连续10几个请求,那么我服务端就对10几个请求做处理,但是响应的时候,是怎么响应的呢?你按照顺序请求,是不是一定能够按照顺序来响应呢?一般呢,我们是怎么请求的,我就怎么响应。所以为了做到这一点呢,我们做处理的时候,就要维持好请求和响应的顺序,这种就叫做,怎么请求的,我就怎么给你响应,叫做,我们http的pipeline技术。说白了就是,别人给我若干个请求,我再给他响应,响应的时候尽量不要乱序。比如说,我总不可能给浏览器先扔几张图片,我是不是得先把网页给别人,因为网页是骨架嘛 ,它再根据图片的位置给我们做渲染,所以请求可能是按顺序过去的,但是响应就不一定,除非浏览器不关心所谓的顺序,它可以多线程向我们服务器发起请求,那么无所谓,但是我必须保证在一条链接情况下,你怎么请求的,我就给你响应,必须按顺序来做,这样的话,能够极大的减少我们对应的问题,如上就是我们关于http后半部分的全部补充内容。
网络基础(一)_第47张图片
》所以http协议呢,我们就相当于全部讲完了,对我们理解它的原理呢,肯定有帮助,但是现在主流的随便访问一个网站,基本已经不再是http了,如果是在20年前互联网的草莽阶段,在那一个数据在网络里面裸奔的年代,当年用的http居多,现在凡是叫得上名字的,都得是https,所以我们不仅仅要了解http了,我们还要了解https。

HTTPS协议

所以https是什么呢?为了保证数据的安全。我们http以下的3层,分别是传输层、网络层、数据链路层,物理层就不画了。我们http呢,直接使用的是传输层提供的接口,不就是套接字嘛 ,它使用传输层的接口来使用协议来进行通信,构建http的responce和reqeust,说白了responce和request不就是二进制流嘛,我们干脆称之为文本流,说文本流不太准确,因为http也可以传视频、音频。但是今天大家知道http是传文本,它是以按行为单位的,它是有自己的格式的。在我看来,你虽然是按行为单位,这是给用户来看,但是,在我们计算机、操作系统来看,就是一行字符串嘛,然后发过去就可以了,发过去之后呢,我们就可以做处理,这个没问题。 但是呢,你的数据和正文都是明文的,那么是明文的话,如果有人不小心拿到你这个数据,那么直接就看到一些东西了,这样是不安全的。
》所以呢,现在主流就是,在应用层http和传输层之间呢,加了一层软件层,SSL/TLS。它主要负责,发送的时候,从上往下执行的就是加密;从下往上,执行的就是解密。所以,对我们来讲,同层的协议呢,并不需要关心如何加密,同层的协议呢,也不需要关心如何解密。发数据之前十明文,收到之后也是明文,并不影响上层http的使用,而是在应用层和传输层之间,加一个软件层的方式来解决我们对应的数据安全的问题。
》所以,至少我们清楚了软件层叫什么名字和在哪里,是在http协议的下面。我们前面写的时候有这个吗?我们写了一个demo,婴儿版的http有吗?是没有的,所以,你可以选这它有,也可以不选择它。不选择它,就是常规的http,选择的话,就叫做数据加密。
》所以,我们把这种协议,包含了http和加密解密层的协议叫做https。http默认bind是80端口,而https呢,bind的是443。所以从协议和服务角度,是两种服务。虽然一个叫http,一个叫https,这是两套服务。说白了就是,http和https就是两套服务,这两套服务呢,就意味着,大家都是web服务器,一个需要加密,一个不用的。有人说,http是不是就被淘汰了呢?答案是:不会的。因为http太经典了,在比较安全的环境当中,它绝对是首选的。因为,只要你做加密和解密,那肯定会比较慢,这是毫无疑问的,因为你多做了很多事情,在一些高并发的环境当中呢,要求的就不是安全为第一位,因为已经保证了,在公司内网当中就是安全的,所以可以在内部使用http来提高自己的效率,当然,http协议也有很多让人诟病的点,所以很经典,下面我们来谈谈https。

HTTPS是什么

HTTPS也是一个应用层协议,是在HTTP协议的基础上引入了一个加密层,同样的必然会有解密层。HTTP协议内容都是按照文本的方式明文传输的,这是默认的,那么别人可能随便阅读和篡改,你也不知道,就会出问题,所以,就必须得在我们的协议之上加上我们的加密和解密相关的细节。
》在正式往下讲之前,得补充一些网络安全的概念,我们的重点还是理解协议本身,这些安全的概念呢,我们没必要做深究。

什么是“加密”

就是,一个普通人他一看就懂的字符串等内容呢,我们一般称为明文,然后对数据做完加密之后呢,我们可以称被加密之后的文本叫做密文。所以,一个数据被加密之后,就成了密文,在没有被加密呢,就是明文,这是第一个概念。
》第二个概念呢,我们在加密的时候,需要有一个东西,就是密钥。密钥说白了就是一段数据,这个密钥呢,根据算法会生成不一样的,但是我们可以通过使用特定的加密算法,采用特定的密钥来对我们的明文来做加缪,加密之后呢,我们就可以形成对应的密文。
》一旦加密的话,必须面临下一个问题就是解密。解密也是需要我们的密钥来进行解密,最典型的情况呢,就是会讲一个小例子来帮助大家理解。但是常识告诉大家,有三个概念明文、密文、密钥。当然,密钥也分好几种类型,对称和非对称,后面再说。
》《火烧圆明园》中有一段,慈禧收到了一封家书,这份家书被其他王公贵层、太监看到呢就是一封普通的家书,慈禧看的时候呢,默默的拿出了另外一张字,这张字抠掉了很多字,然后把家书和这张字叠加起来,真正的家书呢,不是一开始的那张,而是叠加起来之后的内容,暴露出来字才是。这个例子对应的就是,一开始的家书就相当于加密的过程,慈禧拿着一张被扣掉字的纸,将两张叠起来,这就是解密的过程,其中扣掉字的纸呢,就叫做密钥。
》再继续往下讲之前,记住一句话:所有的加密都是为了防止中间有人进行窃取和篡改。这是加密和解密的本质,我们永远都是防止中间人做事情的。再举一个例子:以前皇宫里面,给皇上递上密折,那么密折是怎么上的呢?是不是自己写好折子跑到皇上那里给呢?能够当面见到皇上,那还上什么折子呢?所以什么叫做密折呢?太监是帮皇上和大臣之间传递信息的,皇上和大臣在私下里聊过,为了能够上密折呢,有一个盒子,上面写着大臣的名字,同时盒子还配一把锁,有两把钥匙,其中,皇上一把,大臣一把。大臣以后想上密折该怎么办呢?此时就把写好的折子放在盒子里面,大臣拿着自己的钥匙将盒子锁上,交给对应的太监,因为太监没有所谓的钥匙,是无法打开的,然后太监将盒子呈上皇上了,皇上再用事先说好的钥匙去打开,这样就能保证,中间不会被其他人看。

为什么要加密

我们在网络里面传送的人和数据包,是要经过各种的网络设备。给大家举一个例子,比如你的数据请求等要经过你自己家的路由器,因为你曾经装了网线,如果你用的是电信的,那么你请求会发给电信,然后通过电信帮你的报文进行转发,将报文转发到公网上,再将公网给你的信息传回来。所以你的数据包呢,在网络里面,会经过各种设备,各种设备会处理你的数据,包括各种软件也会对你的数据内容进行获取。
》有没有想过,为什么无法访问外网呢?比如,你输入谷歌的话就访问不了,说白了,别人能够拦截你,一定是别人能够先识别你的请求,百度为什么不拦呢?所以,当我们下载某些软件的时候,中间就会有人给你捣鬼,说白了就是将你的链接换了,比如你要下载天天动听就变成先下载qq浏览器,实际上这种情况呢,是运营商给你做的,运营商将你的下载链接给你换掉了,别人劫持了你,自然就将你的请求改成下载浏览器再发给你。
》就是你要获取的下载链接先会到运营商设备,然后发到千千动听的服务器,千千动听呢给你返回下载链接到运营商设备,运营商中间设备给你篡改了,给你改成qq浏览器了,那么这个时候呢就相当于中间人。
网络基础(一)_第48张图片
当然后面也会讲,如何成为中间人,成为中间人呢,也有他的方法,当然这种方式也有很多的防御手段。当然只会给大家讲原理,当然有些原理是无法了理解,因为很多计算机相关知识都没讲,但是有些肯定是知道的。有时候中间人不一定就是运营商,比如在公共场所,有一些免费的wifi,你连了之后就可访问了,是谁给你提供的免费WIFI呢,你一连上,你的数据包就都转发到提供WI-FI功能的路由器上,此时路由器可能是一部手机或者一台电脑,它最终呢就可以获取你的信息了,反正它可以根据中间人的方式来对你的请求进行拦截的。我们不是讲过重定向嘛,有时候你访问的网站就是一假网站,你去访问的时候,它伪装成真网站,然后你和他进行数据转发,它代替你去访问真网站,那么它就是中间人,你所有的消息都会先发给他,真网站以为是它在访问,所以真网站就将数据响应先发给假网站了,假网站再将数据处理转发给你,这也是一种中间攻击。
网络基础(一)_第49张图片
》所以为了防止我们的数据被中间人各种攻击的情况下呢,我们要对我们的数据做加密。所以明文传送是一件很危险的事情,像下载千千动听就太明显了,吃相太难看,用户都已经感知到了。我们就要用我们的https对我们数据做加密,对信息做进一步的安全保障。
》加密就能解决问题吗?答案是:不能!需要一套综合的方案,单纯靠加密是解决不了的。我们得先讨论一个问题就是,就加密而言,什么是安全,安全这个词经常听到,但是我们这个世界上没有绝对的安全,网络世界也一样,都是会存在漏洞。安全是相对而言的。安全你可以理解成,如果我破解你这个信息带来的收益和我破解你这个需要的成本相比,如果成本大于好收益,这个信息呢,理论上就能称之为安全,当然有一些成本不能拿收益和成本来衡量的,比如说,涉及到国家安全,那么加密级别就必须非常高,但也不是不能够被破解,只不过是当代计算机的算力要算上,上万年才能将结果破解出来。但是,我们要用发展的眼光去看,算力增加了,那么解密和加密都是同样的得到提升,所以双方一直处于博弈状态。

常见的加密方式

一种是对称加密,对称加密就是我们前面举的上密折的例子,他们都有一把钥匙,他们的钥匙都是一样的,也就是说,钥匙就是密钥,我们的密钥的发送方和接收方都是一样的,我们通过密钥加密,再通过密钥解密,这就叫做,对称密钥。
》特点:算法公开、计算量⼩、加密速度快、加密效率⾼。我们最简单的就是,int x =123;int mi = 456; int miwen = x^mi;做完异或呢就会形成加密完之后的数据,你把加密之后的数据经过网络传输,因为中间人不知道你拿的哪个数字进行的异或,所以就没办法去破解你,对方收到之后呢,对方也有一个,那么异或我们也知道,相同为假,相异为真,那么x ^mi ^ mi = x ^ 0 = x;后面就恢复出来了数据,我们这里用异或的最基本例子来告诉大家,我们有一个原始数据x是要发送给对方的,我们有一个密钥的数据呢,可以和我们的原始数据进行计算,得到的我们加密之后的miwen,经过网络传输到对方,对方再经过使用密钥进行解密,就能拿到原始数据,这个过程就是加密和解密的过程。
》我们用相同的密钥来进行加密和相同的密钥进行解密,这种加密方式,叫做对称密钥。那么还有一种方式呢,叫做非对称加密。
》非对称加密有两个密钥,一个叫做公钥,一个叫做私钥。在对称密钥当中呢,两个密钥中的任何一个都可以叫做私钥和公钥。只不过选择其中一个公开,都能看到,中间人也能看到,这就是公钥,然后有一把自己保留好,不让别人看到,这个呢,我们叫做私钥,其中呢,我们可以用公钥加密,用私钥解密。反过来用私钥加密,用公钥解密,这种算法呢,我们就叫做非对称。
》非对称最典型的算法呢,就是RSA、DSA,一般像RSA都是基于大数的因式分解来构建一个加密的非对称算法,我们不考虑,本质上加密和解密都是数学问题。
》特点:算法强度复杂、安全性依赖于算法与密钥但是由于其算法复杂,⽽使得加密解密速度没有对称加密解密的速度快。

数据摘要&&数据指纹

意思就是说,我们有时候,有一个大文本,这个文本呢,短短时候几兆,长的时候几个G,有时候,我们要给文件进行用基本的哈希的散列方式,来对他进行我们对应的计算,来形成一段固定长度的数据的摘要,最典型的呢就是我们MD5这样的算法,它可以将我们长文本形成固定的具有唯一性的字符串。我们在前面讲过一个cookie session的知识点,哪个session_id我们其实就可以直接使用这样的哈希散列的方式来形成唯一值。
》现在的问题就是,我们把文本经过哈希散列之后呢,形成一个固定大小的字符串,他这样做的目的是什么呢?第一就是,未来相对比这两个文本是否完全一样,就不需要再去扫描原始文本了,而是只需要拿着这两个经过哈希散列之后形成的字符串来进行对比,如果两个字符串相等,那么原始的两个文本就是相等的。
》如果你用特定的哈希算法来将文本哈希散列成固定大小的字符串,这个字符串具有唯一性,它还有一个特点就是,如果你将原始文件做一下修改,哪怕改一下符号,再去三列的时候,它形成的散列值呢,变化非常大,所以呢,我们就可以用形成散列值的固定大小的字符串来充当该文本的键值,保证该文本的唯一性,我们可以将固定大小的字符串称之为该文本对应的数据摘要或者叫做数据指纹。
》那么这个数据摘要和数据指纹,属不属于加密的一种呢?宏观上讲也是算的,但是你非得抠字眼将其和我们的加密和解密去对比,严格意义上讲,它是不算的。为什么不算呢,因为大家都知道,哈希算法,往往具有不可逆性,也就是说,你用大文本形成固定大小的字符串,但你没办法通过固定大小的字符串来反向的形成原始文本,所以它不可逆,不可逆也就意味着,用这样的算法是无法解密的,就相当于你只能对原始文本进行哈希散列,但是没有对应的解密行为,就不能称之为加密,一般,我们把这种形成固定大小的字符串称之为数据摘要和数据指纹。
》我给大家说一个现象,不知道你们有没有用百度的网盘,比如说,你今天想要上传一个视频,你上传会发现有时候你传的会非常慢,但是有时候你上传的时候有一个功能就是一瞬间就上传上去了,然后会说秒传。所以呢,让你去设计一个百度网盘这么一个系统,你如何实现秒传功能呢?其实秒传的原理是什么呢?你在上传一部电影的时候,百度他是做搜索的,它第一件事情不是将你的视频给它传上去,而是会在自己的云后台去搜索,曾经有没有客户传过类似的电影,如果有的话,它就不会让你传了,就会在你的云盘目录下形成软链接,那么就直接指向别人已经上传过的视频了,你再打开你的百度云的时候,你就能够看到已经上传好了,我们建立软连接就直接过去了,这样就相当于百度只需要给你维护一份代码就可以。
》现在最大的问题就是,百度怎么知道我即将上传的文件和别人已经上传过的文件是同一个文件呢?很简单,当你在上传之前,你会看到比较慢的过程,它在做什么呢?实际上,它在本地对你的数据做摘要,拿着你的文本数据形成数据指纹,这个数据指纹能够确定你这个文本的唯一性,指纹数据形成之后,将你这个指纹数据传到它的服务器上,然后再拿着指纹做搜索,搜索我现在已经上传的资源当中,有没有指纹和你的是匹配的,如果是匹配的,那么直接就是秒传,将别人的文件给你形成软件链接就完了,此时进度条就百分之百了。如果没有找到,再将你的数据形成二进制,然后一个字节一个字节的往上传,所以数据摘要&&数据指纹是由自己的实际意义的,通过这个方法,可以快速判定大文本是否和另外一个大文本是否是同一个文件!
》有了数据摘要呢,它可能是明文的,但是你无法拿这个摘要去推出原文件,但是这个摘要一般在网络里面,尽量也不要明文传送,所以,我们对摘要进行加密,我们就得到了一个概念,叫做数字签名!关于数字签名,我们后面在讲https通信细节的时候,会给大家详细来谈。如上就是我们对密码学铺垫的一些概念,后面我们根据密码学的知识,如果让我自己去设计https,我应该怎么去设计呢?所以,我们面再来进行我们https的工作探究,我们来提出五到六种的技术方案,然后不断的去推测哪一种方案是什么,它的问题是什么?解决方案又是什么?

数字签名

如果现在有一个大文本,我先对其进行数据摘要形成了一个固定64位大小的数据摘要,这个数据摘要确实不能够被解密,它也可以被获取,所以,未来呢,我们还有一种场景,我们要对你的摘要进行加密,对摘要进行加密呢,此时就得到了非常重要的概念,这个概念就叫做“数字签名”。关于,数字签名呢,现阶段只能告诉大家的结论就是,摘要进行加密,就得到了数字签名,至于是什么意思,为什么要有它,我们后面再讲http的时候,结合场景再讲。
》如上就是我们要对https理解所需要准备的基本工作。

理解链—承上启下

下面呢,给大家开个头,提一提你今天说的在网络通信里面,我们可能是会存在安全的问题,这里的安全问题,我们该怎么去理解呢?会有哪些安全问题呢?并且我还想重点知道的是什么呢,我想重点知道,你上面的那些概念和我们接下来https有什么关系呢?
》第一个,我们首先得明白,对http进行加密解密,能否解决数据通信的安全问题呢?如果能就最好,如果不能的话,问题又是什么呢?
》以及我们后面https为何要采用非对称加密?又为何不全用非对称加密呢?
》https在进行我们对应的密钥协商阶段,进行握手的时候呢,先采用的是非对称加密,后采用的是对称加密。下面呢,我们要对https的工作方式做探究,然后我们要想各种各样的策略来攻击它,然后我们提出4、5种方案来探究最后的方案,就是http所采用的方案。
》既然要保证数据安全,就要进行加密。⽹络传输中不再直接传输明⽂了, ⽽是加密之后的 “密⽂”. 加密的⽅式有很多, 但是整体可以分成两⼤类: 对称加密 和 ⾮对称加密。

方案1—只使用对称加密

我们客户端服务器持有同一种密钥,此时呢客户端经过密钥,将明文数据加密形成密文,然后通过http请求发送给服务端。换句话说呢,对方再拿同样的密钥再进行解密,就拿到了明文,这种方式最好理解的,这种方案可以吗?
》黑客来入侵了,它截获了你的请求,但是数据是加密的,能不能解出来你对应的内容呢?答案是不能,不能够解出来呢,就没办法对你的数据做相关的监测和修改,所以呢,目前来看是没问题的。但是呢,数据只有服务端和客户端知道,黑客即便截获了,但不知道密钥是多少,你就无法解密,也就无法知道真实的数据是什么了。
》但是最大的问题不是这个,而是你,客户端和服务端持有同一种密钥是怎么做到的?什么意思呢?意思就是说,服务器是给多个客户端提供服务的,这么多的客户端,如果每个人的密钥必须是不同的。如果你客户端用的是同一种密钥,你怎么保证,服务端有一个密钥想要客户端和它用同一个了;或者客户端有一个密钥,想要服务器和它用同一个。问题是,你们两个这么拿到同一个密钥。
》有人说,将要用的密钥发送给服务端不就完了吗?如果你将密钥发送给服务端,那么密钥本身可不可能被密钥截获呢?有人说再加密,那请问,服务器怎么解密呢?所以你客户端和服务端想要拿到同一个密钥,这种情况是有一点不太好处理的,这是其一。
》其二呢,服务器是可以给客户端提供服务的,你可以给许多的客户端提供服务,每个人用的密钥呢都必须是不同的,如果是相同的话,那密钥就没什么存在的意义了。因此,服务端要维护每一个客户端和密钥之间的关联关系,这个是很麻烦的事情。
》还有一个事情是什么呢?我们在通信之前呢,我们两个可以先进行密钥协商的阶段。在客户端和服务器建立连接的时候,双方就在应用层将我们的密钥协商好。比如说,客户端说,我们用这个密钥,服务器说可以,此时两方都知道密钥后进行加密和解密了。
》可是,这里有一个问题,如果你直接把密钥的明文传送出去了,那么黑客也就能截获你的密钥了,你们这不就是掩耳盗铃嘛,你们用密钥的意义在哪里呢?
》所以传送密钥的时候,也得给密钥加密传送,可是现在问题来了,你给他加密了,你怎么解密呢?是不是得有另一组密钥,那么对方怎么知道另一组密钥呢?这是不是就出现了“先有鸡还是先有蛋的问题”。所以在进行密钥协商的时候,用对成密钥来进行加密,是行不通的,也就是说,我们确实能够通过某种方法来进行加密,但是你怎么让密钥让双方知道呢?对不起,你对称密钥是做不到的。

方案2–只使用非对称加密

非对称加密呢,是有一个公钥和一个私钥,如果我们正常通信的时候呢,我们可以采取的是,比如,我们首次进行通信的时候,比如客户端给浏览器发起请求的时候,服务器进行密钥协商的时候呢,服务器维护一队公钥和私钥。服务器先以明文的方式传输给浏览器,以明文的方式,说白了就是,客户端问服务器,我们用的公钥是什么呀,服务器给客户端返回的时候,以明文的方式将公钥给了浏览器,那么潜台词就是,你黑客想截取就截取吧。
》所以,此时中间人呢,会将数据拿到,我们先不考虑中间人问题,我们就正常来看完整的过程。把公钥明文的方式给了浏览器,浏览器在以后传送数据的时候,先用这个公钥加密好,然后再传送给服务端。因为,我们今天用的是非对称加密,我们说了,如果用公钥进行加密,就可以用私钥来进行解密;如果用私钥来进行加密,就可以用公钥来进行解密。现在的问题就是,我把公钥发送给客户端了,客户端呢,此时就可以直接拿着我发给他的公钥进行加密,这个密文到了服务器呢,父亲再拿着私钥S来进行解密,就可以得到我们数据的原文。也就叫做明文。
》那么其中私钥呢,只有服务器私自持有,不会暴露在公网上,我们只是将公钥暴露在公网了。那么黑客有没有可能拿到这个公钥呢?拿到了,可是你拿到了公钥又有什么用呢?客户端是用公钥来进行加密的,这个密文呢,中间人将公钥和客户端发的密文都能拿到,拿到之后呢,因为黑客没有对应的私钥,只能公钥来加密,私钥来进行解密,因为中间人没有私钥,所以中间人也就无法对密文做解密,那么其中的密文数据就无法被黑客解析到了。至此我们就保证了客户端到服务端发送的消息是安全的,因为客户端有服务端发来的公钥,以后呢,所有的数据用公钥进行加密,只有服务器有私钥,所以只有服务器能够解密,所以此时客户端到服务器的这条信道的数据是安全的。记住,目前是安全的,其实还是有漏洞。
》有人说,我们客户端发起请求,我们服务器呢用非对成加密,有公钥和私钥,它将公钥公开出来了,客户端就拿到了,拿到之后对要发送的数据加密,因为只有服务器有私钥,也就意味着,世界上只有服务器能够解密,所以密文在网络上随便发,都不害怕,你中间人随便拿,你拿到公钥也无法解密呀,那么此时,服务端就拿着私钥来进行解密。
》那么如果此时我们的服务器想向客户端发消息,服务器此时能不能用公钥加密呢?答案是:不可以。服务器你用公钥进行加密,客户端又没有私钥,所以也就无法能够解密,所以你就只能用私钥来进行加密,加密之后,这个数据发送给客户端,客户端可以用公钥来进行解密,这是能够做到的。但是,拿着你的公钥的,可不仅仅是客户端,中间人也是拿着你的公钥,如果你用私钥发过去,那么这个数据,中间人也是能够得到的,所以反向的从我们服务器端到我们对应的客户端,这条信道是不安全的,暂时还没有解决方案,
》所以,我们今天的方案是什么呢?只使用一对非对称密钥,由服务端来提供,非对称密钥,我们把公钥公开给全网,那么客户端能够拿到,拿到之后,客户端对数据用公钥进行加密,用公钥进行加密的数据只能用私钥来进行解密。但是,服务器最终想给客户端发消息,没办法,因为无论你用私钥和公钥进行加密都是行不通的。
》所以,我们从客户端到服务器的信道似乎是安全的,其实还是有安全问题,后面再说。因为只有服务器才有对应的私钥来进行解密。但是服务器到浏览器的这条路怎么保证呢?
》 如果服务器⽤它的私钥加密数据传给浏览器,那么浏览器⽤公钥可以解密它,⽽这个公钥是⼀开始通 过明⽂传输给浏览器的,若这个公钥被中间⼈劫持到了,那他也能⽤该公钥解密服务器传来的信息了。所以换句话说呢,这种只使用非对称加密,只能够保证一个朝向的方向是安全的。讲到这里,应该也能够理解下面的第三种方案了。

方案3–双方都使用非对称加密

现在你只使用一对非对称加密,我们能够保证客户端到服务器的朝向是安全的,是能够保证的。但是,反向的数据是无法保证安全的,那么我们双方都使用非对称加密,不就行了吗。
》服务端拥有公钥S与对应的私钥S’,客⼾端拥有公钥C与对应的私钥C’,也就是说,我们双方呢,你有你的公钥和私钥,我有我的公钥和私钥,客户端和服务端将公钥交换,说白了就是在通信过程中,客户端给服务器发消息,把公钥推给服务器,服务器收到之后呢,把自己的公钥发送给客户端,所以,客户端和服务器把各自的公钥进行交换,因为是公钥嘛,我们明文传送问题也不大。
》然后客户端给服务器发消息就变成了,客户端用服务器给他的公钥进行数据加密再发送,由于只有服务器有私钥才能够解密;服务器给客户端发消息,先用客户端给他的公钥对数据加密,发送的时候呢,只有客户端有自己的私钥,才能够解密。
》所以,我们用两套公钥和私钥,是不是就能保证双方通信的信道都能够保证数据安全了呀。
》可是呢,这种方案呢,貌似是可行的,但实际上是不行的。有两个理由,第一个理由,效率太低了,也就是从此往后,客户端和服务器双方互相通信的时候,永远都采用的是公钥加密,私钥解密,这种非对称加密,而我们在最早的时候,给大家做理论铺垫的时候,我们说过一句话,叫做非对称加密最典型的特点,也是缺点就是,效率非常的慢,这样就会导致https效率低下,这样也就会导致一些应用场景无法满足,也就不会去采取你的https协议。所以呢,第一个缺点就是效率太低。
》第二个问题,这个方案呢,依旧还是有安全问题,安全问题是什么,与上面的第二种方案的安全问题是同一个问题,我们后面再说。

方案4–非对称加密+对称加密

虽然我们用非对称加密勉强解决了该问题,对称加密呢,效率很高,那我们是不是也可以采用叫做非对称加密 + 对称加密呀,来进行双方的握手协商的问题呢。那么这个是什么意思呢?
》比如说,服务端具有⾮对称公钥S和私钥S’,客户端不提供。客户端此时发起https请求,然后能够获取到服务端的公钥S,紧接着,客户端在本地声称对称密钥C,通过公钥S加密,发送给服务器。意思就是说,客户端拿到了服务器的公钥匙S,我先不着急给你发数据,我客户端先在自己的本地形成一个对称密钥C,通过公钥S加密,发送给服务器。因为这个世界上,只有服务器有公钥对应的私钥S’,只有服务器能够收到公钥S加密的密文,将其解密出来,拿到的就是,服务器与客户端协商形成的对称密钥C。即便你中间人获取了,也无法解密。
》那么前面的步骤就已经初步的将我们方案一的一个问题解决了,方案一有个什么问题呢?就是客户端与服务器双方在通信的时候,可以使用对称密钥,但是客户端和服务器相比呢,你怎做到让他们两拿到同一个密钥呢?以前明文传密钥没有办法,但是通过方案四就可以了。所以,前三步呢,客户端拿着公钥在我们客户端本地形成对称密钥C,通过公钥S来进行加密,发送给服务器。
》由于中间的网络设备没有私钥,所以,即便你截获了数据,你也无法还原出内部的原文,也就无法获取到对称密钥。服务器呢通过私钥解密,还原出客户端发送的对称密钥C,并且使用这个对称密钥加密给客户端返回响应数据。从此往后,我们的客户端和服务器,不再采用非对称加密,而只采用堆成加密,来进行双方的正常通信。由于对称的密钥只有客户端和服务器两台主机知道,所以其他主机是不知道的,所以,这个时候,我们的通信呢,就可以做到,即保证安全,又保证效率的一个好处。由于对称密钥的效率比非对称密钥的效率要高,因此,只是在开始协商密钥的时候使用非对称密钥,后续的数据通信,我们仍然采用对称加密。

中间人攻击–针对上面的场景

首先https是服务端有对应的TLS/SSL这样的加密解密层,客户端也是有的。另外,我们往后谈的形成公钥,形成私钥,形成对称密钥,对应的算法是可以直接形成的。也就是说,客户端和服务器想要形成,它自己是可以形成,因为在TLS、SSL这样的软件层当中,它内部是有算法,直接形成对称密钥和非对称密钥的,包括Linux的命令当中有对应的指令形成公钥和私钥,所以是算法范畴,是可以双方形成的。至于密钥是多少不重要的,重要的是密钥怎么形成的,这才是最重要的。
》可是无论是我们刚刚所谈的方案2、3、4,它们三个方案当中呢,尤其是方案4看起来是无懈可击,但是实际上呢,还是有问题的,那么有什么问题呢?方案四已经比较接近了,但是它依旧是有安全问题的,其中方案2、3、4都存在同一个问题,这个问题是什么呢?
》我们刚刚在讨论中间人的时候,我们是假设中间人在你已经成功完成密钥交换过程当中,假设你的中间人截取了,比如说,服务器给客户端呢,发送一个公钥,是假设公钥发送给客户端之后,然后呢,我们再进行加密,这个时候中间人才来的,那如果中间人早就开始捣乱了呢?
》换句话说,你怎么保证发送过来的公钥就是服务器的呢?中间人可不仅仅的对你的公钥进行截取,甚至可以在最开始将发送来的公钥给你篡改了,这个也是有可能的。所以,我们刚刚讨论的所有问题,都有一个公共问题,就是在最开始的时候,中间人已经开始攻击人了,什么意思呢?

在⽅案2/3/4中,客⼾端获取到公钥S之后,对客⼾端形成的对称秘钥X⽤服务端给客⼾端的公钥S进⾏加密,中间⼈即使窃取到了数据,此时中间⼈确实⽆法解出客⼾端形成的密钥X,因为只有服务器有私钥S’。
》但是中间⼈的攻击,如果在最开始握⼿协商的时候就进⾏了,那就不⼀定了。
》1. 服务器具有⾮对称加密算法的公钥S,私钥S’。当然我们的中间人也准备自己的公钥M和私钥M´。然后客户端向服务器发起请求,服务器明文发送公钥S给客户端,这句就相当于客户端先发起请求,然后服务器就给客户端进行响应,响应的话那么此时服务器就将公钥S明文给客户端。
》明文传送意味着客户端能收到,中间人也是能收到。以前呢,我们公钥S直接到了客户端,然后客户端形成自己的X通过公钥S加密发送给服务器,中间人是什么也做不了。但是中间人这次,并不想这么做。
》他在服务器给客户端响应发送公钥S的时候,就进行截获,中间人将公钥S在自己内部保存起来,然后狸猫换太子,将客户端的公钥S换成中间人的公钥M。什么意思呢,就是,中间人将服务器的公钥保存起来,不删除,也不覆盖,自己保存一份,即中间⼈劫持数据报⽂,提取公钥S并保存好,然后将被劫持报⽂中的公钥S替换成为⾃⼰的公钥M, 并将伪造报⽂发给客⼾端。
》这个过程呢,客户端是不知情的,客户端觉得自己刚刚请求过了,得到的响应的公钥应该是服务器提供的公钥S,然后形成自己的密钥X们,用它所认为是服务器发来的公钥进行加密,其实是用公钥M对自己的数据加密的哦!
》将加密的数据发送出去,也是照样的先被中间人截取,中间人很高兴,因为你用公钥加密的钥匙,是我给你的M呀,只有我中间人有私钥M´来对你的客户端发来的数据进行解密。所以中间人用自己的私钥M´来对数据进行解密,就得到了客户端生成的对称密钥X!这就完了吗?中间人聪明得很,中间人解密出来之后,将对称密钥保存一份,然后曾经不是将公钥截取了吗,接下来再重新的拿着服务端发过来的公钥M进行对客户端的数据进行加密,发送给服务器。什么意思呢?就是我们重新将报文用服务端对应的公钥,再重新对客户端对称密钥X进行加密,把加密之后的结果再交给服务器。
》服务器拿着认为客户端发来的对称密钥X的密文用密钥S´进行解密拿到。服务端很高兴,认为服务端和客户端完成了对称密钥的协商。可是对不起,在你们握手期间,中间人已经盗取了。
》服务器和客户端接下来做什么呢?当然是很happy的使用对称密钥X来进行加密通信。但是,双方发的消息都会先流经中间人这里,中间人也是有对称密钥X来进行解密的,所以服务器和客户端的加密形同虚设。
》所以上面的中间人攻击方案,是根据我们对应的方案4来谈的,但是,我们说的方案2、3所说方案也是有问题的,就是我中间人不在你们公钥交换了之后才攻击,我在一开始的时候就开始捣鬼,那么你的方案都被推翻了。
》上面方案的问题,本质在哪里呢?本质就在于:客户端无法确定收到的含有公钥的数据报文,就是目标服务器发送过来的!大家想想,在我们刚开始握手的时候,只有先开始是服务端先把自己的公钥发送给客户端,第一步是最关键的了,如果成功的被客户端收到,那都不是问题,但是,如果第一次就出问题了,那就全完了。现在本质问题就是,如果客户端能够识别,比如说,我要一个S,我却收到的是M公钥,如果识别到被篡改了,那我就不处理,就丢弃。但是现在客户端根本就无法识别到,它收到的公钥是请求服务器给他的呢,还是某台中间人的机器给他的公钥,这是最本质的问题。
》接下来我们还有其他方案,就是我们https采用的方案,现在的问题就是,我们刚刚出现问题的时候,方案2、3双方都采用对称密钥、非对称密钥,不管采用哪种都是有漏洞的,这个漏洞主要是在最开始的时候,我们中间人截取了公钥,然后狸猫换太子之后发给客户端,客户端拿到之后呢,并不能确定这个公钥是合法的还是非法的,这是最重要的。
》那如何解决这个公钥是我请求目标的服务器发过来的呢?这就要引入一个概念就是证书!

引入证书

整数这个概念呢,是需要花点时间来谈谈的,以及我们的数据签名和数据摘要就出来了。只有将他们理出来,最后中间人攻击的做法所带来的漏洞给他补上。
》所以,我们应该怎么做,才能证明我收到的公钥实际上是合法的主机,就是我要请求的目标网址发过来的。所以我们再来科普一个概念就是证书。
》证书这个概念呢,是用来表征服务器是否合法的这样的策略,是我们全球对应的https的组织所定制出来的一套策略。一般我们平时访问某些网站的时候,可能会给你弹出一些报错,说该网站的证书已经过期,是否继续访问?但是呢,想告诉大家的是,一个网站要能够使用https,它必须得在网站上线之前得先在CA机构申请证书,这个证书的构成一会儿再看。我们可以使用这个证书来标定某些网站的权威性,所以服务端,也就是某些网站使用https之前,它需要先向CA机构申请一份数字证书,数字证书里面呢,包含了证书申请者的信息、姓名,包括公钥信息,这个公钥就是将来发给客户端的公钥 。再下来可能是公司的法人的信息、乱七八糟的其他联系方式、地址和网址域名等都要提交上去。然后,CA机构会进行审核,如果你是企业,就回去企业机构审核你,然后还会有可能和你的公司合作伙伴审核你,甚至走访,但是一般不会。如果没有问题,就会将CA证书颁发给你。
》从此往后,我们客户端在向服务器发起请求的时候,服务器给客户端发来响应报文,里面包含了公钥,从此往后,服务器在收到一个客户请求的时候,它会直接将证书传递给浏览器,浏览器从证书里面拿公钥就行了,这证书呢,就如同身份证,证明了我们服务端公钥的权威性,换句话说呢,我们先初步了解,我们上面说的中间人攻击方法主要是无法确定,客户端收到的公钥是不是请求的合法的服务器发来的数据。对于这个问题呢,就引入了一个概念,就是证书。往后呢,请求一个服务器,服务器会将证书发来,它这个证书里面包含了对应的网址和公钥是什么,这个证书呢,就是一个身份证,它的核心作用就是证明服务端的公钥的权威性。所以,只要有了我们对应的证书,那么我们客户端就可以根据证书来判定目标服务器的合法性。
》没有告诉你原因是什么,只告诉了你结果是什么。最后只要有一个结论就行了,当我们在请求服务器的时候,服务器可不仅仅是把我们的公钥给我们,它是给了我一批数据,将我们所有的数据打一个包,放在一个我们说的证书上面 。至于,证书可不可能被篡改呢?证书如同身份证又是如何做到的呢? 证书证明公钥的权威性,那它又是如何做到的?证书的公钥有没有可能被篡改呢?这些问题,我们一个个的展开。所以,换句话说呢,我们首先要理解一个东西,从此往后,我们的方案四当中,我们客户端请求的时候,我们服务端不再叫做返回公钥S,而是叫做,获取服务端证书,证书呢,是由一堆信息的集合。
》现在呢,我们知道了一个概念, 叫做,证书是服务器提前需要向权威机构申请的,证书是包含了当前服务器的信息的,包括你对应的公钥和域名,还有你网站相关的所有信息。
》下面我们来进行下一个,你证书说到底不就是一个字符串嘛,不也是数据嘛,那么你如何保证你证书的合法性呢?如果证书被篡改了,会有什么问题呢? 中间人在你刚开始和客户端协商的时候,将你的证书里面的公钥给替换掉,那么不就什么都玩完了吗?那你还怎么进行我们对应的,前面说的漏洞呢?所以,我们来理解下一个“数据签名”。

理解数据签名

关于签名,我们在前面是简单的说了一下,但是有一个概念我们说的比较多,就是数据摘要。签名的形成是基于⾮对称加密算法的,注意,⽬前暂时和https没有关系,不要和https中的公钥私钥搞混了。
》现在呢就是,数据签名是基于非对称加密算法形成的,那么,比如说呢,我们有一个原始的数据,我们将原始数据做哈希散列,形成了一个固定的散列值,那么我们在前面就说过了,对文本做哈希散列,最典型的算法是MD5,其中这里的散列值呢,我们把它叫做数据指纹或者数据摘要呀。所以呢,我们先对原始数据进行哈希散列,形成摘要,在摘要的时候呢,注意,我想签名,那么我就用我的私钥进行加密散列值,现在谁来进行加密还不清楚,你就只要记住,我们可以采用对应的私钥来进行加密,就对散列值形成了加密之后的一个数据,这个数据呢,我们称之为签名。
》有了签名之后怎么办呢?我们把原始的数据经过我们的哈希散列之后形成的数据摘要,再经过私钥加密形成的签名,然后与原始数据合起来形成了一个,带签名的数据。
网络基础(一)_第50张图片
也就是说呢,这个原始数据先散列形成摘要,然后再对它进行私钥加密,私钥加密形成签名,将签名的数据和原始数据组合在一起,就叫做,携带了数据签名的数据,这是第一个。
》如果我们将来呢,有人拿到了携带签名的数据,那么比如说,有人对这个内容做修改,对签名做修改,或者对内容和签名同时做修改,目前我们考虑三种。就比如说,带签名的数据是一个完整的数据,我们对他的内容做修改,但是现在呢,我们来谈一谈我们对应的内容没有被修改过的话,是怎样验证的呢?
》它是这样验证的,比如说,你对应的带签名的数据呢,我呢第一,将内容拿出来,就是将原始的文章拿出来,第二,把签名拿出来,然后我们做一个工作,对我们原始的数据进行哈希散列形成散列值,就用同一种散列函数就可以了,这是算法的问题,不牵扯密钥,就直接是相同的哈希算法形成散列值,然后同时对我们刚刚形成的签名用公钥来解密它,也就说将形成签名的数据,用公钥来进行解密,解密完成之后,如果它们两个,因为签名本身就是对散列值的加密,所以解密,我们也得到散列值。所以,如果它们的散列值是相等的,那就证明数字签名和数据就是一致的,如果散列值不相等了,那就证明正文部分被修改过,或者签名部分被修改,或者双方都是被修改过。
》所以,**我们给数据携带数据签名的意义是什么呢?防止内容被篡改!**有人说,这不好改吗?比如说,你现在把它签名完了,我将其内容改掉,内容一改,然后此时,你要注意,你要是把内容改了,你依旧能用散列函数哈希,形成散列值,这个没有问题,可是不好意思,中间人把内容改了,你没有签名者的私钥,那么你也就无法重新形成数据签名,也就是说你将内容一改,因为私钥世界上只有一个人有,所以中间人将内容拿到,能改,改了之后,但是无法修改签名 ,所以,无法同时对这两个内容进行修改,这是第一个。
》第二个,有人说,那我内容不改,我把签名改了,你将签名改了也没有意义,一旦签名和后面不一致,匹配不成功,那么就不行。所以,我们对于一个文本携带数据签名的意义就是,可以防止文档被篡改。
》这里还有问题,还有一个什么问题呢?我们用签名者私钥加密散列值和用签名者公钥加密散列值,这什么意思呢?签名者又是谁呢?你说对比这两个文档,能感受的是,你说了,数字签名一旦给文档携带上,有人对这个数据做任何修改,此时,我们只要在做认证的时候,你只要改就不对了,摘要就不对了,摘要不对的话,那么后续对比就无法对比了。
》这个东西呢,就特别像人民币的防伪标识一样,制作成本特别高,你只要对其做修改就不行。其中这里的文档呢,我们就可以理解成被申请的整个证书,可是最终是怎么工作的,我们还是得再进一步。
》当服务端申请CA证书的时候,CA机构会对该服务端进⾏审核,并专⻔为该⽹站形成数字签名,过程如下:

  1. CA机构拥有⾮对称加密的私钥A和公钥A’
  2. CA机构对服务端申请的证书明⽂数据进⾏hash,形成数据摘要
  3. 然后对数据摘要⽤CA私钥A’加密,得到数字签名S 服务端申请的证书明⽂和数字签名S 共同组成了数字证书,这样⼀份数字证书就可以颁发给服务端了。
    》也就是说,用签名者的私钥,数据就是服务器申请证书的时候,它的证书明文,签名者就是CA机构,形成证书,那么此时签名是要任何一个文档经过哈希散列,只能用CA的私钥加密形成签名。签名之后再和原始文档结合形成了一个叫做数字证书。
    》那么接下来有一个问题,在我们现阶段而言,CA机构是一个权威机构,它的私钥只有它自己有,其他人是没有的,也就是说,只要我们的CA机构持有自己的私钥,那么全球范围内,没有人能够去模仿CA机构去颁发证书,也就是说,只要独有私钥,CA机构就能权威性的去颁发证书了。
    》概念说清楚了吗? 还没有说清楚,还差一口气,里面还有很多细节。我现在清楚的是,我一个网站先申请一个证书,这个证书呢,是有一堆文字信息的,这个文本信息呢,我交给CA,CA对我这个文本做数据摘要,然后摘要完之后,用CA自己的私钥形成数据签名,然后将我申请的文档和它形成的签名合在一起,在颁发给我,此时我这个证书就携带了我们的签名的。此时,任何一个人像我访问的,我会将我的证书推送给客户端了。截止到目前,我们基本上把一个公司https使用之前呢,要做的准备工作就做完了。当然还有很多的细节,这些细节呢,需要在下面的过程之中来帮同学们进行解决。
    》下面呢就是用第五种方案,采用非对称+对称+证书,看看能不能解决,我们前面方案无法解决的问题。其中关于证书认证的话题呢,我们一会儿边看场景边回到上面来进行对比,慢慢的就清楚了。我们刚刚绕了一大半圈,谈了另外一个话题,叫做证书,其中根本原因是因为,我们无法确认一个问题,就是,我们收到的公钥的数据报文,就是目标服务器发送过来的。但是现在呢,我们能够隐隐约约的感受到,我们可以通过证书来识别服务端是否合法。

方案五–非对称加密+对称加密+证书认证

我们刚刚到方案2、3、4都是有漏洞,漏洞出现在最开始的时候,服务端发给客户端的公钥就被中间人截取了,它将服务端的公钥保存起来,并且换成自己的公钥发送给客户端。现在呢,我们引入了证书,加数字签名防伪标识之后,能不能解决问题呢?
》在服务器和客户端一开始建立的时候,客户端给服务器发起请求,服务器给客户端返回证书,证书里面包含了,我们之前谈的公钥,这个很重要,还包含了网站的身份信息、网址、有效期 ,最重要的是有公钥。

客户端进行认证

》我们客户端呢收到这个证书,它会对证书进行校验,主要是为了防止证书是伪造的。怎么做呢?1,它要判定证书的有效期是否过期,如果过期了,那么浏览器就会弹出,该网站的证书已经过期了,是否继续访问。2.判定证书的发布机构是否受信任。说白了就是,计算机里面就已经是内置了受信任的证书发布机构的,所以证书里面携带了,颁发机构是谁。3.验证证书是否被篡改:我们现在拿到的证书,里面包含了证书和CA机构给我们私钥加密形成的签名,所以我们将证书拆成两部分,一部分是我证书的明文,一部分是CA给我这个证书颁发的签名。然后,我要做两个工作,第一、客户端呢,就要采用对你的证书进行使用相同的散列函数进行散列,形成摘要,或这称作指纹,然后,我们的CA机构用曾经自己的私钥对散列值进行加密形成的签名,假设CA机构在美国,用自己的私钥颁发出来的数据签名,附在我的证书明文上面,构建成一个带签名的证书,颁发给我这个网站。我这个网站被请求的时候,我将证书发送给客户端了。那么这里第一个问题就是,给明文做哈希是好做的,我们明显哈希一下就得到了散列值,但现在的问题是,我要验证,证书是否被篡改,那么我就必须得对这个签名进行一下解密呀!解密的时候,怎么做解密呢?好在形成证书是非对称的,CA机构的私钥是自己保存的,特定的CA机构,它的公钥是会内置在笔记本上或者浏览器内部的。一般我们,在进行颁发证书的时候,我们要验证是否过期,还要验证该颁发机构是否受信任。也就是我是认识一些受信任的机构的。再下来就是,颁发机构的公钥是可以暴露给我的,也就是全世界人民都可以知道,我们形成签名的时候,公钥是谁。所以,我们客户端,一、拿着原文做哈希;二、拿着CA机构的公钥对我们的签名做解密,形成我们的摘要。然后将摘要做对比,就能够确认证书是否被篡改过。
》换句话说,我们在认证的时候,我们客户端它怎么知道这个网站呢,是一个合法的网站,是具备合法的网站呢?会从系统当中拿到颁发机构的公钥,对签名进行解密,得到哈希值就是摘要,然后对整个证书,采用同样的哈希算法形成哈希值,然后对比这两个哈希值是否相等,如果相等,说明证书是没有被篡改过。这就是客户端认证的过程。
》换句话说呢,我们服务器在被使用之前,先要向CA机构申请证书,从此往后,所有访问该网站的客户端,它要请求这个网站,它拉取的都是我的证书。所以,对证书呢,我们可以采用,证书里面本来就有证书,以明文的传送的证书,公钥、域名全部都是公开的,只有一个数字签名附在证书上,这个签名呢,因为世界上只有CA有私钥,所以只有CA机构能够对这个明文进行重新签名,其他人签名不了,所以,我们把公钥暴露给所有人,所有人可以对内容做修改,一会儿发现这个方案其实非常完美。
》所以现在呢,我们可以结合客户端和CA机构本身对公钥,然后对签名进行解密得到摘要,并且对证书的明文进行哈希,形成摘要,两个摘要一对比就能知道是否被修改过。
》下面呢,也不先着急,证书这个东西有很多受信任的机构是会内置到我们的计算机里面的,我怎么去查看呢?是可以查看的。我们机器里面是内置了常见的证书的,有时候你的证书没有的话,你访问某些网站的时候,它也会告诉你,该证书没有被安装,你是否选择安装,然后你点击安装就会将信息放到你的电脑,凡是内置的肯定是安全的,有些网站你可能没有访问过,一旦安装后,行为就要你自己负责了。

中间人有没有可能篡改证书

客户端向服务器发起请求,服务器不仅仅返回的是公钥,还返回了携带公钥的证书,证书呢还携带了对应的签名,整个证书和签名都是CA机构给我颁发的。如果我们进行正常的通信的时候,客户端收到了服务器给他发过来的证书,所以客户端呢就可以对证书进行认证,是否过期、是否被篡改过,都没问题,通过之后呢,再提取它的公钥,然后再自己形成私钥X,再用CA机构的公钥S进行加密,再发送回服务器。
》可是,我现在有两个问题。第一个、中间人现在,最开始的时候,以前是对公钥,现在变成了证书了,对证书的做修改,无非就是增加、删除等。不管怎么样,你中间要修改这个证书,有没有这个可能性呢?
》如果来了一个中间人,我们服务端发的不再是公钥了,发的是CA机构颁发的证书,也就是我们的服务端响应的是证书,那么中间人可不可以截获这个证书呢?答案是:当然可以,证书也是数据,我当然是可以截获的。中间人截获了证书,并篡改了中间的明文,注意,证书是由两部分构成,一个是明文,另一个是基于这部分明文,由CA给我们做的数字签名,现在呢,你中间人改了明文 ,但是由于你改了明文之后,正常情况下,要骗过客户端,你是不是既要将明文改了,又要把签名改了!你不可能只把明文改了,或者只把签名改了。
》由于现在中间人将内容改了多少符号,或者是将明文里面的公钥换成自己的公钥了,那么忧郁没有CA机构的私钥,因为证书是CA签发的,你虽然截取了我的明文,你也哈希了,也形成了摘要了,可是,你不是颁发者,你没有CA的私钥,你就无法重新形成签名。所以,由于你没有CA机构的私钥,所以你无法对哈希之后的数据摘要做私钥加密,形成签名,那么也就没有办法对篡改之后的证书形成匹配的签名。如果你强行篡改,那么客户端收到之后,它会发现明文和签名解密之后形成的指纹/摘要不一致,就会知道证书被篡改了,那么证书也就不可信,就终止向服务器传输信息。
》目前来看,中间人没有办法去修改证书的,有人说,我修改不了你的证书,那么我把你证书明文改了,改了之后,我没有CA机构的私钥,我有我的私钥,对摘要进行加密,发送过去,可以吗?答案是,不可以。因为,你将明文改了,然后重新签名,你用你自己的私钥,谁认你呢?客户端认证你这个证书都是只会用匹配的CA机构的公钥来进行解密,你一个中间人还想要我用你的公钥来进行解密吗,你想多了,我们这个中间人无法对你这个证书做任何修改的,即便证书是明文传送,即便是给你打明牌,你也改不了,这叫做中间人没有办法篡改。
》如果中间人没有办法篡改这个证书,那么证书里面的公钥你当然就改不了了呀,那么你就只能眼巴巴的看着我把证书发给客户端,然后用给的公钥进行加密,将对称密钥发送给服务端,你什么也做不了,这就是中间人有没有可能篡改呢?不可能!
》那么又有人说了,既然我无法改你的明文,也无法改你的数据签名,那我直接把你整个的证书全部调包,理论上可以调包的,但是它是特别不合理的。首先中间人是没有CA机构的私钥,无法制作假的证书,你别想,为什么呢?因为客户端认证的时候,全都是用CA的公钥,换句话说,你的私钥要被别人识别,你必须得是受认证的CA机构,你用你自己的私钥,客户端是解不开密的,所以你再怎么去伪造也是无法伪造的。所以,因为CA有私钥,更重要的是,因为所有的电脑都有一个共识,我都要用我信任的CA机构的公钥来解密,群体的力量大了,那么也就意味着,CA机构里面的私钥就特别重要,这个私钥呢,就决定了,我这个CA机构是否具有权利向服务网站办法证书的权利,因为所有的客户都认我,笔记本和浏览器都认我,你这个网站不认我,行呀,你不认我,我就不给你颁发证书,那么你自己形成证书,你自己形成吧,形成之后,浏览器不认你,因为浏览器只认我这个CA机构,所以中间人没有CA私钥是无法形成证书的。
》所以中间人想要调包整个证书,所以中间人只能申请真的证书,用自己申请的真的证书去掉包,你就得有自己的域名和公司、法人等所有的信息都提交到 CA,你也得有自己的公钥和私钥并且暴露出去,那你不就是一个可以被追诉的人嘛,你的信息全部都被暴露出去了 。所以中间人只能申请真的证书,用真多证书去掉包才行。这个确实能够做到掉包,客户端在进行识别的时候,知道没问题,但是呢,这里就有一个特别奇怪的问题,它确实能够做到证书的整体调包,但别忘记了,证书的明文当中除了公钥,还有域名等认证信息。如果你整体调包了,客户端一看要访问的是www.baidu.com,但是呢,它给我返回的证书的明文的域名是www.qq.com,我们客户端发现不对,虽然证书是对的,但是和我要访问的目标网址是不匹配的,所以,我就不访问了
》所以,无论是中间人想对涵盖公钥对证书做中间的篡改,还是整体调包都不具备可行性。换句话说,我们方案2、3、4说的,中间人最开始攻击的问题也就不存在了。只要以证书的方式颁布对应的公钥,不可修改和篡改,那么中间人也就无法做替换了,并且客户端也能够确定收到的公钥是合法公钥。为什么呢?因为只要证书是合法的,就证明证书是被CA机构 认证过的,并且它的域名是我想要访问的网站,所以我的客户端收到证书之后,我就确定是目标服务器发送过来的,今儿就解决了中间人劫持的问题。
》所以无论是https进行通信的时候,双方在进行我们证书认证的时候,我现在的客户端和服务器,种间通信的时候,第一次请求服务器,它要给我返回公钥,它不是把公钥给我,而是返回携带公钥的证书,证书携带了签名,所以我们就可以通过证书+签名+域名等有效信息来确认服务器给我们返回的公钥的权威性,所以中间人不可篡改和调包了。

为什么摘要内容在网络传输的时候一定要加密形成签名呢?

很明显,我们接收到的证书,一方面是证书自身是明文的哈,另一方面是摘要信息是被加密形成签名的,这个问题呢,其实大家也已经知道了,摘要这个东西,只要大家知道哈希算法,只要知道明文,用哈希算法可以形成自己的摘要,如果你不对其做加密的话,你把明文直接传过去,人家改掉之后重新形成摘要,那么你认证就没有意义了,所以它必须得加密,而且这个加密形成的签名呢,这个签名就要用到私钥了,那么就只能用CA的私钥。此时呢,因为CA是被认证的,是被我们相信的,就跟我们派出所和政府一样,我们相信它。相信之后呢,又因为没有人有私钥,即便你能用公钥解,但是对不起,你无法再二次进行加密了,所以这一点我们要注意。

为什么签名不直接加密,而是要先hash形成摘要

就比如说,你有一个证书,我拿着你的证书直接加密,认证你这个证书有没有篡改,其实还有一种很好的办法就是把你整个文本全部加密,加密之后呢,到了客户端再去解密不就完了吗,然后对比一下两个证书是否完全一样,也样也是可以的,但是实际上我们并没有这么干,而是要先hash形成摘要,主要的原因是我们要缩小一下签名密文的长度,把我们签名的速度呢弄快一点,签名速度一快,那么解密的速度也肯定是很快的。也就是说,我们以前要加密要对整个文本加密,但是我们可能只加密其中的一部分只形成16~32字节的数据就可以了,这是其中一个最重要的原因,主要是效率考虑,当然还有一个原因就是,有些算法你也知道,证书有多长不同的网站的证书文档大小也不一样,有些加密算法呢,对我们文本可能就有一些要求。还有一个重要的原因就是,基于CA给我们的非对称加密的速度本来就是很慢的。所以,我们出于算法本身的考量,第二个就是效率的考量,最终我们决定先hash一下,反正哈希之后形成的摘要也能够识别这个文本,更重要的是,我们后续进行摘要对比的时候方便对比,16~32字节,我们直接就可以2进制对比就出来了,要不然你大文本对比的话其实效率很低的。

传输层

很显然上层的应用层,应用的功能呢其实是传输层提供的接口,比如说,你经常会听到http要通信,要通信,http协议底层用的是tcp,所以在我们之前,我们将了https,在进行密钥协商之前,你首先要把客户和服务端双方所对应的链接建立好,这个要比协商更早,所以我们就需要有应用层之下,我们还要来理解传输层协议。
》我们传输层协议最典型的就是,一个是UDP,一个是TCP,对应的就是我们曾经学的UDP套接字和TCP套接字。很显然,有了应用层的理论和实践的经验之后,我们再往下去看TCP和UDP的时候,它呢也就已经有了门槛知识了,相当于以前的门槛可能比较高,现在来看要真正理解呢,其实也不是特别难。在往下谈之前,我们再来谈一个概念,就是端口号。

再谈端口号

端口号呢实际上就是我们标定特定主机上,特定的一个服务器进程的唯一性的,我们不同的应用层服务呢,其中都是需要端口号来进行标定某一些进程的,那么其中最典型的就是,我们在写套接字的时候 ,无论是UDP还是TCP,在服务器启动的时候都必须bind(),bind()的时候,你必须明确告诉我,你服务端的端口号到底是多少,一旦绑定端口号之后呢,底层在收到对应的报文的时候,报文当中可能是我们后续处理的时候会发现,操作系统实际上收到数据,会根据我们所对应的报文呢,进行,可以理解成数据处理,数据处理呢,会根据我们的端口号,把我们所对应的数据呢推送到某一个特定的服务当中。
》这里一定要比较清楚的一点呢就是,端口号是服务端必须得有的,而我们的操作系统在收到报文之后,一定会根据特定的报文当中携带的端口号,将报文推送给某一个服务的进程。所以,一般在进行网络通信的时候,我们在TCP/IP的场景中,我们一定是需要有源IP和源端口号,目的IP和目的端口号,这四个概念是非常重要的。
》这四个概念在我们写套接字的时候,我们早就用过了,源IP和源端口号Port,目的Ip和目的端口号Port其实就是我们构建的套接字。源IP标定主机唯一性和源端口标定进程唯一性。但是呢,我们今天还得再加上一个叫做,“协议号”。
》协议号,这个东西只是给我们用户,让我们程序员能够清楚知道,究竟你是在采用谁跟谁通信,发采用的是怎么样的协议来通信的。通过源IP、源端口号、目的IP、目的端口号、协议号,这五元组来标定一个通信。
》举一个比较简单的例子, 我们现在有客户端A和客户端B,他们都要进行访问我们同一个服务器,那么当他们在访问同一个服务器的时候,你像客户端B呢和客户端A,有两个画面。说白了就是我们浏览器,在访问某一个网站的时候,有两个画面,都是访问同一个网站,比如打开了两个窗口www.baidu.com。访问同一个网站呢,接下来看一下,客户端A有两个画面都是访问同一个网站的画面,客户端B呢,也失访问同一个网站。
》对于服务器来讲,最大的挑战是什么呢?最大的挑战是,第一,我必须把响应呢,是给A还是给B。我把这个响应给A还是给B,取决于,它们是A在请求还是B在请求,或者是A和B都在请求,这是第一个挑战。第二个挑战就是,服务器要吧信息准确的推送到同一台主机的不同画面,或者也能说是不同的进程,所以呢,我们对应的客户端A和B的区分,在服务端呢就可以通过收到的请求的源IP地址,来区分是A还是B,因为你们两个的IP地址肯定是不一样的,这是第一个。第二个就是,我们客户端A呢,虽然有两个画面是一样的,但是他们对应的端口号是不一样的,所以,服务器在推送消息的时候,IP地址都是一样的,它一定会推送给客户端A,但是呢,因为你们两端口号不一样,所以最终可以把不同的报文推送给不同的进程或应用。

》所以,我们实际上在进行标定一个网络请求呢,就是采用源IP、目的IP、源端口号、目的端口号。当然还有一个协议号,这个协议号呢,我们后面在TCP或者UDP协议当中,或者是在靠近底层的协议当中,我们是能够看出来的,后面再说。然后就是有我们对应“数据”。 因为IP和TCP相比呢,TCP是在IP的上层的,所以TCP呢属于我们的传输层协议,它呢考虑的是端口号的问题,或者说,再TCP往上就是应用层了,所以TCP你要帮我去解决我要把数据交付给谁的问题。而IP呢是帮我们做报文在路上进行路由的,所以他必须得有源IP和目的IP,通过IP地址来区分从哪来到哪儿去的问题,这个呢,会在后面的两层协议都会提到。
》换而言之,上面所说的所有的结论呢,其实就是想告诉大家,网络在通信的过程之中,第一、在写套接字的时候,你所绑定的IP、端口Port信息,它是在不同的网络层次当中被采用和使用的。就好比,端口号会在我们的TCP协议当中被使用,IP地址呢会在我们的IP协议当中被使用。这里呢,就比如说呢,我们以前在绑定端口号的时候,我们知道,也特地将端口号的位数给大家说过,端口号呢看到的位数其实是16位的端口号。那么为什么我们在应用层写套接字时,对应的端口号是16位呢?其实原因很简单,因为TCP或UDP它的报头当中的端口号就是16位的,也就是说,在内核里面,端口号就是16位的,所以,你在应用层使用系统接口的时候,你传入的端口号也只要16位就够了,这个在后面能看出来。

端口号范围划分

端口号的种类比较多,我们目前接触到的,比如自己用的8080、8081等,以及我们学习http的时候443、40端口号,这些端口呢,其中要给大家说一下的是,目前我们在服务端呢,端口一共有0~65535的端口,也就是2^16这么多嘛。其中,我们将0~1023端口,我们称之为知名端口号。什么叫做知名端口号呢,也就是它的端口号和服务是一一对应的,从端口号到我们的协议名称,再从协议名称到端口是1:1的,那么这些端口号就是知名端口号,也就是每一个0~1023端口号呢,早已被特定的服务所采用的,它的端口号必须得是固定的,这个的关系呢,就跟110和警察一样,而且一般用户在写服务的时候0~1023端口号不会让你去使用的,自己能使用的呢是1024~65535。
》我们以前说过,我们服务端必须得自己bind(),但是客户端需要端口号,但不需要你自己显示bind(),操作系统自己会给你申请,这个呢,叫做动态申请端口号。并且端口号自己指定bind()基本都是1024~65535之间。
》我们xhell能够登陆linux服务器呢,本质上是你的云服务器呢,默认会给我们安装一个服务sshd,它是守护进程,一直在后端运行,在你本地打开xshell的时候,你其实是调用的sshd客户端,然后链接服务,然后由这个服务呢帮助我们登录Linux,这个呢就是我们的ssh服务。它一般绑定的端口号就是22号。
》我们如果想要知道知名服务绑定的端口号是什么呢,可以用#cat/etc/services来查看。

两个问题

1.一个进程是否可以bind()多个端口号?
2.一个端口号是否可以被多个进程bind()呢?
这两个问题,我们前面是有聊过的,端口号是用来标定进程的唯一性的,所以从端口号到进程一定是1:1的,所以一个端口号可以被多个进程bind吗?答案肯定是:错的!
》但是呢,一个进程可以被多个端口号bind()吗?答案是:可以的!一个进程是可以bind多个端口号的。

netstat

netstat是一个用来查看网络状态的重要工具。
》netstat默认执行的时候,会把我们对应的套接字的所有连接呢,都会给我们呈现出来。我们一般是搭配特定的选项去查看。当然选项是可以组合起来的。
语法:netstat [选项]
功能:查看网络状态
常用选项:
n 拒绝显示别名,能显示数字的全部转化成数字
l 仅列出有在 Listen (监听) 的服務状态
p 显示建立相关链接的程序名
t (tcp)仅显示tcp相关选项
u (udp)仅显示udp相关选项
a (all)显示所有选项,默认不显示LISTEN相关

pidof

在查看服务器的进程id时非常方便.
语法:pidof [进程名]
功能:通过进程名, 查看进程id

UDP协议

我们接下里就是传输层协议中的UDP协议。
》我们之前折腾了很长的时间,写本地的序列化和反序列化,写套接字,写聊天系统,包括我们学习http 、https,写网络版本计算器,这些都是属于应用层范畴。我们下面呢就学习传输层两个典型的协议,一个UDP协议,一个TCP协议。
》换而言之,我们接下来要讲的呢,其实是属于内核的范畴,然后我们以前的UDP套接字,就是使用的是传输层给我们提供的UDP套接字所对应的操作系统接口。下面我们就来看看UDP协议。
网络基础(一)_第51张图片

》我们以前不是用UDP写了一个简单的聊天,我们在应用层写了一些套接字代码,我们给目标服务器发了一个hello的时候,我们调用的是sendto()这样的接口,其实你发的时候,并不是将你的hello这个信息发送给对方的,而是你把hello交给了下一层,我们在网络基础一的时候说过,当将数据交给下一层之后,那么UDP协议呢,会在你发送的数据前面携带“报头”,当然你也可以不仅仅是当层的报头,你也可以有自己的报头,比如我们写网络版本的计算器,我里面也有自己的报头就是我的正文长度,再包括我的有效载荷,包括在应用层的时候,有http、https对应的报头。
》那么,对我们来讲呢,我们首先要想清楚的非常关键的点就是,应用层也有报头,但是不重要。当你数据交给了下一层传输层之后,那么传输层会自动给我们添加报头,添加报头当中呢,应用层交给它传输层呢,比如说你,sendto()发送下来的数据,就被当做了UDP报文当中的有效载荷,我们将添加完UDP报头的报文呢,我们称作UDP报文,然后UDP报文由报头构成的部分是操作系统给我们添加的,而后面还有一部分是属于应用层交给它的,我们称之为有效载荷。
》换而言之呢,我们以前调用的sendto()其实并不是调用sendto(),而是将我们应用层的数据交给了下一层传输层,然后由协议栈自顶向下进行封装交给对方。所以,我们接下来要考虑的是,由应用层到传输层的时候UDP报头在添加的时候是什么样子的。这里是需要给大家说一下的。
》很早之前关于协议,我就提过两个关键的结论,我们也说过,这两个结论呢,我们会在后面一直使用。其实,我们无论是在讲自定义的计算器,还是我们在讲我们的http的时候,因为已经是应用层协议了,所以不牵扯向上交付的问题,但是呢,无论是自己写的,还是我们应用的是成熟的https协议,最典型的就是,我们不得不面对的第一个问题叫做,封装完报头之后,如何将有效载荷做分离,必须得分离,因为将来的UDP报文呢经过网络传送到同层协议,同层协议呢,需要根据你的报头,将有效载荷做分离,分离之后呢,根据报头决定,你的数据“hello”是交给应用层哪一个协议。因为,应用层存在一大堆绑定了特定端口的应用层进程,那么我们的传输层必须得帮我们决定将我们的有效载荷交给上层的哪个服务。
》所以,我们要面临的第一个问题就是, 当我们在考虑报头的时候,必须得考虑如何封装和报头又是如何解包,它的策略又是什么?第二个,我们今天是传输层是什么?上有老下有小,上面有应用层,下面有网络层,所以呢,传输层在报头添加之后,还要解决的另外一个重要问题,叫做如何决定将自己的有效载荷交给上一层的哪一个协议,这个过程叫做分用的过程,所以,我们再提一下,我们未来学习的所有的协议,你必须考虑两个共性问题:1.如何封装和解包;2.如何分用。所以,后面无论是看到的不管是TCP还是UDP,这两种哪一种协议,你一定首先要在脑海里面想清楚这两个问题。
》下面我们来看看UDP协议的格式。

UDP协议的格式

网络基础(一)_第52张图片
我们先看看UDP报文的字段:第一个就是里面有一个编号就是0~31,这个意味着是报文的宽度是0~31的。UDP报文呢,并不复杂,非常简单,他由两大部分构成,由前8个字节,有4个字段,这4个字段呢,我们可以叫做UDP报头,而剩下的部分呢,我们称之为叫做,有效载荷,换而言之,我们如果发一个hello,添加报头的话,那么hello就在有效载荷的部分,而操作系统给我们hello报文添加的报头就是,上面图中的8个字节,我们后面来解释一下字段是什么。
》首先,我们能够看到UDP采用的是定长报头,那么我的第一个问题就是,请问UDP是如何封装和解包的?那是不是很简单呀,当上层应用告诉我一个要发送的hello,那么我在传输层添加UDP报头的时候,其实就是给原始数据前面给他重新写上8字节的数据就可以了。
》第二个,如果对方收到报文,那又是如何解包呢?因为我们的UDP报文呢,采用的是定长的报头,所以当对方收到一个完整的UDP报文之后,对方只需要从报文当中提取前8个字节,那么8个字节呢就是我们所谓的报头,剩下的就是报文的有效载荷了,这个时候就能做到封装和解包了。
》UDP上面呢还有应用层服务,比如我们以前写的聊天室,绑定的端口号是8080,其中呢,我们的UDP接收方收到一个报文之后,它怎么知道把报文转发给上层的哪一个应用呢?其实也很简单,这就引出了源端口号和目的端口号字段。一般当我收到UDP报文的时候,我先提取报头,对他的报头和有效载荷进行分离,分离之后再解析它的报头,识别它的目的端口号,那么它的目的端口号填的就是服务的端口号,这也就是为什么,大家之前写UDP套接字,服务器必须绑定一个端口,并且,你如果想让客户端发报文,必须得告诉客户端sendto()接口,你要发给哪一个IP上的哪一个端口的服务。所以,就是因为你填了目的端口号,实际上是会被将来构建UDP报文的时候,报头里面目的端口号的字段,就是客户端设置好的端口号,然后报文发到服务器的时候,就知道交给应用层哪一个服务的。所以这一点要能够注意。
》换而言之呢,我们可以看到,我们的报文当中呢,源端口和目的端口,尤其是目的端口呢,它用来解决的就是我们传输到应用层的分用的问题。那如果,我已经交付完了,把数据解包了,那我又如何将hello找到对应的应用层的协议呢?
》这块给大家说一下,我们以前讲过,我们的服务器呢在绑定端口号,其实本质是进程和端口号相关联,那么在操作系统内部,实际上会将所有的进程维护起来之外,它在网络应用当中为了能够快速的找到特定的进程呢,操作系统内部是会维护一张哈希表,用来进行从端口到进程的映射关系。所以呢,我们的一个报文被传输层,也就是操作系统收到之后,操作系统呢就要提取报文当中的报头的目的端口号,根据目的端口号查哈希表,就能找到你这个进程是谁,这是第一个。
》第二个呢,我们曾经也讲过,叫做Linux下一切皆文件,实际上,我们的网络套接字,本质也是在内核当中,以文件形式呈现的,最典型的代表,你凭什么这么说,最典型的代表就是,我们一般创建好的套接字其实也是一个文件描述符,而只要是一个文件描述符,那么本质呢,其实就是进程和文件对应的关系,说白了文件描述符不就是一张表嘛,我们是能够通过文件描述符表呢查找到对应的文件对象的。换而言之,只要我们能找到进程,我们就能够根据进程找到当前进程对应的网络文件。文件打开呢,我们也知道,文件是由自己的内核缓冲区的,所以当网络当中收过来的数据呢,在我们的内核当中,首先呢,根据你报文当中的目的端口号找到,经过哈希找到应用层当中的目标进程,然后再目标进程当中呢,再根据目标进程所打开的文件,就找到该文件对应的缓冲区,然后将数据填到缓冲区里面,至此,你的进程就可以文件的方式读取文件了。说白了,就是将我们的文件和网络进行了整合,所以它的数据接收和发送呢,理解成本也并不高,这一点可以给大家说一下。
》我们下面再来继续,所以,我们可以再来看看,回头来看,如果我的源端口号和目的端口号填的是谁呢,比如我给你发,我的端口是666,你的端口号是888,所以我源端口号填的是我自己的,目的端口号填的是你的,我发给你的,你收到这个报文,你就可以采用报头和有效载荷分离之后,根据端口号识别到发给你端口号888的,然后你的进程就能收到我发给你的数据。
》其中呢,对我们来讲,除了源/目的端口号之外,还有两个字段。第一个就是16位UDP长度,16位UDP长度呢,它不叫做UDP报头长度,因为报头长度本来是固定8字节的,它叫做16位UDP长度,也就是说呢,是UDP报文整体的长度,这个就相当于报头➕有效载荷,就是整个报文的长度。我们一会儿就知道,UDP呢,是面向数据报,那么我们以前呢,在写我们的UDP套接字的时候,我们创建UDP套接字,叫做用户数据报套接字。那么用户数据报体现在哪里呢?主要体现在,它不是流式的,它有自己报文和报文明显的边界,就好比,你把一个UDP报文收到交给上层了,那下一个呢?报文和报文之间,你怎么保证两个报文不会互相干扰呢?我们能够保证,因为UDP报文呢是有自己的UDP长度的,所以它直接提取报文的前8个字节,即报头,然后再提取UDP长度,然后再从对应的接收缓冲区里面把剩下的若干个字节数读出来,就把一个UDP报文读完了,这个道理就跟我们曾经讲的http协议的Content-Lenth是一个道理。
》再下来,16位校验和,主要是为了防止我们报文当中的有些字段出现一些偏差,然后我们就需要校验,这个校验呢我们可以不考虑,我们只要知道有就行了。
》其中这就是UDP报文,算是比较简单的了。换而言之呢,无论是收还是发,上层在进行交互的时候,交互下来的数据呢,在传输层看来就是有效载荷,添加上UDP报头,就是8个字节,字段一填好就发出去,就这么简单。
》那么下一个问题就是,我们教材或者书上经常画报文,它本质是什么东西呢?所谓的添加报头,又究竟是在干什么呢?所以,我们可以给大家举一个简单的例子。
》其中呢,我们要明确一点,大家也知道,Linux呢,网络协议栈TCP/IP协议,是在内核中实现的,大家也能够理解。像我们应用层呢,就是在应用层实现的;我们讲的UDP/TCP是操作系统内部实现的。那么在操作系统内部实现的,是在内核实现的,而内核使用C语言实现的。现在问题就是,那么这个报头究竟是什么东西?我们看到图中的报头是画的固定的0~31位的固定大小,8字节,然后就是4个字段。所以报头本质上是什么呢?我们也定制过协议,不知道还记不记得,我们当时在定制协议的时候,我们与协议强相关的一组概念就是,序列化和反序列化。你为什么要序列化呢?序列化是什么呢?就是将结构化的数据变成字符串。说白了,我们为什么要进行所对应的序列化呢?原因就是因为我们曾经定制的协议就是结构化的数据。C语言里面叫做struct,C++叫做class类。所以,我们内核当中实现的协议呢也是一个道理,所以所谓的报头呢,本质上也是一个对象。
》它是什么呢,比如说,它其实在内核里面就是一个struct,比如struct udp_hdr{unsigned int src_port:16;unsigned int dst_port:16;unsigned int udp_len:16;unsigned int udp_check:16;};这在内核里面就称作UDP报头,其中这个结构体使用到了位段。位段在申请空间的时候,它是以16位申请的,还是以unsigned int类型大小申请的呢?我们写的结构体的大小是多少呢?老师是讲过的,我们写的位段呢,不是以后面16位为单位申请的,而是以前面的类型unsigned int大小申请的,然后在用的时候,你第一个字段没有用完,第二个字段还能用,那么就把第二个字段放在第一个4字节整数里面。所以,因为它是以第一个类型大小分配的,所以报文的宽度是0~31位。然后,位段中的:16代表的是,我想用这4字节当中的前2个字节,也就是前16个bit位,这就是我们所谓的位段。如果我们今天呢,有一个大的缓冲区是我们UDP的,上层呢想要写一个我们所对应的hello,那是怎么写呢?实际上这个工作就相当于是我们把hello拷贝到我们对应的缓冲区当中,拷贝好之后怎么办呢?然后我们的内核呢,会帮我们做一件事情,叫做struct udp_hdr h;定义一个udp_hdr对象 ,然后填充h.src_port;我们曾经在绑定的时候呢,其实说白了就是将我们的相关的套接字信息呢,设置进操作系统内部。操作系统实际上是能够获取你自己的端口号的,比如说,你的端口号是1234,即h.src_port = 1234;比如说呢,你是想访问8080的端口号,那么操作系统会自动把你访问的目的端口号填好,即h.det_port = 8080;其他的udp_len是多少也能填进去,也能将我们的udp_check校验和也填好,即h.udp_len = XXX;h.udp_check = YYY;所以我们最终就会发现操作系统帮我们构建出来了一个udp_hdr对象,然后将对象h拷贝到有效载荷hello的前面就可以了,此时这个工作呢,我们就叫做添加报头的工作,即添加报头的本质,其实就是拷贝对象!
网络基础(一)_第53张图片
实际上操作系统想要设计这个过程呢,它其实定义一个伪指针,将上层用户的数据拷贝过去,我添加报头的有效位置在哪里呢?我们的伪指针先减去字符串长度,预留出一部分空间,然后将上层用户的数据拷贝进来就可以。现在你又想添加报头,然后让伪指针减去sizeof(udp_hdr)的大小的长度,将我们的伪指针强转成udp_hdr类型,然后就可以直接用我们指针的方式将字段填进来就可以了。你未来还想添加其他报头,它继续将我们指针减去报文的长度,就得到了一段空间,那么将报文填进去之后就是一个字符串的或者二进制风格的报文结构了。
》我们是为了不让大家感性的去看待报文的样子,而是要能够让大家展开语言级的想象。相当于报头的所有字段呢都是位段。其中对应的数据呢,在数据前面添加报头就是将我们的对象一定义,位段属性一填,将对象拷贝到我们的数据的前面,这样就形成了一个报文,这就叫添加报头的操作。
》UDP呢有一个校验和,如果校验和失败了,说明我们的数据是有问题的,比如说你的数据可能在传输过程中出现bit位翻转等其他问题,或者干脆数据出错了,那么此时这个报文就会直接被丢弃,这是其一;其二呢,16位UDP长度是包含UDP包头和有效载荷的。

UDP的特点

UDP呢是面向数据报,它不是面向字节流的。UDP的传输过程呢,它的特点有3个:无连接、不可靠、面向数据报。
》其中无连接很好理解,因为我们前面写UDP套接字的时候,从来没有见过连接,我们将服务器启动后,客户端直接向服务器发消息,这就是我们之前没有连接。
》不可靠的特点,现在感受不是特别好,因为我们还没见过可靠的,只有见过可靠的,才能感受到不可靠。不可靠,现阶段就理解成,UDP报文一旦丢失了,那么就真的丢了。有人说,一个报文丢了,那么可以不丢吗?答案是:可以的。我们后面在谈TCP的时候,它底层会帮我们去进行设置各种各样的安全策略等,其中UDP是不可靠的。无连接和不可靠是非常好理解的。
》关于,不可靠呢,想要尝试去理解,其实也不难,比如说,我们的数据本来就是通过网络去传输的,谁能保证在中间不出问题呢?有可能路径算法有问题,有可能路径设备有问题,有可能防火墙安全级别设置过高导致报文直接丢弃,或者干脆报文在传输过程中直接出问题了,被目标路由器直接丢弃了,这样的报文呢,最终我们服务端没有收到。没收到,影不影响呢,当然不影响,因为这个报文呢,丢了就是丢了。有人说,丢了还不影响吗?是的,丢了一般不影响。根本原因在于,你为什么选这UDP,UDP都告诉你了,我的特点就是不可靠, 这是我的缺点,但我在说我的缺点的时候,对应的一定也有优点,现在的问题就是,不可靠意味着什么呢?意味着,我们UDP一定不需要为了可靠性做过多的 设计。比如说,TCP,为了保证可靠性 ,你就要有确认应答,有重传,快重传,拥塞控制,流量控制等一大堆的策略,可是我UDP不可靠,虽然是缺点,但一定伴随着优点,既然不可靠,那就意味着,不需要为了可靠性作过多的工作,那么也就决定了UDP一定会非常简单。简单的东西,维护起来的成本一定会非常的低,包括写应用层代码,UDP也是最好写的。所以呢,不可靠这个特点呢,我们一会儿在给大家讲UDPheTCP对比的时候呢,我们再重新矫正一个观点,不可靠是UDP的特点,我们比较喜欢叫特点,而不应该叫缺点。一定要结合场景去选择不同的传输协议。
》面向数据报呢是UDP的一个特点,什么叫做面向数据报就要给大家说一说了。以前我们在写网络版本计算器的时候,在使用我们对应的TCP通信的时候,我说过TCP是面向字节流的,面向字节流本身也不难理解,不过现在还是有点障碍的,但是在当时有一个最典型的特点就是,我们在读取的时候告诉大家, 我们今天在读取TCP时,你怎么保证你将一个报文读完了,你怎么保证对方给你发了100,你就收到100呢?当时我们说,对不起,我们不能保证,所以我们当时在设计TCP,以及TCP协议的时候,是需要添加我们整个报文长度的,然后报文内部,我们自己要做序列化和反序列化。其中呢,我们在读取的时候,读取的数据不够我们的期望长度的话, 我们也是什么都不做的,一直等到他能够将我们的数据读到就绪,这个有问题吗?答案是:没有问题。 可是呢,我们在UDP这里是不存在这样的问题的,UDP呢,只要我收到了报文,要么不收 ,要收,那一定是一个完整的报文,我是站在应用层的,因为有UDP长度,它给我的一定是一个UDP完整的报文,因为它也有一个校验和。换句话说呢,对方给我发了一个UDP报文,不考虑丢失的问题,我们这边在接收的时候,一定也是需要一次来接收的,因为UDP它叫做UDP用户数据报,它的报文和报文是有明显的边界的,因为报文有自己的报头长度和总长度。当我收到了一批二进制,当我提取的时候,我可以不断的正常去取一个一个完整的UDP报文,对方给我发一次,我就要收一次,对方给我发100个报文,我就收100个报文,不像TCP,对方给我发了5次,我一次全部读完了,对方给我发了10次,我可能需要100次才能读完,这种就叫做TCP面向字节流。而UDP呢,是你一给我发一个报文,我就能收到一个报文,这就叫做面向数据报,所以才有了面向数据报的这种通信方式,有点像寄信和发邮件,我给你写了一封信,给你的时候呢,你收的话,一定是能收到这封信,给你写10封信,就一定能收到10封信,信和信都用信封区分开来了,这就叫做数据报。
》一般应用层交给UDP多长的报文,UDP照样原样发送,即不会拆分,也不会合并,如果我们用UDP传送100个数据,你发送端调用了一次sendto(),然么就发出了100个字节,接收端就必须得调用一次recvfrom(),接收100个字节,绝对不能循环调用10次recvfrm(),每次收10个字节,这样是不行的,因为一次recvfrm()读到的一定是一个完整的报文,这就叫做面向数据报的问题。
》以上就是UDP的三个特点。

UDP缓冲区的问题

UDP协议呢,没有真正的发送缓冲区,后面关于缓冲区的话题呢,会后面专门在给大家谈TCP的时候,专门来谈。不过现在呢,我们先这样去理解,我曾经给大家说过,当时是在讲TCP的时,是这样说的,当我们调用我们的write这样的接口的时,write接口是系统调用接口。实际上,当你在调用wrtie、sendto()接口的时候,实际上你并没将数据发送到网络里面,而是你将数据交给了操作系统,你要发送数据呢,你往网络里面发,可是网络里面是什么情况,你并不清楚,只有操作系统呢,对网络的整体情况呢是比较了解的,所以真正的发送数据的过程,根本就不是我们发的,而是我们将我们的数据交给操作系统,让操作系统帮我们发的。
》所以,我们以前讲的所有的网络或者文件类的接口,实际上根本就不能叫做发送或者写入接口,而是应该叫做拷贝函数。我们以前在讲系统的时候,讲过文件,我叫同学们将数据通过文件描述符写到文件当中,我说了,我们其实并不是写到磁盘当中的特定的文件里面,而是你将数据写给了操作系统的缓冲区里面,然后操作系统根据自己的策略呢,将数据刷新到外设,比如写到磁盘上,那么其中呢,我们以前在学文件所调用的write接口,它能叫做将数据写到磁盘上,将数据写入磁盘吗?答案是:不能。它 叫做拷贝函数。是将应用层的数据拷贝到内核,再由操作系统定向的向,根据对应的策略进行IO,更新到磁盘上。
》同样的,在我们未来学习网络呢,大部分我们曾经用套接字发送的接口,发送的数据,其实你根本就没有发数据,而是你将数据拷贝到了一个叫做TCP/UDP所对应的缓冲区当中。对我们来讲,将数据拷贝到缓冲区里面,然后呢,数据什么时候发,怎么发,工作就是由操作系统决定的,所以不要再觉得你自己发的了,不要再觉得你自己send()怎么样,实际上你的数据只是交给了操作系统。
》我这个结论呢,现在还没有得到非常充分的证明,我们现在呢,在UDP这里还不太明显,在学习对应的TCP协议,在后面写高级IO的时候,到我们后面讲多路转接的时候,我们会发现感受会特别明显,到那个时候,我们再重新给大家讨论IO的时候,这样大家又会有一个不一样的理解了。
》所以可能会存在缓冲区这样的概念,我们再想一想UDP缓冲区又会是什么样的呢?UDP传统上没有,也不需要发送缓冲区,也相信大家很好理解,为什么不需要呢?因为UDP的数据呢,太简单了,你上层拷贝下来的数据,到了内核,内核直接给你添加8个字节,也就是几个字段,操作系统给你填,填完之后呢,操作系统直接就吧你的报文交给了网络协议栈,到了我们的网络层,后续就是IP的任务了,不需要维护什么可靠性,不需要将数据暂存起来,那么所以,它只要把数据交给内核,这样的工作就完了,所以,它根本就不需要真正的发送缓冲区。
》但是呢,作为UDP它呢,在进行数据接收的时候,比如说,我来了一个数据,来了一个数据呢,操作系统会告诉我数据已经OK了,比如说,最典型的就是,数据不OK的时候,在应用层的表现就是调用recvfom()接口是阻塞的,相当于你卡在那里,当你数据就绪的时候,你的recvfom()才会返回,并且把数据拷贝到你提前设置进recvfom()函数参数的缓冲区里面,拷贝到应用层的数据缓冲区里面。但是呢,如果我们收到的数据呢,当前操作系统压力很大,收到的数据呢,还没来得及调度你这个进程的话,那么这个时候UDP数据呢,如果内核不存在接收缓冲区,那么一定会存在数据丢失的问题。有同学会说,你说了,UDP不保证可靠性,是的,不保证可靠性,但是并不代表它在可靠性上任意妄为,如果没有接收缓冲区,一旦我应用层进程来不及接收,那么这个数据就直接丢了,这个道理呢,就好比,我不保证可靠性,但并不代表,我不需要尽量的保证我的数据不丢失,不对它负责,尽量的还是不要让我的UDP报文丢失,所以UDP呢是具备接收缓冲区的。
》接收缓冲区的数据呢,如果上层来不及处理,会暂时给我们报存在接收缓冲区里面的,等到后面应用层来得及读取的话,到时候呢,就把数据给你拷回去就可以了。所以UDP有接收缓冲区,但是没有发送缓冲区。
》另外呢报文在网上发的,再给大家聊一下。比如说,我是发送方,给你发了10个报文,先后发出去1~10,时间差别不大,但是有先后。向你发送10个报文,你如果不丢包的话,那么你收到的报文,一定是1~10号,或者一定是10个报文,因为我发多少次,你就收多少次,这是UDP面向数据报的特点。可是呢,我不丢包的情况,是不是我发1~10,那么你收的时候一定是按1~10收呢?答案是:并不一定。因为10个报文在网络当中走的时候呢,在中间的网络环节当中,在进行路由选择的时候,大家选择的不一样,有的可能起点慢,有的起点低,但是走的快。有的可能起点高,但是走的太慢了,一般是大概率事件,服务端收到报文之后呢, 会出现数据报文乱序的问题,乱序问题算不算可靠性呢?如果你不乱序,那么你发送的就是可靠的,有可能乱序就是不可靠的。
》所以UDP呢,一定要清楚的知道一点,就是UDP呢,不保证可靠性,也就决定了不保证数据的按序到达,所以呢,虽然我有接收缓冲区,但是接收缓冲区里面的接收顺序我不关心。谁不关心呢?UDP不关心、操作系统不关心,那么你应用层用户不关心最高,如果你关心,那么你自己对数据报文做一下重排序吧。这个时候呢,就相当于我们应用层协议定制呢,就要给你的报文带上编号了,所以这一点要记住了。所以,UDP是有缓冲区的,有接收缓冲区,但是缓冲区里面可能是乱序的,这是第一;第二呢,如果缓冲区满了,满了的话,这个时候我们的UDP来不及处理,就只能直接被丢弃了,所以这个就是UDP缓冲区的问题。所以发送的时候呢,直接交给操作系统,操作系统直接向下交付,这个速度很快,不需要缓冲区。第三个呢,接收的时候呢,我们可能是由应用层读取决定的,我的应用层可能非常忙,可能忙其他事情,没来得及读,你数据就绪了就先放那里,这叫做接收缓冲区。
》再下来就是,对于UDP呢,它是既能读,也能写的,这样的问题呢,我们也早就说过了。当时我们基于UDP搞了一个多线程客户端嘛,一个从套接字里面读,另一个现场向套接字里面去写,我们可以同时的进行读写。为什么,能够同时进行读写呢?因为UDP是全双工的,全双工怎么体现呢,就是UDP不需要发送缓冲区,它有接收缓冲区。这句话的潜台词就是UDP的报文出的路径和报文入的路径是两条路径,互相是不干扰的,所以叫做全双工,如上就是UDP内容。
》所以UDP就是全双工的,如果你写一个UDP服务器,你可以多线程,一个线程呢不断地向我们文件描述符发,另一个线程呢不断的向我们文件描述符里面读取数据,然后读到的数据呢,扔到我们后端的数据池里面,写一个生产者和消费者模型,这个时候就能实现数据转发了。
》UDP在使用的时候,可以看到细节,UDP采用的协议,按照你说的,UDP报头当中有16位长度字段,那么只有16位长度,那么UDP最大传输数据大小就是2^16,去掉报头的话基本上就是63K多,这个数据小不小呢?答案是:在现在的网络当中算是小的了,有没有办法呢?答案是:没有办法。如果你发送的数据量超过了64KB,你只能是将你数据手动分包,说白了就是将缓冲区里面的数据能发多少就发多少,重复循环的去发,最后呢你自己手动拼接,这是没有办法的,因为协议定死了,就是在我们Linux内核当中定好了,所以你只能这么做。
》第二个问题呢,关于UDP这里的理论可以看出来,它其实非常简单,那么现在呢,我有一个问题就是,我们说UDP协议不可靠,那我为什么要用你呢?即便是在现实生活中有一些丢包的场景,但是为什么UDP有存在的意义呢?为什么不选择TCP呢,TCP保证可靠性,我们应用层也就不关心,直接交给TCP就很放心,TCP自己会帮我们维护可靠性,是这么个道理。但是,可靠性很复杂,不要觉得,我们曾经说过为什么网络很复杂呢,原因就是单纯的距离长了。距离长是事实,但也衍生出很多问题。中间丢包怎么办?怎么定位目标服务器,怎么路由嘛,这是IP层关心的。但是丢包问题呢 ,有各种丢法,数据错了,但没丢,这是一种丢法;数据包真的丢了;数据包延迟了,阻塞在某个路由器上了,可能到了很长时间才到目的地;乱序问题;网络出问题…等等问题,很复杂。这么复杂的问题呢,UDP没有实现,所以它就不可靠,那么也就意味着,它不需要为了可靠性编写复杂的代码。那么UDP,1.非常简单;2.它的效率一般也不慢。所以对我们来讲,UDP不可靠这句话的潜台词是它更简单,所以关于UDP特性当中呢,UDP不可靠是一个中性词,它是一种特性的说明,现在的网络环境一般不丢包 ,丢包,如果丢了一些也不影响,但是因为很简单,不需要建立复杂的链接,所以,它也是要在不同的场景当中被采用的,其中最经典的场景就是直播。

你可能感兴趣的:(Linux,linux,服务器,网络)