关于UDP和TCP 我们就要重点聊一聊传输层(负责数据能够从发送端传输接收端.)
一、关于端口号
1.1 端口号的理解
端口号(Port)标识了一个主机上进行通信的不同的应用程序;
每一个应用层服务(进程)都绑定着自己的协议,而具体这个数据要传输给哪一个应用程序,是要根据具体的端口号来决定的,而交给哪个服务的本质也是交给哪个进程 又因为进程bind了自己的端口号 所以OS究竟会把数据交给谁,其实是根据端口号来确认的!
在TCP/IP协议中, 用 "源IP", "源端口号", "目的IP", "目的端口号", "协议号" 这样一个五元组来标识一个通信(可以通过netstat -n查看);
1.2 端口号范围的划分
0 - 1023: 知名端口号, HTTP, FTP, SSH等这些广为使用的应用层协议, 他们的端口号都是固定的.
1024 - 65535: 操作系统动态分配的端口号. 客户端程序的端口号, 就是由操作系统从这个范围分配的.
1.3 认识知名端口号
有些服务器是非常常用的, 为了使用方便, 人们约定一些常用的服务器, 都是用以下这些固定的端口号:
1、ssh服务器, 使用22端口
2、ftp服务器, 使用21端口
3、telnet服务器, 使用23端口
4、http服务器, 使用80端口
5、https服务器, 使用443
执行下面的命令, 可以看到知名端口号
cat /etc/services
我们自己写一个程序使用端口号时, 要避开这些知名端口号.
1.4 一个进程可以bind多个端口号
一个端口号不能被多个进程bind,但是一个进程可以被多个端口号bind的(一个进程可以有多个文件描述符,而每一个文件描述符都可以对应一个端口号,)!!
问题:什么情况下需要一个进程绑定多个端口号呢??
---->比如HTTP和HTTPS,同一个Web服务器可能需要监听80和443端口,分别处理HTTP和HTTPS的请求(每次调用bind都会为该进程创建一个独立的socket,这些socket是由内核管理的,他们之间是相互独立互不影响的,所以bind多个套接字就对应监听多个套接字!!)
1.5 相关命令
1、netstat是一个用来查看网络状态的重要工具.
语法:netstat [选项]
功能:查看网络状态
常用选项:
n 拒绝显示别名,能显示数字的全部转化成数字
l 仅列出有在 Listen (监听) 的服务状态
p 显示建立相关链接的程序名
t (tcp)仅显示tcp相关选项
u (udp)仅显示udp相关选项
a (all)显示所有选项,默认不显示LISTEN相关
其中unix是域间套接字,用来进行本地通信(懂了网络套接字之后学习成本就不大了!)
2、pidof在查看服务器的进程id时非常方便.
语法:pidof [进程名]
功能:通过进程名, 查看进程id
二、UDP报头
2.1 研究协议的一些具体问题
1、报头和有效载荷是如何进行分离的??
2、有效载荷应该交付给上层的那一个协议呢?(协议字段、方案)
3、认识报头的组成
4、学习协议的周边知识
2.2 UDP协议端格式
问题1:报头和有效载荷是如何分离的呢??
------>UDP采用的是定长分离(8字节),通过读取前8个字节,可以读到UDP的长度(16位UDP长度, 表示整个数据报(UDP首部+UDP数据)的最大长度;),然后数据的大小就是UDP长度-8,就实现了报头和有效载荷的分离 ——>固定长度+自描述字段
问题2:有效载荷又是如何交付给上层的??
——>分离之后通过报头信息的源端口号和目的端口号,确认应该交付给那个进程(一般来说已经bind相关端口号了)
问题3:UDP可以保证可靠性么??
——> 虽然不保证可靠性,但是至少要是对的,其中UDP校验和就是一种检验UDP数据包是否有误的校验机制,他通过了某种算法将UDP数据包中的所有数据进行计算然后存储在报头字段中,以便确保接收方在收到数据包后进行校验。如果校验失败的话,就直接把这个数据包丢弃!!
问题4:UDP有什么应用场景?
——>比如我们的视频传输就是UDP协议,其实就是无数张相片的组成传输过来,而你切换清晰度无非就跟图片的一些像素信息有关,像素越高越清晰一般就会越大,可能就会导致udp的传送速度变慢,有时候你电视剧看着看着出现缓冲了,你可能就会把画质调低一点这样传输就会快一点,甚至有些时候实在网络太拥塞了可能会出现丢包的情况,那么视频就会出现异常,但是UDP对这种现象是不负责任的!!因为他半身就不保证可靠性。。而如果你想保证可靠性那你就应该用TCP协议,所以研究UDP的应用场景,其实就是基于上层不同协议对应的应用场景是否需要选择UDP!
问题5:如果UDP数据包太大怎么办??
——>我们注意到, UDP协议首部中有一个16位的最大长度. 也就是说一个UDP能传输的数据最大长度是64K(包含UDP首部). 然而64K在当今的互联网环境下, 是一个非常小的数字.如果我们需要传输的数据超过64k,就需要在应用层手动的分包, 多次发送并在接收端手动拼装(面向数据报)
2.3 UDP的特点
UDP传输的过程类似于寄信、寄快递。
1、无连接: 知道对端的IP和端口号就直接进行传输, 不需要建立连接;
2、不可靠: 没有确认机制, 没有重传机制; 如果因为网络故障该段无法发到对方, UDP协议层也不会给应用层 返回任何错误信息;
3、面向数据报: 不能够灵活的控制读写数据的次数和数量;
应用层交给UDP多长的报文, UDP原样发送, 既不会拆分, 也不会合并,要么就收一个,要么就不收,没有半个或者一个半的情况!!
举例:用UDP传输100字节的数据,如果如果发送端调用一次sendto, 发送100个字节, 那么接收端也必须调用对应的一次recvfrom, 接收100个 字节; 而不能循环调用10次recvfrom, 每次接收10个字节;
2.4 UDP的缓冲区
1、UDP没有真正意义上的发送缓冲区(因为他不需要!!). 调用sendto会直接交给内核, 由内核将数据传给网络层协议进行后 续的传输动作;
2、UDP具有接收缓冲区. 但是这个接收缓冲区不能保证收到的UDP报的顺序和发送UDP报的顺序一致; 如果缓冲区满了, 再到达的UDP数据就会被丢弃;
3、UDP的socket既能读也能写,也是全双工
2.5 基于UDP的应用层协议
NFS: 网络文件系统
TFTP: 简单文件传输协议
DHCP: 动态主机配置协议
BOOTP: 启动协议(用于无盘设备启动)
DNS: 域名解析协议
当然, 也包括你自己写UDP程序时自定义的应用层协议;
2.6 UDP的结构
因为Linux系统是用C语言写的,所以UDP报头的结构其实涉及到结构体的位段。
填上报头数据,然后带上有效载荷形成UDP报文,我们就可以发送了!!
UDP并不需要有发送缓冲区,是因为发送方准备好了就可以直接发了,但是他需要有接收缓冲区,因为他把数据发给对方之后可能对方并不能立即去处理这个数据,所以在对方的接收缓冲区里可能会存在多个UDP报文,那么也就必然要求OS必须将多个UDP报文给管理起来!!所以需要先描述再组织!!
sk_buff就是服务端管理UDP报文的缓冲区 其中start指向缓冲区的头部(报文的头部)、end指向缓冲区的尾部、pos指向有效数据的尾部。然后再用链表形式去链接管理起来。 此时我们OS将对UDP报文的管理转化成了对sk_buff结构体的管理
所以新的UDP数据就会连接在这个链表上,而UDP数据的丢弃就是把他对应的结构体给释放了
三、TCP报头
3.1 传输控制协议的理解
TCP全称为 "传输控制协议(Transmission Control Protocol"). 人如其名, 要对数据的传输进行一个详细的控制;
我们平时调用的write、read、recv、send其实本质上都是拷贝函数,要么将用户缓冲区的内容拷贝到发送缓冲区,要么将接受缓冲区的内容拷贝到用户缓冲区进行处理, 本质来说就是不断把数据放到网络中,而具体数据要什么时候发送,发送多少,出错了怎么办,是由TCP协议自主决定的!!!!
其实通过TCP协议控制网络传输可靠性将数据从一台主机的发送缓冲区安全地交付到另一台主机的接收缓冲区 ,要求数据原封不动的发送过去,所谓发送,本质上是一种也是一种跨网络的拷贝!!
当然,对方也可以对我们发送,因为TCP是全双工的,有自己的发送缓冲区和接受缓冲区!
问题1:TCP和文件系统的关联?
----->TCP其实和文件系统很像,就是我们使用系统调用接口本质上是将用户层缓冲区数据刷新到内核的文件缓冲区,但是具体这个文件缓冲区什么时候刷新到磁盘上是由OS自主决定的,而网络只不过是将磁盘换成了网卡,这进一步说明了Linux中一切皆文件的思想 , 他们IO的思路是一模一样的,只不过文件是在本地的话,出错的概率很低,而网卡文件需要经过网络,所以需要有TCP协议来确保可靠性!!
问题2:先暂时不考虑可靠性,先分析数据在网络中的流动是怎样的??
-----> 所以我们想要发送一个网络数据,先在应用层通过相关的协议对结构化数据做序列化,然后放到发送缓冲区中,然后TCP会帮我们在适当的时机把数据发送到对方的接收缓冲区,等待对方的应用层通过read读取。此时一般会出现两种情况
(1)对方上层由于忙碌始终无法处理,使得多次发送的报文都堆积在接收缓冲区,当他不忙碌时可能一次read就都读上去了!!然后上层再需要对这些数据解析成一个个完整报文(因为是面向字节流的),然后反序列化成结构化数据供上层使用。而如果凑不齐一个报文,就将他们暂时存储在自己的用户层缓冲区,等下次read的时候如果凑齐了再一起拿出来解析
(2)对方的上层会在合适的时机进行read,但是如果发送方始终没有发送,或者网络太拥堵导致接收缓冲区一直没有数据,那么read就会阻塞住,然后OS就会把这个进程设置为S状态,而当缓冲区有数据的时候(也就是说OS某些资源就绪的时候)OS会再次调度这个进程去把数据从缓冲区读上来!!!
所以以前我们觉得OS某些资源不就绪从而使得进程暂时进入S状态大多数指的是因为硬件的速度比较慢,所以需要等待硬件资源就绪,但是今天我们发现在网络的情况中,也有可能是因为对方始终不给你发数据,或者由于网络拥塞造成接收缓冲区没有数据可处理,也是属于资源不就绪的情况,此时调用read的这个进程就必须阻塞住!!
问题3:为什么UDP不需要有发送缓冲区,而TCP必须要有发送缓冲区呢?
------>因为UDP发了就是发了,他并不关心对方的接收缓冲区是不是满了,如果满了他就会丢包,此时你也是无感的,UDP不保证可靠性,但是TCP需要保证可靠性啊!!比如说对方如果来不及把数据拿到上层处理导致对方接受缓冲区快满了,TCP会意识到对方接受能力不行(后面会说),就会减缓发送的速度,但是我们上层用户是无感的啊!你可能会一直往缓冲区里写!所以此时数据就会被存储在发送缓冲区里了!!当然TCP也不会任由这种情况发展,他会通过某种方式要求对方赶紧清空自己的接收缓冲区进行接收!
3.2 TCP协议段格式
问题1:TCP报头如何将数据有效地分离呢??
-——>和UDP一样,通过固定长度+自描述字段,先读取定长的20个字节,然后再从里面读取4位首部长度(4位首部长度是包含选项的,他会先算出选项的大小,然后再读取若干选项,那么剩下的就是数据了!!)
问题2: 然后将有效载荷交付给上层呢
---->通过自描述字段中的源端口号和目的端口号,就可以知道交付给上层的哪个进程了!!
问题3:关于16位校验和
----->16位校验和: 发送端填充, CRC校验. 接收端校验不通过, 则认为数据有问题. 此处的检验和不光包含TCP首部, 也包含TCP数据部分.
问题4:为什么TCP的报头没有长度呢??
——>因为TCP是面向字节流(后面会说)的,一直发的话他就会一直收。所以不需要长度
3.3 TCP确认应答机制
TCP凭什么保证可靠性呢??必须要基于确认应答机制!!当对方成功收到了数据,就需要对你做出响应!!
应答机制其实广泛发生在外面的生活中,举例:假设我和我的好朋友打电话,我总会是先喊“喂”,此时我必须得听到对方的回复我才能确定他听到了我的声音,而我又得再回复一次他的回复才能让他知道我收到了这个回复,如此往复,后一条应答都可以证明前一条消息被对方给收到了!!可是这个世界上并不存在百分之百的应答!!!因为最新的一条消息是不可能收到应答!否则就会陷入死循环!所以我们是无法保证所有发出去的消息都能得到百分之百的回应,我们只能保证我们历史发过的消息可以得到回应!!
因此在TCP的方案里,并不要求对应答做应答,因为是客户端给服务器发消息,服务端是被动的,所以我客户端能收到应答就可以保证我发出的数据被服务器收到了!!(我的客户端没有必要给服务端发消息确保应答是否收到!!) ——>这保证了服务端到客户端方向上的可靠性!!!因为我们的目的是保证通信双方两个方向上的可靠性!
所以应答是否有收到呢???万一丢了怎么办??TCP对于发送失败会有自己另外的策略,有时候会有需要重发的场景,因此我们会把没收到响应的数据暂时保存在缓冲区中维持一段时间!!
记住!!应答不一定是单独发的!!因为如果此时服务端也正好想发消息给你,先发应答再发消息显然效率是比较低的!所以他会在发消息的时候顺便应答,这就是捎带应答!!
3.4 16位窗口大小
我们前面提到了一个关于对方接受能力的问题,如果对方接受能力不足但是你用户层一直发消息导致丢包问题显然是不可靠的,所以我们的TCP除了应该有发送缓冲区的存在,还应该有一些配套的解决方案!!
首先TCP里面有一种机制就是超时重传(下一篇会说),就是他在发送的时候不会立马将数据从缓冲区清空,而是如果在规定时间内没有收到对方的应答,他就会判定数据丢失了然后再补发。但是这种方案显然不够合理,因为我这个数据千里迢迢来到了你这里,浪费了这么多的网络资源,却因为你的接收能力不行导致我在没有明显错误的情况下被你丢弃,那么曾经的发送不就是没有意义了的吗??所以我们更好的方式就是应该考虑在发送的时候就去控制传输速度避免报文的丢失!!
而通过控制用户发送速度来让对方能够来得及接收从而规避大量的丢包情况,这种方案叫做流量控制(下一篇会说)。
可是,发慢一点的依据是什么呢???我怎么知道对方的缓冲区接受能力是多少呢??别忘了TCP是有确认应答机制的!!我发给你一个报文,你是要响应的!!而且你响应的时候会加上报文,而报文里的字段包含了16位的窗口大小,而窗口大小填充的就是服务端剩余空间的大小!!TCP可以因此推断出对方的接收能力从而控制传输速度!!
同理,双方在通信的时候都会有报文的往来,所以其实在建立连接的时候双方不仅仅只是建立连接了,同时也协商了双方的接收能力,那么双方都可以互相进行流量控制!
问题: 16位数字最大表示65535, 那么TCP窗口最大就是65535字节么?
——>实际上, TCP首部40字节选项中还包含了一个窗口扩大因子M, 实际窗口大小是窗口字段的值左移 M 位;
3.5 序号解决数据包乱序
tcp的原始过程,发一条消息确认响应之后再发送下一条,但是如果我们想要发送很多信息,但是服务端却一直不应答,显然这种串型的效率是很低的!!所以我们的客户端一般会一次向服务端发送一堆消息!!
批量化发请求,也意味着需要批量化应答!!那么在提高效率的同时就会伴生出两个问题(1)数据乱序(发出去的数据并未按照顺序被接收) (2)哪个应答对应哪个请求
问题1:解决数据包乱序的问题
——>报文里会携带序号!!是用来保证数据的按时到达的,另一方接收缓冲区会根据序号做排序!!其实这里的序号对应的就是发送的数据的最后一个字符的下标!! TCP对每个字节的数据都进行了编号,即为序列号
但是要记住的是,虽然是按照字节做的编号,但是发送的时候不是一个字节一个字节发的,而是一个数据块一个数据块地发!!
问题2:一次返回这么多应答,你怎么确定哪个应答对应的是哪个数据呢??
——>需要引入确认序号(填充的是收到的报文序号+1)的定义:表示确认序号之前的数据都已经被接收到了,下一次发送,请从确认序号指定的数字开始发送!!
问题3:应答不是只有报文没有数据吗??那我为什么不直接把你的序号+1返回去而是要同时拥有序号和确认序号呢??
——>(1)因为可能会存在“捎带应答”的情况(双重身份),可能我服务器也会顺便发送我的缓冲区数据,本身的数据是用的顺序序号,而应答用的是确认序号,所以必须分开不能复用!!
(2)服务端和客户端地位是对等的,所以客户端给服务端发消息的时候服务端也可能在给客户端发消息,所以捎带应答的情况是经常会出现的!!所以必须要有两组序号!!
问题4:序号会越界或者跟别的数据序号出现冲突吗??
——>序号一般是回绕的,而且回绕特别大,一般不会冲突!!
3.6 引入六个标记位
我们都知道,TCP协议是基于链接的,在正常通信之前需要进行链接,在正常通信之后需要进行断开连接,可是我怎么知道你是这个报文是打算做哪个工作呢???——>所以我们可以知道TCP报文一定有各种不同的类型!!而不同的类型决定了服务端要做不同的工作!!
接收方如何得知报头的类型各自是什么呢??——>需要引入6个标记位,标记位存在的意义就是为了区分TCP报头类型的
我们要知道服务端:客户端是1:n,所以服务端必然是存在多个链接的!所以OS必须想办法把这些“链接”管理起来——先描述再组织!所以当双方建立连接成功后比如会建立一个相关的结构体,然后断开连接后会各自释放空间。
(1)ACK: 确认号是否有效(应答)
(2)SYN: 请求建立连接; 我们把携带SYN标识的称为同步报文段(请求三次握手)
(3)FIN: 通知对方, 本端要关闭了(请求四次挥手)
(4)RST: 对方要求重新建立连接; 我们把携带RST标识的称为复位报文段(链接重置)
我们要知道,TCP为了保证可靠性,提供了很多健全的方案,但是这并不代表可以覆盖所有的情况,因为很多时候会出现一些不可抗力因素(比如协议bug、网络bug等等) 所以TCP是允许链接失败的!!比如某次握手没有成功(因为最后一个ACK可能没有收到!)
对于客户端来说,只要把第三次握手的报文发出,他就认为链接建立好了,然后把自己的准备工作给做好了!!可是一旦ACK真的丢失了,那么服务端会认为链接没有建立好,也就不会做这些准备工作,因为中间存在时间窗口,所以双方会出现认知不一致的情况,此时客户端会直接发送数据,当服务端接收到后会觉得很奇怪“你明明没有和我链接成功,为什么要给我发数据呢??” 于是这是他会意识到可能是客户端误以为跟自己链接成功了,所以他会赶快通过RST告诉客户端“兄弟,你别传了,你的连接是失败的,你再重新连接一次吧” 这其实就是链接重置!!
(5)PSH: 提示接收端应用程序立刻从TCP缓冲区把数据读走+
首先我们要知道,其实接收缓冲区就相当于一个内存空间,而OS负责接收数据,上层负责拿数据,其实本质上就是一个生产消费模型,所以流量控制本质上就是对生产消费模型的一种同步机制!!
问题:因为得知对方接受缓冲区没啥空间了,于是TCP就控制几乎不往缓冲区里写了,可是我得等到什么时候呢??万一对方已经把接收缓冲区的数据刷走了,那我又怎么知道呢???
——>方案1:发送方会定期询问对方,看看对方的接收缓冲区是不是读完了!!
方案2:接受方知道自己之前数据快满了,所以刷新之后会给对方发个响应告诉他说可以继续发了
这两种策略同时存在,如果对方就是一直不拿走,那么TCP会给对方发送PSH,警告他尽快拿走!
(6)URG: 紧急指针是否有效
3.7 紧急指针
报文有一个地方是16位紧急指针,他记录的是紧急数据(需要高优先级处理的数据 )的偏移量!
问题1:我知道了偏移量,但是具体有多大呢???
——>他被要求了一个报文只能携带一个字节的紧急数据,所以我们需要软件功能来提供一些状态编号!!
问题2:紧急数据要怎么读怎么写呢??
——>send和recive,他们中的选项有一个叫做MSG_OOB
紧急数据也叫做带外数据
问题3:紧急数据的场景?
——>比如说你当前服务器正在进行一个周期性的服务或者是服务器爆满导致卡顿,此时你无论如何申请都得不到对方的响应,这个时候你很好奇服务器究竟怎么了,于是你需要发送询问,但是这个询问必须通过带外数据的方式去发送,才能让服务端优先处理,然后服务端会通过一个描述的状态码,在将一个带外数据返回给你,你就知道服务端究竟是什么情况了!!