TCP/IP模型
- 物理层:负责光、电信号的传播。物理层的能力
决定了最大传输速率 - 数据链路层:负责相邻设备之间的数据帧传输,完成帧同步,进行差错控制、链路管理、流量管理。
有以太网、令牌环网、无线LAN等标准,交换机就工作在数据链路层 - 网络层:负责地址管理和路由选择。这一层中著名的协议就是IP协议。
IP表示了唯一的主机,通过路由表规划出两台主机间的数据传输线路,也就是路由。
路由器,就工作在网络层 - 传输层:负责两个主机之间的数据传输,如TCP协议
- 应用层:负责应用程序间的沟通
MAC地址
- MAC地址用来标识数据链路层中相邻的节点,是6个字节48位
的16进制数字加上冒号表示的,本身和网络是没有关系的 - 网卡的mac地址大多在出场时就确定了,不能修改
碰撞
- 没有交换机的情况下,一个局域网就是一个碰撞域。在同一时刻中,
只允许有一个机器向网络中发送数据,如果有多个主机同时发送数据,就会产生
数据碰撞。 - 所以所有主机在发送数据时,都要进行碰撞检测和碰撞避免,效率更高,更省流量
- 主机在接收到数据时,就像人群中的交谈,并不是听到的所有话都是自己的,所以会通过数据中
携带的mac帧去检查数据是否是发给自己的
封包、解包和分用
- 自顶向下每一层都要在自己的数据(有效载荷)加上一个数据首部(header),
其中包含了首部长度、上层协议情况、有效载荷长度等等 - 在源主机中,数据包每一层都要加上自己的报头,最后通过物理层发送到目标主机,
目标主机收到报文之后,每一层解包,获得有效载荷和上层协议,将有效载荷交付给上层处理 - 不同层完整报文的名称:传输层,段(segment);网络层,数据包(datagram);数据链路层,帧(frame)
- 最后在源主机和目标主机看来,每一层都在和对方相应层进行直接通信
IP地址
- IP地址用来标识网络中不同的主机的地址
- IPv4是一个四字节,32位的整数,通常采用点分十进制表示,例如172.17.0.1,用点分割的每个数字表示一个字节,范围是0~255;IP地址分为两部分使用,第一部分是网络号,标识了具体的子网,只要在同一子网当中,网络号都相同,第二部分是主机号,同一子网中,主机号一定不同
- mac地址用来标识数据链路层中两个相邻的节点,在数据从源主机发送
到目标主机的过程中,mac地址是在不断变化的,而ip地址作为目标地址,
始终没有发生改变 - ip地址在路由中的作用:在网络层中查阅路由表时,发现目标主机ip并不在本局域网所在主机ip中,就会将内容发送给路由器,由路由器去完成后续过程。
- 一个网络由网段标识,ip中含有网段信息,在一个子网中分配的所有ip都带有自己子网的网段信息;当一个主机从一个网络移动到另一个网络时,他的ip地址可能会发生变化,因为网段信息发生了改变
- 一个路由器通常具有两个ip,一个是WAN口IP,一个是LAN口IP(内网ip)。不同路由器的子网IP大多是相同的,192.168.1.1,不过要求同一子网中主机号不同,也意味着不同子网中可以相同;需要进行公网范围的通信时,就通过NAT技术,将LAN口的子网首部ip替换为WAN口的公网ip,逐级替换,就完成了子网ip到公网ip的转换
- 根据网络号、主机号在32位ip地址中的长度,曾有人提出A、B、C、D、E五种划分方案,其中A类ip的单个子网中可以容纳的主机最多。实际上大部分组织都申请B类ip导致ip地址的浪费非常严重。所以后来引入了无类别域间路由CIDR(classless interdomain route),使用一个同样是32位的整数作为子网掩码,不管ip是那种类型,都可以通过子网掩码与原来的ip地址做与运算,就可以得到网络号
Socket
端口号:
- 从网络上取到的数据终归是要给程序去给应用层的程序使用的,而进程是资源分配的基本单位,在这里就担当
起了主机间沟通的使者。使用ip+端口,就是标记了网络上某一台主机中的某一个进程 - 端口号是一个两字节16位的整数,取值范围0~65535,其中0~1023是知名端口号,分给一些固定的进程;其余的是
可以供给用户自由使用的端口号 - 一个进程可以有多个端口号,但是一个端口号只能对应一个进程
Socket
- IP + 端口号,就是Socket
网络字节序
发送时,源主机按照低地址到高地址的顺序发送;接收时,目的主机按照从低地址到高地址的方式接收到缓冲区。
TCP/IP协议规定,无论机器是大端机还是小端机,发送时都通过大端的方式发送,即低字节序存储在高地址(顺着)
转换函数
cpp
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohl(uint16_t netshort);
h,表示源主机;n表示目的主机,l表示32位长整型,s表示16位整型。例如htonl表示从源主机到目的主机,转为长整型
这四种转换函数,对于已经是小端机的情况,不会进行处理;否则转换为大端字节序之后再返回
sockaddr结构
IPv4, IPv6, Unix Domain Socket,这些网络底层协议的地址格式并不相同
cpp
//IPv4 16位地址类型 14字节地址数据
//IPv6 16位地址类型 2字节端口号 4字节ip地址 8字节填充
//Unix 16位地址类型 108字节路径名
为了通用性,我们拿到struct sockaddr*之后,都要转换成sockaddr_in来使用(IPv4的地址格式)。这样的转化使得
每种网络协议间都能够进行网络连接,因为可以通过起始2个字节的地址类型来判断出需要以那种网络协议来使用这个结构体
常用函数
cpp
int socket(int domain, int type, int protocol);
一般使用IPv4网络协议进行编程时,domain设置为AF_INET,type为SOCK_DGRAM(数据报),protocol设置为0则自动选择
cpp
int bind(int sockfd, const sockaddr* addr, socklen_t len);
UDP
将sockfd设置进内核。(网络层和传输层都是在操作系统这一级别中)
cpp
ssize_t recvfrom(int socket, char* buffer, ssize_t size, int flag, const sockaddr* src_addr, socklen_t* len);
ssize_t sendto(int socket, char* buffer, ssize_t size, int flag, const sockaddr* des_addr, socklen_t len);
两个函数负责发送与接收。recvfrom将socket标记的进程所接收到的信息存进buffer,如果最后两个参数不设置为空,
那么就作为输出型参数带出发送方的信息;sendto是根据des_addr所提供的信息,将buffer中的信息发送过去
UDP报文结构
- TCP/IP协议通过源ip、源端口、协议号、目的IP、目的端口标识一个通信
- UDP数据报由以下部分组成:
cpp
|------------------------|
| 源端口号 | 目的端口号
| 最大长度 | 校验和
| 报文数据
- 前面的四个是UDP的头部信息,各是2字节;最大长度是UDP允许的最大数据报长度,即65535字节,包括报头,这意味着一旦使用udp传输一定量的数据,就要大量的切分发送;校验和是对报文数据的校验,如果不符合直接丢弃报文
- 不可靠与无连接:UDP不建立链接,只要知道目的主机的ip和端口就直接发送;UDP也没有真正意义上的发送缓冲区,调用sendto时,会直接将内容交给操作系统内核的网络层,进行发送处理;但是有接收缓冲区,不过接收缓冲区中报文顺序不一定和发送顺序一致,并且如果缓冲区已经满了,那么就会丢弃之后到来的报文
- 不允许切分接收:比如发送方发送十个字节,接收方也要一次接受十个字节,不能十次接受一个字节
TCP
cpp
int listen(int sockfd, int backlog);
监听sockfd,最多可以有backlog个线程
cpp
int accept(int sockfd, sockaddr* addr, socklen_t* len);
从操作系统中获取链接。前面我们就说过,linux中一切皆文件,包括我们的网卡,在tcp协议中,我们可以直接
用read,write通过操作文件的逻辑,完成信息的发送和接受
cpp
int connect(int sockfd, sockaddr* addr, socklen_t len);
让客户端获取服务器的套接字,直接使用传入的sockfd进行文件逻辑的消息发送接收即可
为什么tcp是全双工的?为什么被称作传输控制协议呢?
- 在接受和发送的两台主机上,都有接受和发送缓冲区,发送方调用write系统调用后,本质上会将需要发送的序列化的内容拷贝到发送缓冲区中,这个区域在操作系统内核中;随后发送到对端接收缓冲区中,拷贝给目标进程
- 在上面的过程中,怎么发送、发送失败处理,这些都是tcp协议自说了算的,所以tcp协议也被称作传输控制协议
TCP的报文结构
cpp
|--------------------------------------------
| 16位源端口 | 16位目的端口
| 32位序号
| 32位确认序号
| 4位首部长度|6位保留位|4位标志位| 16位窗口大小
| 16位检验和 | 16位紧急指针
| 选项
| 数据
|--------------------------------------------
-
4位首部长度
标识TCP首部长度是多少个四字节,所以tcp首部长度最大是15 * 4 = 60个字节
-
4位标志位
- URG,标识紧急指针是否有效,16位紧急指针指向数据中的紧急数据
- ACK,确认序号是否有效
- PSH,催促接收方立刻从缓冲区中读取走所有数据
- RST,发送方要求重新建立TCP链接
- SYN,同步信号,发送方请求建立连接
- FIN,发送方请求关闭连接
三次握手、四次挥手的流程
- 客户端请求建立连接,发送包含SYN标志位的TCP报文给服务端,服务端接收到信号后,发送包含SYN和ACK的TCP报文,客户端接收到后发送包含ACK的报文,标识自己同意建立连接。这就是三次挥手的过程,不过某种意义上也可以认为是四次,因为服务端的应答中包含了两个信息(ACK SYN)。seq标识请求序号,ack是响应中的确认序号,一般ack是seq + 数据长度,但是三次握手的过程中,确认序号位请求序号 + 1;
- 双方数据传输:超时重传。一方在发送完数据之后,会进入等待状态,如果超过这些时间仍然没有接收到接收方返回的ACK报文,就会重新发送,这段时间在Linux下一般是500ms,再次等待的超时时间为2 * 500ms,之后再翻倍,到达一定长度后,发送端会认为对方已经下线,关闭连接
- 当客户端请求断开连接时,会发送包含有FIN信号的TCP报文,随后进入FIN_WAIT1;服务端在接收到客户端的请求后会发送含有ACK信号的报文,表示同意断开连接,进入CLOSE_WAIT状态;在处理完此处的数据后,服务端发送FIN信号,进入LAST_ACK状态;客户端接收到之后,进入FIN_WAIT2状态,发送同意ACK报文,同时进入TIME_WAIT状态,经过两个MSL(minimum section lifetime)后客户端关闭连接;而我们的服务端在接收到客户端的LAST_ACK之后,就断开连接,完成四次挥手的过程,如果服务器没有正确关闭连接,就会导致服务器上存在大量CLOSE_WAIT状态
- minimum section lifetime存在的意义:
- 规定主动断开的乙方在相应状态下应当等待2个MSL
- 保证双方都已经完成了各自的数据传输,因为msl是tcp报文最长的存活时间,两方的数据要么在这段时间到达,要么已经丢失
- 在上面的例子中,客户端接收到服务端的FIN后,进入TIME_WAIT状态,同时客户端发送ACK报文,如果这个报文丢失了,那么服务端还能再次发送FIN信号等待客户端的ACK信号,这样理论上就能保证四次挥手的顺利进行
解决等待时间中bind失败的问题
- 在一些情况下,例如一个服务器需要接受大量的TCP链接,但是每个连接很快就能完成数据交互,这是2个MAL的等待时间就是不合适的
cpp
int setsockop(int sockfd, SOL_SOCKET, SO_REUSEADDR, optval, optlen);
这样就能设置端口重用,允许设置其他端口相同但是ip不同的socket描述符
发送时的滑动窗口
- 所谓滑动窗口,就是允许不等待上一段ACK到来就可以接着发送的最大值。比如说现在滑动窗口的大小是五个段,每个1000个字节,这五个同时进行发送,当接收到第一个段的ACK后,告诉发送方接着发送1001个字节(但是我们是五个段同时发送的)滑动窗口向后移动,开始发送第六个段(5001~6000个字节),以此类推,就像算法题中的滑动窗口一样,操作系统也会为了维护滑动窗口开辟一块发送缓冲区,只有接收到应答的部分才能够从发送缓冲区中删除
- 丢失报文的两种情况
(1)响应丢失,这种情况不需要处理,因为可以通过后续的ACK进行确认。这样理解,接收端接收到数据,就会从这段数据的下一段的第一个字节请求发送方继续发送,如果后续请求丢失响应的后面的内容,那么就意味着前面没有接收到的内容一定已经被接受了,否则接收端会一直发送这一段的响应请求。
(2)报文丢失,这种情况会一直发送接收端从开始连续接收到的最后一个字节的下一个字节的请求,即使后面断层的接收到了后面的报文。在连续接收到三次同位置的请求之后,发送方就会触发重传,再次发送对方请求的段,这被称为快重传机制
流量控制
- 如果不管接收方的接受能力就一股脑的发送的话,把接收方的缓冲区打满了,就可能会导致丢包重传,反而降低传输效率
- 所以为了控制流量,滑动窗口一开始会已较小的大小发送,接收端根据自身接受能力和剩余缓冲区大小在TCP包头中那个16位的滑动窗口大小中设置发送端滑动窗口大小;当接收端接收缓冲区被打满时,会在发送响应时将滑动窗口大小设置为0。随后发送方会暂停发送,但会时不时发送请求确认新的滑动窗口大小(请求时如果仍然接收缓冲区为满,那么继续等待);当接收方缓冲区可以接受后,会发送新的华东窗口大小给发送方,继续发送
拥塞控制
- 流量控制解决的是接收方接受能力的控制,但是网络传输还有一个重大的影响因素就是网络的拥塞情况。
- 这里引入一个概念拥塞窗口,实际窗口大小,是拥塞窗口大小和接收方发送的滑动窗口哦大小的较小值。
- 一开始当拥塞窗口很小时,窗口大小成指数级别增加,当到达慢启动阈值时(滑动窗口最大大小),开始加法增长,每次加一;出现网络拥塞时(大量丢包,少量的超时重传不算),拥塞窗口回归到1,同时慢启动的阈值变为原来一半
延迟应答
- 结合上面的机制,可以知道如果立即返回接收到的数据的响应的话,可能会导致对端窗口过小。只要没有引起网络拥塞和对方接受能力不足,滑动窗口越大,网络吞吐量越大,效率越高
- 所以操作系统会等待一定时间再进行应答,不同操作系统等待的次数和时间不同,一般是每隔两个包就应答一次,或者超过最大等待时间200ms
捎带应答
- 其实就是让确认应答搭乘顺风车
- 粘包问题
- 对于定长的包,直接按长度读取即可
- 对于变长的包,可以在包头上加上定义包的长度等信息的字段;也可以加上特定的分隔符在包内容中
- 对于udp则不存在这种问题,因为一个是最大长度确定,且包头中有字段记录长度,没有解包分用前可以使用;在应用层,不存在发送一半的情况,要么已发送,要么未发送,所以不存在粘包问题
总结
tcp可靠性
校验和、序列号、确认应答、超时重发、连接管理、流量管理、拥塞控制
tcp效率
滑动窗口、延迟应答、捎带应答、快速重传
HTTP协议
- 超文本传输协议,是TCP/IP协议栈中的应用层协议,HTTP协议支持一定时间内的tcp连接保持,这个链接可以用于两端通信;分为状态行、请求头、主体,其中,记录用户某个网站信息的cookie就保存在HTTP协议头部字段
- HTTP协议是无状态的协议,在最初的设计中http协议是接受到一个请求,返回一个应答后直接关闭连接的,现在的HTTP请求是长连接还是短链接,取决于connection字段,如果被设置为close,则是短链接,如果是keep_alive,则是长连接
常见方法
- GET方法:用于向服务器请求资源,长度有限制,http协议本身并没有规定url的长度,但是浏览器和服务器会对其有限制
- POST方法:提交表单供给服务器处理,不会被缓存,也不会呈现在浏览器的历史纪录当中
- PUT方法:提交在某URI下存储的文件,如果已经存在则更新,否则新建
- DELETE方法:删除指定URI下的文件
- HEAD方法:只返回请求头,用于确认路径的有效性及更新日期等
状态码
1. 1xx
标识请求正在处理
2. 2xx
标识处理成功
3. 3xx
标识重定向操作,301标识永久重定向,302标识临时重定向,并通过Location字段来设置新的url
4. 4xx
标识坏请求(如常见的404 请求的资源不存在,403禁止访问)
5. 5xx
服务器内部问题,如503服务器内正在维护,500服务器问题
cookie
- cookie是存储在客户端的文本文件,保存着服务器返回内容中Set-Cookie字段中的信息,包括用户id,密码等等,下次访问如果cookie仍未失效的话,就可以直接利用cookie中的信息进行登陆访问
- 在HTTP协议中,cookie是明文传输的,但是HTTPS因为整体加密,所以cookie也是加密传输的
cookie的基本属性
- domain,域名,如果一个cookie的域名设置为.google.com,那么以google.com结尾的所有域名都可以访问这个cookie,第一个字符必须为.
- path,路径,如果设置为/webPath/,那么只有这个路径下的程序可以访问这个cookie,最后一个字符必须为/
- httponly,设置httponly之后,js脚本就无法再获取cookie,理论上更加安全
- secure,cookie是否只能被安全的协议使用,例如https
- expires,cookie的存活时间,默认只在浏览器会话存在期间有效,也可以设置为某个具体日期时失效删除
session
- 为了克服cookie具有的安全性和文件大小限制的问题,有了session会话管理。session由服务端存储,保存着会话信息,cookie只要负责传递会话id即可,这样即使有恶意程序修改会话id也不会影响session服务器中存储的会话信息。但是session的存储也需要大量的资源,牺牲资源换取了更高的安全性
- session对开发人员更加友好,可以存放各种类型的数据,但是cookie只能存放字符串
- session默认的有效时间是30分钟
- cookie保存在浏览器中,由单个大小的上线为4kb,并且总数也有限制,很多浏览器限制一个站点最多允许使用20个cookie
无论session还是cookie都是为了解决http无状态的问题,都是为了记录客户端的状态
MAC地址
- MAC地址是六字节的整数,通常使用两位16进制数字 + 冒号表示,用于区分数据链路层中相邻的节点
- IP地址表明的是最终的目的,MAC地址表示的是一段路程中的目的地,在物理层完成传输后,就要进行MAC地址的检验,如果收到报文中的MAC地址和本机不符合,就丢弃报文
MTU
- 最大传输长度,max transfor unit,是受物理层的影响,数据链路层帧的长度最小46字节,短的补充零,最长1500字节,如果长度超标,就会分片,到对端再进行重组,如果重组时出现了丢包,那么视为全部失败
- IP首部中存有当前分片的偏移量,是以8字节为单位的,因此要求除了最后一个分片,其他分片的大小都是8字节的倍数
对IP的影响
每个小数据包ip中的id都是相同的,表示是同一种数据包;三个标志位中,第二位如果为0,代表允许进行分片,第三位表示是否是最后一个包,是的话为1
如果出现丢包导致失败,IP曾不负责重新传输
对于UDP协议的影响
- UDP本身就有最大长度限制,如果超过65535个字节会分开发送;同时因为MTU的限制,也可能会再次分包,加上UDP不可靠不重传的机制,会使UDP更加不稳定
对TCP协议的影响
- 虽然理论上TCP协议数据可以任意长度,但是受制于MTU,TCP长度有限制,这个限制被称作MSS(max segment size)
- 不同的网络MTU有所差异,在三次握手的过程中,在TCP头部字段中双方会携带自己的MTU大小,随后会取双方MTU的较小值作为MSS的大小
ARP协议
- 应用层的程序知道目的主机的IP和端口号,但是并不知道对方的MAC硬件地址,而前面有提到错误的MAC地址会导致传输失败,所以ARP协议就是来解决这个问题
- 在发送之前,会通过ARP协议在本网段中广播,目标主机在收到ARP报文后会返回本主机的MAC地址给源主机,这样就得到了对端的硬件地址,也就解决了不知道对端MAC地址的问题
NAT
当一个局域网中的主机请求外网中的服务器时,路由器会通过NAT技术将IP首部中的目的IP替换为公网IP,从而能够在公网上进行通信
NAPT
虽然NAT解决了局域网的主机到公网请求的问题,但是当请求的服务器返回数据时,路由器怎样区分将数据交给哪个进程呢?NAPT通过IP + 端口的一起映射方式完成将数据回复的工作(具体的主机路由器中有映射表保存,但是具体交付给哪个进程就要通过ip + 端口一起映射的方式
正向代理服务器
- 客户端将请求发送给正向代理服务器,有正向代理服务器向服务器发起请求
- 可以缓存内网中申请的一些资源,需要时不再像外网中的服务器请求,而是直接发送缓存的内容;也可以对访问的内容进行限制
反向代理服务器
- 作用在服务端,客户端发送请求时,不直接发送给直接处理的服务器,而是发送给反向代理服务器,由反向代理服务器决定具体将需求交给哪个服务器处理
- 负载均衡、缓存加速、动静态资源分离等等
内网穿透和内网打洞
- 内网穿透是链接公网上的一个服务器,这个服务器建立这个主机和该服务器某个端口的映射关系。只要其他机器需要访问外部局域网中的某台主机,就可以访问内网穿透服务器的特定端口,从而建立映射,服务器负责两方的数据传输,形象地说内网穿透服务器就是个中介
- 内网打洞,只需要一个主机访问公网中的服务器,告诉服务器自己的公网IP和端口,另一台主机同样,通过服务器交换双方的公网IP和端口,随后两者直接建立通信(P2P),效率更高,更省流量,所谓的"洞",其实就是链接,让原本两个局域网中无法直接通信的两个主机能两个局域网临时通信
UDP TCP MAC IP报头长度
udp报头长度8个字节,tcp20个字节,mac14个字节,ip20个字节