在之前利用套接字进行通信的时候,我们都是利用 “字符串” 进行流式的发送接收,但是我们平常进行交流通信肯定不能只是简单的发送字符串。
比如我们用QQ进行聊天,我们不仅需要得到对方发送的消息,还要知道对方的昵称,头像等一系列数据,也就是说我们需要得到一个结构化的数据!
而想要正确的得到一个结构化的数据,就需要我们定制一层应用层协议或者使用特定的应用层协议。
无论是哪一层的协议,我们都会给数据添加自定义的报头,应用层协议的报头也不例外。
报头:该层协议所需要的一些特定的数据
而只有添加了报头后,再将数据根据协议内容来序列化或者反序列化才能保证正确传输数据。
序列化:将结构化的数据通过协议内容转化为可以进行网络传输的形式(如字符串)
反序列化:将接收到的数据通过协议内容转化为结构化的数据
而添加报头就需要明确报文和报文边界,有以下三种方式
虽说我们能够自己定义协议,但是定义协议费时费力,还不能出差错。
而现实中已经有很多大佬给我们定制了现成的协议,让我们使用,比如——Http协议;
Http默认端口---80;
http协议的URL格式如上。
实际上,我们访问的资源都是存储在对应网站的服务器的磁盘上的,因此我们访问的URL也需要带上访问的文件的路径。
而这个服务器地址就是服务器的ip地址。
最后,ip地址加上端口号和文件路径,就能够成功的请求到服务器的资源。
在URL中,有一些特殊字符被当做特殊意义理解了,比如 '/','@' 之类的,因此当我们使用浏览器进行搜索的时候,浏览器会将搜索的内容做一些特殊处理。
比如我们搜索c++,就会发现 ++ 这两个字符被替换成了 '%2B' 的样式,这就是urlencode;
而 urldecode 则是将对应的字符翻译成原来的模样。
每当我们向服务器请求资源时,浏览器就会向服务器发起请求。
每一个浏览器向服务器发送请求时,浏览器会发送这么长一串报头给服务器。
这个报头有很多信息,比如 Conten-Length(正文长度)等一些信息。
当服务器收到请求后,就会发送对应的响应。
在这么多方法中,Get和Post是用的最多的,那么Get和Post之间有什么区别呢?
当我们使用Get方式提交表单的时候,我们发现URL中会存在我们输入的用户名和密码
也就是说Get方法是通过URL来传参的。
而是用Post方式来访问则不会在URL中看到账号和密码
Post方式中的账号和密码都是存储在正文当中的。
虽说Post更具有私密性,但是实际上它们二者都不是安全的,只是Post更加具有私密性罢了
其中3开头的状态码需要详细讲一下。
重定向分为临时重定向和永久重定向。
现象:
一张完整的网页一般由多种元素组成,因此需要多次请求,但是http是基于TCP协议的,而TCP是面向连接的,因此请求一张网页也许会存在频繁创建连接的问题。
而长连接就是为了解决该问题而存在的。
它通过建立一条链接,获取资源的任何行为都必须通过这个链接完成,从而解决频繁创建连接的问题。
会话保持这个功能是用户在实际体验过程中所需要的一个功能,表面现象就是当你登录了一个网站,那么你即使退出了该网站或者退出了该浏览器,下一次访问该网站时,网站依旧知道你是哪个用户。
会话保持有两种方法。
老方法是通过在客户端保存用户信息,每次访问同一个网站浏览器都会自动推送历史保留信息,从而实现会话保持。
而这个信息称为cookie。
Cookie
- 用来保存用户信息
- cookie文件:浏览器关闭后cookie会以文件形式保存下来
- cookie内存:浏览器关闭后cookie不会以文件形式保存下来
但是老方法有一个问题,那就是cookie信息是保存到客户端的,一般的客户端很难防止木马盗取用户信息,就会导致用户账号被盗。
而新方案则另辟蹊径。
新方案将用户信息存在服务器中,用session保存文件,并且返回对应的session id,而每次客户端请求就是通过session id 来访问对应的session文件。
虽然客户端的session id 依旧可能会被盗,但是这样至少可以保证用户的信息没有被泄露。
而且服务器可以通过查看用户的 ip 地址等方案来判断 session id 是否需要失效,从而保证了用户信息的安全。
了解了一些http协议的细节,那么自然也需要来了解Https协议。
https协议实际上就是单纯的在http协议和传输层之间添加了一层加密协议,从而保证用户信息的安全。
由于http协议内容都是明文传输的,因此需要添加加密协议防止用户信息被篡改。
- 将明文通过一系列变换生成密文。
- 将密文通过一系列变换生成明文。
一般加密过程中,都需要多个密钥来辅助加密和解密。
有时候我们上网下载东西时,明明搜索下载的是A,但是下载完却发现下载的是B,这就是我们在请求下载时,响应被劫持了。
我们在网络上传输的任何数据包都会经过运营商的网络设备(路由器等),运营商的网络设备就可以解析出你传输的数据内容并进行篡改。
当我们未被劫持时,我们下载的东西就是正确的。
而当我们被劫持了,下载的东西就是别的软件了。
因此我们需要通过加密来防止这种中间人攻击。
为了防止中间人攻击,https退出了两种加密方式。
非对称加密有一个特点,那就是两个密钥可以反着使用。
可以用公钥加密,私钥解密,也可以用私钥加密,公钥解密。
当我们了解到https的加密手段后,我们再来看看加密方案。
在客户端和服务器之间的交流之中,都是通过客户端发送请求后,服务器再将密钥X发送给客户端。
乍一看,也许没问题,但是如果有中间人攻击的话,很容易就能将数据劫持掉。
而且由于密钥是明文传送的,黑客持有了密钥,后续的加密操作也没有作用了。那么无论客户端发送什么密文给服务器,黑客都能够随意修改。
也许有同学会想,那我将密钥给加密怎么样呢?
这就陷入了一个套娃的循环了。
如图所示,采用非对称加密时,服务器发送的响应必须通过私钥加密,但是私钥加密的密文可以通过公钥解密,此时黑客已经劫持了公钥,那么响应就能够被随便修改了。
当双方都采用非对称加密时,客户端将数据通过 FS 加密,因此只有服务器能够解密,而服务器发送的响应能够通过客户端的 KS 加密,因此只有客户端能够解密。
但是这样有一个缺点:慢。
之前提过,非对称加密很慢,而这样双方都使用非对称加密就更慢了。
像这样,服务器明文发送了公钥给客户端,客户端内部决定密钥为 X 然后通过 FS 加密发送给服务器,这样客户端和服务器就通过 密钥 X 来加密,而且即便中间人后面来劫持也无法篡改密文。
但是这样依旧有一个问题——那就是假如一开始中间人就开始劫持会怎么样?
如果中间人一开始就劫持了,那么中间人就能够篡改发送的公钥 FS,这样后面的加密解密就都在中间人的监视下了。
而这个问题上面四个方案都存在。
在上面几个方案中,只要中间人一开始就已经开始攻击了,那么中间人能够轻易获取公钥并篡改公钥为中间人自己的公钥,而且服务器和客户端都无法察觉,这样客户端和服务器之间的交流都会被中间人获取到。
为了解决这个问题,我们需要了解一个新概念。
每个服务器在使用HTTPS之前,都要向CA申请一份数字证书,数字证书中包含申请者信息,公钥信息等。
而有证书的服务器会将证书传输给客户端,客户端直接从证书中获取公钥。
每个证书中包含了以下信息:
- 证书发布机构
- 证书有效期
- 公钥
- 证书所有者
- 签名
CA机构中有他们自己维护的公钥和私钥,由此来保证证书的可靠性。
那么具体过程是如何做的呢?
CA机构的证书通过数据签名来保证证书的可靠性。
当服务器申请证书时,CA在对服务器进行审核后,会为该网站专门形成数字签名。
签名
验证
需要验证的信息:
- 证书是否过期
- 证书的发布机构是否可信
- 验证证书是否被篡改
最后方案5是在方案4的基础上加入证书。
由于摘要算法的特性:定长,分散,不可逆,导致我们判断证书是否相同的标准是摘要是否一致,若是不采用CA私钥加密,客户端没有CA的公钥来进行解密,中间人只需要将内容修改,同时将摘要修改,那么客户端就无法分辨了。
而有了CA私钥加密,就能够保证中间人无法篡改证书了。
为了减少密文的长度,加快数字签名的速度。
总结:
HTTPS工作一共有三组密钥。
- 非对称加密:用于检验证书是否被篡改。客户端天然持有CA机构的公钥,服务器发送证书给客户端,客户端通过该公钥验证证书是否被篡改
- 非对称加密:用于协商对称加密的密钥。客户端在证书中获取服务器的公钥,用公钥给密钥加密,发送给服务器
- 对称加密:服务器和客户端通过该密钥进行加密解密。
Http和Https两个协议都是在应用层的协议,而传输层则是负责将数据从发送端传输到接收端
端口号是用于标示一台主机上进行通信的不同程序;
在TCP/IP协议中,用“源IP”,“源端口号”,“目的IP”,“目的端口号”,“协议号”这五元组来表示一个通信。
我们平时使用端口号时,会发现有的端口号不可以使用。
这是因为端口号是有范围的。
一些知名端口号
- SSH服务器:22端口号
- ftp服务器:21端口号
- telnet服务器:23端口号
- HTTP服务器:80端口号
- HTTPS服务器:443端口号
平时我们自己使用端口号时,需要避开这些端口号。
想要了解UDP协议,就必须了解它的协议格式。
所谓协议实际上可以看做是结构化的数据。
UDP也不例外,它的报头采用的是定长大小的策略,报文包含了8字节大小的报头。
报头中包含了源端口号,目的端口号,报文长度等信息,用来传递信息。
UDP的特点中需要注意的是面向数据报这个特点。
使用UDP协议传输的数据一次传输的数据不能拆分也无法合并,UDP会原样发送。
注意事项
由于UDP的UDP长度是16位,因此UDP报文的长度最长不过64K(包括报头),因此我们使用UDP传输数据时,需要先在应用层将数据分成64K大小后再传输,接收端则在接收后手动拼装。
UDP客户端通过 send/write 将数据发送到网络中,然后服务器把数据放到接收缓冲区中后,服务器通过报头判断自己是否为目的地,再从接收缓冲区中获取信息,否则直接丢弃。
而UDP协议之中,它的socket既可以读又可以写,因此是双全工的。
此外还有用户自己UDP程序在应用层配置的协议。
TCP协议全称为“传输控制协议”,正如它的名字,它需要对数据的传输进行详细的控制。
而我们都知道TCP协议的特点是:有连接,可靠,面向字节流。其三大特点就是由其报头造成的。
TCP的报头是一个自描述的变长报头。
首先,它的报头固定大小为20字节,其中有一个变量是4位首部长度,用来表示报头+选项的总长度。
报头总长度:4位首部长度 * 4字节。
4位首部长度的单位是4字节,假设报头是40字节,那么4位首部长度值为10.
TCP解包就是通过4位首部长度确定报头长度,然后再一直读取到下一个报头的位置,就读取了一个完整的报文了。
在TCP传输中,传输数据的速度不可过快过慢,应该根据对方的接收缓冲区的剩余空间大小来决定发送多少数据。
因此使用TCP发送数据时,都会往16位窗口大小填充自己接收缓冲区的剩余空间大小来进行流量控制。
我们TCP平时可能会传各种各样的数据,有的是普通的数据段,有的是确认数据段(ACK)。
因此TCP协议使用了六个标记位来表示不同的报文。
- ACK:确认标记位,表示是否为确认报文
- PSH:对方的接收缓冲区已满时,告知对方尽快读取缓冲区中的数据
- URG:表示紧急指针是否有效
- RST:对方要求重新建立连接,带有RST的报文称为复位报文段
- SYN:请求报文段,携带该标记位的称为同步报文段
- FIN:通知对方,本端要关闭了,带有FIN的称为结束报文段
其中有几个标记位需要展开说明。
URG标记位
一般报文都会有序号保证报文按序到达,但是有时候也有高优先级的报文,这时候就需要插队了。
带有URG标识位的报文就表示该数据要尽快读取。
这种报文就需要去16位紧急指针,该指针通过记录紧急数据在有效载荷的偏移量来指向紧急数据。
一般紧急数据只有一字节。
RST标记位
若是三次握手与四次挥手失败了,或者说通信过程中单方面出错了,出错的一方认为连接不存在了, 但是对方依旧认为连接存在,对方依旧会发送报文,此时出错的一方收到报文后就会返回一个带有RST的报文,来重新建立连接。
TCP有一个最大的特点就是它的可靠性,它能够保证数据能够成功传输给对方,若是失败也会告知用户,那么它是如何做到的呢?
用过网购的童鞋们都知道,我们收到网购的东西后,需要在软件上确认收货商家才能收款,否则商家就无法确认你是否收到了商品。
而TCP也是如此,当客户端发送请求或者数据给服务器时,服务器需要返回一个确认数据段给客户端,这样客户端才能知道服务器确实收到了数据。
当然,这里服务器并不是单纯的返回一个确认数据段,而是一段报文,该报文的报头有一段数据就是确认数据段,在TCP中,这段确认数据段就是报头中的32位确认序号。
每次TCP的发送段发送数据的时候,TCP协议会给每一个字节的数据添加一个序号,这个序号记录在32位序号中,当接收段接收数据的时候,接收端会根据这个序号进行排序,并判断是否丢失数据。
而当接收端返回一个ACK时,就会在32位确认序号中返回最后一个位序号的值+1,告知对方已成功接收确认序号之前的所有数据,只用从确认序号开始发送数据即可。
这样就保证接收端能够将报文全部接收,并且发送端能够知道该从哪里继续发送。
此外由于TCP协议也是全双工的,双方都要相互发送数据,相互应答,因此会有两组序号分别记录对方的位序号。
为了保证可靠性,TCP还有确认应答机制。
该机制分为两种情况。
第一种情况很简单,重点是第二种情况,主机B重复收到了相同的数据,那么它就需要进行去重操作。
而去重操作也是通过之前的32位序号进行——通过对比数据序号来去重。
对于主机A来说,它需要重新发送相同的数据,那就是说发送的数据并不直接移出缓冲区,而是维持一段时间,也许是等到应答后,也许是操作系统自己决定。
理想情况下就是找到一个最小的时间,保证确认应答会在这个时间内返回。
但是实际上由于网速不同,这个时间长短时变化的,过长会降低效率,过短会频繁重复传包
因此TCP是动态的计算这个时间间隔。
- 任何操作系统都是以500ms为一个单位,时间间隔都是500ms的整数倍。
- 重发一次依旧等不到应答就等待2*500ms的时间。
- 依旧等不到应答就重发等待4*500ms,以指数增长。
- 重传一定次数,就认为对方异常,关闭连接。
TCP作为面向连接的协议,它具有一套机制用来管理连接。
一般来说,TCP每次连接都需要三次握手的,每次断开连接则需要四次挥手。
三次握手的过程
正常来说,一次成功的建立连接过程就如上,之后就能够传输数据了。
但是网络通信并不一定都一帆风顺,数据总有丢包风险,因此我们的三次握手过程也有可能失败。
在三次握手过程中,客户端发送SYN时,确认应答机制会起作用,告知客户端服务器是否收到数据;而客户端一旦收到 SYN+ACK时,它就认为已经成功建立连接了,因此此时客户端就不会等待应答,而是直接传输数据给服务器了。
但是从服务器上来看,它必须收到 ACK 才会认为成功建立连接,但是ACK是会丢包的。
因此三次握手可能会出现客户端认为建立连接,但是服务器却不认为在建立连接的情况。
这种情况下,一般就是客户端发送数据给服务器后,服务器发现未建立连接,就会返回一个带有RST的报文用来重新建立连接,或许也有其他方案,不过大差不差。
为什么是三次握手?
三次握手为什么不是一次两次或者是四次握手呢?首先我们要先连接三次握手的优点。
对于TCP协议来说,通信双方都是对等的,双方可以互相读写信息,也就是所谓的全双工。
而三次握手就是最小成本验证全双工通信是通畅的。对于客户端来说,它不仅需要发送SYN报文给服务器,还需要读取 SYN+ACK 报文,对于服务器来说,它也是要读取 SYN 和 ACK 两个报文,发送 SYN+ACK 两个报文,正好双方都至少读写了一次。
此外,三次握手能够有效的防止单机攻击服务器。
SYN洪水:一种攻击手段,通过给服务器发送海量连接请求,使得服务器需要维护多个半连接,导致服务器资源被占用。
若是一次握手就能够成功建立连接,对于客户端来说,它只需要发送一个链接就好了,也只需要维护一个链接,但是服务器是同时维护多个链接的,维护每一个连接都是需要成本的,若是一次握手就会使的客户端攻击服务器的成本降低,二次握手也是一样。
而多次握手则是成本又比三次握手高,因此采用三次握手。
四次挥手过程
需要注意,四次挥手并不一定是客户端关闭,服务器也能主动关闭。
挥手期间双方状态发送变化
CLOSED_WAIT状态
TCP层协议规定,被动关闭连接的一方需要处于CLOSE_WAIT状态,这个状态会一直持续到服务器处理完数据,调用 close 函数会结束。
那么也就是说我将 Sever 的 close 函数给注释掉,Sever就一直是这个状态。
此时客户端还在连接中,看下状态。 当我们关闭客户端时,再看看状态。
发现TCPSever确实一直处于CLOSE_WAIT状态。
大量出现CLOSE_WAIT原因
- 服务器没有进行 close 的动作
- 服务器有压力,一直在推送消息给客户端,无法进行close
TIME_WAIT状态
当我们使用TCP协议使用一些端口来启动服务器时,用这个端口关闭的服务器在一定时间内无法再次使用这个端口,就是由于TIME_WAIT状态。
那么为什么要处于TIME_WAIT状态呢?
- 主动关闭的一方需要返回一个ACK给被动关闭的一方,但是ACK可能丢包,确认应答机制导致主动关闭的一方需要重新接收FIN来补发一个ACK。
- 双方断开连接时,网络中也可能还有滞留的数据,需要保证这些报文已经消失。
而主动关闭连接的一方等待两个MSL后,就能够保证至少补发一个ACK报文,并且保证网络中的滞留数据消失。
解决办法
那么我们如何才能够快速重新使用这个端口呢?就需要调用下面这个函数。
使用setsockopt()设置socket描述符的选项SO_REUSEADDR为1, 表示允许创建端口号相同但IP地址不同的多个 socket描述符
当我们调用这个函数后就能够重复使用同一个端口了。
在确认应答机制中,并不是每次发送数据都需要应答的,因为若是每次都应答会降低性能,因此每次发送数据实际上是一次发送多个数据。
在TCP的报头中,有一个16位窗口大小,其值是指不用等待确认应答,而可以继续发送数据的最大值, 其大小根据对方的接收缓冲区的剩余空间的大小而变化。
根据窗口大小,我们能够一次发送多个数据,但是这些数据有可能在网络中丢包,根据确认应答机制需要重新发送数据,而这些发送出去而未收到应答的数据就需要滑动窗口来管理。
实际上滑动窗口并不是一个真正的窗口,它实际上是两个下标。
如果收到的应答是滑动窗口中间或结尾的ACK怎么办?
也许收到的应答并不是2001,而是 4001或者直接是结尾呢?
这样只有两种情况。
- 丢包了,数据没丢,但是应答丢了。
- 数据丢包了。
首先是第一种情况,这种情况下窗口是直接滑动的,因为序号的定义就是对方已接收到该序号之前的所有数据,因此可以直接滑动。
第二种情况也很简单,根据序号定义,应答的ACK应该是对方所接收到的数据的序号+1,因此接收端就知道哪些数据丢包了,就会补发一次。
序号还支持滑动窗口的滑动规则。
一直向右滑动,空间不够了怎么办?
从刚刚的说法来说,发送缓冲渠的空间不是无穷的,因此滑动窗口一定回到缓冲区的尾部。
实际上发送缓冲区的结构是一个环形数组,因此不会出现这种情况。
接收段处理数据的速度有限,如果发的太快,接收端处理不及时,会出现丢包的情况,因此需要流量控制。
- 接收端将自己的缓冲区的剩余空间大小放在报文中的窗口大小字段中,通过ACK通知发送端。
- 窗口越大,吞吐量越大。
- 接收端发现缓冲区快满了,就会减小窗口大小。
- 若是窗口设置为0,发送端停止发送数据,但是会定期发送窗口探测数据。
16位窗口最大为65535,但是TCP窗口不止65535,在报头的选项中,有一个窗口扩大因子M,TCP窗口大小为窗口字段的值右移M位。
一般在网络上,同时会有很多计算机互相通信,即使TCP有滑动窗口来可靠的发送大量数据,但是如果一口气就发送大量数据依旧会造成一些问题。
在客户端和服务器互相通信的时候,网络中依旧有很多计算机在互相通信,这样会造成网络阻塞,因此服务器只应答了几条报文。
像这种大量的丢包,我们就认为是网络出现了阻塞。
为了应对这种情况,TCP引入了慢启动机制,先发少量数据来查看网络拥堵状态,再来决定传输的速度。
像上图中的拥塞窗口大小按指数级增长,但是拥塞窗口不能这样增长,否则后期会增长过快。
因此需要一个阈值来控制速度,当拥塞窗口大小超过该阈值时,就需要按线性方式增长。
通过拥塞控制,TCP能够尽快的将数据传给对方,而不对网络造成太大压力。
有时候,接收端主机若是刚接收到报文就立马应答,它的接收缓冲区可能剩余空间很小,返回的窗口也很小,这样发送端发送的数据也会变少。
这就是延迟应答,但是延迟应答也是有条件的。
数量限制:每隔N个包应答一次
时间限制:超过最大延迟时间就应答一次
N一般为2,最大延迟时间为200ms
这样返回的窗口越大,其传输的速度反而更快了。
有时候服务器和客户端之间是 “一发一收” 的状态,这个时候服务器就能够顺带把ACK和回复的报文放一起。
比如四次挥手的过程,服务器能够将最后的 ACK 和 FYN 放一起给客户端。
创建TCP的socket时,还会创建对应的接收缓冲区和发送缓冲区。
由于缓冲区的存在,TCP的读写互不影响。
可以一次写100个字节的数据,也可以写100次一个字节的数据,读取也是。
对于TCP来说,TCP没有报文长度的字段,只有一个序号的字段。
对于TCP来说,它可以按照序号将数据排序,但是从应用层来说,它只看到了一连串的字符数据,这样应用层就分辨不了哪个数据是哪个报文的。
为了避免这个问题,就需要明确包与包之间的边界。
- 对于定长的包,可以按固定大小读取。
- 变长的包,可以在包头的位置约定一个包总长的字段
- 也可以在包与包之间约定一个特殊的分隔符
但是对于UDP来说就不存在这种问题了。
一个是UDP报头有一个报文长度的字段,并且UDP是一个报文一个报文发给应用层的。
进程终止:这种情况会释放文件描述符,依旧可以发送FYN。
机器重启:这种情况下类似于进程终止。
机器断开电源/拔网线:这种情况下如果有写入操作,接收端会发现连接不在了,就会reset,并且TCP内部也有定时器,定时询问连接是否存在。
可靠性:
提高性能
其实二者并无优劣之分,只是需要根据不同的应用场景来使用不同的协议。
TCP适用于可靠传输的场景,而UDP适用于高速传输和对实时性要求高的场景。
第二个参数backlog实际上是为上层维护的链接队列的长度,这个长度不可没有,也不可太长。
我们将gbacklog设为2,然后注释掉accept。
然后用四个客户端来连接这个服务器,然后使用netstat命令查看服务器连接情况。
我们发现连接队列中只有三个连接是被维护着的,也就是全连接状态,还有一个连接从服务器角度来看还在SYN_RECV中,也就是所谓的半连接状态。
这样backlog的含义就知道了,它是指服务器维护的全连接队列的长度,长度为backlog + 1.
如果服务器一直未accept,或者说服务器内部的链接已满,还未退出,那么这个全连接队列就无法accept。
全连接队列:已建立连接,但是未accept的链接。
以上就是应用层的HTTP+HTTPS的细节,以及传输层UDP以及TCP详解。
了解了HTTP的报头,协议格式,以及方法, 状态码,session,cookie,还有HTTPS是如何加密的。
了解了UDP报头,协议格式等。
了解了TCP报头格式,如何连接,实现可靠性,还了解了超时重传机制,确认应答机制,连接管理机制,序号的作用,滑动窗口,拥塞控制,流量控制,快速重传,延迟应答和捎带应答。
对于网络基础有了一定的了解,但是对于网络层和数据链路层我们依旧未知,还是需要继续努力。