目录
再谈协议
网络版计算器
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
像 / ” />
简化认识
如何理解普通用户的上网行为?
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的应用层协议
NFS: 网络文件系统 TFTP: 简单文件传输协议 DHCP: 动态主机配置协议 BOOTP: 启动协议(用于无盘设备启动) DNS: 域名解析协议 udp的socket,既能读,又能写,recvfrom,sendto可以被同时调用,这个概念被称为全双工, 类似于两人吵架,同时在说,且同时能 听到对方在说,而如果两人交叉式地说和听,也就可被称为 半双工! TCP 首部长度 tcp标准长度是20个字节! 4位首部长度的单位是4字节,而首部长度的范围是[0000,1111],所 以首部长度的大小为标准长度/4,也就是5,即0101,所以首部长度一般是0101 tcp如何做到封装,解包和分用,则和udp一样! tcp叫做保证可靠性:必须理解tcp中可靠性,最核心的机制:基于序号的确认应答机制!!! 第一层理解 如下图,当client给server发送消息时,如果server没有给响应,那么client就无法确认server是 否,所以当server响应后,也就是反馈后,client也就知道server收到了,通过应答,来保证上一 条信 息被对方100%收到了 只要有一条消息有应答,我们就能确认该消息被对方100% 收到了!!! 因为最新的一条消息不会有确认,所以tcp并不是100%可靠的! 第二层理解 如下图,client给server发送消息,server确认了,server给client发送消息,client确认了,双方 对 历史消息都做了应答机制,我们就保证历史数据被对方可靠收到,这种可靠性就是100%的!而 可靠性不仅仅是要对对方收到数据的可靠,也要对对方没有收到数据的可靠 第三层理解 如下图,发送的数据,也就是报文,可能不是单个的,而是一批的,发送的报文的顺序是1,2, 3,4,5,接收方收到的顺序不一定是1,2,3,4,5,而如果发送的报文的顺序和接收的报文的 数据不一致,那就不可靠了,因为顺序不一致可能误导对方!所以可靠性,除了保证被对方收到, 还要保证按序到达! 第四层理解 如何确认 tcp报头中涵盖一个叫做确认序号,是对历史确认报文的序号+1 收到确认应答tcp报文之后,可以通过确认序号,来辨别是对哪一个报文的确认,比如:13,13之 前的所有的报文我已经全部收到了,下次发送请从13号报文开始发送! 第五层理解 一个报文里面,既有序号,又有确认序号,为何是两个独立的字段,而不是只有序号? 只有序号,确实可以确认client发来的数据,以及发送数据,但tcp是一个全双工通信协议!也就是 说client和server,可能既在发送数据,又在收到数据! 双方通信的时候,一个报文,既可以携带要发送的数据,也可能携带对历史报文的确认!比如:你 室友约你去打篮球,但是你说天气太热了,别去打篮球了,还是去打乒乓球吧,既有对室友话的回 复,即对历史报 文的确认,又有自己的建议,即要发送的数据! 注意:无论是数据,还是应答,本质都是报文!我发的和我收的都是tcp完整的报文,可以不携带 数据, 但是一定要具有一个完整的tcp报头!
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字节么” />
慢启动机制:先发少量的数据, 探探路, 摸清当前的网络拥堵状态, 再决定按照多大的速度传输数
据,所以又引入了拥塞窗口的概念!其实它就是一个数字!
发送开始的时候, 定义拥塞窗口大小为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%利用的,而不至于当有人走了的时候,用餐位置空缺,造成资源浪费!当然,如果人实
在太多,等待区位置都不够了,那就只能流失一部分客人了!
队列如果太长,那后来的客人等待的时间也会变长,可能会失去耐心,同时,队列太长,占地面积
也会大大增加,座椅成本也会增高,那还不如扩大用餐区的面积,增加座椅来让更多的人可以吃饭