admin管理员组

文章数量:1530085

目录

再谈协议

网络版计算器

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,一般所有的网站,都要有默认首页!

如下图,/a/b/c则是我们要请求的资源的具体路径

代码实现

首先在当前目录下再添加一个wwwroot目录,以及在其下创建一个.html文件,并编写内容

在http文件中定义两个宏,用来确定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具有收

发数据的能力,而对于server,收到了SYN,证明server知道了client有发数据的能力,但只有当

client给server发送ACK的时候,服务器端才能知道client具有发收数据的能力!毕竟没有回复的

话,无法知道发的数据对方是到了,还是丢了!

4、5、6次握手等等,会过多的建立连接,会浪费过多的时间+空间成本!

第三个理由

如下图,对于1次握手,client给server不断地发SYN,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断开连接的报文,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%利用的,而不至于当有人走了的时候,用餐位置空缺,造成资源浪费!当然,如果人实

在太多,等待区位置都不够了,那就只能流失一部分客人了!

队列如果太长,那后来的客人等待的时间也会变长,可能会失去耐心,同时,队列太长,占地面积

也会大大增加,座椅成本也会增高,那还不如扩大用餐区的面积,增加座椅来让更多的人可以吃饭

本文标签: 协议网络