网络基础(二)
1. 应用层
1.1 协议定制
已知每一层都有协议,那么应用层的协议是程序员来定制的,由两头共同协商好的协议。
eg :
发送方要发送个人信息的结构体,在TCP中,网络传输是字节流形式的,,无法传结构体,所以一般都是将所有的数据转化为字符串形式来进行传输,并且规定分割符号,边界,长度等操作来防止多读或者少读
将特定的对象或者结构体以字节流的形式传出,这个过程称为序列化。
将从网络中获取的字节流转化为特定的对象或者结构体,这个过程称为反序列化。
但是,其实一般不用我们来手写序列化和反序列化,现在有成熟的技术
- json
- protobuf
- xml
当读取的时候,需要自己根据协议来保证能够每次读取的报文是一个完整的报文,比如一次读取到n个分隔符,或者一次读取到长度刚好满足一个报文的条件
1.2 HTTP协议
HTTP协议是在应用层的一个协议,常用与浏览器,但是现在基本上已经被淘汰,因为他的安全性不是很高,基本上都被https所取代了,但是用法差不多,所以需要先了解http
认识url
当访问一个网站的时候,域名其实就是一个ip地址,这个过程是由浏览器来进行解析的,访问网站内容,其实就是访问对方网站服务器中的资源(图片视频都是资源),那么就是相当于访问服务器中的特定路径下的文件,具体点击访问哪个文件是由前端来写的,写好后部署在服务器上即可
urlencode和urldecode
有些特殊符号\
,?
等特殊符号,url中存在,所以如果出现的话那么就会出问题,所以需要用特定算法转化为一种其他格式
- urlencode转义规则
将需要转码的字符转为16进制,然后从右到左,取4位(不足4位直接处理),每2位做一位,前面加上%,编码成%XY 格式
- urldecode
urldecode就是urlencode的逆过程
HTTP协议格式
GET请求是HTTP协议中浏览器发出的请求
请求
第一行为请求行,分别是 请求方式 url http版本 以\r\n结尾
之后的n行为请求报头,该内容是浏览器发起,所以一般不需要我们去更改等,每行以\r\n结尾
空行\r\n分割正文之前的内容,之后就是正文了
//请求格式
GET / HTTP/1.1
Accept:image/gif.image/jpeg,*/*
Accept-Language:zh-cn
Connection:Keep-Alive
Host:localhost
User-Agent:Mozila/4.0(compatible;MSIE5.01;Window NT5.0)
Accept-Encoding:gzip,deflate
username=jinqiao&password=1234
响应
第一行为状态行 http版本 状态码 状态码描述\r\n
状态码一般是返回给客户端的,代表结果怎么样,200代表正确,常见的还有404,代表页面不存在
之后的n行为响应报头,可以指定返回资源的格式的属性以及其他属性每行以\r\n结尾
空行\r\n分割正文之前的内容,之后就是正文了
html
//响应格式
HTTP/1.1 200 OK
Server:Apache Tomcat/5.0.12
Date:Mon,6Oct2003 13:23:42 GMT
Content-Length:112
Content-Type:text/html
Last-Moified:Mon,6 Oct 2003 13:23:42 GMT
Content-Length:112
<html>
<head>
<title>HTTP响应示例<title>
</head>
<body>
Hello HTTP!
</body>
</html>
请求的时候,一般HTTP会请求url只有一个/
,所以在服务器端可以判断,假设url为/
那么就是访问主页,然后可以将路径设为wwwroot/index.html的文件,然后读取文件放到响应的正文里,发送给浏览器
HTTP content-type
-
一般是指网页中存在的 Content-Type,用于定义网络文件的类型和网页的编码,决定浏览器将以什么形式、什么编码读取这个文件
-
响应的时候,文件类型都不一样,所以响应的报头需要指定这个类型conten-type,可以设计一个哈希表,用访问文件的后缀来对应类型
HTTP常见Header
Content-Type: 数据类型(text/html等)
Content-Length: Body的长度
Host: 客户端告知服务器, 所请求的资源是在哪个主机的哪个端口上;
User-Agent: 声明用户的操作系统和浏览器版本信息; referer: 当前页面是从哪个页面跳转过来的;
location: 搭配3xx状态码使用, 告诉客户端接下来要去哪里访问;
Cookie: 用于在客户端存储少量信息. 通常用于实现会话(session)的功能
POST方法,cookise和sesson id
2. 传输层
2.1 端口号
端口号(port)用来标示一个主机进行通信的应用程序
为什么要用端口号?用pid等什么的不行吗?
每次程序重新启动都会重新分配pid所以引入了端口号的概念,向端口号写入或者读就是对该进程读写
一. 方便找到目的进程
二. 与使网络与操作系统解耦
端口号和进程的关系
一个端口号只能绑定一个进程,但是一个进程可以绑定多个端口号
在TCP/ip协议中,用五元组源ip
,源端口号
,目的ip
,目的端口号
,协议号
来标示一个通信
在应用层,其实有端口号就能找到对应得应用层协议了,因为应用层协议是写在程序里得,那么,将数据通过端口号发送给该进程后,不就是相当于隐式的找到对方的应用层协议了
端口号范围划分
0 - 1023: 知名端口号, HTTP, FTP, SSH 等这些广为使用的应用层协议, 他们的端口号都是固定的.
1024 - 65535: 操作系统动态分配的端口号. 客户端程序的端口号, 就是由操作系统从这个范围分配的
2.2 UDP协议
UDP全称:用户数据报协议
UDP数据报格式
UDP存放源端口和目的端口号
16位UDP长度表示整个数据报(UDP首部+UDP数据)的最大长度,最多为:64KB=2^16B
UDP特点
无连接 : 只需要知道目的ip和端口即可发送消息。
不可靠 : 没用重传机制,无法确认对方是否收到消息。
面向数据报 : 不能够灵活的控制读取次数和数量。
面向数据报
UDP的传输是面向数据报的,即一次为多少就一次性发送多少,不会拆分也不会合并
UDP的缓冲区
- UDP没有发送缓冲区,直接调用send或者其他接口将数据直接发送给操作系统内核,然后操作系统再进行报头的封装,然后发送给网络层进行后续动作
- UDP有接受缓冲区,UDP接受缓冲区的顺序不一定是发送的顺序,当缓冲区满时候,再来数据那么将来的丢弃掉
UDP的socket既能读, 也能写, 这个概念叫做全双工
UDP一次性发送的数据最多为64K,在现在的网络中,是很难满足这个的,所以,要么就用手段分开发送数据报然后手动拼接,但是这样太繁琐,所以UDP不适用于大部分场景。
基于UDP的应用层协议
NFS: 网络文件系统
TFTP: 简单文件传输协议
DHCP: 动态主机配置协议
BOOTP: 启动协议(用于无盘设备启动)
DNS: 域名解析协议
以及自己写的UDP应用层协议
2.3 TCP协议
TCP全名:传输控制协议
TCP传输流程
传输数据的本质其实就是拷贝,将应用层的数据拷贝到传输层缓冲区中,然后另一方从网络中收到拷贝到接受缓冲区,然后应用层再读取拷贝到应用层的缓冲区中
TCP是全双工的
TCP协议段格式
16位源端口:存放源端口号
16位目的端口:存放目的端口
32位序号:存放当前数据段的序号
32位确认序号:返回给对方的确认应答的序号
4位首部长度:表示报头有多少个bit位,代表有多少个4个字节,4字节是单位,因为选项不确定有没有,所以4位首部长度代表有多长,eg : 上边如果选项没有,那么应该是有5个4字节的,因为报头长度为5,每行是4个字节,所以4位首部长度应为:20bit=4个字节*5行。
6个标志位:后边详谈
16位窗口大小:标志整个报文的长度
16位紧急指针:指向紧急数据的位置
6个标志位:
-
URG : 紧急标志位,报文处理时候是按队列的方式先后处理的,当收到的报文该标志位为1的时候,代表当前的事情是紧急的,优先放到最前边来解决
-
ACK:确认标志位,当收到该标志位的报文时候,代表对方发来了报文,确认应答,其实有效回复的数据段也算是一种确认应答,大部分通信时候回复的应答ACK基本上都存在
-
PSH:提示接收方缓冲区赶紧把数据读走
-
RST:有时候突发情况断开连接时候,检测到对方连接断开了,要求重新连接的时候,给该标志位
-
SYN:建立链接的时候,三次握手,需要给该标志位
-
FIN:要断开链接,四次挥手的时候给该标志位
确认应答机制
确认应答也可以是有效数据的应答,也可以是只是一个只有报头的数据段,都算确认应答,因为只要你收到了对方的消息,就可以证明上一个我发的消息你肯定接受到了,确认应答不一定是确认数据段,有时候正常的数据段也算确认应答。
只有当收到对方的确认应答的消息的时候,才能100%确定上一条消息被对方收到了
所以永远有最后一条消息不能保证被对方收到,因为最后一次发消息对方是不会回复的
上述的流程是串行的,但实际上真实的是并行的
按字节编号
TCP将每个字节都进行了编号,发送给对方我序号是最后一个字节的下标位置,对方发送过来的确认序号是你发送的+1,意思是你应该从哪个序号开始了
超时重传机制
超时重传有两种可能性,发送方丢包了,或者响应方传回来的时候丢包了,这种情况都会造成超时重传
-
情况一:发送方丢包
-
情况二:响应方丢包
不管那一方丢包,都会造成超时重传机制
当发送方发给对方一个数据段的时候,如果对方迟迟没用响应,那么就有可能是发送的数据丢失了,此时那么将重新发送该数据,判断是否丢包是
判断是否要超时重传是根据时间的推移,假设对方长时间没用回应,那么就有可能是丢包了,此时就响需要重发,但是判断的时间是不稳定的
时间长了,导致大量的等待时间被浪费。
时间短了,会导致一定时间内频繁向对方重发。
TCP为了保证无论在任何环境下都能比较高性能的通信, 因此会动态计算这个最大超时时间
Linux中(BSD Unix和Windows也是如此), 超时以500ms为一个单位进行控制, 每次判定超时重发的超时 时间都是500ms的整数倍.
TCP连接管理机制 (三次握手,四次挥手)
- 三次握手
为什么是三次握手?一次两次三次四次......可以吗?
假设是一次挥手,那么客户端只需要发起SYN就可以与服务端建立连接了,那么假设此时客户端打一个死循环疯狂向服务端发送SYN请求,那么就是一个恶意攻击,建立连接服务端也要消耗内存的,所以一次是不行的,这种攻击方式被称为SYN洪水攻击
两次握手同一次一样,也可以发起SYN洪水攻击
那么三次为什么可以行呢?
- 三次握手是最小的成本验证全双工的手段,如下图解析
- 三次握手可以有效防止单机对服务器的攻击
因为一旦三次握手成功,那么Client也会在自己主机上建立号连接,那么也会消耗内存开销,那么如果一直建立连接,单主机的内存肯定耗不过一个服务器集群的能力。
四次是的最后一次是服务端发送的,服务端没有办法能确认客户端收到了,所以四次是不行的,可以说是所有的偶数次都是不行的
大于3的奇数次握手都是可以的,但是既然3次都可以了,如果多的话就是浪费系统资源和内存
上边已经讲述了为什么要三次握手也等同了为什么要有三次握手,要想建立连接就要进行三次握手
三次握手不一定肯定成功,最担心的就是最后一个ACk丢失,如果丢失了,就算建立失败,但是有超时重传机制。
连接肯定的被管理起来的,服务端那么就存在大量的连接,那么就要管理起来,那么就是先描述再组织。
- 四次挥手
客户端和服务端地位是同等的,断开连接是双方共同的事情,双方都要同意,所以四次挥手不止一方要发起。
以下讲解的例子客户端比作发起方,服务端比作接受方,反过来也是同样的现象
①当客户端的文件描述符关掉时,会去发起断开连接的请求,发送带有FIN标志位的数据段,客户端发出FIN的数据段后状态变为FIN_WAIT_1
②服务端收到客户端发出的断开请求,那么便返回客户端给ACK,服务端发出后状态变为CLOSE_WAIT状态,客户端收到ACK后状态变为FIN_WAIT_2状态
③通常情况下,客户端断开fd后服务端读完数据后是要关闭自己的对应的套接字的,只有当服务端的对应的套接字关闭时,才会向客户端发起带FIN的数据段,当发出该数据段时,服务端的状态变为LAST_ACK
④当收到服务端的FIN时,此时客户端会发送最后一次ACK,发送完这个ACK后,客户端将进入TIME_WAIT状态,服务端收到该ACK后进入CLOSED状态
- 有时候②和③有概率会一起发送给客户端,有时候会出现三次挥手的现象
- 如果服务端不关闭对应的套接字,那么就会造成一直卡在CLOSE_WAIT状态(写程序时候造成的漏洞),那么连接则一直不会断开,占用资源
TIME_WAIT 状态详解
当客户端发起最后一次ACK请求的时候,进入的状态为什么不是CLOSED而是TIME_WAIT呢?
原因1:确保最后一次发送的ACK能被服务端收到,确保不丢包
因为第一次的FIN和第二次的FIN和第一次的ACK都不怕丢,因为丢了会发生超时重传,所以不需要担心前三次的数据丢包如果最后一次ACK发生丢包了,因为后续不会有会应了,所以只存在丢包的第一种情况,压根没发到达给服务端,此时服务端如果长时间没有收到ACK那么就会进行超时重传,再次给客户端发送FIN等待应答ACK
原因2:双方断开的时候,网络中还有滞留的报文,保证滞留报文进行消散
- TIME_WAIT持续的时长以及造成的影响
客户端在发送最后的ACK后要进入TIME-WAIT状态,这个状态时长为2倍MSLMSL它是任何报文在网络上存在的最长的最长时间,超过这个时间报文将被丢弃。
这个时间其实根据不是最终按照2MSL,各个系统内核有配置的时间,可以通过
cat /proc/sys/net/ipv4/tcp_fin_timeout
查看msl的值我们使用Ctrl-C终止了server, 所以server是主动关闭连接的一方, 在TIME_WAIT期间仍然不能再次监听 同样的server端口;
- 解决TIME_WAIT状态引起的bind失败的方法
当服务端进程有连接主动退出时候,连接没有断开,此时再重新连接是不允许的,这样不合理,因为假设是多个客户,如果长时间后才能重启,那么就会造成大量的亏损和流失,所以我们可以手动运行再次创建套接字
要在绑定listenfd之前操作完成
c
int opt=1;
setsockopt(listenfd,SOL_SOCKET,so_REUSEADDR,&opt,sizeof(opt));
//使用setsockopt()设置socket描述符的 选项SO_REUSEADDR为1,
//表示允许创建端口号相同但IP地址不同的多个socket描述符
滑动窗口
TCP在内核层的发送缓冲区可以按区域划分
所以可以分为三个区域,一开始全是未发送数据,滑动窗口就会向右边滑动
窗口大小指的是无需等待确认应答而可以继续发送数据的最大值.
滑动窗口的特点体现在无需等待确认应答就可以继续下去,实现并行的发送(提高性能)。
- 滑动窗口的大小是怎么设定的?
之前接收方的接受缓冲区,会在应答的时候告诉发送方,所以发送方的滑动窗口大小是动态变化的,根据接收方剩余缓冲区的大小决定的
- 滑动窗口只有向右移动和原地不动的现象
向右移动:当接收方应用层读走时那么此时接收方缓冲区会有空余通知发送方发数据并且给发送方应答,此时发送方的滑动窗口会向右移动,左指针右移动,右指针右移动 (这里的指针是下标)
原地不动:当接收方应用层就是一直不读,或者读的慢的时候,那么此时接收方的缓冲区会逐渐变满,此时会告诉发送方接受自己的剩余窗口大小,此时发送方的滑动窗口会不动,如果应用层一直不读那么就是(左指针和右指针都不动),还有一种情况就是发送方发送完了所有数据,此时(左指针一直向右,右指针不动,窗口减小)
- 滑动窗口不会出现空间不够的现象,因为本质是一个环形队列(用数组模仿),所以可以一直有空间
- 假设滑动窗口最左侧的数据发出去但是丢包了,或者滑动窗口中间的数据丢包了,那么滑动窗口动还是不动?(如下图)
假设是丢包,丢包分为两种情况,一种是压根没发出去,另一种是对方的回应没有送到,过程中丢了
那么如果是第一种压根没有发送到对面,此时假设滑动窗口后边的发送了也,但是因为①号没有发送过去,服务端收到的是后边的,服务端会发现序号不对,此时返回给客户端的确认序号依然是最开头的(①号的下标),所以后边返回的所有数据段的确认序号都是①号开头的确认序号,提醒下一个发送的是①号开头的下标,所以此时就会发现丢包,那么此时就得重发①号数据
如果是第二种,服务端收到了,但是客户端没有收到,那么此时滑动窗口就会向最右侧继续移动,假设①号没有收到应答,但是②号收到了,并且确认序号是②号下标,那么此时滑动窗口就会移动到②号,因为如果收到得确认序号是②号,确认序号得意思就是代表确认序号之前的都收到了,所以对方没有收到无所谓我收到了,给对方的确认序号是大的,所以滑动窗口可以继续移动,到最新的位置就行了
所以假设是①号丢了,分为两个情况,看哪一方丢包了,第一种的话就会发生重传,第二种就会跳过
中间,右边的位置也是如此,因为会慢慢移动,②号③号位置随着窗口移动会变成最左侧的位置
- 滑动窗口没有空间了怎么办?
滑动窗口采用的是环形队列(数组模拟实现)所以不会出现空间不够的情况,只会覆盖前边的已发送&&收到应答的区域。
拥塞控制
虽然有滑动窗口,能够高效的发送大量数据,但是如果在一开始就每次发送大量数据,那么就有可能会造成拥塞控制
因为网络也是资源,所以假设网络上现在很多用户都大量发送消息,就可能会造成网络很堵很慢,网络很拥堵的话,此时还一次发送大量数据,收不到回应,那么此时超时重传,又发送了大量数据,因为此时网络正拥堵,所以就是浪费系统资源,并且让网络更加拥堵。
所以就引出了拥塞控制的概念。
- 发送开始的时候,拥塞窗口为1
- 收到一个ACK窗口大小*2倍
- 每次的滑动窗口的大小=
min(对方的反馈的窗口大小,拥塞窗口的大小)
所以拥塞窗口的大小是一种慢启动,指数级增长的策略,当大到一定时候,就按对方反馈的大小了,因为是取较小值
为了不会增长那么块,因此不能一种*2倍的指数级增长,当到达一个阈值的时候,变为线性级的增长
- 当TCP开始启动的时候, 慢启动阈值等于窗口最大值;
- 在每次超时重发的时候, 慢启动阈值会变成原来的一半, 同时拥塞窗口置回1
当TCP通信开始后, 网络吞吐量会逐渐上升; 随着网络发生拥堵, 吞吐量会立刻下降;
拥塞控制其实就是为了提高网络传输效率而设计的一种折中方案。
延迟应答
有时候,在回应的时候可能会故意延迟一会才会发送,这个原因就是为了增加概率事件的发生进而提高效率
假设正要给对方回应,此时刚好发出去了,刚好应用层从上边取走了许多数据,那假设晚一会,那么刚刚能给对方回应更大的窗口大小,对方就能发送更多的数据了,所以,可以在这个基础上,等一会,有可能等的这一会时间应用层从传输层缓冲区拿走数据了,此时就可以反馈给对方更大的窗口大小了
但是不能每次都故意等一会,要是每次都故意等一会,那么就不是提高效率了,所以一般会有策略进行延迟应答
- 数量限制: 每隔N个包就应答一次;
- 时间限制: 超过最大延迟时间就应答一次
捎带应答
在延迟应答的基础上, 我们发现, 很多情况下, 客户端服务器在应用层也是 "一发一收" 的.
意味着客户端给服务器说 了 "How are you", 服务器也会给客户端回一个 "Fine, thank you";
那么这个时候ACK就可以搭顺风车, 和服务器回应的 "Fine, thank you" 一起回给客户端
四次挥手的时候,对方的ACK和FIN有时候也会捎带应答,变成"三次挥手"了
listen的第二个参数
listen的第二个参数是类似一个排队队列的最大数量,当接受的连接的数量满的时候,此时不能再accept了,所以只能排队等待,这个参数就是排队的长度
此时客户端已经完成了三次握手,此时服务端接受不了连接了所以一直不accpte(直到有空余),客户端等待服务端accept,客户端状态正常, 但是服务器端出现了 SYN_RECV 状态(因为没有accept), 而不是 ESTABLISHED 状态(在服务端看来,没有真正完成三次握手)
如果长时间没有握手,那么就自动被server关闭
tcp底层最多允许listen第二个参数+1个的半链接