从零开始搭建TCP/IP协议(这次叫PCT协议)这篇博客是看完《图解TCP/IP协议》和《TCP/IP协议详解卷一:协议》的总结我从0搭建了一个可靠的双工基于有序流协议称为PCT协议:)OSI七层模型和TCP/IP四层模型说到计算机网络,肯定会说到OSI七层模型和TCP/IP四层模型。让我们从分层开始。为什么我们需要分层软件开发?在软件开发过程中,我们经常听到“解耦”、“高内聚、低耦合”等词,也经常听到写Java的同学讲“桥接模式”、“面向接口”等话,那么他们说的这些话的核心问题是什么?先从一个简单的问题开始:现在我们要搭建一个推送系统,连接Android和iOS系统,大家都知道,苹果有统一的推送通道,APNs,所以我们只需要连接这个就行了,但是安卓推送在国内是有争议的.以我公司之前访问推送通知为例。连接极光和小米可能需要接入华为推送。那我怎么从具体的push中抽象出来呢?使用面向对象的思想,我们很容易认为我们有一个名为BasePush的父类,它的子类是具体的MiPush,JPush,HMSPush。父类中有push_by_id、push_by_tag等方法,由子类重写。这样我们就实例化了子类,在实现的时候调用对应的方法。这种思路其实就是面向接口的编程。在Java中,我们可以改变编程方式,把继承变成接口。在Python中,我们可以直接拼凑出这种写法。用图来表示,当我们纯面向对象的时候,我们的思路是这样的:把上面的图反过来,就变成了面向接口的:使用面向接口之后,我们做了这样一个假设:defpush(pusher,id):pusher.push_by_id(id)即传递给push函数的pusher实例必须有push_by_id方法。正是基于这样一个假设,我们可以把具体的业务代码和具体的推送提供者分开。这就是所谓的抽象,是一种分层。分层的原因也浮现出来,就是为了把不同事物之间错综复杂的关系分离开来,也就是老话说的“利刃斩乱麻”的感觉。在两种网络模型的日常编程中,我们用的最多的是TCP,UDP也有,但很少。下面是一些常见的例子:DNS->UDP连接MySQL->TCP连接Redis->TCPRPC->TCP访问网站->TCP当然,这只是一种常见的实现方式,也可以用UDP来实现。我们暂时不会在本博客中讨论UDP。先看看TCP/IP的四层是怎么分层的:ascii表其实挺好看的,就是最后渲染的时候因为宽字符格式有点乱,下同+----------+--------------------+|层|示例|+------------+--------------------+|应用层|HTTP协议|+------------+---------------------+|传输层|TCP|+------------+-----------------------+|互联网层|IP|+------------+---------------------+|网络接口层|如网线、双绞线、Wi-Fi|+------------+------------------------+我们直接将TCP/IP四层协议映射到OSI七层协议:+------------+---------------+----------------+|OSI七层协议|举例|对应TCP/IP四层|+------------+----------------+----------------+|应用层|HTTP协议||+---------------+------------+||表示层||应用层|+--------------+----------------+||会话层|||+------------+----------------+----------------+|传输层|TCP|传输层|+-------------+--------------+--------------+|网络层|IP|互联网层|+-------------+---------------+----------------+|数据链路层|互联网,Wi-Fi||+------------+------------+网络接口层||物理层|双绞线、光缆||+------------+---------------+-----------------+接下来从底层开始逐层分析网络,最后简单介绍一下TCP(TCP的知识够写好几本书了,用博客来介绍是远远不够的。不信可以看看TCP/IP协议三卷多厚的详细解释)。物理层物理层,顾名思义,就是物理的、可见的。也就是我们通常所说的光纤、Wi-Fi(无线电波)等。我们知道计算机是用0和1来表示的,对应不同的媒体有不同的表现形式,所以为了屏蔽实现物理层例如Wi-Fi可以通过波峰和波谷来表示0和1的状态(我们通常说的1和-1,对应计算机中的1和0)。对应到电,我们可以用高压和低压来表示1和0。就像开头说的例子,我们不关心具体的介质是什么,我们只知道我们使用的介质有办法代表1和0。数据链路层如果我们去邮局写一??封信,填写收件人后,邮局分发的顺序可能是,先投递到指定国家,再投递到具体省份,然后是城市。..一件一件送达。那么我们在玩电脑的时候,电脑是如何准确地将A发给B的信息传递给B的呢?每个人都必须有一个地址。上一节我们知道,不同的媒体有自己的1和0的表示方式,那我们在媒体的两端加上地址,我们称之为MAC地址,怎么样?我们以路由器为例。路由器的MAC地址称为路由器,手机的MAC地址称为电话机。为了将其表示为0和1,我们分别取字符串的ASCII二进制表示,路由器调用111001011011111110101111010011001011110010,手机调用:111000011010001101111110111011010101nowwenow最终可以通过两个相邻的东西来发送信息,至少介质发送信息,所以我们做这样一个约定:约定其实就是一种约定:)首先我们发送111表示信息的开始。那么,我们先有48位表示发送方的MAC地址,再有48位表示接收方的MAC地址后,就是我们要发送的信息了。最后我们发送000表示结束。如果开头和结尾不是这样的,那就说明这是一条假消息。知道上面的手机为什么叫phoner而不是phone吗:)就是为了保证名字的长度一样。“你好”的二进制表示是“11010001100101110110011011001101111”。如果路由器要向手机发送“hello”,那么发送这样一串二进制(用换行符分隔,这样比较容易看):这种表示方式貌似可行,但是有个问题,如果出现000怎么办在这串二进制文件的中间?因为电脑读的时候是从头读,所以电脑会乱。为了解决这个问题,我们修改协议,在111之后加上发送方地址+接收方地址+要发送的报文长度。我们用16个字节来表示,也就是说不超过2**16位可以在中间发送。所以协议就变成了:首先我们发送111来表示消息的开始然后我们用16位来表示数据包的长度然后,我们首先有48位来表示发送者的MAC地址,然后是48位表示接收方的MAC地址后,就是我们要发送的信息了。最后我们发送000表示结束。如果开头和结尾不是这样的,那就说明这是一条假消息。发送方地址+接收方地址+hello的位长为6*8+6*8+5*8=136,二进制表示为:0000000010001000所以发送的整个消息变成:网络层现在我们终于可以发送留言。但是有一个缺点。只有当它们彼此相邻时,我们才能发送信息。有没有办法通过两两传输的方式实现异地发送信息?对,就是我们的网络层,也就是ip(我们是能遇到的最容易理解的名词,暂时把它当作网络层的代名词也不为过).刚才我们学习了一个技巧,就是分配一个地址,简称MAC地址,我们用它来定位相邻的两个节点。其实这个地址也可以用来在多个节点之间找人,基于这样一种技术:每个节点都知道与自己相邻的节点的MAC地址,那么,比如这样的连接方式:A-B-C-E\/-D-A给E发消息,所以可以这样:A给B发消息,D:把我发给E给B,D收到后,发现消息来源是A,所以只发消息toC:sendmetoE去C接收消息,发现消息来源是B和D,于是发消息给E:SendmetoE,E收到消息后发现收件人是我自己,所以我吞下了信息。别说了,这种方式好像行得通,只不过有一个明显的问题。A向E发送消息,最后E收到两份。稍后我们需要对其进行重复数据删除。让我们先放一个TODO标签。还有一个细节问题。不知道你有没有发现。刚才我们说了MAC地址用于两个相邻节点之间的通信。里面有源地址和目的地址。如果我们按照上面的方式传输,每个节点都只是传递里面的信息,但是源地址需要改写成自己的MAC地址。不然B不知道这个信息是来自A还是来自C,对吧?那么问题就来了,E如何知道信息实际上是从A发送的呢?没办法,我们只好在传输的信息中写上真正的源地址,所以我们另设一个协议,我们称之为ip:MACcarry信息开头是源ip地址,32位表示然后是目标ip地址,32位代表然后是我要带的信息。将其与上述数据链接层协议相结合,假设源地址是192.168.1.1,目标192.168.1.2,发送发送的信息信息信息信息信息信息“Hello”,整个整个就就:111(111(111)(111)Mac地址)0111000001101111111111111111111110011111111001010010(Mac地址)11000000001010100000000000010000000001(来源IP地址)11000000001010100000000000000001000010(目标IP地址)它必须。哦,终于可以跨节点发送消息了,开心~但是还有一个问题,如果我要保证A发送的信息一定要投递到E呢?如何提供可靠性?IP层不提供可靠性,它只是说尽可能多地交付。看来还需要一层!在传输层,我们知道一台电脑上可能运行着很多程序,那么如何区分不同的程序呢?所以我们给程序加一个id,叫pid。那么如何区分计算机网络通信呢?假设n个进程要与另一台机器上的某个进程通信?我们应该做什么?为什么我们不分配另一个id,他们将共享这个id。我们称这个id为端口。这样我们就可以通过ip地址来识别计算机,通过端口可以确定一个或多个进程。我们继续做协议,但是这次我们希望协议是可靠的,所以我们需要做更多的工作。其实如果按照七层协议来实现的话,这一层就不需要做那么多事情了。不同的层做不同的事情,对吧?但是为了理解TCP协议,我们也仿效自己编了一个协议,还是叫PCT比较好。继续,我们需要在发送的ip的信息中这样规定:首先是源地址的端口号,8位来表示,因为ip地址已经在ip中等待了,不再赘述这里,然后是目的地址和端口号,用8位来表示。这样,简单的PCT协议就做好了。还有一个问题就是我们要保证信息的发送是有序的,因为有的信息可能走光纤,有的信息可能走Wi-Fi,它们的传输速率是不一样的。所以我们在协议中这样写:首先是源地址的端口号,用8位表示,因为ip中已经存了ip地址,这里不再赘述,然后是源地址的端口号目的地址,用8位表示,然后是包的序列号,用8位表示,但是我们已经同意让这个协议成为一个可靠的协议,我们不能食言。我想了想,怎么让他靠谱,无非就是我发个信息,你告诉我你收到了,你不告诉我,我发到你告诉我为止。这就是它的意思。但是,我不想构建多个不同的协议。要知道,在编程时写一堆if-else树会很痛苦。然后更改协议:首先是源地址的端口号,用8位表示,因为ip地址已经在ip中等待,这里不再赘述,然后是目标地址的端口号,用8位表示bits,然后是这个包裹的序号,8bits代表然后是你要确认的包裹的序号,8bits代表嘿嘿,画龙点睛,确认包裹的序号,因为我们是两个-方式沟通,当我给他发信息的时候你也可以顺便确认一下我收到了他的包,真的是一箭双雕。TCP是面向流的协议。什么是流量?车流、水流、车流更加生动。汽车之间是分开的,但是当速度增加时,它们就可以看作是相连的。ThesameistrueforTCP.Individualpacketsareseparated,buttheycanberegardedasconnected.Why?Becauseeachpacketcontainsanipaddressandportnumber.Iftheipaddressandportnumberarethesame,itcanberegardedasItisconnected:)Sowecanimaginethatouripaddressis192.168.1.1,theportnumberis1,andthetarget'sipaddressis192.168.1.2,andtheportnumberis2.那我们发送这样的包:111(开始)0000000011101000(长度)011100100110111101110101011101000110010101110010(来源MAC地址)011100000110100001101111011011100110010101110010(目标MAC地址)11000000101010000000000100000001(来源ip地址)11000000101010000000000100000010(目标ip地址)00000001(来源的端口号)00000010(目标的Portnumber)00000001(theserialnumberofthesentpackageis1)00000000(theserialnumberoftheconfirmedpackageis0,whichmeansthereisnothing)0110100001100101011011000110110001101111(string"hello")000(end)duang,inthisway,webuildWehaveestablishedourownreliableflow-basedduplexprotocol:)Bytheway,wehavealsocompletedtheaboveTODO,andwecanjudgewhetherthepacketisrepeatedthroughtheserialnumber,hahaha,onearrowncarving~TCPthree-wayhandshakefourtimesIwon’ttalkaboutwavingslidingwindowcongestioncontrol,etc.,let’sgoto《TCP/IP协议详解卷一》:)Attheapplicationlayer,wecanfinallysendmessageswithconfidenceandboldness.ThePCTprotocolisaresponsibleprotocol.Ifitcanbedelivered,hewilldefinitelyItwillbedeliveredinanorderlymanner.Ifthenetworkisbrokenanditisreallyimpossibletoconnect,hewilltellmethatthenetworkcannotbeconnected.Thismakesprogrammingmucheasier.NowIwonderhowthebrowserandservercommunicate.Let'stakealookatBaidu.$telnetwww.baidu.com80Trying183.232.231.173...连接到www.baidu.com.Escapecharacteris'^]'.GET/HTTP/1.1HTTP/1.1302MovedTemporarilyDate:Sat,12Aug201710:45:14GMTContent-Type:text/htmlContent-Length:215Connection:Keep-AliveLocation:http://www.baidu.com/search/error.htmlServer:BWS/1.1X-UA-Compatible:IE=Edge,chrome=1BDPAGETYPE:3Set-Cookie:BDSVRTM=0;path=/
