网络基础(二)
序列化和反序列化
假设客户端给服务端发送了一个消息,这个消息我们将其放到如下的结构体中,结构体如下所示:
struct message
{
std::string nick_name;
std::string image;
std::string message;
};
首先了解序列化(encode)和反序列化(decode):
问题:
1、直接发送同样的结构体对象是不可取的,虽然在某些情况(比如双方定制好了解析协议)下是可取的,它确实可以(需要进行序列化和反序列化)
2、我们在定制协议的时候,在序列化之后需要将长度放入序列化之后字符串的开始之前。
应用层
- 基本系统socket接口的使用
- 定制协议(协议既可以自己定制,也可以使用别人已经写好了,比如http、https、smtp、ftp、DNS)
- 编写业务(完成什么功能,比如网络版本的计算器完成的就是对客户输入的表达式进行计算的功能,这就是业务)
HTTP协议
认识URL
平时我们俗称的 “网址” 其实就是说的 URL。
http://user:pass@www.example.jp:8080/dir/index.htm?uid=1#ch1
http: 协议名
user:pass: 登录信息(认证)
www.example.jp: 域名, 必须转换成为IP
8080: 服务器端口号(使用确定协议的时候,会缺省端口号。注意:缺省端口号并不是说明不发送端口号,只是在URL链接中没有端口号,在发送链接请求时必须添加端口号)
所以,浏览器在访问指定的URL的时候,必须给我们自动添加port。
问:浏览器如何得知,url匹配的port是谁?
答:特定的,众所周知的服务,端口号是确定的。[0, 1023]就是特定的端口号,我们无法绑定这些,自己进行绑定的时候只能设置1024即1024之后的端口号。
/dir/index.htm: 该文件就是我们访问的网页文件在服务器上的所在路径(注意:第一个/并是根目录,而是web根目录,但是web根目录可以设置为根目录)
uid=1: 查询字符串
ch1: 片段标识符
问:http是做什么的?
答:http是向特定的服务器申请特定的“资源”,获取到本地,进行展示或者某种使用的。
问:如果我们client没有获取的时候,资源在哪里呢?
答:就在网络服务器(软件)所在的服务器上(硬件,计算机上)。当用户想要查看某个文件时,服务器要将这个文件加载到服务器的内存上将其打开,然后通过相应的接口才能发送给用户。
urlencode和urldecode
像 / ? : 等这样的字符, 已经被url当做特殊意义理解了. 因此这些字符不能随意出现。比如, 某个参数中需要带有这些特殊字符, 就必须先对特殊字符进行转义。
举例:
转义的规则如下:
将需要转码的字符转为16进制,然后从右到左,取4位(不足4位直接处理),每2位做一位,前面加上%,编码成%XY格式
urlencode:编码,例如在上面的例子中,我们输入的时C++,但是浏览器却将其编码成为了C%2B%2B
urldecode:解码,例如在上面的例子中,服务端会将C%2B%2B解码成为C++。
HTTP协议格式
HTTP请求:
- 首行: [方法] + [url] + [版本]
- Header: 请求的属性, 冒号分割的键值对;每组属性之间使用\n分隔;遇到空行表示Header部分结束
- Body: 空行后面的内容都是Body. Body允许为空字符串. 如果Body存在, 则在Header中会有一个 Content-Length属性来标识Body的长度;
HTTP响应:
- 首行: [版本号] + [状态码] + [状态码解释]
- Header: 请求的属性, 冒号分割的键值对;每组属性之间使用\n分隔;遇到空行表示Header部分结束
- Body: 空行后面的内容都是Body. Body允许为空字符串. 如果Body存在, 则在Header中会有一个 Content-Length属性来标识Body的长度; 如果服务器返回了一个html页面, 那么html页面内容就是在 body中
任何协议的request或者response都是由报头和有效载荷构成的。
问:在请求或者相应中,http如何保证自己的报头和有效载荷被全部读取呢?
答:
1、读取完整报头:按行读取,直到读取到空行为止。
2、请求或者相应属性中,“一定”要包含正文的长度。
了解telnet命令的使用(举例)
telnet www.baidu.com 80 //80是端口号
Escape character is '^]'. //此时应该输入ctrl + ],然后输入回车
GET /index.html HTTP/1.1 //输入左边的命令后回车即可获取百度首页(其实也就是请求行的内容)
//退出的时候按quit即可
图例:
HTTP的方法
我们的网络行为总体来说总共分为两种:
- 我们想把远端的资源拿到本地:GET /index.html http/1.1
- 我们想把我们的属性字段,提交到远端(GET/POST)
GET VS POST
GET方法
在HTTP中GET方法会以明文即URL的方式将我们对应的参数信息,拼接到URL中
POST方法
post方法提交参数,会将参数以明文的方式,拼接到http的正文中来进行提交
总结:
1、GET通过URL传参,POST通过正文传参
2、GET方法传参不私密,POST方法因为通过正文传参,所以相对比较私密一些
3、GET通过URL传参,POST方法通过正文传参,所以一般比较大的内容都是通过POST传参,同时URL没有类型字段,而正文有正文字段,所以通过正文传参即POST的方式相对更加私密一些。
其它方法
其中最常用的就是GET和POST方法。
HTTP的状态码
HTTP 状态码由三个十进制数字组成,第一个十进制数字定义了状态码的类型。响应分为五类:信息响应(100–199),成功响应(200–299),重定向(300–399),客户端错误(400–499)和服务器错误 (500–599):
最常见的状态码, 比如 200(OK), 404(Not Found), 403(Forbidden), 301(永久重定向),302(Redirect, 临时重定向), 504(Bad Gateway)
注意:无论是临时还是永久重定向,本质都是客户端做的,服务端发送了响应的重定向状态码,作为客户端的浏览器收到该状态码识别出是重定向状态码后自动进行跳转。
问:当用户请求一个资源,例如a.html,但是服务端并没有这个资源,HTTP返回的状态码应该是4开头的还是5开头的,即这是客户端的错误还是服务端的问题?
答:是客户端的问题。除此之外,客户端的请求出现语法错误也都是客户端的错误。
问:什么情况下是属于服务端错误?
答:服务器上的比如创建多线程失败或者进程挂掉了,无法再继续给客户端提供服务了,此时就属于服务端错误。
HTTP常见Header
Content-Type: 数据类型(text/html等)
Content-Length: Body的长度
Host: 客户端告知服务器, 所请求的资源是在哪个主机的哪个端口上(一般就是IP地址+端口号);
User-Agent: 声明用户的操作系统和浏览器版本信息(给用户进行适配并且提供不同的资源);
referer: 当前页面是从哪个页面跳转过来的; (从哪里来)
Location: 搭配3xx状态码使用, 告诉客户端接下来要去哪里访问;(到哪里去)
Cookie: 用于在客户端存储少量信息. 通常用于实现会话(session)的功能
http协议特点之一: 无状态,即用户所有资源请求的行为,我们对应的HTTP协议本身并不会做任何的记录,也不会对用户访问资源进行限制。当然,HTTP协议可以采取一些手段来维护浏览痕迹,或者控制用户访问资源的权限。
一旦登陆,会有各种会话保持的策略,方便我们下次打开网页时自动登录,这是由浏览器和服务端共同为我们实现的,即cookie策略。
上面的这种方式是不安全的,一旦cookie信息或者文件被盗取,用户的个人信息就会被泄漏,用户的账号也会被盗用人随意登录,所以我们一般采用cookie+session的方式:
Connection:这个字段只在HTTP1.1协议中存在。它决定了客户端和服务器进行了一次会话后,服务器是否立即关闭网络连接。在客户端最直接的表现是使用read方法(readLine方法也是一样)读完客户端请求的Web资源后,是否立即返回-1(readLine返回null)。Connection有两个值:Close和Keep-Alive。当使用Connection:Close时,和HTTP1.0协议是一样的,当read方法读完数据时立即返回;而使用Connection:Keep-Alive时,read方法在读完数据后还要被阻塞一段时间。直接读取数据超时时间过后,还继续往下执行。(Keep-Alive就是长连接,Close就是短连接)
HTTP协议的另一个特点:短连接,即一次只处理一个HTTP请求,由于这种方案在每一次请求和回应的时候都要进行一次三次握手和四次挥手,所以现在HTTP1.1版本都变成了长连接,即在一次三次握手成功后,客户端一个tcp链接里可以有多个HTTP请求,服务端在收到这些请求之后进行响应。
总结HTTP协议:超文本传输协议,是一个无链接无状态的应用层协议。为了使响应和请求相匹配,需要运用到pipeline技术,即对请求做出有序的响应。
HTTPS协议
HTTPS 是什么
注意:所有的加密,都是为了防止中间有人篡改数据。
HTTPS 也是⼀个应⽤层协议. 是在 HTTP 协议的基础上引⼊了⼀个加密层. HTTP 协议内容都是按照⽂本的⽅式明⽂传输的. 这就导致在传输过程中出现⼀些被篡改的情况
1. 什么是"加密"
加密就是把明⽂(要传输的信息)进⾏⼀系列变换, ⽣成密⽂ .
解密就是把密⽂再进⾏⼀系列变换, 还原成明⽂ .
在这个加密和解密的过程中, 往往需要⼀个或者多个中间的数据, 辅助进⾏这个过程, 这样的数据称为密钥
2. 为什么要加密
因为http的内容是明⽂传输的,明⽂数据会经过路由器、wifi热点、通信服务运营商、代理服务 器等多个物理节点,如果信息在传输过程中被劫持,传输的内容就完全暴露了。劫持者还可以篡改传 输的信息且不被双⽅察觉,这就是中间⼈攻击
,所以我们才需要对信息进⾏加密。
HTTPS 就是在 HTTP 的基础上进⾏了加密, 进⼀步的来保证⽤⼾的信息安全。
3. 常⻅的加密⽅式
对称加密
- 采⽤单钥密码系统的加密⽅法,同⼀个密钥可以同时⽤作信息的加密和解密,这种加密⽅法称为对称加密,也称为单密钥加密,特征:加密和解密所⽤的密钥是相同的
- 常⻅对称加密算法(了解):DES、3DES、AES、TDEA、Blowfish、RC2等
- 特点:算法公开、计算量⼩、加密速度快、加密效率⾼
⼀个简单的对称加密, 按位异或
假设 明⽂ a = 521, 密钥 key = 1314
则加密 a ^ key 得到的密⽂ b 为 1835
然后针对密⽂ 1835 再次进⾏运算 b ^ key, 得到的就是原来的明⽂ 521
(对于字符串的对称加密也是同理, 每⼀个字符都可以表⽰成⼀个数字) 当然, 按位异或只是最简单的对称加密. HTTPS 中并不是使⽤按位异或
非对称加密
- 需要两个密钥来进⾏加密和解密,这两个密钥是公开密钥(public key,简称公钥)和私有密钥 (private key,简称私钥)。
- 常⻅⾮对称加密算法(了解):RSA,DSA,ECDSA
- 特点:算法强度复杂、安全性依赖于算法与密钥但是由于其算法复杂,⽽使得加密解密速度没有对称加密解密的速度快。
⾮对称加密要⽤到两个密钥, ⼀个叫做 “公钥”, ⼀个叫做 “私钥”
公钥和私钥是配对的. 最⼤的缺点就是运算速度⾮常慢,⽐对称加密要慢很多
- 通过公钥对明⽂加密, 变成密⽂
- 通过私钥对密⽂解密, 变成明⽂
也可以反着⽤
- 通过私钥对明⽂加密, 变成密⽂
- 通过公钥对密⽂解密, 变成明⽂
⾮对称加密的数学原理⽐较复杂, 涉及到⼀些 数论 相关的知识. 这⾥举⼀个简单的⽣活上的例⼦
A 要给 B ⼀些重要的⽂件, 但是 B 可能不在. 于是 A 和 B 提前做出约定:
B 说: 我桌⼦上有个盒⼦, 然后我给你⼀把锁, 你把⽂件放盒⼦⾥⽤锁锁上, 然后我回头拿着钥匙来开锁 取⽂件
在这个场景中, 这把锁就相当于公钥, 钥匙就是私钥,公钥给谁都⾏(不怕泄露), 但是私钥只有 B ⾃⼰持有。持有私钥的⼈才能解密.
4. 数据摘要 && 数据指纹
- 数字指纹(数据摘要),其基本原理是利⽤单向散列函数(Hash函数)对信息进⾏运算,⽣成⼀串固定⻓度 的数字摘要。数字指纹并不是⼀种加密机制,但可以⽤来判断数据有没有被窜改。
- 摘要常⻅算法:有MD5、SHA1、SHA256、SHA512等,算法把⽆限的映射成有限,因此可能会有 碰撞(两个不同的信息,算出的摘要相同,但是概率⾮常低)
- 摘要特征:和加密算法的区别是,摘要严格意义不是加密,因为没有解密,只不过从摘要很难反推原信息,通常⽤来进⾏数据对⽐
举例:
比如百度云盘上面的秒传就是因为百度云盘服务端发现我们上传资源的数据摘要和服务端已有的数据库中某个资源的数据摘要是完全一样的,所以百度云盘就不需要上传我们的文件了,而只是为我们的个人用户维护一个数据摘要即可,当我们想要从百度云盘上下载资源的时候,通过数据摘要即可找到我们想要下载的资源即可进行下载。
5. 数字签名
摘要经过过加密,就得到数字签名
6. 理解链 - 承上启下
- 对http进⾏对称加密,是否能解决数据通信安全的问题?问题是什么?
- 为何要⽤⾮对称加密?为何不全⽤⾮对称加密?
HTTPS 的⼯作过程探究
既然要保证数据安全, 就需要进⾏ “加密”。⽹络传输中不再直接传输明⽂了, ⽽是加密之后的 “密⽂”。加密的⽅式有很多, 但是整体可以分成两⼤类: 对称加密和⾮对称加密。
方案 1 - 只使用对称加密
如果通信双⽅都各⾃持有同⼀个密钥X,且没有别⼈知道,这两⽅的通信安全当然是可以被保证的(除非密钥被破解)
引⼊对称加密之后, 即使数据被截获, 由于⿊客不知道密钥是啥, 因此就⽆法进⾏解密, 也就不知道请求 的真实内容是啥了
但事情没这么简单. 服务器同⼀时刻其实是给很多客⼾端提供服务的. 这么多客户端, 每个⼈⽤的秘钥都 必须是不同的(如果是相同那密钥就太容易扩散了, ⿊客就也能拿到了)。因此服务器就需要维护每个客户端和每个密钥之间的关联关系, 这也是个很麻烦的事情。
⽐较理想的做法, 就是能在客户端和服务器建⽴连接的时候, 双⽅协商确定这次的密钥是什么
但是如果直接把密钥明⽂传输, 那么⿊客也就能获得密钥了。此时后续的加密操作就形同虚设了。
因此密钥的传输也必须加密传输!
方案 2 - 只使用非对称加密
鉴于⾮对称加密的机制,如果服务器先把公钥以明⽂⽅式传输给浏览器,之后浏览器向服务器传数据 前都先⽤这个公钥加密好再传,从客户端到服务器信道似乎是安全的(有安全问题),因为只有服务器有相应的私钥能解开公钥加密的数据。
但是服务器到浏览器的这条路怎么保障安全?
如果服务器⽤它的私钥加密数据传给浏览器,那么浏览器⽤公钥可以解密它,⽽这个公钥是⼀开始通过明⽂传输给浏览器的,若这个公钥被中间⼈劫持到了,那他也能⽤该公钥解密服务器传来的信息了。
方案 3 - 双方都使用非对称加密
- 服务端拥有公钥S与对应的私钥S’,客⼾端拥有公钥C与对应的私钥C’
- 客⼾和服务端交换公钥
- 客⼾端给服务端发信息:先⽤S对数据加密,再发送,只能由服务器解密,因为只有服务器有私钥 S’
- 服务端给客户端发信息:先⽤C对数据加密,在发送,只能由客户端解密,因为只有客户端有私钥 C’
问题:
- 效率太低
- 依旧有安全问题
方案 4 - 非对称加密 + 对称加密
-
服务端具有非对称公钥S和私钥S’
-
客户端发起https请求,获取服务端公钥S
-
客户端在本地⽣成对称密钥C, 通过公钥S加密, 发送给服务器.
-
由于中间的⽹络设备没有私钥, 即使截获了数据, 也⽆法还原出内部的原⽂, 也就⽆法获取到对称密钥(真的吗?)
-
服务器通过私钥S’解密, 还原出客⼾端发送的对称密钥C. 并且使⽤这个对称密钥加密给客⼾端返回的响应数据.
-
后续客户端和服务器的通信都只⽤对称加密即可. 由于该密钥只有客户端和服务器两个主机知道, 其 他主机/设备不知道密钥即使截获数据也没有意义
由于对称加密的效率⽐⾮对称加密⾼很多, 因此只是在开始阶段协商密钥的时候使⽤⾮对称加密, 后续的传输仍然使⽤对称加密
虽然上⾯已经⽐较接近答案了,但是依旧有安全问题
⽅案 2,⽅案 3,⽅案4都存在⼀个问题,如果最开始,中间⼈就已经开始攻击了呢?
中间⼈攻击 - 针对上⾯的场景
- Man-in-the-MiddleAttack,简称“MITM攻击”
确实,在⽅案2/3/4中,客户端获取到公钥S之后,对客⼾端形成的对称秘钥X⽤服务端给客户端的公钥 S进⾏加密,中间⼈即使窃取到了数据,此时中间⼈确实⽆法解出客户端形成的密钥X,因为只有服务器有私钥S’
但是中间⼈的攻击,如果在最开始握⼿协商的时候就进⾏了,那就不⼀定了,假设hacker已经成功成 为中间⼈
- 服务器具有⾮对称加密算法的公钥S,私钥S’
- 中间⼈具有⾮对称加密算法的公钥M,私钥M’
- 客⼾端向服务器发起请求,服务器明⽂传送公钥S给客户端
- 中间⼈劫持数据报⽂,提取公钥S并保存好,然后将被劫持报⽂中的公钥S替换成为⾃⼰的公钥M, 并将伪造报⽂发给客户端
- 客户端收到报⽂,提取公钥M(⾃⼰当然不知道公钥被更换过了),⾃⼰形成对称秘钥X,⽤公钥M加 密X,形成报⽂发送给服务器
- 中间⼈劫持后,直接⽤⾃⼰的私钥M’进⾏解密,得到通信秘钥X,再⽤曾经保存的服务端公钥S加 密后,将报⽂推送给服务器
- 服务器拿到报⽂,⽤⾃⼰的私钥S’解密,得到通信秘钥X
- 双⽅开始采⽤X进⾏对称加密,进⾏通信。但是⼀切都在中间⼈的掌握中,劫持数据,进⾏窃听甚⾄修改,都是可以的
图解:
上⾯的攻击⽅案,同样适⽤于⽅案2,⽅案3 问题本质出在哪⾥了呢?客户端⽆法确定收到的含有公钥的数据报⽂,就是⽬标服务器发送过来的!
引入证书
CA认证
服务端在使⽤HTTPS前,需要向CA机构申领⼀份数字证书,数字证书⾥含有证书申请者信息、公钥信息等。服务器把证书传输给浏览器,浏览器从证书⾥获取公钥就⾏了,证书就如⾝份证,证明服务端公钥的权威性
这个 证书 可以理解成是⼀个结构化的字符串, ⾥⾯包含了以下信息:
- 证书发布机构
- 证书有效期
- 公钥
- 证书所有者
- 签名
- …
理解数据签名
签名的形成是基于⾮对称加密算法的,注意,⽬前暂时和https没有关系,不要和https中的公钥私钥搞 混了
给数据文档进行数据签名的意义: 防止内容被篡改。
当服务端申请CA证书的时候,CA机构会对该服务端进⾏审核,并专⻔为该⽹站形成数字签名,过程如 下:
- CA机构拥有⾮对称加密的私钥A和公钥A’
- CA机构对服务端申请的证书明⽂数据进⾏hash,形成数据摘要
- 然后对数据摘要⽤CA私钥A’加密,得到数字签名S
服务端申请的证书明⽂和数字签名S共同组成了数字证书,这样⼀份数字证书就可以颁发给服务端了
方案 5 - 非对称加密 + 对称加密 + 证书认证
在客户端和服务器刚⼀建⽴连接的时候, 服务器给客户端返回⼀个证书,证书包含了之前服务端的公钥, 也包含了⽹站的⾝份信息.
客户端进行认证
当客户端获取到这个证书之后, 会对证书进⾏校验(防⽌证书是伪造的)
- 判定证书的有效期是否过期
- 判定证书的发布机构是否受信任(操作系统中已内置的受信任的证书发布机构).
- 验证证书是否被篡改: 从系统中拿到该证书发布机构的公钥, 对签名解密, 得到⼀个 hash 值(称为数据摘要), 设为 hash1. 然后计算整个证书的 hash 值, 设为 hash2. 对⽐ hash1 和 hash2 是否相等. 如果相等, 则说明证书是没有被篡改过的.
查看EDGE浏览器的受信任证书发布机构
常见问题
1、中间人有没有可能篡改该证书?
- 中间⼈篡改了证书的明⽂
- 由于他没有CA机构的私钥,所以⽆法hash之后⽤私钥加密形成签名,那么也就没法办法对篡改后的证书形成匹配的签名
- 如果强行篡改,客⼾端收到该证书后会发现明⽂和签名解密后的值不⼀致,则说明证书已被篡改, 证书不可信,从⽽终⽌向服务器传输信息,防止信息泄露给中间⼈
2、中间⼈整个掉包证书?
- 因为中间⼈没有CA私钥,所以⽆法制作假的证书(为什么?)
- 所以中间⼈只能向CA申请真证书,然后⽤⾃⼰申请的证书进⾏掉包
- 这个确实能做到证书的整体掉包,但是别忘记,证书明⽂中包含了域名等服务端认证信息,如果整 体掉包,客户端依旧能够识别出来。
- 永远记住:中间⼈没有CA私钥,所以对任何证书都⽆法进⾏合法修改,包括⾃⼰的
3、为什么摘要内容在网络传输的时候⼀定要加密形成签名?
常⻅的摘要算法有: MD5 和 SHA 系列
以 MD5 为例, 我们不需要研究具体的计算签名的过程, 只需要了解 MD5 的特点:
- 定⻓: ⽆论多⻓的字符串, 计算出来的 MD5 值都是固定⻓度 (16字节版本或者32字节版本)
- 分散: 源字符串只要改变⼀点点, 最终得到的 MD5 值都会差别很⼤
- 不可逆: 通过源字符串⽣成 MD5 很容易, 但是通过 MD5 还原成原串理论上是不可能的
正因为 MD5 有这样的特性, 我们可以认为如果两个字符串的 MD5 值相同, 则认为这两个字符串相同
假设我们的证书只是⼀个简单的字符串 hello, 对这个字符串计算hash值(⽐如md5), 结果为 BC4B2A76B9719D91
如果 hello 中有任意的字符被篡改了, ⽐如变成了 hella, 那么计算的 md5 值就会变化很⼤. BDBD6F9CF51F2FD8
然后我们可以把这个字符串 hello 和 哈希值 BC4B2A76B9719D91 从服务器返回给客户端, 此时客⼾端 如何验证 hello 是否是被篡改过?
那么就只要计算 hello 的哈希值, 看看是不是 BC4B2A76B9719D91 即可
但是还有个问题, 如果⿊客把 hello 篡改了, 同时也把哈希值重新计算下, 客户端就分辨不出来了
所以被传输的哈希值不能传输明⽂, 需要传输密⽂
所以,对证书明⽂(这⾥就是“hello”)hash形成散列摘要,然后CA使⽤⾃⼰的私钥加密形成签名,将 hello和加密的签名合起来形成CA证书,颁发给服务端,当客⼾端请求的时候,就发送给客⼾端,中间⼈截获了,因为没有CA私钥,就⽆法更改或者整体掉包,就能安全的证明,证书的合法性。
最后,客⼾端通过操作系统⾥已经存的了的证书发布机构的公钥进⾏解密, 还原出原始的哈希值, 再进 ⾏校验.
4、为什么签名不直接加密,而是要先hash形成摘要?
缩小签名密⽂的⻓度,加快数字签名的验证签名的运算速度
5、如何成为中间⼈
- ARP欺骗:在局域⽹中,hacker经过收到ARP Request⼴播包,能够偷听到其它节点的 (IP, MAC) 地址。例, ⿊客收到两个主机A, B的地址,告诉B (受害者) ,⾃⼰是A,使得B在发送给A 的数据包 都被黑客截取
- ICMP攻击:由于ICMP协议中有重定向的报⽂类型,那么我们就可以伪造⼀个ICMP信息然后发送给 局域⽹中的客⼾端,并伪装⾃⼰是⼀个更好的路由通路。从⽽导致⽬标所有的上⽹流量都会发送到 我们指定的接⼝上,达到和ARP欺骗同样的效果
- 假wifi && 假网站等
完整流程
总结
HTTPS ⼯作过程中涉及到的密钥有三组
第一组(非对称加密):用于校验证书是否被篡改.服务器持有私钥(私钥在形成CSR文件与申请证书时获得),客户端持有公钥(操作系统包含了可信任的CA认证机构有哪些,同时持有对应的公钥).服务器在客户端请求时,返回携带签名的证书.客户端通过这个公钥进行证书验证,保证证书的合法性,进一-步保证证书中携带的服务端公钥权威性。
第二组(非对称加密):用于协商生成对称加密的密钥.客户端用收到的CA证书中的公钥(是可被信任的)给随机生成的对称加密的密钥加密,传给服务器,服务器通过私钥解密获取到对称加密密钥。
第三组(对称加密):客户端和服务器后续传输的数据都通过这个对称密钥加密解密。
其实⼀切的关键都是围绕这个对称加密的密钥. 其他的机制都是辅助这个密钥⼯作的:
第⼆组⾮对称加密的密钥是为了让客户端把这个对称密钥传给服务器.
第⼀组⾮对称加密的密钥是为了让客户端拿到第⼆组⾮对称加密的公钥
传输层
负责数据能够从发送端可靠传输接收端。
再谈端口号
端口号(Port)标识了一个主机上进行通信的不同的应用程序;
在TCP/IP协议中, 用 “源IP”, “源端口号”, “目的IP”, “目的端口号”, “协议号” 这样一个五元组来标识一个通信(可以通过 netstat -n查看);
端口号范围划分
- 0 - 1023: 知名端口号, HTTP, FTP, SSH等这些广为使用的应用层协议, 他们的端口号都是固定的
- 1024 - 65535: 操作系统动态分配的端口号. 客户端程序的端口号, 就是由操作系统从这个范围分配的
认识知名端口号(Well-Know Port Number)
有些服务器是非常常用的, 为了使用方便, 人们约定一些常用的服务器, 都是用以下这些固定的端口号
- ssh服务器, 使用22端口
- ftp服务器, 使用21端口
- telnet服务器, 使用23端口
- http服务器, 使用80端口
- https服务器, 使用443
执行下面的命令, 可以看到知名端口号
cat /etc/services
我们自己写一个程序使用端口号时, 要避开这些知名端口号
两个问题
- 一个进程是否可以bind多个端口号? 可以的,比如一个进程中可以让一个端口号是用来发送数据的,一个端口号是用来发送指令的
- 一个端口号是否可以被多个进程bind? 不可以的,因为一个端口号只能指向一个进程,这是用类似map来进行映射的。
常用命令
netstat
netstat是一个用来查看网络状态的重要工具
语法:netstat [选项]
功能:查看网络状态
常用选项:
- n 拒绝显示别名,能显示数字的全部转化成数字(num)
- l 仅列出有在 Listen (监听) 的服务状态 (listen)
- p 显示建立相关链接的程序名 (可以查看PID和程序名,但是如果想要查看的话必须提升权限)
- t (tcp)仅显示tcp相关选项 (tcp)
- u (udp)仅显示udp相关选项 (udp)
- a (all)显示所有选项,默认不显示LISTEN相关(all)
pidof
在查看服务器的进程id时非常方便
语法:pidof [进程名]
功能:通过进程名, 查看进程id
使用举例:
UDP协议
UDP协议格式
问:UDP协议是如何封装和解包的?
答:封装:在数据(有效载荷)的前面添加8个字节的UDP报头。解包:将收到的UDP报文中提取前8个字节,这8个字节就是UDP报头,剩下的就是有效载荷。
问:UDP协议是如何实现分用的?
答:当收到完整的UDP报文后,先将有效载荷和报头进行分离,然后提取16位目的端口号,服务端会根据目的端口号决定交付给上层的哪个协议。同时,也能根据端口号和进程PID的映射关系找到该端口号对应的进程,进而找到对应的文件描述符,然后将收到的内容写到文件描述符对应的文件缓冲区中,至此,该进程就能通过文件的形式来读取收到的内容了。
网络协议栈的tcp/ip协议是在内核中实现的,而内核是用C语言实现的,而UDP报头是用C语言的结构体实现的:
struct udp_hdr { unsigned int src_port:16; unsigned int dst_port:16; unsigned int udp_len:16; unsigned int udp_check:16; };
添加报头的本质,就是将struct udp_hdr类型的结构体对象拷贝到报文的前面。
16位UDP长度, 表示整个数据报(UDP首部+UDP数据)的最大长度;
如果校验和出错, 就会直接丢弃;
UDP的特点
UDP传输的过程类似于寄信
- 无连接: 知道对端的IP和端口号就直接进行传输, 不需要建立连接;
- 不可靠: 没有确认机制, 没有重传机制; 如果因为网络故障该段无法发到对方, UDP协议层也不会给应用层 返回任何错误信息;
- 面向数据报: 不能够灵活的控制读写数据的次数和数量
面向数据报
应用层交给UDP多长的报文, UDP原样发送, 既不会拆分, 也不会合并; 用UDP传输100个字节的数据:
如果发送端调用一次sendto, 发送100个字节, 那么接收端也必须调用对应的一次recvfrom, 接收100个 字节; 而不能循环调用10次recvfrom, 每次接收10个字节;
与面向数据报相对应的就是TCP的面向字节流:
发送端一次发送100个字节,接收端可以一次读完,发送端发送10次,一次是10个字节,接收端可以一次读取100个字节。
UDP的缓冲区
树立一个观念:对文件的任何写入操作都是拷贝操作,我们无法直接的向文件进行写入,当我们调用write或者send系统调用接口的时候,实际上我们并没有把数据发送到网络里或者把数据写到在内存上打开的文件上,而是我们把数据交给了操作系统,即将数据拷贝到操作系统对应的内核缓冲区中,再由操作系统根据自己的策略来决定何时将数据刷新到磁盘文件或者发送到网络中去。
同样的,对文件的读取操作也是如此,在读文件时,我们一般会在栈或者堆上定义一个缓冲区,与之对应的,其实在内核中也会有相应的缓冲区,也就是进程PCB在Linux中的task_struct中有对应的file结构体中有对应的缓冲区,在读取文件时,操作系统会先将数据放入自己的内核缓冲区,然后拷贝到我们用户自己定义的用户缓冲区中。
UDP没有真正意义上的发送缓冲区. 调用sendto会直接交给内核, 由内核将数据传给网络层协议进行后续的传输动作;
UDP具有接收缓冲区. 但是这个接收缓冲区不能保证收到的UDP报的顺序和发送UDP报的顺序一致; 如果缓冲区满了, 再到达的UDP数据就会被丢弃
UDP使用注意事项
我们注意到, UDP协议首部中有一个16位的最大长度. 也就是说一个UDP能传输的数据最大长度是64K(包含UDP首部).
然而64K在当今的互联网环境下, 是一个非常小的数字. 如果我们需要传输的数据超过64K, 就需要在应用层手动的分包, 多次发送, 并在接收端手动拼装;
基于UDP的应用层协议
- NFS: 网络文件系统
- TFTP: 简单文件传输协议
- DHCP: 动态主机配置协议
- BOOTP: 启动协议(用于无盘设备启动)
- DNS: 域名解析协议
当然, 也包括我们自己写UDP程序时自定义的应用层协议。
TCP协议
TCP通信原理
TCP报头
TCP全称为 “传输控制协议(Transmission Control Protocol”),也就是进行传输控制的协议,主要解决的是传输中的可靠性的问题,同时也解决传输效率的问题。
首部长度
首部长度标识的是TCP报头的长度,单位是4个字节,不包含数据的长度,所以TCP报头的范围是[0, 60]字节。
问:如何解包?
答:四位首部长度的范围:0000 ~ 1111,即[0,15],首部长度是有单位的,单位是4个字节,即长度范围是[0,60]字节。而我们的首部至少是20字节,那么它的首部长度至少是0101。
问:为什么TCP协议报头中没有首部长度?
答:因为TCP协议是面向字节流的,TCP不需要关心数据的长度,只有应用层需要关心数据的长度,TCP协议只需要把数据放在对应的缓冲区即可,其它的不需要关心。
32位序号
TCP协议层操作系统在将消息发出的时候,受限于TCP报文大小的限制,所以操作系统会将所要发送的消息进行划分并且标识序号,如下所示:
如图所示,图中的1、2和3就是我们划分的序号,也就是TCP报头中的32位序号。
问:32位序号的起始序号一定是从1开始的吗?
答:不一定,该起始序号往往是随机生成的,防止的就是中间有人给另一方发送序号是正确的但是内容是完全错误的消息。例如:随机生成的起始序号是12,但是该消息被分为了1000个TCP报文,所以该1000个报文的序号就是从12到1012。
问:当序号出现溢出会怎么办?
答:每次生成序号之后,都会堆序号进行取模操作,也就是会回绕,类似环形队列。
32位确认序号
表示该确认序号之前的报文都已经收到。例如:
问:为什么TCP报文中既有序号和确认序号?
答:因为TCP协议是全双工的,即客户端在给服务端发送消息的同时,服务端也在给客户端发送消息,所以既要有序号和确认序号,保证服务端在回复确认客户端消息的同时也能给客户端发送消息(发送序号和确认序号)。这种机制叫作确认应答机制。
6位标志位
SYN(同步):请求建立连接,SYN需要被设置为1; 我们把携带SYN标识的称为同步报文段
FIN(结束):通知对方, 本端要关闭了, 我们称携带FIN标识的为结束报文段
ACK(确认):确认标记位,表示该报文是对历史报文的确认(并不妨碍携带其它信息,这个确认只是捎带的,并且在一般大部分正式通信的情况下,ACK都是1)
PSH(敦促):提示接收端应用程序立刻从TCP缓冲区把数据读走(这种情况一般是接收端的读取条件不满足所以接收端长时间没有进行读取,发送端长时间无法发送数据,此时发送端就会发送一个PSH位为1的消息给服务端)
URG: 紧急指针是否有效(urgent)
如果数据是必须在TCP中进行按序到达的话,也就是说,如果有一些数据优先级更高,但是序号较晚,无法做到数据被优先紧急读取,但是如果我们将该报文的TCP报头中的URG设置为1,该报文就可以被优先读取。
16位紧急指针指向的数据中的一个数据(注意:只有这一个数据)就是我们想要读取的紧急数据(通过16位紧急指针以偏移量的方式可以找到这个数据)。(注意:该紧急数据一般是有特定的含义,按照之前发送的相关报文来进行相应的处理回复相应的信息,该紧急指针一般是用来询问对方主机的状态信息的)
问:为什么一定要以紧急指针的方式来进行处理?
答:因为如果我们以之前的普通报文形式来进行处理的话,就会按照序号优先级来进行排队,无法达到我们的目的。
RST:对方要求关闭当前连接然后重新建立连接; 我们把携带RST标识的称为复位报文段(reset)
应用场景:
当客户端和服务端进行两次握手后,此时客户端会认为连接已经建立好了,然后客户端会给用户发送信息,但是此时携带ACK的应答请求服务端并没有收到,此时服务端收到了客户端的消息之后,因为没有建立合理的三次握手,所以此时服务端没法对用户发送的信息进行合理的相应,此时服务端就会向客户端发送一个带有RST报头的报文,要求关闭连接然后重置连接。
16位窗口大小
client和server进行通信的时候,如果client想要给server发送消息,那么就需要知道server的接收能力是多少来表示server接收缓冲区的剩余空间大小是多少。
16位窗口大小表示接收缓冲区剩余空间大小的就是16位窗口大小。
问:如果server给client发送报文,填充的窗口大小,填充的是自己的还是对方的?
答:填充的是自己的,因为自己只能知道自己的缓冲区剩余空间的大小,server给client发送的报头中有着标识server剩余的接收缓冲区大小的值,同样的,client给server发送的报头中也有着标识client剩余接收缓冲区的大小的值,双方根据窗口值的大小随时调整给对方发送的流量的大小,这种双向控制的机制叫作流量控制机制。
问:如果16位窗口大小不够大,我们该如何发送更大的数据呢?
答:在选项中存在相应的放大因子来解决该问题,进而达到解决发送更多数据的问题。
16位检验和
是为了保证数据发送前和发送后没有被修改,有点类似数据签名。
TCP是如何保证可靠性的问题的
1、
问:什么是不可靠?
答:不可靠有丢包、乱序、校验失败等
2、
问:怎么确认一个报文是丢了还是没丢?
答:只要得到相应的应答就可以知道该报文对方是否收到,如果没有收到应答,就是不确定的。从这个地方就可以知道,在长距离交互的时候,永远有一条最新的数据是没有应答的。但是只要我们发送的数据有对应的应答,我们就认为我们发送的数据,对方是收到的。
确认应答机制
TCP将每个字节的数据都进行了编号. 即为序列号:
超时重传机制
主机A发送数据给B之后, 可能因为网络拥堵等原因, 数据无法到达主机B;
如果主机A在一个特定时间间隔内没有收到B发来的确认应答, 就会进行重发;
但是, 主机A未收到B发来的确认应答, 也可能是因为ACK丢失了;
因此主机B会收到很多重复数据。那么TCP协议需要能够识别出那些包是重复的包,并且把重复的丢弃掉,这时候我们可以利用前面提到的序列号, 就可以很容易做到去重的效果。(通过序号进行去重)
那么, 如果超时的时间如何确定?
- 最理想的情况下, 找到一个最小的时间, 保证 “确认应答一定能在这个时间内返回”
- 但是这个时间的长短, 随着网络环境的不同, 是有差异的
- 如果超时时间设的太长, 会影响整体的重传效率
- 如果超时时间设的太短, 有可能会频繁发送重复的包
TCP为了保证无论在任何环境下都能比较高性能的通信, 因此会动态计算这个最大超时时间
Linux中(BSD Unix和Windows也是如此), 超时以500ms为一个单位进行控制, 每次判定超时重发的超时时间都是500ms的整数倍.
如果重发一次之后, 仍然得不到应答, 等待 2*500ms 后再进行重传.
如果仍然得不到应答, 等待 4*500ms 进行重传. 依次类推, 以指数形式递增.
累计到一定的重传次数, TCP认为网络或者对端主机出现异常, 强制关闭连接.
连接管理机制
注意:建立连接不需要服务端accept的参与,服务端accept之前就已经建立好了连接,accept所谓的获取连接只是将连接拿到上层,方便用户进行直接使用。
服务端状态转化:
[CLOSED -> LISTEN] 服务器端调用listen后进入LISTEN状态, 等待客户端连接;
[LISTEN -> SYN_RCVD] 一旦监听到连接请求(同步报文段), 就将该连接放入内核等待队列中, 并向客户端 发送SYN确认报文
[SYN_RCVD -> ESTABLISHED] 服务端一旦收到客户端的确认报文, 就进入ESTABLISHED状态, 可以进行 读写数据了
[ESTABLISHED -> CLOSE_WAIT] 当客户端主动关闭连接(调用close), 服务器会收到结束报文段, 服务器 返回确认报文段并进入CLOSE_WAIT
[CLOSE_WAIT -> LAST_ACK] 进入CLOSE_WAIT后说明服务器准备关闭连接(需要处理完之前的数据); 当服务器真正调用close关闭连接时, 会向客户端发送FIN, 此时服务器进入LAST_ACK状态, 等待最后一个 ACK到来(这个ACK是客户端确认收到了FIN)
[LAST_ACK -> CLOSED] 服务器收到了对FIN的ACK, 彻底关闭连接
客户端状态转化:
[CLOSED -> SYN_SENT] 客户端调用connect, 发送同步报文段
[SYN_SENT -> ESTABLISHED] connect调用成功, 则进入ESTABLISHED状态, 开始读写数据
[ESTABLISHED -> FIN_WAIT_1] 客户端主动调用close时, 向服务器发送结束报文段, 同时进入 FIN_WAIT_1
[FIN_WAIT_1 -> FIN_WAIT_2] 客户端收到服务器对结束报文段的确认, 则进入FIN_WAIT_2, 开始等待服 务器的结束报文段
[FIN_WAIT_2 -> TIME_WAIT] 客户端收到服务器发来的结束报文段, 进入TIME_WAIT, 并发出LAST_ACK
[TIME_WAIT -> CLOSED] 客户端要等待一个2MSL(Max Segment Life, 报文最大生存时间)的时间, 才会 进入CLOSED状态
注意:
在断开连接的时候,应用层上只需要双方各自调用一个close函数,四次挥手都是由TCP协议给我们完成的
当客户端主动断开连接的时候,服务端收到断开连接的FIN请求之后会进入半关闭状态,如果此时服务端不想断开连接,此时就会进入半关闭状态,此时连接还要继续维持着,仍然会占用着服务端的资源。
主动断开连接的一方会进入TIME_WAIT状态,此时不会立即进入CLOSED状态,还要继续等一段时间。
问:为什么一定要等待一段时间才进入CLOSED状态?
答:因为此时客户端发送的ACK报文可能会丢失,如果丢失之后,此时服务端由于长时间没有收到ACK报文,就会超时重传FIN报文,如果此时客户端已经断开了连接,最终服务端会异常断开连接,如果没有断开连接,此时客户端就会再一次发送ACK报文,从而正常断开连接。
问:为什么是三次握手?
答:1、如果一次和两次握手,那么服务端会面临SYN洪水的威胁最终造成系统崩溃,三次握手可以就将连接建立失败的风险给了客户端
2、以最小成本的方式验证了全双工(通信双方相互告知序列号起始值 )。
问:为什么是四次挥手?
答:因为客户端想要向服务端断开连接,但是服务端未必想跟客户端断开连接,再一次体现了全双工机制。客户端断开连接是关闭了客户端的发送能力,而并没有关闭其接收能力,只有当服务端也关闭连接时,客户端才也关闭了接收能力。
理解TIME_WAIT状态
现在做一个测试,首先启动server,然后启动client,然后用Ctrl-C使server终止,这时马上再运行server, 结果是:
$ ./server
bind error: Address already in use
这是因为,虽然server的应用程序终止了,但TCP协议层的连接并没有完全断开,因此不能再次监听同样的server端口. 我们用netstat命令查看一下:
$ netstat -apn | grep 8000
tcp 1 0 127.0.0.1:33498 127.0.0.1:8000 CLOSE_WAIT 10830
tcp 0 0 127.0.0.1:8000 127.0.0.1:33498 FIN_WAIT2 -
- TCP协议规定,主动关闭连接的一方要处于TIME_ WAIT状态,等待两个MSL(maximum segment lifetime) 的时间后才能回到CLOSED状态
- 我们使用Ctrl-C终止了server, 所以server是主动关闭连接的一方, 在TIME_WAIT期间仍然不能再次监听 同样的server端口;
- MSL在RFC1122中规定为两分钟,但是各操作系统的实现不同, 在Centos7上默认配置的值是60s;
- 可以通过 cat /proc/sys/net/ipv4/tcp_fin_timeout 查看msl的值;
问:为什么是TIME_WAIT的时间是2MSL?
答:
简单说就是1、MSL确保最后一个ACK能够完全到达 2、确保两个传输方向上的尚未被接收或迟到的报文段都已经消失
- MSL是TCP报文的最大生存时间,因此TIME_WAIT持续存在2MSL的话,就能保证在两个传输方向上的尚未被接收或迟到的报文段都已经消失(否则服务器立刻重启,可能会收到来自上一个进程的迟到的数据, 但是这种数据很可能是错误的);
- 同时也是在理论上保证最后一个报文可靠到达(假设最后一个ACK丢失, 那么服务器会再重发一个FIN. 这时虽然客户端的进程不在了, 但是TCP连接还在, 仍然可以重发LAST_ACK)
解决TIME_WAIT状态引起的bind失败的方法
在server的TCP连接没有完全断开之前不允许重新监听, 某些情况下可能是不合理的
- 服务器需要处理非常大量的客户端的连接(每个连接的生存时间可能很短, 但是每秒都有很大数量的客户端来请求).
- 这个时候如果由服务器端主动关闭连接(比如某些客户端不活跃, 就需要被服务器端主动清理掉), 就会产生大量TIME_WAIT连接.
- 由于我们的请求量很大, 就可能导致TIME_WAIT的连接数很多, 每个连接都会占用一个通信五元组(源ip, 源端口,目的ip, 目的端口, 协议). 其中服务器的ip和端口和协议是固定的. 如果新来的客户端连接的ip和 端口号和TIME_WAIT占用的链接重复了,就会出现问题
使用setsockopt()设置socket描述符的 选项SO_REUSEADDR为1, 表示允许创建端口号相同但IP地址不同的多个 socket描述符
作用:使端口号可以实现复用的功能(作用原理是不让操作系统进行判定该端口号是否被占用而是直接强制进行绑定),即使服务端由于异常退出,再一次重启的时候可以直接绑定之前的端口号,不需要等2*MSL结束变为CLOSED的状态。
理解 CLOSE_WAIT 状态
客户端主动提出关闭,即发送FIN位为1的报文,服务端收到该报文后,就会变成CLOSE_WAIT状态,但是如果服务端始终都没有close,服务端就会始终处于CLOSE_WAIT状态。
总结:对于服务器上出现大量的 CLOSE_WAIT 状态,原因就是服务器没有正确的关闭socket,导致四次挥手没有正确 完成,这是一个 BUG,只需要加上对应的 close 即可解决问题
滑动窗口(主要)
意义:在保证安全的前提下,以较高的效率给对方发送大量的数据。
确认应答策略,对每一个发送的数据段,都要给一个ACK确认应答,收到ACK后再发送下一个数据段,这样做有一个比较大的缺点,就是性能较差,尤其是数据往返的时间较长的时候
既然这样一发一收的方式性能较低, 那么我们一次发送多条数据, 就可以大大的提高性能(其实是将多个段的等待时 间重叠在一起了)
- 窗口大小指的是无需等待确认应答而可以继续发送数据的最大值. 上图的窗口大小就是4000个字节(四个 段)
- 发送前四个段的时候, 不需要等待任何ACK, 直接发送;
- 收到第一个ACK后, 滑动窗口向后移动, 继续发送第五个段的数据; 依次类推;
- 操作系统内核为了维护这个滑动窗口, 需要开辟发送缓冲区 来记录当前还有哪些数据没有应答; 只有确认应答过的数据, 才能从缓冲区删掉;
- 窗口越大, 则网络的吞吐率就越高;
扩展:
1、如何理解滑动缓冲区和滑动窗口?
答:整个滑动缓冲区本质上就是一个char数组。维护滑动窗口的方法就是通过两个整数,比如start_index和end_index两个整数来进行维护。滑动窗口向右滑动的过程就是两个整数变大的过程。
2、当维护滑动缓冲区的两个整数越界了怎么办?
答:tcp的发送缓冲区其实是被设计成为环状结构的。
3、滑动窗口一定要向右滑动吗?滑动窗口可以变大吗?可以变小吗?
答:不一定,如果对方的接收能力没有增大反而越来越小的话,那么滑动窗口就不会向右进行移动,只有维护左边界的start_index在逐渐增大,滑动窗口的大小在变得越来越小。滑动窗口的大小由谁决定,由对方的接收能力决定,即由我收到的TCP数据报头中的窗口大小决定。
4、滑动窗口是如何进行移动的?
答:ACK会改变左边界,即ACK,每次发送回来的窗口大小就是(end_index - start_index)的值,如果窗口的大小大于当前(end_index - start_index)的值,此时end_index就应当增大,即滑动窗口向右移动。当对方发送回来的窗口大小为0了,就会出现start_index和end_index值相等的情况。
start_index不变,end_index增大,说明对方的接收能力在变大,end_index不变,start_index在增大,说明对方的接收能力在变小。
伪代码:
//更新 start_index = ackNum;//ack是对方传来的确认序号 if(winNum > end_index - start_index) { end_index = start_index + winNum;//winNum是窗口大小 }
5、如果客户段给服务端发送的数据报文丢失了怎么办?如果ACK丢失怎么办?
情况一:数据报文没丢,ACK发生了丢失:
这种情况下, 部分ACK丢了并不要紧, 因为可以通过后续的ACK进行确认
比如主机B向主机A发送了1001、2001、3001、4001、5001、6001六个报文,但是1001、3001、4001的报文发生了丢失,最终主机A还是会以接收到的ACK=6001为ackNum,即start_index = 6001。(确认序号为6001表示6001序号之前的数据报文已经全部收到了)
情况二:数据包就直接丢了
- 当某一段报文段丢失之后, 发送端会一直收到 1001 这样的ACK, 就像是在提醒发送端 “我想要的是 1001” 一样;
- 如果发送端主机连续三次收到了同样一个 “1001” 这样的应答, 就会将对应的数据 1001 - 2000 重新发送;
- 这个时候接收端收到了 1001 之后, 再次返回的ACK就是7001了(因为2001 - 7000)接收端其实之前就已经收到了, 被放到了接收端操作系统内核的接收缓冲区中
这种机制被称为 “高速重发控制”(也叫 “快重传”),如果无法达到3个同样的确认,此时就要遵循超时重传机制。
流量控制(主要)
接收端处理数据的速度是有限的。如果发送端发的太快,导致接收端的缓冲区被打满,这个时候如果发送端继续发送,就会造成丢包,继而引起丢包重传等等一系列连锁反应
因此TCP支持根据接收端的处理能力,来决定发送端的发送速度,这个机制就叫做流量控制(Flow Control)
- 接收端将自己可以接收的缓冲区大小放入 TCP 首部中的 “窗口大小” 字段,通过ACK端通知发送端;
- 窗口大小字段越大,说明网络的吞吐量越高;
- 接收端一旦发现自己的缓冲区快满了, 就会将窗口大小设置成一个更小的值通知给发送端;
- 发送端接受到这个窗口之后, 就会减慢自己的发送速度;
- 如果接收端缓冲区满了,就会将窗口置为0;这时发送方不再发送数据,但是需要定期发送一个窗口探测数据段,使接收端把窗口大小告诉发送端
接收端如何把窗口大小告诉发送端呢? 回忆我们的TCP首部中, 有一个16位窗口字段, 就是存放了窗口大小信息; 那么问题来了, 16位数字最大表示65535, 那么TCP窗口最大就是65535字节么?
实际上, TCP首部40字节选项中还包含了一个窗口扩大因子M,实际窗口大小是 窗口字段的值左移 M 位;
拥塞控制(主要) <- 慢启动算法
问:拥塞控制是什么?
答:网络中传输的数据太多了,出现大量丢包问题(网络问题)。
虽然TCP有了滑动窗口这个大杀器, 能够高效可靠的发送大量的数据. 但是如果在刚开始阶段就发送大量的数据, 仍然可能引发问题.
因为网络上有很多的计算机, 可能当前的网络状态就已经比较拥堵. 在不清楚当前网络状态下, 贸然发送大量的数据, 是很有可能引起雪上加霜的.
TCP引入慢启动机制, 先发少量的数据, 探探路, 摸清当前的网络拥堵状态, 再决定按照多大的速度传输数据;
此处引入一个概念程为拥塞窗口(没有体现在TCP报头当中,相当于TCP连接中的一个属性字段,表示这个窗口的数据字段以内,网络不会出现拥塞问题,超过这个字段,可能就会出现网路拥塞问题)
发送开始的时候, 定义拥塞窗口大小为1; 每次收到一个ACK应答, 拥塞窗口加1;
每次发送数据包的时候, 将拥塞窗口和接收端主机反馈的窗口大小做比较, 取较小的值作为实际发送的窗口
一次向目标主机发送的数据量 = min(对方的接收能力,拥塞窗口)
滑动窗口的大小 = min(对方的窗口大小,拥塞窗口)
end_index = start + min(窗口大小, 拥塞窗口大小)
当网络好的时候,根据对方窗口大小来进行发送,当网络不好的时候,根据拥塞窗口来进行发送。
像上面这样的拥塞窗口增长速度, 是指数级别的. “慢启动” 只是指初使时慢, 但是增长速度非常快
- 为了不增长的那么快, 因此不能使拥塞窗口单纯的加倍
- 此处引入一个叫做慢启动的阈值
- 当拥塞窗口超过这个阈值的时候, 不再按照指数方式增长, 而是按照线性方式增长
注意:ssthresh值是上一次拥塞阈值的一半。
- 当TCP开始启动的时候, 慢启动阈值等于窗口最大值
- 在每次超时重发的时候, 慢启动阈值会变成原来的一半, 同时拥塞窗口置回1;
问:为什么前期采用指数增长?
答:指数增长前期慢,后期快。因前期慢是为了给网络一个恢复的时间,后期快是为了网络恢复好之后,迅速的恢复网络正常的通信的速度。
少量的丢包, 我们仅仅是触发超时重传; 大量的丢包, 我们就认为网络拥塞;
当TCP通信开始后, 网络吞吐量会逐渐上升; 随着网络发生拥堵, 吞吐量会立刻下降;
拥塞控制, 归根结底是TCP协议想尽可能快的把数据传输给对方, 但是又要避免给网络造成太大压力的折中方案
延迟应答
如果接收数据的主机立刻返回ACK应答, 这时候返回的窗口可能比较小
- 假设接收端缓冲区为1M. 一次收到了500K的数据; 如果立刻应答, 返回的窗口就是500K;
- 但实际上可能处理端处理的速度很快, 10ms之内就把500K数据从缓冲区消费掉了;
- 在这种情况下, 接收端处理还远没有达到自己的极限, 即使窗口再放大一些, 也能处理过来;
- 如果接收端稍微等一会再应答, 比如等待200ms再应答, 那么这个时候返回的窗口大小就是1M
一定要记得, 窗口越大, 网络吞吐量就越大, 传输效率就越高. 我们的目标是在保证网络不拥塞的情况下尽量提高传输效率;
那么所有的包都可以延迟应答么? 肯定也不是;
- 数量限制: 每隔N个包就应答一次;
- 时间限制: 超过最大延迟时间就应答一次
具体的数量和超时时间, 依操作系统不同也有差异; 一般N取2, 超时时间取200ms; (注意:这个最长的时间不会超过超时重传的时间)
捎带应答
在延迟应答的基础上, 我们发现, 很多情况下, 客户端服务器在应用层也是 “一发一收” 的。意味着客户端给服务器说 了 “How are you”, 服务器也会给客户端回一个 “Fine, thank you”;
那么这个时候ACK就可以搭顺风车, 和服务器回应的 “Fine, thank you” 一起回给客户端
简单说,就是服务端收到了客户端发来的数据的同时,服务端在回复确认序号的同时,也可以发送数据给客户端。
面向字节流
创建一个TCP的socket, 同时在内核中创建一个发送缓冲区和一个接收缓冲区
调用write时, 数据会先写入发送缓冲区中;
如果发送的字节数太长, 会被拆分成多个TCP的数据包发出;
如果发送的字节数太短, 就会先在缓冲区里等待, 等到缓冲区长度差不多了, 或者其他合适的时机发送出去;
接收数据的时候, 数据也是从网卡驱动程序到达内核的接收缓冲区; (通过CPU和网卡驱动程序的硬件中断来提醒CPU读取内核的接收缓冲区)
然后应用程序可以调用read从接收缓冲区拿数据; 另一方面, TCP的一个连接, 既有发送缓冲区, 也有接收缓冲区, 那么对于这一个连接, 既可以读数据, 也可 以写数据. 这个概念叫做全双工
由于缓冲区的存在, TCP程序的读和写不需要一一匹配,也不需要关心数据的格式,这就是面向字节流,例如:
- 写100个字节数据时, 可以调用一次write写100个字节, 也可以调用100次write, 每次写一个字节;
- 读100个字节数据时, 也完全不需要考虑写的时候是怎么写的, 既可以一次read 100个字节, 也可以一次 read一个字节, 重复100次;
tcp是面向字节流的,不关心任何的数据格式,但是要正确使用这个数据,必须得有特定的格式,这个格式是由应用层进行解释处理的。
粘包问题
- 首先要明确, 粘包问题中的 “包” , 是指的应用层的数据包
- 在TCP的协议头中, 没有如同UDP一样的 “报文长度” 这样的字段, 但是有一个序号这样的字段
- 站在传输层的角度, TCP是一个一个报文过来的. 按照序号排好序放在缓冲区中
- 站在应用层的角度, 看到的只是一串连续的字节数据
- 那么应用程序看到了这么一连串的字节数据, 就不知道从哪个部分开始到哪个部分, 是一个完整的应用层数据包
那么如何避免粘包问题呢? 归根结底就是一句话,明确两个包之间的边界
- 对于定长的包, 保证每次都按固定大小读取即可; 例如上面的Request结构, 是固定大小的, 那么就从缓冲区从头开始按sizeof(Request)依次读取即可;
- 对于变长的包, 可以在包头的位置,约定一个包总长度的字段,从而就知道了包的结束位置;
- 对于变长的包,还可以在包和包之间使用明确的分隔符(应用层协议,是程序猿自己来定的,只要保证分隔符不和正文冲突即可);
问:对于UDP协议来说, 是否也存在"粘包问题" 呢?
- 对于UDP, 如果还没有上层交付数据,UDP的报文长度仍然在,同时, UDP是一个一个把数据交付给应用层. 就有很明确的数据边界
- 站在应用层的站在应用层的角度, 使用UDP的时候, 要么收到完整的UDP报文, 要么不收,不会出现"半个"的情况
TCP异常情况
进程终止: 进程终止会释放文件描述符, 仍然可以发送FIN. 和正常关闭没有什么区别
机器重启: 和进程终止的情况相同
机器掉电/网线断开: 接收端认为连接还在, 一旦接收端有写入操作, 接收端发现连接已经不在了, 就会进行reset. 即使没有写入操作, TCP自己也内置了一个保活定时器, 会定期询问对方是否还在. 如果对方不在, 也会把连接释放
另外, 应用层的某些协议, 也有一些这样的检测机制. 例如HTTP长连接中, 也会定期检测对方的状态. 例如QQ, 在QQ 断线之后, 也会定期尝试重新连接
TCP小结
为什么TCP这么复杂? 因为要保证可靠性, 同时又尽可能的提高性能
可靠性:
- 校验和(保证数据的准确性)
- 序列号(按序到达)
- 确认应答
- 超时重传
- 连接管理
- 流量控制(不要超过对方的接收能力,进而避免丢包问题)
- 拥塞控制(保证数据包在网络中不丢失)
提高性能:
滑动窗口(并发式的向对方发送大量消息)
快速重传(连续收到三个相同确认序号)
延迟应答
捎带应答
其他:
- 定时器(超时重传定时器, 保活定时器, TIME_WAIT定时器等)
基于TCP应用层协议
- HTTP
- HTTPS
- SSH
- Telnet
- FTP
- SMTP
TCP/UDP对比
我们说了TCP是可靠连接, 那么是不是TCP一定就优于UDP呢? TCP和UDP之间的优点和缺点, 不能简单, 绝对的进行 比较
- TCP用于可靠传输的情况, 应用于文件传输, 重要状态更新等场景;
- UDP用于对高速传输和实时性要求较高的通信领域, 例如, 早期的QQ, 视频传输等. 另外UDP可以用于广播;
归根结底,TCP和UDP都是程序员的工具, 什么时机用, 具体怎么用, 还是要根据具体的需求场景去判定
用UDP实现可靠传输
参考TCP的可靠性机制, 在应用层实现类似的逻辑;
例如:
- 引入序列号, 保证数据顺序
- 引入确认应答, 确保对端收到了数据
- 引入超时重传, 如果隔一段时间没有应答, 就重发数据
- …