目录
再谈协议
网络版计算器
HTTP
HTTPS
UDP
TCP
面向字节流
粘包问题
listen的第二个参数
再谈协议
如下图,在网络传输结构化的数据时,会有一个从结构化的数据->字符串数据->结构化数据的过程
为什么要进行序列化和反序列化
如果没有转化,直接传输结构化的数据,数据可能会发生变化,比如长度等等,所以结构化的数据
是不便于网络传输的,而字符串是便于网络传输的,所以这么这么做是为了应用层网络通信的方便
为了方便上层进行使用内部成员,将应用层和网络进行了解耦!这样,应用层就只关心结构化数
据,不用关心你数据怎么传输,怎么序列化和反序列化
网络版计算器
约定方案一
客户端发送一个形如"1+1"的字符串;
这个字符串中有两个操作数, 都是整形;
两个数字之间会有一个字符是运算符, 运算符只能是 + ;
数字和运算符之间没有空格;
如下图,是方案一的做法,不过序列化和反序列都要由我们自己来做,就很麻烦,不推荐!
约定方案二
定义结构体来表示我们需要交互的信息;
发送数据时将这个结构体按照一个规则转换成字符串, 接收到数据的时候再按照相同的规则把字符
串转化回结构体;
这个过程叫做 "序列化" 和 "反序列化"
如下图,这种方法没有经过序列化和反序列化,所以也不太好,所以得加上序列化与反序列化的过
程,也就是方案二的做法!
代码实现
version1 ——无序列化,短服务
首先定义一个Sock类来对创建套接字,绑定,连接等函数做一个封装
然后在协议的文件中,定义两个结构体(定义协议的过程,目前就是定义结构体数据的过程)
对服务器端
首先还是采用命令行输入的方式来给定端口号,然后就是完成创建、监听、绑定和接收连接操作!
然后就是读取客户端发送来的数据,并进行计算,针对除零或操作符非法情况等,返回退出码,然
后将返回的结果写入,最后再关闭sock!
对客户端
同样,采用的是命令行的方式来连接服务器
然后创建套接字,发起连接,输入要计算的数据和操作符并写入,然后读取,读取成功,就打印计
算结果和退出码
运行结果
json:是一个第三方库,可以进行序列化和反序列化
序列化
有StyledWriter和FastWriter两种
StyledWriter
FastWriter
反序列化
R是为了防止字符串中的字符转义
version 2 ——序列化与反序列化
先定义协议文件中把四个序列与反序列化函数
对服务器端
以字符串的形式读取文件内容,然后进行反序列化,转为结构体数据,去计算
计算完成后,进行序列化,将数据转为字符串写入
对客户端
先进行序列化,将结构体数据转为字符串写入
再以字符串的形式读取文件内容,再进行反序列化,转为结构体数据打印
运行结果
HTTP
本质上,在定位上和前面所写的网络计算器没有区别,都是应用层协议服务
我们请求的图片、html、css、js、视频、音频等,这些都被称之为资源!
IP+PORT唯一的确定一个进程,但无法确认唯一的确定的一个资源,而IP+Linux路径,就可以唯
一的确认一个网络资源!
ip——通常是以域名的方式呈现的,路径可以通过目录名+/确认
urlencode和urldecode
像 / ? : 等这样的字符, 已经被url当做特殊意义理解了. 因此这些字符不能随意出现. 比如, 某个参数
中需要带有这些特殊字符, 就必须先对特殊字符进行转义
转义的规则如下: 将需要转码的字符转为16进制,然后从右到左,取4位(不足4位直接处理),每2位
做一位,前面加上%,编码成%XY格式
简化认识
如何理解普通用户的上网行为?
1、从目标服务器拿到你要的数据
2、向目标服务器中上传你的数据
无论是请求还是响应,基本上http都是按照行(\n)为单位进行构建请求或者响应的!
无论是请求还是响应,几乎都是由3或者4部分组成
http请求或者响应,是如何被读取的?http请求是如何被发送的?
可以将请求和响应整体看做一个大的字符串!如下图
http如何解包?如何封装?如何分用?
空行是特殊字符,可以用空行将长字符串一切为二
分用不是http解决的,是具体的应用代码解决,http需要有接口来帮助上层获取参数!
代码实现
还是用前面封装的sock类,另外这里采用多线程的形式
这里采用新的接口recv和send,来读和写数据,这两个接口和read,write,除了多了一个flags
参数外,其它的一模一样,flags传参传0即可!
运行结果
服务器端收到的信息
客户端收到的信息
Content—Length
这种读法是不正确的,只不过目前没有被暴露出来罢了!
客户端可能一次发起多个请求,而如果你要读1025个字节,而一个完整的http request是
1024个字节,那就可能会读取到下个http request的内容,造成一个数据多余,另一个数据残缺的
结果!所以要有两个保证
第一:保证每次读取都是读取完整的一个http request
第二:保证每次读取都不要将下一个http请求的一部分读到
而要做到这两个保证,在报头中就有了Content—Length属性!
当读到空行时,就表示报头部分读完了,而决定后面还有没有正文,与请求方法有关!
如果有正文,则Content—Length表明正文部分有多少个字节!通过Content—Length,我们可
以读取到完整的http请求或响应,同时根据空行能够做到将报头和有效载荷进行分离(解包)!
当没有正文的时候,就不存在Content—Length
请求方法
请求方法有很多,如GET,POST,HEAD,PUT,DELETE等等!这里只讲GET和POST方法!
如下图,http请求的/并不是根目录,而叫做web根目录
/:我们要一般请求的一定是一个具体的资源,但如果请求是/,意味着我们要请求该网站的首页,
即index.html或index.htm,一般所有的网站,都要有默认首页!
如下图,/a/b/c则是我们要请求的资源的具体路径
代码实现
首先在当前目录下再添加一个wwwroot目录,以及在其下创建一个.html文件,并编写内容
在http.cc文件中定义两个宏,用来确定html文件的路径
然后定义一个结构体,里面有一个输出型参数buf,buf里面有一个数据就是文件的大小,这里也就
是用来得到正文的字节数
Content-Type是正文部分的数据类型,text/html则表示正文是.html文件
然后打开文件,成功就一行一行地读取缓冲区中的内容,然后添加到响应字符串中,再发送给客户
端,失败则打印错误信息!
运行结果
其中wwwroot,就叫做web根目录,wwwroot目录下放置得到内容,都叫做找资源!wwwroot目
录下的index.html就叫做网站的首页!
验证GET和POST方法
GET方法,如果提交参数,是通过url方式进行提交的!
POST方法是通过正文进行提交参数的!
结论
概念问题
GET:方法叫做获取,是最常用用的方法,默认一般获取所有的网页,都是GET方法,但是如果
GET要提交参数(它能的!),通过url来进行参数拼接,从而提交给server端
POST:方法叫做推送,是提交参数比较常用的方法,但是如果提交参数,一般是通过正文部分提
交的,但是不用忘记,Content—Length:XXX表示参数的长度
区别
参数提交的位置不同,POST方法比较私密(私密 != 安全),不会回显到浏览器的url输入框!GET方
法不私密,会讲重要信息回显到url的输入框中,增加了被盗取的风险
GET是通过url传参的,而url是有大小限制的!和具体的浏览器有关!POST是通过正文部分传参
的,一般大小没有限制!
如何选择
如果提交的参数,不敏感,数量非常少,可以采用GET,否则就使用POST方法
http协议处理,本质就算文本分析
所谓的文本分析:http协议本身的字段;提取参数,如果有的话
GET或者POST是前后端交互的一个重要方式!!!
状态码
应用层是人要参与的,人水平参差不齐,http的状态码,很多的人,根本就不清楚如何使用,又因
为浏览器种类太多了,导致大家可能对状态码的支持并不是特别好,类似于404的状态码,对浏览
器没有任何指导意义,浏览器就是正常显示你的网页!
对于404状态码,我们自己来处理,如下图
404属于客户端错误,就类似于你在淘宝上,你想看腾讯视频上的电影!
HTTP的状态码
3XX的状态码是有特殊含义的:
重定向:当访问某一个网站的时候,会让我们跳转到另一个网址
永久重定向:301
如下图,当我们访问老的网址的时候,它会返回,然后浏览器自动给我们跳转到新地址,然后对于
收藏的老的地址会被浏览器替换为新地址,例如网址搬迁,域名更换
临时重定向:302 或 307
等我访问某种资源的时候,提示我登录,跳转到了登录页面,输入完毕密码,登录的时候,会自动
跳转回来(登录,关闭下单)
重定向是需要浏览器给我们提供支持的,浏览器必须识别301,302,307,server要告诉浏览器,
我应该再去哪里,所以报头中就又有了一个属性Location:新的地址!
代码实现
永久重定向,如下图,会自动跳转到腾讯网的首页
临时重定向,与上面的比较起来,效果不明显,无法看出区别
长短链接
短链接
http/1.0采用的网络请求的方案是短链接,过程即request->response->close,通过这些过程来返
回一个资源!
一般而言,一个大网页是由多个元素组成的,访问一个由多个元素构成的网页的时候,http/1.0,
就需要多次进行http请求,http协议是基于tcp协议的,tcp要通信,就得建立链接->传输数据->断
开连接,而每一次http request都要执行这个过程,非常耗时!
长链接
为了解决上面效率低的问题,所以http/1.1支持长链接,通过减少频繁建立tcp链接(链接不关闭),
来达到提高效率的目的!
cookie与session
cookie
当我们进入gitee网站时,它会提示我们要登录,当登录进去之后,再退出该网页,重新进入,它
就不要我们登录了!经验:在网站中,网站是认识我的,各种页面跳转的时候,本质其实就是进行
各种http请求,网站照样认识我!但是,http协议本身是一种无状态(不会保留之前请求的信息)的
协议!看起来就显得很矛盾
其实,网站认识我,并不是http协议本身要解决的问题,它主要是帮我们解决网络资源获取的问
题,http可以提供一些技术支持,来保证网站具有"会话保持的"功能,也就有了cookie,来进行会
话管理,所以网站能够认识我!
站在浏览器角度:cookie其实是一个文件(保存在浏览器中),该文件里面保存的是我们的用户的私
密信息
站在http协议:一旦该网站对应有cookie,在发起任何请求的时候,都会自动在request中携带该
cookie信息!!!
基本理解,如下图
代码验证
Set-Cookie: Key=value
运行结果
有cookie文件时,后面都会带有password和id
没有cookie文件时
如果别人盗取我们的cookie文件,别人:1、可以以我的身份进行认证访问特定的资源;2、如果
保存的时我们的用户名密码,那么就非常糟糕了,所以单纯使用cookie,是具有一定的安全隐患
的,所以还需要有session
cookie分为文件版和内存版,也就是它的存储位置,对于不同的浏览器有所不同!
session
核心思路:将用户的私密信息,保存在服务器端!
如上图,即使采用了session,我们还是有cookie文件被泄漏(也能去访问我们的对应的网址)的风
险,但是可以有一些衍生的防御方案了!比如当你的QQ号被在缅甸的某个人盗了,而IP是分地域
的,这时就会提示登录异常,让你重新登录,也就是服务器端给你重新生成一个cookie文件,而
缅甸的那个人的cookie文件也就失效了!
为什么网站需要认识用户?cooki+session
本质:提高用户访问网站或者平台的体验!
HTTPS
https = http + TLS/SSL(http数据的加密解密层)
背景知识1
加密方式
对称加密,密钥(只有一个)X,用X加密,也要用X解密
非对称加密,有一对密钥:公钥和私钥
可以用公钥加密,但是只能用私钥解密,或者用私钥加密,只能用公钥解密
一般而言,公钥是全世界公开的,私钥是必须自己进行私有保存的!
背景知识2
如何防止文本中的内容被篡改,以及识别是否被篡改?
概念
校验
采用什么样的方式加密
方式一:对称加密
客户端或者服务器端如何得知密钥X呢?采用预装的话,成本高,而且你能预装,那别人也能预
装,所以不行,而采用密钥协商的方式,第一次就不能有加密,因为你必须得让服务器端知道密
钥!而后面再加密也就没意义了,毕竟已经暴露了,所以只采用对称加密的方式不行!!!
方式二:非对称加密
一对非对称密钥
如下图,数据从客户端到服务器端是安全的,但数据从服务器端返回给客户端是不安全的,因为如
果你用私钥S‘加密,而公钥S又是公开的,别人也就能解密,所以不安全了,只能单向安全!
两对非对称密钥
理论上,下图这种方式是可行的,然而,事实并非如此,1、依旧有被非法窃取的风险;2、非对称
加密算法,特别费时间,所以效率太低。而对称加密是比较节省时间的!
方式三:实际上,对称+非对称
如下图,服务器端的公钥S,会先被客户端拿到,然后用S对X加密,再发送给服务器端,服务器端
通过S'解密,拿到X,然后客户端的数据经过X加密给服务器端,服务器端用X解密,拿到数据;同
理,服务器端的数据经过X加密给客户端,客户端用X解密,拿到数据
什么叫做安全
不是让别人拿不到,就叫做安全,而是别人拿到了,也没法处理,而从经济角度来看,别人拿到了
加密的数据,要对加密的数据解密,花费的成本比收益还要高,那也能称为安全!
上图中的做法也是有数据被暴露的风险的,在网络环节中,随时都有可能存在中间人来,偷窥、修
改我们的数据!!!
如下图,当服务器端给客户端发送自己的公钥S时,可能会被中间人截获,然后将其换成自己的公
钥M,再发送给客户端,而此时客户端并不知道服务器端发送给自己的报文被篡改了,所以会继续
对自己的私钥X加密,再发送,此时,再次被中间人截获,解密,将私钥X自己拷贝一份,再用之
前截获的报文中的S来对其加密,再发送给服务器端,服务器端也就收到了X,此后,每次客户端
与服务器端的数据都会被中间人拷贝一份,而两者都不知道!
本质问题:client无法判断发来的密钥协商报文是否是从合法的服务方发来的!
解决方法
CA证书机构,该机构有两大特点:权威;有自己的公钥A和私钥A‘
只要一个服务商,经过权威机构认证,该机构就是合法的
创建证书
如下图,当服务器端发给客户端的是一个证书时,中间人截获了该证书,也就只有如下三种改法:
1、改变内容,然后发给客户端,客户端可以如同背景知识二做对比,就能发现数据被修改了!
2、改变数字签名,因为CA机构的公钥是被公开的,所以中间人也是可以拿到的,然后对数字签名
做修改,但是没有CA机构的私钥,也就无法再像之前一样加密,只能用自己的私钥加密,但这样
做,客户端也就能发现数据可能被修改了!
3、改变内容和数字签名,假设中间人也是合法的服务方,它制作一个新的证书再发送给客户端,
但是域名会和服务器端的有所不同,所以客户端也能发现,而如果域名设置一样的话,那在申请证
书时,就申请不了
client必须知道CA机构的公钥信息!!!
client如何知道CA机构的公钥信息?
1.一般是内置的!
2.访问网址的时候,浏览器可能会提示用户进行安装!
UDP
udp协议端格式
UDP是如何做到封装和解包的?
当要封装时,就加上8字节定长的报头,当要解包时,就去掉8字节定长的报头!
UDP是如何做到向上交付的(分用问题)?
分用要解决下面两个问题,而第一个问题由上就可以解决,第二个问题,如下图,UDP报文中有
16位的目的端口号,而我们在编写套接字的时候,需要绑定端口号,所以也就能因此将有效载荷交
付给上层应用
a.报头和有效载荷分离;
b.根据目的端口号,交付有效载荷给上层应用
端口号为什么是16位?
这是协议规定的!!!
在Linux内核是C语言写的,如何看待UDP报文(报头)
UDP的特点
无连接;不可靠;面向数据报
面向数据报
应用层交给UDP多长的报文, UDP原样发送, 既不会拆分, 也不会合并,比如:
如果发送端调用一次sendto,发送100个字节,那么接收端也必须调用对应的一次recvfrom,接
收100个字节,而不能循环调用10次recvfrom,每次接收10个字节
udp的缓冲区
read/recv;write/send,与其说是收发函数,不如说是拷贝函数
对于udp,只有接收缓冲区,没有发送缓冲区,发送数据,会调用sendto会直接交给内核, 由内核
将数据传给网络层协议进行后 续的传输动作
udp具有接收缓冲区,但是这个接收缓冲区不能保证收到的UDP报的顺序和发送UDP报的顺序一
致,如果缓冲区满了,再到达的UDP数据就会被丢弃
UDP协议首部中有一个16位的最大长度. 也就是说一个UDP能传输的数据最大长度是64K(包含
UDP首 部),如果我们需要传输的数据超过64K, 就需要在应用层手动的分包, 多次发送, 并在接收
端手动拼装
为什么传输层要有缓冲区?
是为了提供传输数据的策略!!!
基于UDP的应用层协议
tcp缓冲区
tcp协议,是自带发送和接收缓冲区的!而这两个缓冲区可以看作是tcp malloc出来的2段内存空间
应用层进行send,并不是把数据发送到网络上,而是把数据拷贝到tcp的发送缓冲区
为什么要有缓冲区?
1、提高应用层效率,当数据从应用层拷贝到发送缓冲区后,应用层也就不用再管了!
2、只有os tcp协议可以知道网络,乃至对方的状态明细,所以,也就只有tcp协议,能处理如何
发,什么时候发,发多少,出错了怎么办等细节问题!即传输、控制、协议,所以tcp也被称为传
输控制协议!而因为缓冲区的存在,所以可以做到应用层和tcp进行解耦!!!
16位窗口大小
当server中,应用层读的太慢,接收缓冲区满了的时候,server来不及接收,那么发过去的报文也
就只能被丢弃,而这样会浪费很多资源,所以就需要流量控制!
可以在应答报文:在报头里面天上:我自己的接收缓冲区中剩余空间的大小(接收能力),比如两个
人给一个杯子倒水,倒水的被蒙上眼睛,另外一个则当他每倒一次水,就告诉它还剩多少空间倒
满,这样就能防止水溢出,这个接收缓冲区剩余空间的大小也就是16位窗口大小!同理,反过来
server给client发送报文也是一样的!!!
6个标记位
tcp是面向链接的,tcp socket,要通信的时候,需要现connect,所以通信前,要先建立链接,
如何建立?
三次握手,也就三次数据交换,即交换三次报文!
server可能在任何一个时刻,都有可能有成百上千个报文在向server发送数据,就类似于一个店里
有直接来店里吃的顾客,也有点美团外卖的顾客,还有点饿了吗的顾客等等,那server首先面临的
是,面对大量的tcp报文,如何区分各个报文的类别!店服务员是通过衣服来进行区分顾客的,比
如你穿带有美团样式的衣服的,就是美团外卖,而server则是通过标记位来进行区分是什么类型的
报文,比如ACK是表示确认的标记位,SYN则是表示链接的标记位等等!
ACK:确认标记位
如下图,当server给client发送确认报文时,只需要将报文中的ACK标记位置为1即可!!!
SYN:发起链接
如下图,是建立链接,三次握手的过程,client向server发送链接请求的报文,server则回复
client链接报文且确认,表示同意链接,而如果ACK的值为0,则不同意链接,然后client则向
server发送确认报文,即链接成功!如同一男一女两人,男生对女生说:"做我女朋友吧!",女生
说:"好呀,什么时候开始呢?",男生说:"就现在!",表明两人情侣关系确立!
server存在大量的链接,那就需要管理——先描述,再组织!
建立链接的本质:三次握手成功,一定要在双方的OS内,为维护该链接创建对应的数据结构,而
双方维护链接是有成本的(时间+空间)
为什么是三次握手,而不是1、2、4、5次?
a.确定双方主机是否健康
b.验证全双工,三次握手,是能看到双方都有收发的最小次数!
如下图,client给server发送SYN,而且之后还收到了SYN+ACK,证明client具有收发数据的能
力,而对于server,收到了SYN,证明server有收数据的能力,但只有当client给server发送ACK
的时候,服务器端才能知道自己具有发数据的能力!毕竟没有回复的话,无法知道发的数据对方是
收到了,还是丢了!
4、5、6次握手等等,会过多的建立链接,会浪费过多的时间+空间成本!
第三个理由
如下图,对于1次握手,client给server不断地发server,server就得不断地给client建立链接,就
会浪费很多资源!因为发一次SYN,就建立一个链接!发来的SYN也被称为SYN洪水,对于client
建立链接销毁的成本太低;对于2次握手,和1次握手区别不大,也是在client给server发送SYN,
server发出应答后,就建立链接,消耗的资源是一样多的!对于3次握手,因为每建立一次链接,
client就会消耗和server一样的资源,对于4,5,6次握手,会浪费不必要的资源,所以3次握手是
最合理的!!!
不要以为三次握手就必须成功!而三次握手是以大概率成功建立的过程!而三次握手只用考虑最后
一次发的报文会不会丢,因为前两次发的报文都有响应!
如果client发送的确认报文丢了,而client认为链接已经建立成功了,就给server发送请求报文,
而server此时就会觉得很奇怪,链接都没成功,你怎么能给我你的请求呢,所以就发了一个重置异
常链接的报文,要求client重新建立链接,再给我发请求!
RST:重置异常链接的
只要是双方链接出现异常,都可以进行reset,来进行链接重置!
一般而言,双方握手成功,是有一个短暂的时间差的!!!
PUSH:告知对方,尽快将接受缓冲区中的数据进行向上交付
URG
目前,因为tcp有按序到达,每一个报文,什么时候被上层读取到基本是确定的!而如果想让一个
数据尽快的被上层读到,可以设置URG,表明该报文中携带了紧急数据,需要被优先处理,而要
传输的一个数据通过16位紧急指针找到
注意:tcp的紧急指针,只能传输一个字节!
如下图,如果想传输紧急数据,就可以将send函数中的参数flags设为MSG_OOB,接收也类似!
FIN:断开连接
一般而言,建立链接的一般是client,而断开连接是双方的事情,双发随时都有可能
四次挥手
如下图,client向发起断开链接的报文,server发送确认报文,此时,就断开了client向server发
送消息的渠道,同理,反过来也是一样!如同一对夫妻离婚,男:"我不想过了,字我签了",
女:"好的",女:"我也不想过了,字我也签了",男:"好的",以4次挥手的方式,达到链接关闭的一
致认识
为什么是四次挥手
断开链接本质:双方达成链接都应该断开的共识,就是一个通知对方的机制
四次挥手是协商断开链接的最小次数!!!
TIME_WAIT状态
主动断开链接的一方,要进入一个TIME_WAIT状态,此时四次挥手已经完成,但链接还没有被释
放,是为了保证让主动断开连接的一方最后发送的ACK被对方收到!!!
一旦进入TIME_WAIT,服务是无法立即重启的,也就会出现我们所看到的bind error!
为什么要有TIME_WAIT状态?
MSL:MSL是TCP报文的最大生存时间,也就是一个报文从一端到另一端所花费的时间
1、尽量保证历史发送的网络数据在网络中消散
TIME_WAIT状态,有2MSL的等待时间,而在网络中的数据,就可以不再卡在网络中,而是发送到
它的目的地
2、尽量的保证,最后一个ACK被对方收到
当主动断开链接的一方进入TIME_WAIT状态后,会有2MSL的等待时间,如果主动的一方最后发送
的ACK丢了,那另一方就会给它发送FIN,那主动的一方就可以再次发送ACK
bind error的原因
当主动断开链接的一方进入TIME_WAIT状态后,链接无法断开,所以端口也就被占用了,当你想
再次链接的时候,自然也就链接不了,因为一个端口只能被一个进程绑定
解决方式
进程虽然在等待,但是因为不需要发送数据了,所以端口也就不需要了,所以可以通过下面的接
口来重启,以此来让其它进程来绑定!
CLOSE_WAIT状态
如果只完成前面两次挥手,即服务器端的套接字不关闭,而此时客户端已经离开了,服务器就会进
入CLOSE_WAIT状态
启示:
一个fd被用完,千万不要忘记释放!因为fd是有限的,不释放,会出现fd泄漏问题!
序列号
缓冲区可以看成是一个大数组,而序列号就可以认为是数组的下标client,发送数据,就发送要发
送数据的最大坐标,比如下方,发送坐标1000之前的数据,即发送带有序号为1000的报文,
server就回复带有确认序号1001的报文,表明你下次发送的数据从1001开始发送
超时重传机制
当我们发送完对应的报文,发送方没有收到ACK,那就有两种可能,1、发送的数据报文丢了;
2、server发送的ACK报文丢了。解决方式,那就是重新发送报文,如果是发送的数据报文丢了,
重发没问题,但如果是ACK报文丢了,那就会重复,也就不可靠了!所以需要通过序列号来确定
重复的报文,对于重复的就丢弃掉
重传就要设置一个定时器,但如果超时时间设的太长, 会影响整体的重传效率,如果超时时间设的
太短, 有可能会频繁发送重复的包
时间间隔:网络是变化的,网络通信的效率是变化的,发送数据得到ACK报文时间也是浮动的,
所以超时重传的时间一定是浮动的!
如何设置定时器
Linux中(BSD Unix和Windows也是如此),超时以500ms为一个单位进行控制,每次判定超时重
发的超时时间都是500ms的整数倍.如果重发一次之后,仍然得不到应答,等待 2*500ms 后再进行
重传。如果仍然得不到应答,等待 4*500ms 进行重传。依次类推,以指数形式递增。累计到一定
的重传次数,TCP认为网络或者对端主机出现异常,强制关闭连接
注意
三次握手是双方的OS种,tcp协议自动完成的,用户完全不参与!!!
在tcp种,不要认为用户的发送行为,会直接影响tcp的发送逻辑
滑动窗口
因为数据在实际中,是不可能一发一收的形式发送的,这样效率太低了,所以可以一次性发送一批
数据,而一次给多少?所以就有了滑动窗口!
窗口大小指的是无需等待确认应答而可以继续发送数据的最大值
如下图,发送缓冲区可以分为三部分,如下,所以滑动窗口其实是发送缓冲区的一部分,是和对方
的接收能力有关,即与16位窗口大小有关!
1、已经发送,已经确认
2、可以/已经发送,但是还没有收到确认(可以暂时不要)
3、没有发送
当服务器端每发送确认一个报文,滑动窗口的左边框就会往右移动,而右边框会不会移动,与16位
窗口大小强相关,当服务器端接收缓冲区的数据被应用层读走后,16位窗口就会变大,所以滑动窗
口也会变大,而如果没有被应用层读走,那16位窗口就会变小,滑动窗口也会变小!所以滑动窗口
的大小不是一直不变的!
例如:当16位窗口大小为0后,当给客户端发送确认报文后,win_start会++,直到win_start =
win_end为止,也就是滑动窗口大小为0了;当服务器端的接收缓冲区的数据全部被应用层读走
后,win_end就会加上接收缓冲区的大小!
对于丢包情况,如何重传,这里分为两种情况
情况一: 数据包已经抵达, ACK被丢了
如果是前面或中间数据的ACK丢了,那也没多大关系,只要有后面的ACK就行了,因为确认序号
就表明前面的数据我都收到了,比如发了7000个报文,收到了5001的ACK和7001的ACK,5000-
6000的ACK丢了,但因为有7001,所以也客户端也就认为7001前发的数据全部被服务器端收到
了,这是确认序号的定义,而如果只有7001的ACK丢了,那客户端就进行超时重传,所以tcp允许
部分ACK丢失!
情况二:数据包就直接丢了(少量)
比如1001-2000的数据包丢了,那服务器端就会给客户端重复三次及以上的1001的确认应答,来告
知客户端,1000之后的数据包丢了,至于是多少,客户端无法得知,只能发1001-2000的数据包,
如果服务器端继续给客户端发送2001三次以上,那客户端就发2001-3000的数据包,不过如果是大
量的丢包,肯定不会这么干,效率太低了!这种对于服务器端发送三次以上重复应答,客户端就补
发数据的重传,高速重发控制(快重传)!
快重传与超时重传
快重传是有条件的,需要三次以上确认应答才行,而如果客户端只收到2个ACK,丢了一个,那客
户端也就只能超时重传,无法快重传,所以超时重传是给快重传兜底的,而快重传则是为了尽可能
提高效率的!
流量控制
接收端处理数据的速度是有限的,如果发送端发的太快,导致接收端的缓冲区被打满,这个时候如
果发送端继续发送,就会造成丢包,继而引起丢包重传等等一系列连锁反应
TCP支持根据接收端的处理能力(16位窗口大小),来决定发送端的发送速度(滑动窗口),这个机制
就叫做流量控制
第一次如何确定滑动窗口的大小?
取决于对方什么时候给我发送的第一个报文!而事实上,在三次握手时,我们就已经交互了!握手
期间,协商窗口大小!即根据对方的窗口大小来设置自己的滑动窗口的初始值!!!
如果我的接收缓冲区为0,怎么办?
当服务器端的16位窗口大小为0后,客户端的滑动窗口也会为0,所以此时tcp就支持两种策略,第
一个是客户端给服务器端发送窗口探测(携带PSH的报文),没有数据,只有报头,来询问服务器端
我是否能发送数据了,如果不能,服务器端也会给客户端发送16位窗口大小为0的报文,能的话,
就发送自身16位窗口大小的报文;第二个是当应用层把数据从接收缓冲区读走后,服务器端给客户
端发送自身16位窗口大小的报文!
那么TCP窗口最大就是65535字节么?
实际上, TCP首部40字节选项中还包含了一个窗口扩大因子M,实际窗口大小是 窗口字段的值左移
M 位!!!
拥塞控制
如下图,网络上有大量的计算机,可能当前的网络就比较拥堵,出现了大量的丢包情况,此时,就
不能采用重传机制,因为如果不清楚网络状态的话,贸然发送大量的数据,很可能会雪上加霜!所
以tcp引入了慢启动机制
慢启动机制: 先发少量的数据, 探探路, 摸清当前的网络拥堵状态, 再决定按照多大的速度传输数
据,所以又引入了拥塞窗口的概念!其实它就是一个数字!
发送开始的时候, 定义拥塞窗口大小为1,每次收到一个ACK应答, 拥塞窗口加1,滑动窗口 = min
(拥塞窗口,对方的窗口大小)
"慢启动" 只是指初始时慢, 但是增长速度非常快,因为是指数增长。初始时的阈值是对方的窗口大
小,阈值过后,就改为线性增长,当出现网络拥塞时,又重新将拥塞窗口置为1,按原来的方式增
长,只是阈值变为网络堵塞时拥塞窗口的一半!
延时应答
如果接收数据的主机立刻返回ACK应答,这时候返回的窗口可能比较小,所以如果应用层读取数
据比较快的话,就可以等一等,再做应答,这样滑动窗口也就会越大,, 网络吞吐量就越大, 传输效
率就越高!!!
延时应答有两种策略,一般N取2, 超时时间取200ms
数量限制: 每隔N个包就应答一次
时间限制: 超过最大延迟时间就应答一次
捎带应答
客户端服务器在应用层也是 "一发一收" 的,意味着客户端给服务器说了 "How are you",服务器
也会给客户端回一个 "Fine, thank you",ACK就可以搭顺风车,和服务器回应的 "Fine, thank
you" 一起回给客户端
例如:三次握手,实际上是四次握手,因为SYN+ACK其实是先发ACK,再发SYN的,但因为有捎
带应答,所以就一起发了!
面向字节流
如下图,在应用层将数据write到发送缓冲区后,内核有没有将数据发出去,是不一定的,因为这
与前面所说的窗口大小和拥塞窗口有关,而应用层read接收缓冲区的数据时,你是一次读10字
节,还是一次读100字节,接收缓冲区是不关心的,这就是面向字节流,两个缓冲区之间如同流水
一样传输数据,而不关心应用层是如何write/send和read/recv!!!就如同打开水龙头,你是用
桶接水,还是杯子接水,水龙头不关心
粘包问题
因为tcp是面向字节流的,而且没有udp一样的报文长度,而应用层在读数据的时候,无法知道从
哪里开始,到哪里结束是一个完整的报文!所以就有可能读取的一个报文中就多了下一个报文中的
信息,造成粘包问题!而tcp是无法解决的,需要应用层来解决!
解决方式
明确两个包之间的边界!!!
第一种策略,使用明确的分隔符\n,利用\n来一行一行读取,然后从报头中,找到
content-length,从而知道要读取多少字节数据!
第二种策略,可以在包头的位置, 约定一个包总长度的字段, 从而就知道了包的结束位置
udp不存在粘包问题
对于UDP,如果还没有上层交付数据,UDP的报文长度仍然在,同时,UDP是一个一个把数据交付给应用层,就有很明确的数据边界
站在应用层的站在应用层的角度,使用UDP的时候,要么收到完整的UDP报文,要么不收,不会
出现"半个"的情况
tcp异常情况
进程终止: 进程终止会释放文件描述符,仍然可以发送FIN,和正常关闭没有什么区别
机器重启: 和进程终止的情况相同
机器掉电/网线断开: 是一瞬间的事儿,接收端无法检测发送端状态,接收端认为连接还在,一旦接
收端有写入操作,接收端发现连接已经不在了,就会进行reset,即使没有写入操作,tcp自己也内
置了一个保活定时器,会定期询问对方是否还在,如果对方不在,也会把连接释放
应用层的某些协议, 也有一些这样的检测机制,例如当你QQ长时间挂着的时候,可能qq已经被断
开了,即图标变色了,当你鼠标移动,又会重新连接,这样是为了防止资源不必要的浪费!
基于tcp应用层协议
HTTP,HTTPS,SSH,Telnet,FTPS,MTP
tcp与udp的应用场景
tcp用于可靠传输的情况,应用于文件传输,重要状态更新等场景
udp用于对高速传输和实时性要求较高的通信领域,例如,早期的QQ,视频传输等,可用于广播
listen的第二个参数
如下图,对于服务器,listen 的第二个参数设置为 2,并且不调用accept
listen的第二个参数+1,就在tcp层建立正常连接的个数,当再有想链接服务器的客户端时,就会
被置为SYN_RECV状态!
注意:不要对上面所述理解为只能在服务器端维护几个链接!!!
Linux内核协议栈为一个tcp连接管理使用两个队列:
半链接队列(用来保存处于SYN_SENT和SYN_RECV状态的请求)
全连接队列(accpetd队列)(用来保存处于established状态,但是应用层没有调用accept取走
的请求),长度为listen的第二个参数+1
为什么要维护队列(全连接)?为什么这个队列不能太长?
以餐厅为例:
如下图,是一个餐厅布局图,当餐厅吃饭位置没有了的时候,同时又有新的客人到来,而餐厅不想
让流失这些客人,毕竟这也是一笔利润,就会有一个等待区,摆了一些座位,供暂时无法用餐的客
人休息,只要有用餐的人吃完饭了,那等待的人先到的人就能去用餐了,这样就可以保证,资源始
终是100%利用的,而不至于当有人走了的时候,用餐位置空缺,造成资源浪费!当然,如果人实
在太多,等待区位置都不够了,那就只能流失一部分客人了!
队列如果太长,那后来的客人等待的时间也会变长,可能会失去耐心,同时,队列太长,占地面积
也会大大增加,座椅成本也会增高,那还不如扩大用餐区的面积,增加座椅来让更多的人可以吃饭