网络基础
1.什么是协议,常见协议
? 协议本质上为数据传输和数据解释的规则。随着不断完善和使用人数的增加后面变成了标准协议。
各层中涉及的协议的简单解释:
- 应用层
·DHCP(动态主机分配协议) · DNS (域名解析) · FTP(File Transfer Protocol)文件传输协议 · Gopher (英文原义:The Internet Gopher Protocol 中文释义:(RFC-1436)网际Gopher协议) · HTTP (Hypertext Transfer Protocol)超文本传输协议 · IMAP4 (Internet Message Access Protocol 4) 即 Internet信息访问协议的第4版本 · IRC (Internet Relay Chat )网络聊天协议 · NNTP (Network News Transport Protocol)RFC-977)网络新闻传输协议 · XMPP 可扩展消息处理现场协议 · POP3 (Post Office Protocol 3)即邮局协议的第3个版本 · SIP 信令控制协议 · SMTP (Simple Mail Transfer Protocol)即简单邮件传输协议 · SNMP (Simple Network Management Protocol,简单网络管理协议) · SSH (Secure Shell)安全外壳协议 · TELNET 远程登录协议 · RPC (Remote Procedure Call Protocol)(RFC-1831)远程过程调用协议 · RTCP (RTP Control Protocol)RTP 控制协议 · RTSP (Real Time Streaming Protocol)实时流传输协议 · TLS (Transport Layer Security Protocol)安全传输层协议 · SDP( Session Description Protocol)会话描述协议 · SOAP (Simple Object Access Protocol)简单对象访问协议 · GTP 通用数据传输平台 · STUN (Simple Traversal of UDP over NATs,NAT 的UDP简单穿越)是一种网络协议 · NTP (Network Time Protocol)网络校时协议 - 传输层
·TCP(Transmission Control Protocol)传输控制协议 · UDP (User Datagram Protocol)用户数据报协议 · DCCP (Datagram Congestion Control Protocol)数据报拥塞控制协议 · SCTP(STREAM CONTROL TRANSMISSION PROTOCOL)流控制传输协议 · RTP(Real-time Transport Protocol或简写RTP)实时传送协议 · RSVP (Resource ReSer Vation Protocol)资源预留协议 · PPTP ( Point to Point Tunneling Protocol)点对点隧道协议 - 网络层
IP(IPv4 · IPv6) Internet Protocol(网络之间互连的协议) ARP : Address Resolution Protocol即地址解析协议,实现通过IP地址得知其物理地址。 RARP :Reverse Address Resolution Protocol 反向地址转换协议允许局域网的物理机器从网关服务器的 ARP 表或者缓存上请求其 IP 地址。 ICMP :(Internet Control Message Protocol)Internet控制报文协议。它是TCP/IP协议族的一个子协议,用于在IP主机、路由器之间传递控制消息。 ICMPv6: IGMP :Internet 组管理协议(IGMP)是因特网协议家族中的一个组播协议,用于IP 主机向任一个直接相邻的路由器报告他们的组成员情况。 RIP : 路由信息协议(RIP)是一种在网关与主机之间交换路由选择信息的标准。 OSPF : (Open Shortest Path First开放式最短路径优先). BGP :(Border Gateway Protocol )边界网关协议,用来连接Internet上独立系统的路由选择协议 IS-IS:(Intermediate System to Intermediate System Routing Protocol)中间系统到中间系统的路由选择协议. IPsec:“Internet 协议安全性”是一种开放标准的框架结构,通过使用加密的安全服务以确保在 Internet 协议 (IP) 网络上进行保密而安全的通讯。 - 数据链路层
802.11 · 802.16 · Wi-Fi · WiMAX · ATM · DTM · 令牌环 · 以太网 · FDDI · 帧中继 · GPRS · EVDO · HSPA · HDLC · PPP · L2TP · ISDN - 物理层
以太网物理层 · 调制解调器 · PLC · SONET/SDH · G.709 · 光导纤维 · 同轴电缆 · 双绞线 各层功能及协议的简单解释
2.网络应用设计模式 BS/CS优缺点
什么是C/S模式
C/S模式是传统的网络应用设计模式,客户机(client)/服务器(server)模式。需要在通讯两端各自部署客户机和服务器来完成数据通信。如(QQ、微信、LOL、CF、DNF等游戏需要下载客户端)
什么是B/S模式
浏览器(browser)/服务器(server)模式。只需在一端部署服务器,而另外一端使用每台PC都默认配置的浏览器即可完成数据的传输。
优缺点
C/S优点:
-
协议选择灵活,由于客户端和服务器都是自己开发,可以自定义协议进行消息传递。例如,腾讯公司所采用的通信协议,即为ftp协议的修改剪裁版 -
可以本地进行大量缓存,速度快,体验好。例如,知名的网络游戏魔兽世界。3D画面,数据量庞大,使用C/S模式可以提前在本地进行大量数据的缓存处理,从而提高观感。
C/S缺点:
-
安全性低,客户端的安装可能会导致本地用户信息泄露 -
跨平台性,需要为不同操作系统进行客户端定制 -
开发工作量大,由于服务器+客户端都需要一个团队完成开发量倍增
B/S优点: 安全性高,跨平台性好,开发工作量小。因为不需要安装客户端,只需要浏览器即可完成数据交互
B/S缺点:
-
由于使用第三方浏览器,因此网络应用支持受限。 -
没有客户端放到对方主机上,缓存数据不尽如人意,从而传输数据量受到限制。应用的观感大打折扣。 -
必须与浏览器一样,采用标准http协议进行通信,协议选择不灵活。 因此在开发过程中,模式的选择由上述各自的特点决定。根据实际需求选择应用程序设计模式。
3.分层模型(物数网传会表应)
- 物理层:主要定义物理设备标准,如网线的接口类型、光纤的接口类型、各种传输介质的传输速率等。它的主要作用是传输比特流(就是由1、0转化为电流强弱来进行传输,到达目的地后再转化为1、0,也就是我们常说的数模转换与模数转换)。这一层的数据叫做比特。
- 数据链路层:定义了如何让格式化数据以帧为单位进行传输,以及如何让控制对物理介质的访问。这一层通常还提供错误检测和纠正,以确保数据的可靠传输。如:串口通信中使用到的115200、8、N、1
- 网络层:在位于不同地理位置的网络中的两个主机系统之间提供连接和路径选择。Internet的发展使得从世界各站点访问信息的用户数大大增加,而网络层正是管理这种连接的层。
- 传输层:定义了一些传输数据的协议和端口号(WWW端口80等),如:TCP(传输控制协议,传输效率低,可靠性强,用于传输可靠性要求高,数据量大的数据),UDP(用户数据报协议,与TCP特性恰恰相反,用于传输可靠性要求不高,数据量小的数据,如QQ聊天数据就是通过这种方式传输的)。 主要是将从下层接收的数据进行分段和传输,到达目的地址后再进行重组。常常把这一层数据叫做段。
- 会话层:通过传输层(端口号:传输端口与接收端口)建立数据传输的通路。主要在你的系统之间发起会话或者接受会话请求(设备之间需要互相认识可以是IP也可以是MAC或者是主机名)。
- 表示层:可确保一个系统的应用层所发送的信息可以被另一个系统的应用层读取。例如,PC程序与另一台计算机进行通信,其中一台计算机使用扩展二一十进制交换码(EBCDIC),而另一台则使用美国信息交换标准码(ASCII)来表示相同的字符。如有必要,表示层会通过使用一种通格式来实现多种数据格式之间的转换。
- 应用层:是最靠近用户的OSI层。这一层为用户的应用程序(例如电子邮件、文件传输和终端仿真)提供网络服务。
4.以太网帧协议(为什么需要+报文分析+演示)
1)为什么需要以太网帧协议?
? 计算机之间通信本质上是通过网卡+网线来传递高低电压实现的,单纯的电信号0和1没有任何意义,必须规定电信号多少位一组,每组什么意思,以太网帧协议就是高低电压在网卡间数据传输和数据解释的规则。
? 以太网帧协议需要注明源地址(从那个网卡来)、到目的地址(那个网卡去)、消息体的类型(相当于咱们websocket消息头里的code)、消息体。
以太网帧协议的主要三大类型:0800(IP)、0806(ARP)、8035(RARP)
? 以太网帧中的数据长度规定最小46字节,最大1500字节,ARP和RARP数据包的长度不够46字节,要在后面补填充位。****最大值1500称为以太网的最大传输单元(MTU)****,不同的网络类型有不同的MTU,如果一个数据包从以太网路由到拨号链路上,数据包长度大于拨号链路的MTU,则需要对数据包进行分片(fragmentation)。ifconfig命令输出中也有“宝贝”。注意,MTU这个概念指数据帧中有效载荷的最大长度,不包括帧头长度。
2)ARP协议的作用?
? 我们在进行网络请求的时候往外只知道服务器IP但是不知道服务器的mac的地址,而以太网协议依靠mac地址才能进行网卡间的信息传递,ARP协议就是用来根据IP查询目标服务器mac地址的(请求分组时广播 响应分组是单播)。
查看ARP抓包,ARP缓存演示,ARP协议抓包分析,广播,单播(192.168.168.162)
每台主机都维护一个ARP缓存表,可以用arp -a命令查看,交换机上没有ARP缓存表,因为交换机是二层协议。
科普:什么是广播风暴?交换机在局域网内信息传递时起到什么作用?
- 想要搭建多个计算机的局域网就避免不了使用交换机,交换机工作机制(一个VLAN是一个广播域)。
-
转发:是决定数据帧转向那个端口发出,这个过程需要借助交换机表完成! -
过滤:决定一个数据帧是应该转发到接口还是丢弃,需要借助交换机表! 注:比如mac1发送到mac9,广播后找不到mac9会在交集表上记录一个mac9=en0,下次mac1再访问mac9时看表就知道已经广播过了,可以直接过滤了。 -
自学习:建立MAC地址和端口的一一对应,当收到数据帧后检查目的MAC,通过查找交换表,从而查出要从那个端口把这个数据发出去!
? 广播风暴就是在一个较大的局域网内多台计算机发起广播导致整个局域网网络拥挤甚至瘫痪,这时候就需要路由器来隔离广播域。路由器有WAN(链接外网)和LAN(链接内网)两种端口,LAN端口和交换机的端口一样可以进行广播转发,但如果有消息到了WAN端口路由器就会分析以太网帧协议内部的IP协议结合路由表来判断是否要转发这个消息(广播)。会将目标IP地址和自己的掩码相与,发现目标IP的网络号和自己接受这个数据包的端口所在的网络号一致,属于同一个网段,数据内部转发,根本不需要通过路由器WAN端口,所以就会把这个数据包丢弃!
3)RARP协议作用
? RARP协议广泛应用于无盘工作站引导时获取IP地址。RARP允许局域网的物理机器从网关服务器ARP表或者缓存上请求其IP地址。
6.IP协议(为什么需要+报文分析+演示)
1)为什么需要IP协议?
? 试想全世界的电脑都在一个局域网内仅仅依靠以太网帧协议根据mac地址进行消息传递,A计算机想访问B计算机但是不知道B计算机的mac地址于是发起了ARP广播询问B计算机的mac地址这个广播将传到全球所以的计算机上,而且世界上每时每刻都有很多计算机直接进行交流,如果采用这种模式将造成全世界级别的广播风暴。所以广播域不能太大,需要将全球的广播域切分成一个个小的广播域。mac地址是由厂商决定的无规律,无法区分广播域,所以产生了IP协议作用与网络层用于区分不同的广播域(子网)。
2)IP协议怎么区分广播域?
? IP地址分为网络部分和主机部分,网络部分表示ip所属子网,主机部分表示ip所属主机,单纯的ip地址是无法区分改ip所属的子网的,必须配合子网掩码。
? 所谓”子网掩码”,就是表示子网络特征的一个参数。它在形式上等同于IP地址,也是一个32位二进制数字,它的网络部分全部为1,主机部分全部为0。比如,IP地址192.168.93.101,如果已知网络部分是前24位,主机部分是后8位,那么子网络掩码就是11111111.11111111.11111111.00000000,写成十进制就是255.255.255.0。
知道”子网掩码”,我们就能判断,任意两个IP地址是否处在同一个子网络。方法是将两个IP地址与子网掩码分别进行AND运算(两个数位都为1,运算结果为1,否则为0),然后比较结果是否相同,如果是的话,就表明它们在同一个子网络中,否则就不是。
比如判断 192.168.93.101/24和192.168.93.103/24是否属于同一子网
192.168.93.101——》11000000.10101000.10111010.11001010
255.255.255.0——》 11111111.11111111.11111111.00000000
等于11000000.10101000.10111010.00000000=192.168.93.0
192.168.93.103——》11000000.10101000.10111010.11001110
255.255.255.0——》 11111111.11111111.11111111.00000000
等于11000000.10101000.10111010.00000000=192.168.93.0
所以192.168.93.101/24和192.168.93.103/24 属于同一子网
IP地址的子网掩码设置不是任意的,随意设置过大或者过小都会影响网络数据传输。
如果将子网掩码设置过大,也就是说子网范围扩大,那么,根据子网寻径规则,很可能发往和本地主机不在同一子网内的目标主机的数据,会因为错误的判断而认为目标主机是在同一子网内,那么,数据包将在本子网内循环,直到超时并抛弃,使数据不能正确到达目标主机,导致网络传输错误;如果将子网掩码设置得过小,那么就会将本来属于同一子网内的机器之间的通信当做是跨子网传输,数据包都交给缺省网关处理,这样势必增加网关的负担,造成网络效率下降。
3)IP协议段分析
这里主要关注TTL和8位协议,其中TTL(Time to live)代表这个数据包的存活时间,没调一个路由节点减一,如果减到0则丢弃该包,防止由于部分路由器故障导致网络阻塞(画图);8位协议则是记录上层使用的协议如TCP、UDP。
4)路由表,路由规则
在Linux内核中都维护了一张路由表,可以使用route -n查看,它存储了本地计算机可以到达的网络目的地址范围和如何到达的路由信息。路由表是TCP/IP通信的基础,本地计算机上的任何TCP/IP通信都受到路由表的控制。当我们要访问一个目标IP时数据包从那个网口发出,下一跳的以太网帧目的mac地址填什么都要基于路由表+路由规则计算得到。
列名 | 备注 |
---|
Destination/目的地 | Destination 为 default(0.0.0.0)时,表示这个是默认网关,当路由表中匹配不到路径时数据包发到这个网关。 | Gateway/网关 | 网关地址,0.0.0.0 表示当前记录对应的 Destination 跟本机在同一个网段同一个广播域,通信时不需要经过网关,ip为目标ip,mac为目标ip对应的mac,可以通过ARP广播获取。 | Genmask/子网掩码 | Destination 字段的网络掩码,Destination 是主机时需要设为 255.255.255.255,是默认路由时会设置为 0.0.0.0,0.0.0.0则会匹配全部目标地址。 | Flags/标记 | ● U 该路由可以使用。 ● H 该路由是到一个主机,也就是说,目的地址是一个完整的主机地址。如果没有设置该标志,说明该路由是到一个网络,而目的地址是一个网络地址:一个网络号,或者网络号与子网号的组合。 ● G 该路由是到一个网关(路由器)。如果没有设置该标志,说明目的地 是直接相连的。 ● R 恢复动态路由产生的表项。 ● D 该路由是由改变路由(redirect)报文创建的。 ● M 该路由已被改变路由报文修改。 ● ! 这个路由将不会被接受。 | Metric/路由距离 | 到达指定网络所需的中转数,是大型局域网和广域网设置所必需的。 | Ref/路由引用次数 | 路由项引用次数 | Iface/网卡 | 网卡名字,例如 eth0。 | Use/此路由项被软件查找的次数 | 此路由项被路由软件查找的次数 |
首先当要访问一个ip时首先根据路由规则确定路由项即需要走路由表的的哪一行:
1. TCP/IP使用需要通信的目的IP地址和路由表中每一个路由项的网络掩码进行相与计算,如果相与后的结果匹配对应路由项的网络地址(Destination),则记录下此路由项。
2.当计算完路由表中所有的路由项后,
-
TCP/IP选择记录下的路由项中的最长匹配路由(网络掩码中具有最多“1”位的路由项)来和此目的IP地址进行通信。 -
如果存在多个最长匹配路由,那么选择具有最低跃点数的路由项。 -
如果存在多个具有最低跃点数的最长匹配路由,那么:均根据最长匹配路由所对应的网络接口在网络连接的高级设置中的绑定优先级来决定(一般有线(eth0) > 无线 (wlan0) > 移动信号(4G))。 -
如果优先级一致,则选择最开始找到的最长匹配路由。
在确定使用的路由项后,网关和接口通过以下方式确定:
1 、如果路由项中的网关地址为空(*)或者为0.0.0.0,那么在发送数据包时:
(a) 通过路由项中对应的网络接口发送;
(b) 源IP地址为此网络接口的IP地址;
? 源MAC地址为此网络接口的MAC地址;
(d) 目的IP地址为接收此数据包的目的主机的IP地址;
(e) 目的MAC地址为接收此数据包的目的主机的MAC地址;
2、 如果路由项中的网关地址并不属于本地计算机上的任何网络接口,那么在发送数据包时:
(a). 通过路由项中对应的网络接口发送;
(b) 源IP地址为路由项中对应网络接口的IP地址;
? 源MAC地址路由项中对应网络接口的MAC地址;
(d) 目的IP地址为接收此数据包的目的主机的IP地址;
(e) 目的MAC地址为网关的MAC地址;
7.TCP协议(为什么需要+原理+演示)
1)为什么需要传输层协议(TCP协议)?
有了IP协议和路由器的帮忙数据已经能进行跨网络传递了,可以从互联网的一个电脑将消息发到互联网的另一台电脑,但是一个电脑上可以有多个服务器进程,每个服务器进程也都可以与多个客户端进行通信,这个时候ip协议就不能满足了,需要一个规则能区分同一个电脑上与其他电脑的多个连接,所以有了传输层协议。
2)TCP/IP协议(传输控制协议/网际协议)的四元组
在TCP/IP协议中四元组:源IP地址、目的IP地址、源端口、目的端口是每个连接的唯一标识。比如:计算机A与计算机B的80端口一共可以创建65535的个连接。还可以在和计算机B的81再创建65535的个连接。
注:TCP和UDP也不会产生端口冲突,这时候就要考虑五元组了
我们常见bind:address already in use好像与上面四元组描述的有些不一致,关键就在这个bind,上述四元组一般在客户端会谈到。
在TCP协议栈里有bind和connect两个系统调用,bind为监听某个端口,当某个端口被监听后这个端口就会被缓存到一个数组,每当bind被调用都回去比对,bing函数常用与服务端监听端口。connect入参为目标服务器ip:port,常用在客户端,每当connect被调用时都会去已建立的四元组缓存中去比对。
3)TCP协议体分析
? TCP协议报文由源端口号和目的端口号,通讯的双方由IP地址和端口号标识。32位序号、32位确认序号、窗口大小(可接收的数据大小,取值从接受方滑动窗口和发送方拥塞窗口中取小者)稍后详细解释。4位首部长度和IP协议头类似,表示TCP协议头的长度,以4字节为单位,因此TCP协议头最长可以是4x15=60字节,如果没有选项字段,TCP协议头最短20字节。URG、ACK、PSH、RST、SYN、FIN是六个控制位
-
SYN表示建立连接,FIN表示关闭连接;ACK表示响应;PSH表示有 DATA数据传输;RST表示连接重置;URG表示紧急指针字段有效。 -
SYN、FIN、ACK位:其中ACK是可能与SYN,FIN等同时使用的,比如SYN和ACK可能同时为1,它表示的就是建立连接之后的响应。如果只是单个的一个SYN,它表示的只是建立连接。TCP的几次握手就是通过这样的ACK表现出来的。但SYN与FIN是不会同时为1的,因为前者表示的是建立连接,而后者表示的是断开连接。 -
RST位:用于复位因某种原因引起出现的错误连接,也用来拒绝非法数据和请求。如果接收到 RST 位时候,通常发生了某些错误。如请求ip没有端口导致 RST 、取消一个已存在的连接、发送消息时对方已关闭等导致 RST。当RST=1时,表明TCP连接出现了异常,必须释放连接。 -
PSH位(Push):PSH为1的情况,一般只出现在 DATA内容不为0的包中,也就是说PSH为1表示的是有真正的TCP数据包内容被传递。TCP的连接建立和连接关闭,都是通过请求-响应的模式完成的。当两个应用进程进行交互式的通信中,有时在一端的应用程序希望在键入一个命令后立即收到对应的响应。在这种情况下,TCP就可以使用推送操作。通常的数据中都会带有PSH,但URG只在紧急数据才设置,也称“带外数据”。 -
URG位(URGent):当URG=1时,表示紧急指针字段有效。他告诉系统次报文段有紧急指针,应该尽快的处理(相当于高优先级的数据),而不要按照原来的排序序列来传送。若不使用紧急指针,那么这两个字符将存储在接收TCP的缓存末尾。只有在所有数据段被处理完毕后这两个字符才能被交付到接收方的网应用进程。URG是一个正偏移,与TCP首部中序号字段的值相加表示紧急数据后面的字节,即紧急指针是指向紧急数据最后一个字节的下一字节。
4)TCP通讯时序
? 在这个例子中,首先客户端主动发起连接、发送请求,然后服务器端响应请求,然后客户端主动关闭连接。两条竖线表示通讯的两端,从上到下表示时间的先后顺序,注意,数据从一端传到网络的另一端也需要时间,所以图中的箭头都是斜的。双方发送的段按时间顺序编号为1-11,各段中的主要信息在箭头上标出,例如段2的箭头上标着SYN, 8000(0), ACK11, ,表示该段中的SYN位置1,32位序号是8000,该段不携带有效载荷(数据字节数为0),ACK位置1,32位确认序号是11, 建立连接(三次握手)的过程(半连接、全连接):
- 客户端发送一个带SYN标志的TCP报文到服务器,这是三次握手过程中的第一段报文。SYN位表示连接请求。序号是10,这个序号在网络通讯中用作临时的地址,每发一个数据字节,这个序号要加1,这样在接收端可以根据序号排出数据包的正确顺序,也可以发现丢包的情况,另外,规定SYN位和FIN位也要占一个序号,这次虽然没发数据,但是由于发了SYN位,因此下次再发送应该用序号11。
- 服务器端回应客户端,是三次握手中的第2个报文段,同时带ACK标志和SYN标志。它表示对刚才客户端SYN的回应;同时又发送SYN给客户端,询问客户端是否准备好进行数据通讯。ACK 11代表ACK位置1,32位确认序号是11“我已经接受到序列号10及其之前的全部包,请你下次发送序号为11的包”。
- 客户端必须再次回应服务器端一个ACK报文,这是报文段3。对服务器的连接请求进行应答,确认序号是8001(单独的ACK确认包可以不携带32位序号)。在这个过程中,客户端和服务器分别给对方发了连接请求,也应答了对方的连接请求,其中服务器的请求和应答在一个段中发出,因此一共有三个段用于建立连接,称为“三方握手(three-way-handshake)”。在建立连接的同时,双方协商了一些信息,例如双方发送序号的初始值、最大段尺寸等。
数据传输的过程: - 客户端发送32位序号是11,携带有效数据50字节,此处的ACK8001,是一个保障机制,预防段3 的ACK消息由于网络原因丢失。
- 服务器发送32位序号是8001,携带有效数据100字节,ACK位置为1,确认序号为61。“我已经接收到了11-60的全部包,请你下次发送序号为61的全部包”。
- 服务器发送32位序号是8101,携带有效数据100字节,ACK位置为1,确认序号为61。
- 客户端对服务器的响应做一个ACK确认,ACK位置为1,确认序号为8201。
关闭连接(四次分手)的过程: - 客户端发送FIN,61(0),FIN位表示关闭连接的请求
- 服务器回应ACK,应答客户端的关闭连接请求,确认序号位62因为FIN也占用一字节
- 服务器发送FIN,8201(0),其中也包含FIN位,向客户端发送关闭连接请求
- 客户端回应ACK,应答服务器的关闭连接请求,确认序号位8202因为FIN也占用一字节
问:为什么要三次握手?
? 看下两次握手会产生什么影响就可以理解为什么要三次握手了。ClientA发送给ServerA 第一次发送SYN申请建立连接,但是因为网络原因阻塞ServerA没收到迟迟没有回复,于是第二次发送SYN申请建立连接,这次ServerA收到了并且回应了ACK给ClientA,在两次握手情景下此时连接建立,互相发送完请求和相应后正常关闭连接。但就在此时ClientA第一次发送的SYN数据包网络顺畅了到达了ServerA,但是ClientA已关闭甚至关机,但是由于两次握手ServerA回复ACK后建立连接,此时此连接会长期存在,会造成服务器资源浪费。
问:为什么要四次挥手?
? 看下三次挥手会产生什么影响就可以理解为什么要四次挥手了,ClientA发送FIN给ServerA申请关闭连接,如果是三次挥手ServerA接到后就必须回复ACK+FIN 回复ClientA的关闭请求以及向ClientA发起关闭请求,然后ClientA回复ServerA的关闭请求发送ACK。在三次挥手的此次流程里有一个问题就是serverA必须同时回复ACK+FIN,在很多时候client申请关闭连接时ServerA还在处理之前请求或者为响应完数据,此时serverA不应该发送FIN,应该先发送ACK等数据发送结束后再发送FIN申请关闭。所以需要四次挥手才能满足以上场景。
下面再在套接字层面上解释一下四次挥手干了什么:
网络套接字:socket(插座)
一个文件描述符指向一个套接字(该套接字内部由内核借助两个缓冲区实现。)在通信过程中, 套接字一定是成对出现的。
首先在Linux系统中一切皆文件,套接字也是一种特殊的文件为了建立网络通讯。欲建立连接的两个进程各自有一个socket来标识,即socket肯定时成对出现的,socket的包括读缓存区和写缓存区两部分。如下图,client的读缓存区对应server的写缓存区,client的写缓存区对应server的读缓存区。
在client发送FIN时就是告诉server“我要准备关闭啦,不会再给你发消息了,但是还能接收你的消息,你没发完的赶紧发”,server接收到以后回应ACK,此时client的写缓存区就关闭了,server的读缓存区关闭,此时称为半关闭状态。但是server还有消息没发完,等server发完消息以后,发送FIN给client“我消息发送完了,我也准备关闭了”,client收到以后回给server ACK,然后server的写缓存区和client的读缓存区关闭,连接真正关闭,释放资源。
5)滑动窗口
在UDP协议中如果客户端发送消息很快,服务器处理的比较慢会导致服务器的读缓存区满掉,后面的消息就回丢失,在TCP协议中利用“滑动窗口”这一机制解决了这个问题。
-
发送端发起连接,声明最大段尺寸是1460,初始序号是0,窗口大小是4K,表示“我的接收缓冲区还有4K字节空闲,你发的数据不要超过4K”。接收端应答连接请求,声明最大段尺寸是1024,初始序号是8000,窗口大小是6K。发送端应答,三方握手结束。 -
发送端发出段4-9,每个段带1K的数据,发送端根据窗口大小知道接收端的缓冲区满了,因此停止发送数据。 -
接收端的应用程序提走2K数据,接收缓冲区又有了2K空闲,接收端发出段10,在应答已收到6K数据的同时声明窗口大小为2K。 -
接收端的应用程序又提走2K数据,接收缓冲区有4K空闲,接收端发出段11,重新声明窗口大小为4K。 -
发送端发出段12,带有1K数据 ? PS:累计确认? ? TCP并不是每一个报文段都会回复ACK的,可能会对两个报文段发送一个ACK,也可能会对多个报文段发送1个ACK【累计ACK】,比如说发送方有1/2/3 3个报文段,先发送了2,3 两个报文段,但是接收方期望收到1报文段,这个时候2,3报文段就只能放在缓存中等待报文1的空洞被填上,如果报文1,一直不来,报文2/3也将被丢弃,如果报文1来了,那么会发送一个ACK对这3个报文进行一次确认。 ? 上图第10步为累计确认,假如第6步报文丢失,则在第快重传的机制下,7/8/9步骤后都会收到服务端的ACK,2049 ;ACK,X确认机制代表了已收到确认序号X之前的全部数据,请继续发送SEQ=X的后续报文。如果服务端接收出现了乱序则会重复发送最后一个正常排序的报文ACK。
发送端窗口(发送滑动窗口大小,取值从接受方滑动窗口和发送方拥塞窗口中取小者,实时调整变动)
在这幅图中,涉及滑动窗口的四种概念:
- 已经发送并确认的报文段:发送给接收方后,接收方回回复 ACK 来对报文段进行响应,图中标注绿色的报文段就是已经经过接收方确认的报文段。
- 已经发送但是还没确认的报文段:图中绿色区域是经过接收方确认的报文段,而浅蓝色这段区域指的是已经发送但是还未经过接收方确认的报文段。
- 等待发送的报文段:图中深蓝色区域是等待发送的报文段,它属于发送窗口结构的一部分,也就是说,发送窗口结构其实是由已发送未确认 + 等待发送的报文段构成。
- 窗口滑动时才能发送的报文段:如果图中的 [4,9] 这个集合内的报文段发送完毕后,整个滑动窗口会向右移动,图中橙色区域就是窗口右移时才能发送的报文段。
滑动窗口也是有边界的,这个边界是 Left edge 和 Right edge ,Left edge 是窗口的左边界,Right edge 是窗口的右边界。
当 Left edge 向右移动而 Right edge 不变时,这个窗口可能处于 close 关闭状态。随着已发送的数据逐渐被确认从而导致窗口变小时,就会发生这种情况。
当 Right edge 向右移动时,窗口会处于 open 打开状态,允许发送更多的数据。当接收端进程读取缓冲区数据,从而使缓冲区接收更多数据时,就会处于这种状态。
? TCP 滑动窗口的 Left edge 永远不可能向左移动,因为发送并确认的报文段永远不可能被取消,就像这世界上没有后悔药一样。这条边缘是由另一段发送的 ACK 号控制的。当 ACK 标号使窗口向右移动但是窗口大小没有改变时,则称该窗口向前滑动。
如果 ACK 的编号增加但是窗口通告信息随着其他 ACK 的到达却变小了,此时 Left edge 会接近 Right edge。当 Left edge 和 Right edge 重合时,此时发送方不会再传输任何数据,这种情况被称为零窗口 。此时 TCP 发送方会发起窗口探测 ,等待合适的时机再发送数据。
接收端窗口
? 接收方也维护了一个窗口结构,这个窗口要比发送方的简单很多。这个窗口记录了已经接收并确认的数据,以及它能够接收的最大序列号。接收方的窗口结构不会存储重复的报文段和 ACK,同时接收方的窗口也不会记录不应该收到的报文段和 ACK。下面是 TCP 接收方的窗口结构。
? 如上图,假如接收到了5、6此时Left edge不会移动,TCP协议栈会回复客户端ACK,4提示客户端需要发送序号为4的报文,直到接收到4,然后回复ACK,6并移动Left edge。
6)状态转换
-
CLOSED:表示初始状态。 -
LISTEN:该状态表示服务器端的某个SOCKET处于监听状态,可以接受连接。 -
SYN_SENT:这个状态与SYN_RCVD遥相呼应,当客户端SOCKET执行CONNECT连接时,它首先发送SYN报文,随即进入到了SYN_SENT状态,并等待服务端的发送三次握手中的第2个报文。SYN_SENT状态表示客户端已发送SYN报文。 -
SYN_RCVD: 该状态表示接收到SYN报文,在正常情况下,这个状态是服务器端的SOCKET在建立TCP连接时的三次握手会话过程中的一个中间状态,很短暂。此种状态时,当收到客户端的ACK报文后,会进入到ESTABLISHED状态。 -
ESTABLISHED:表示连接已经建立。 -
FIN_WAIT_1: FIN_WAIT_1和FIN_WAIT_2状态的真正含义都是表示等待对方的FIN报文。区别是: FIN_WAIT_1状态是当socket在ESTABLISHED状态时,想主动关闭连接,向对方发送了FIN报文,此时该socket进入到FIN_WAIT_1状态。 FIN_WAIT_2状态是当对方回应ACK后,该socket进入到FIN_WAIT_2状态,正常情况下,对方应马上回应ACK报文,所以FIN_WAIT_1状态一般较难见到,而FIN_WAIT_2状态可用netstat看到。 -
FIN_WAIT_2:主动关闭链接的一方,发出FIN收到ACK以后进入该状态。称之为半连接或半关闭状态。该状态下的socket只能接收数据,不能发。 -
TIME_WAIT: 表示收到了对方的FIN报文,并发送出了ACK报文,等2MSL后即可回到CLOSED可用状态。如果FIN_WAIT_1状态下,收到对方同时带 FIN标志和ACK标志的报文时,可以直接进入到TIME_WAIT状态,而无须经过FIN_WAIT_2状态。 -
CLOSING: 这种状态较特殊,属于一种较罕见的状态。正常情况下,当你发送FIN报文后,按理来说是应该先收到(或同时收到)对方的 ACK报文,再收到对方的FIN报文。但是CLOSING状态表示你发送FIN报文后,并没有收到对方的ACK报文,反而却也收到了对方的FIN报文。什么情况下会出现此种情况呢?如果双方几乎在同时close一个SOCKET的话,那么就出现了双方同时发送FIN报文的情况,也即会出现CLOSING状态,表示双方都正在关闭SOCKET连接。 -
CLOSE_WAIT: 此种状态表示在等待关闭。当对方关闭一个SOCKET后发送FIN报文给自己,系统会回应一个ACK报文给对方,此时则进入到CLOSE_WAIT状态。接下来呢,察看是否还有数据发送给对方,如果没有可以 close这个SOCKET,发送FIN报文给对方,即关闭连接。所以在CLOSE_WAIT状态下,需要关闭连接。 -
LAST_ACK: 该状态是被动关闭一方在发送FIN报文后,最后等待对方的ACK报文。当收到ACK报文后,即可以进入到CLOSED可用状态 PS:半开启?半关闭? 半关闭就是FIN_WAIT_2状态; 半开启是指在Client和Server正常完成握手链接后,一方由于断电等原因关闭且未能发送FIN,此时剩余的一方在发送消息前是无法感知的,这种状态称为半开启状态,很少见,再发送消息时会得到服务器的RST从而关闭。
抛出一个疑问为什么主动发起的一方在发送完最后的ACK之后还要再等2ML时间后才能真正关闭? 答:当Client最后的ACK由于网络原因未到Server时,Server还会重新发送FIN申请关闭,此时如果Client已经关闭则Server会一直卡在LAST_ACK状态无法关闭,所以Client延时关闭就是为了防止最后ACK消失的情况。
注:全连接半连接问题
rtt=网络+排队+真正服务时间
半连接队列的大小由/proc/sys/net/ipv4/tcp_max_syn_backlog 控制,Linux的默认是1024。
全连接队列的大小通过/proc/sys/net/core/somaxconn 指定Linux的默认是128,在使用listen函数时,内核会根据传入的backlog 参数与系统参数somaxconn,取二者的较小值。
SYN Cookie 技术可以让服务器在收到客户端的SYN 报文时,不分配资源保存客户端信息,而是将这些信息保存在SYN+ACK 的初始序号和时间戳中。对正常的连接,这些信息会随着ACK 报文被带回来。
/proc/sys/net/ipv4/tcp_abort_on_overflow=0默认
7)javaTCP编程
服务器
package com.lago;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.charset.Charset;
import java.util.Iterator;
import java.util.Set;
public class SelectorServer {
public static void main(String[] args) throws Exception {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(8080));
serverSocketChannel.configureBlocking(false);
Selector selector = Selector.open();
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true){
int select = selector.select();
Set<SelectionKey> selectionKeys = selector.selectedKeys();
for (SelectionKey selectionKey : selectionKeys) {
if(selectionKey.isAcceptable()){
ServerSocketChannel serverSocket = (ServerSocketChannel) selectionKey.channel();
SocketChannel client = serverSocket.accept();
client.configureBlocking(false);
client.register(selector,SelectionKey.OP_READ);
System.out.println("已经有客户端"+client.socket().getInetAddress().getHostAddress()+":"+client.socket().getPort());
}else if(selectionKey.isReadable()){
SocketChannel socketClient= (SocketChannel) selectionKey.channel();
ByteBuffer byteBuffers=ByteBuffer.allocateDirect(200);
long read = socketClient.read(byteBuffers);
byteBuffers.flip();
String receiveData= Charset.forName("UTF-8").decode(byteBuffers).toString();
byteBuffers.clear();
System.out.println("接收到客户端"+socketClient.socket().getInetAddress().getHostAddress()+":"+socketClient.socket().getPort()+":"+receiveData);
}
}
selectionKeys.clear();
}
}
}
客户端
public class Client01 {
public static void main(String[] args) throws Exception {
Socket socket = new Socket("192.168.2.170", 8080);
Scanner scanner=new Scanner(System.in);
while (true){
String content = scanner.nextLine();
socket.getOutputStream().write(content.getBytes());
}
}
}
8)分片问题
要想理解分片问题首先需要科普几个名词:分片、分段、MTU、MSS
-
MTU:以太网(Ethernet)数据帧的长度必须在46-1500字节之间,这是由以太网的物理特性决定的。这个1500字节被称为链路层的MTU(最大传输单元). -
分片:当传输层传入网络层的数据过大,加上网络层IP前缀后大于1500是,为了满足MTU限制,会在网络层对数据进行分片,使每一片都小于MTU,在接收端的网络层在进行重新组合,这样做的弊端是当其中一片数据包丢失时,导致无法组合,在UDP协议中此时如果上层(应用层)没有重传机制则只能丢弃全部数据,即使有重传机制也要将整个数据包进行重传(重传机制应用层肯定要为数据包做编号,应用层的编号只能加到数据报文的开始和结尾,如果触发了分片就算应用层设置了编号也不能确认那个分片丢失,需要整个数据报文重发),所以不管上层是什么协议尽量要避免网络层触发数据分片。 -
MSS:MSS就是TCP数据包每次能够传输的最大数据分段。TCP协议在连接建立阶段进行三次握手的时候会确认此次连接的MSS值,默认取通讯双方MSS的的最小值作为此次连接的MSS最大值即TCP数据包每次能够传输的最大数据分段。 -
分段:分段是TCP的一个特性,当应用层传输的数据过大超过MSS时,TCP在传输层会进行分段,保证每个分段都小于MSS,TCP的每个分段都带有序号,在接收端的传输层可以进行重新排序组合,相比与网络层的分片组合,如果有一个分段消失,结合TCP的“确认和重传机制 ”可以支持单个分段的重传而不用全部重传。 看完上面四个名词的解释,大家应该也明白网络分片带来的影响不会出现的TCP协议中,因为MSS的值会小于MTU,所以如果传输层使用的是TCP协议到达网络层后都满足MTU,不用进行分片,所以在使用TCP协议时,无需在应用层(用户程序)去控制每次发送数据包的大小。
9)总结
? 常规总结肯定是,TCP协议是一种作用在传输层的面向连接的可靠的数据传输控制协议;但是这里的总结是结合上面所讲来分析这就话的加粗部分体现在哪儿。
- 面向连接:TCP在发送应用层真正的数据之前都会先进行三次握手创建连接,之后全部数据发送都是基于这连接进行发送。(三次握手在客户端调用connect函数时完成)
- 可靠:TCP之所以可靠,并不是因为面向连接才可靠(面向连接并不代表什么,关键是在建立连接时所作的准备比如:通信双方确认首次序号、确认滑动窗口大小、确认MSS大小、记录网络路由通道等),而是因为TCP具有确认和重传机制、数据排序、流量控制等机制。确认和重传机制体现在:当发送方发送的某个数据包在指定时间未接受到ACK回应时会重新发送;数据排序机制体现在:每个数据包都带有序列号,如果到达服务端是乱序,服务端可以根据序列号排序;至于流量控制机制滑动窗口就是TCP对它的体现。在使用TCP时这些机制都由内核帮我们实现不用我们操心。PS:下图只是方便大家理解这几个机制在哪里触发完成,解释可能不是特别恰当(▽)!!,之前画的图了,懒得在画了,不是重点(▽)!!
9)与UDP比较
报文比较
UDP的报文相对于TCP来看简单了很多,因为UDP要做的事情也比TCP简单的多,甚至都不需要建立连接,在发送端只负责将应用层要发送的数据包丢给网络层,在接收端只需要将网络层中接收的数据包丢给应用层,不保证数据是否丢失。
通信时序比较
由于UDP无需三次握手创建连接,也无需四次挥手断开连接,所以UDP的通讯时序图没什么意思,每条交互都是双方互发数据,没什么特别含义,这里用socket的系统调用图来展现UDP的交互细节,同时附一个TCP版的方便比较: 根据上图比较可看成,UDP客户端缺少了connect(),服务端缺少了accept(),在TCP的总结里也有提到connect函数触发了三次握手创建连接,这里也论证了UDP无需面向连接这一理论。
代码编程比较
服务器 PS:此服务器为简单版UDP服务器,仅仅添加了个消息收到确认机制,并未添加超时重发、排序等机制。
public class SelectorUDPServer {
public static void main(String[] args)throws Exception {
DatagramChannel datagramChannel=DatagramChannel.open();
datagramChannel.bind(new InetSocketAddress("192.168.2.170",8080));
datagramChannel.configureBlocking(false);
Selector selector = Selector.open();
datagramChannel.register(selector, SelectionKey.OP_READ);
while (true){
int select = selector.select();
if(select>0){
Set<SelectionKey> selectionKeys = selector.selectedKeys();
ByteBuffer byteBuffer=ByteBuffer.allocateDirect(1480);
for (SelectionKey selectionKey : selectionKeys) {
if(selectionKey.isReadable()){
DatagramChannel channel= (DatagramChannel) selectionKey.channel();
InetSocketAddress receive = (InetSocketAddress) channel.receive(byteBuffer);
byteBuffer.flip();
String receiveData= Charset.forName("UTF-8").decode(byteBuffer).toString();
System.out.println("服务器接收到客户端"+receive.getAddress().getHostAddress()+":"+receive.getPort()+"发来的消息》》"+receiveData);
channel.send(ByteBuffer.wrap(("以接收到消息**"+receiveData+"**").getBytes()),receive);
byteBuffer.clear();
}
}
selectionKeys.clear();
}
}
}
}
客户端 PS:客户端与服务器基本类似故未增加过度注释
public class UDPClient {
public static void main(String[] args) throws Exception {
DatagramChannel client=DatagramChannel.open();
client.connect(new InetSocketAddress("192.168.2.170",8080));
Selector selector=Selector.open();
client.configureBlocking(false);
client.register(selector, SelectionKey.OP_READ);
while (true){
Scanner scanner=new Scanner(System.in);
String content = scanner.nextLine();
client.write(ByteBuffer.wrap(content.getBytes()));
int select = selector.select();
if(select>0){
Set<SelectionKey> selectionKeys = selector.selectedKeys();
ByteBuffer byteBuffer=ByteBuffer.allocateDirect(1480);
for (SelectionKey selectionKey : selectionKeys) {
if(selectionKey.isReadable()){
DatagramChannel channel= (DatagramChannel) selectionKey.channel();
InetSocketAddress receive = (InetSocketAddress) channel.receive(byteBuffer);
byteBuffer.flip();
String receiveData= Charset.forName("UTF-8").decode(byteBuffer).toString();
System.out.println("客户端接收到服务器"+receive.getAddress().getHostAddress()+":"+receive.getPort()+"发来的消息》》"+receiveData);
byteBuffer.clear();
}
}
selectionKeys.clear();
}
}
}
}
分片问题比较
? UDP比起TCP就简单暴力的多,应用层给我什么数据我就一次性都给到网络层,并没有MSS等限制,所以在应用层一次想要发送大量数据时就会触发网络层的数据分片,然而UDP的接收端也不会考虑网络层重组的分片是否完整直接将数据给到应用层,所以应用层最好控制每次发送数据包的大小,保证到达网络层后数据包小于MTU不会触发网络分片(应用层的编号只能加到数据报文的开始和结尾,如果触发了分片就算应用层设置了编号也不能确认那个分片丢失,需要整个数据报文重发),除此之外需要为每个数据包设计编号,并且包含超时重发机制。
? PS:UDP常说收到消息时顺序有可能错乱,并不是由分片引起的,分片是指发送端发送一段报文数据过大,被网络层进行分片处理,这里分片处理后到达接收端的网络层肯定可以按顺序排好序的;UDP乱序情景是发送端发送多段符合MTU的报文时,到达接收端后由于每个段报文走的路由可能不同可能导致多段数据到达顺序错乱,所以如果对数据展示顺序有严格要求时应用层最要有排序机制。同时也是因为UDP应用层需要考虑很多机制所以不可能到达一个报文展示一个报文,所以也推荐有缓存机制,接收端消息模块 处理完一组报文后放入缓存,接收端展示模块去读缓存中正确排序且完整的数据进行展示。
UDP总结
? 同TCP一样这里的总结的主要内容就是解析“UDP协议是一种如用在传输层的无连接的不可靠的数据传输控制协议”中加粗字的含义体现在哪里。
- 无连接:从2.1通信时序中可以看到使用UDP协议时无需创建连接,无需三次握手,客户端可以直接给服务器发送消息。
- 不可靠:如果因为网络故障该段无法发到对方,UDP协议层也不会给应用层返回任何错误信息;如果发送多段数据包,并且在网络上经过不同的路由,到达接收端时顺序已经错乱了,UDP协议层也不保证按发送时的顺序交给应用层;通常接收端的UDP协议层将收到的数据放在一个固定大小的缓冲区中等待应用程序来提取和处理,如果应用程序提取和处理的速度很慢,而发送端发送的速度很快,就会丢失数据包,UDP协议层并不报告这种错误。说白了UDP在发送端只负责将应用层要发送的数据包丢给网络层,在接收端只需要将网络层中接收的数据包丢给应用层。完全套用底层的IP协议来传送报文,同IP一样提供不可靠的无连接数据包传输服务,不提供报文到达确认、排序、及流量控制等功能。
10)TCP拥塞控制
TCP的拥塞控制机制是TCP中相对较复杂的一块,但是有利于我们根据报文规律分析当前网络状态。
? 在某段时间,若对网络中某一资源的需求超过了该资源所能提供的可用部分,网络性能就要变坏,这种情况就叫做网络拥塞。
在计算机网络中数位链路容量(即带宽)、交换结点中的缓存和处理机等,都是网络的资源。
若出现拥塞而不进行控制,整个网络的吞吐量将随输入负荷的增大而下降。
TCP有四种拥塞控制算法:
这里需要先假定一个场景:
-
数据是单方向传送,而另一个方向只传送确认 -
接收方总是有足够大的缓存空间,因而发送方发送窗口的大小由网络的拥塞程度来决定(正常情况下还会受接收方的滑动窗口大小限制) -
以TCP报文段MSS的个数为讨论问题的单位,而不是以字节为单位 慢开始算法和拥塞避免算法 ? 发送方维护一个叫做拥塞窗口cwnd的状态变量,其值取决于网络的拥塞程度,并且动态变化 拥塞窗口cwnd的维护原则:只要网络没有出现阻塞,拥塞窗口就增大一些,如果出现阻塞,拥塞窗口就变小一些。判断网络拥塞的依据:没有按时收到应当到达的确认报文即发生了超时重传。 在假定2的情况下,发送方将拥塞窗口作为发送窗帘swnd,即swnd=cwnd。 维护一个慢开始门限ssthresh状态变量:
-
当cwnd<ssthresh时使用慢开始算法; -
当cwnd>ssthresh时使用拥塞避免算法 -
当cwnd=ssthresh时使用慢开始算法或使用拥塞控制算法都可以 ? 慢开始算法是每轮传输后窗口翻倍是指数增加,直到cwnd=ssthresh此时切换拥塞避免算法,每轮传输后窗口+1,直到出现拥塞后(出现因丢包导致的超时重传现象),将ssthresh值更新为出现拥塞时cwnd的一半,将cwnd置为1并重新开始执行慢开始算法。 慢开始:是指一开始先网络注入的报文段少,并不是指拥塞窗口cwnd的增长速度慢。 拥塞避免:并非指完全能够避免阻塞,而是指在拥塞避免阶段拥塞窗口增长比较慢不太容易出现拥塞。 快重传算法和慢恢复算法 快重传算法和慢恢复算法是对慢开始和拥塞避免算法的优化。 ? 因为有时个别报文段会在网络中丢失,但是网络并未发生拥塞,在没有快重传算法时,在假定1的前提下将导致发送方超时重传,误认为网络发生了拥塞,重新启用慢开始算法,因而降低了网络传输的效率。采用快重传算法可以让发送方尽早知道发生了个别报文的丢失并,避免触发超时重传。 ? 所谓快重传就是使发送方尽快进行重传,而不是等超时计时器超时再重传。
-
要求接收方不要等待自己发送数据时才进行捎带确认,而是要立即发送确认 -
即使收到了失序报文段也要立即发出对已收到报文段的重复确认 -
发送方一旦收到3个连续的重复确认就将相应的报文立即重传,避免超时计时器超时 如果没有快重传算法,在M3报文丢失后除非接受方有业务数据主动传给发送方(捎带发送确认M2)将不会再次回复确认M2,此时网络并没有拥塞而发送方法必然触发超时重传导致启动慢开始算法。有了快重传机制后在接收方接受失序报文M4、M5、M6时也会回复确认M2(对已收到报文段的重复确认),发送方即可在M3报文确认回复超时计时器超时前发现M3未发送成功并重新发送,由于网络正常很快就能收到M3的确认回复(上图的确认M6为累计确认现象,代表M6之前报文已经全部收到了)避免触发慢开始算法。据监测快重传算法可以使网络吞吐量提升百分之20。 发送方一旦收到了三个重复确认(触发了快重传),就知道只是丢失了个别报文段,并不是网络阻塞此时不启动慢开始算法而是启动快恢复算法。
IO模型
? 此节通过java代码IO模型实现配合内核调用日志来分析linux的IO模型由阻塞IO——>非阻塞IO——>IO多路复用 的演变过程,以及演变原因,由于信号驱动 IO模型与TCP协议不适配,异步IO在linux中不成熟故不进行讨论。
1.BIO
BIO为阻塞型IO模型,在接收客户端连接(accept)和读取客户端发送数据(recv)时会发生阻塞。 解释一下这里的阻塞:
- 白话解释:服务端执行了accept函数调用后,当前线程就会进入“等待”状态释放cpu的占用,直到有客户端连接服务器为止;服务端调用recv读取客户端发来的数据包时,当前线程就会进入“等待”状态释放cpu的占用,直到有客户端发来消息为止;
- 原理分析:socket主要有三部分组成发送缓冲区+接收缓冲区+等待列表,当服务器socket调用accept函数,就回将当前线程的引用挂在等待列表中,如果socket接收到了客户端连接则唤醒等待列表中的线程。
1.1单线程下的BIO
首先我们看下单线程下的BIO服务器实现:
- 弊端:当存在clientA连接服务器后但是不发消息,当前服务器将阻塞在等待读取clientA发送消息的read方法,此时无法再次执行accept()方法,无法再与其他客户端建立连接
- 解决方案:当服务器每接收到一个连接就创建一个新的线程,在线程内等待读取客户端发送消息的阻塞,这样就不影响服务器接收下一个连接了====》多线程BIO
package com.lago;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
public class OneThreadBIOServer {
public static void main(String[] args)throws Exception {
ServerSocket serverSocket = new ServerSocket();
InetSocketAddress inetSocketAddress = new InetSocketAddress("192.168.2.170", 8080);
serverSocket.bind(inetSocketAddress);
while (true){
Socket client = serverSocket.accept();
System.out.println("已经有客户端"+client.getInetAddress().getHostAddress()+":"+client.getPort());
byte[] buffer=new byte[200];
int read = client.getInputStream().read(buffer);
String body = new String(buffer, 0, read, "UTF-8");
System.out.println("接到客户端"+client.getInetAddress().getHostAddress()+":"+client.getPort()+"消息:"+body);
}
}
}
1.2多线程下的BIO
结合1.1中的弊端和解决思路看下多线程下的BIO服务端实现,此时服务器支持同时处理多个连接但是也存在显而易见的问题
- 弊端:没建立一个连接就需要创建一个线程,随着连接数量增大,线程创建越来越多会消耗大量内存资源以及线程上下文切换浪费时间
- 解决方案:尝试单线程完成socket服务器编写,这个前提下是操作系统能提供一个不阻塞的socket()函数来解决socket服务器accept等待客户端连接阻塞和recv读取消息阻塞问题,否则单线程无法满足接收多个连接的需求。
package com.lago;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
public class ManyThreadBIOServer {
public static void main(String[] args) throws Exception{
ServerSocket serverSocket = new ServerSocket();
InetSocketAddress inetSocketAddress = new InetSocketAddress("192.168.2.170", 8080);
serverSocket.bind(inetSocketAddress);
while (true){
Socket client = serverSocket.accept();
System.out.println("已经有客户端"+client.getInetAddress().getHostAddress()+":"+client.getPort());
Thread thread = new Thread(new ClientThread(client));
thread.start();
}
}
public static class ClientThread implements Runnable{
private Socket client;
public ClientThread(Socket client){
this.client=client;
}
@Override
public void run(){
byte[] buffer=new byte[200];
while (true){
String content = null;
try {
int read = client.getInputStream().read(buffer);
content = new String(buffer, 0, read, "UTF-8");
} catch (IOException e) {
e.printStackTrace();
}
System.out.println("接到客户端"+client.getInetAddress().getHostAddress()+":"+client.getPort()+"消息:"+content);
}
}
}
}
2.NIO(non-blocking)非阻塞IO
应用户需求,linux内核进行了升级推出来新的函数fcntl可以标记socket为非阻塞,标记了非阻塞后的socket在调用acept和recv函数时无论有无连接或数据都会返回不会阻塞。看下NIO模型单线程下支持并发的服务器的代码,在某些情境下还是存在弊端:
- 弊端:虽然可以单线程完成支持多连接的socket服务端,但是如果有1万个连接但是只有一个发送消息,还是会调用函数recv读取一万遍,其中有9999遍是无效调用,要知道应用程序是不能直接调用内核函数的,应用程序调用内核函数时会触发软件中断,效果类似线程上下文切换,会暂停当前应用程序让出CPU切换至内核函数执行,执行完后再切换回应用程序,这是会有进程的现场保护和现场恢复过程会占用CUP的时间和资源。
- 解决方案:如果可以吧socket集合交给内核去管理,让内核帮我们去遍历socket集合,返回给我们有数据可读的客户端,然后我们只进行有效读取。
package com.lago;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.LinkedList;
import java.util.List;
public class NIOServer {
public static void main(String[] args)throws Exception {
List<SocketChannel> sockets=new LinkedList<>();
ServerSocketChannel serverSocketChannel=ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
InetSocketAddress inetSocketAddress = new InetSocketAddress("192.168.2.170", 8080);
serverSocketChannel.bind(inetSocketAddress);
while (true){
Thread.sleep(1000);
SocketChannel client = serverSocketChannel.accept();
if (client!=null){
client.configureBlocking(false);
System.out.println("已经有客户端"+client.socket().getInetAddress().getHostAddress()+":"+client.socket().getPort());
sockets.add(client);
}else {
System.out.println("无客户端连接...");
}
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(200);
for (SocketChannel socketChannel : sockets) {
int read = socketChannel.read(byteBuffer);
if(read>0){
byteBuffer.flip();
byte[] buffer=new byte[byteBuffer.limit()];
byteBuffer.get(buffer);
String content = new String(buffer, 0, read, "UTF-8");
System.out.println("接到客户端"+socketChannel.socket().getInetAddress().getHostAddress()+":"+socketChannel.socket().getPort()+"消息:"+content);
byteBuffer.clear();
}
}
}
}
}
3.IO multiplexing 多路复用IO
应用户需求,内核升级提供了三个多路复用器函数依次是selector、poll、epoll,目前在并发量很大的情景下用的最多的是epoll,而且优势明显。多路复用即让一个进程去监听多个socket。
依次看下这三个多路复用器的实现原理:
3.1 selector
要想真真正正的理解selector就必须要理解fd_set这种数据结构(selector之fd_set),是一种long类型的数组,每一个元素都能与一个一打开的文件句柄(文件描述符)建立联系,建立关心的过程由程序员完成,当调用select()时由内核根据文件的IO状态来修改fd_set中对应位置的元素值,由此来通知执行了select()的进程那个socket或文件是可读的。
了解的selector的fd_set后下面分别从代码实现(应用程序角度)和实现原理(内核角度)两方面来剖析selector,并分析其被取代的原因。
3.1.1 selector代码实现
由于java中IOAPI中的Selector.open();在最新的linux中显式调用时底层都是采用的epoll并未采用selector,所以selector的代码实现我们采用c++来显式调用,并且用c++实现更能体现fd_set的妙用和重要性,方便理解。对c++不熟悉的同学,可以只关注我备注的代码行。
sockfd =socket(AF_INET,SOCK_STREAM,0);
memset(&addr,0,sizeof(addr));
addr.sin_family=AF_INET;
addr.sin_port=htons(2000);
addr.sin_addr.s_addr=INADDR_ANY;
bind(sockfd,(struct sockaddr*)&addr,sizeof(addr));
listen(sockfd,5);
for(i=0;i<5;i++){
memset(&client,0,sizeof(client));
addrlen=sizeof(client);
fds[i]=accept(sockfd,(struct sockaddr*)&client,&addrlen);
if(fds[i]>max){
max=fds[i];
}
}
while(1){
FD_ZERO(&rset)
for(i=1;i<5;i++){
FD_SET(fds[i],&rset);
}
select(max+1,&rset,null,null,null);
for(i=0;i<5;i++){
if(FD_INSET(fds[i],&rset)){
memset(buffer,0,MAXBUF);
read(fds[i],buffer,MAXBUF);
}
}
}
在代码实现层面(应用程序角度)分析selector的弊端:
- 在执行select时每次都要将相同的rset赋值到内核
- 要想获取真正有数据的socket文件描述符引用,每次都要讲全部链接遍历一遍判断是否在执行完select的rset里,如果链接过多性能则下降
3.1.2 selector原理分析
selector原理分析的原理剖析其实就是剖析select()函数到底干了什么事情,在应用程序执行select函数时会有两种情景1.select要监听的socket集合已接收到客户端消息,读缓存区中已有可读取数据;2.select要监听的socket集合未接收到客户端消息,读缓存区中没有可读取数据。接下来分别进行分析。
- 当程序执行select程序时会触发软件终端,从用户态切换值内核态并将rset等参数向内核进行copy
- 在内核中 遍历 rset,通过调用recv进行判断,如果下标对应的socket有可读取数据,当前元素不变,如果未有可读数据则当前下标对应的元素值置0
- 如果遍历完rset里还有1,即说明socket集合里存在可读socket,直接返回,切换成用户态,并且将rset复制回来。
-
当前情景下遍历完rset会发现rset所有元素都是0,即说明socket集合中无接收到消息的socket,当前线程需要进入阻塞状态,从运行队列中取出来,遍历 socket集合为全部socket的等待队列添加当前进程的引用。 -
当socket1对应客户端程序发来消息时,在消息到达服务器网卡时会触发网卡中断执行中断程序,首先将消息内容copy到socket1的读缓存区中,然后遍历全部socket解除进程中在缓存区中的引用(必须解除全部缓存区引用进程A才能解除阻塞,进入运行队列,分取cpu资源)。 -
然后切换成用户态,并且将rset复制回来,由应用程序进行消息读取
3.1.3 selector弊端总结+解决设想
selector弊端总结:
- 首先在应用程序层面在select函数返回后,并不清楚那个socket可以读取,需要遍历socket的fds[ ]与rset进行比对,对无用连接进行遍历。
- 每次只需select函数都会将rset向内核进行copy
- 如果全部socket都没有可读数据,需要遍历socket向等待队列中绑定进程引用;当有1个socket收到消息后,要遍历全部socket解除等待队列中进程的引用
- 在selector中rset的最大值为1024,限制了客户端的链接数量(补充弊端)
解决设想:
- 在未出现新socket的情况下不需要重复向内核中copy 已添加的socket信息
- 当有1个socket收到消息时不要遍历全部socket就能释放应用程序
- 多路复用器能保存并且返回收到消息的socket文件描述符引用,这样应用程序执行完select函数后就可以,只遍历有效socket,直接进行读取数据了。
- 多路复用器能打破1024的限制。
3.2 poll
poll的代码实现和实现原理与selector一样,只是rset不在限制数量,打破了1024的限制,可以监听更多socket,但是并没有解决selector的弊端,在大量并发的情景下,由于应用程序和内核都需要进行多次socket的遍历以及越来越大rset copy,随着并发量的增加性能会越来越低。
3.3 epoll
epoll的出现彻底打破了selector的弊端,下面还是在代码实现(用应程序角度)和实现原理(内核角度)来分析epoll是如何打破selector的弊端完成我们的解决设想的
3.3.1 epoll代码实现
此处采用了java代码进行实现,但是要知道java之所以跨平台是因为jdk已经在底层进行了封装为我们屏蔽了操作系统的差异性,epoll在linux中一共提供了三个核心函数epoll_create/epoll_ctl/epoll_wait,在下面的代码备注中有注明。
package com.lago;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.charset.Charset;
import java.util.Iterator;
import java.util.Set;
public class SelectorServer {
public static void main(String[] args) throws Exception {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(8080));
serverSocketChannel.configureBlocking(false);
Selector selector = Selector.open();
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true){
int select = selector.select();
Set<SelectionKey> selectionKeys = selector.selectedKeys();
for (SelectionKey selectionKey : selectionKeys) {
if(selectionKey.isAcceptable()){
ServerSocketChannel serverSocket = (ServerSocketChannel) selectionKey.channel();
SocketChannel client = serverSocket.accept();
client.configureBlocking(false);
client.register(selector,SelectionKey.OP_READ);
System.out.println("已经有客户端"+client.socket().getInetAddress().getHostAddress()+":"+client.socket().getPort());
}else if(selectionKey.isReadable()){
SocketChannel socketClient= (SocketChannel) selectionKey.channel();
ByteBuffer byteBuffers=ByteBuffer.allocateDirect(200);
long read = socketClient.read(byteBuffers);
byteBuffers.flip();
String receiveData= Charset.forName("UTF-8").decode(byteBuffers).toString();
byteBuffers.clear();
System.out.println("接收到客户端"+socketClient.socket().getInetAddress().getHostAddress()+":"+socketClient.socket().getPort()+":"+receiveData);
}
}
selectionKeys.clear();
}
}
}
在代码层面分析比较epoll的优势:
- selector 在执行select时完成两部分工作维护等待队列+进程阻塞,致使每次都要copy rset到内核;epoll将这两部分工作拆分成了两个函数epoll_ctl(添加新的socket引用,维护等待队列)和epoll_wait(进程阻塞,等待结果),因此epoll在不出现新连接的情况下不需要向内核copy socket的文件引用句柄,只会单纯调用epoll_wait。
- epoll执行完epoll_wait后可以直接获取到准备就绪的fd集合引用直接进行遍历读取,无需遍历全部fds集合,相比selector在高并发情况下效果显著
3.3.2 epoll 原理分析
epoll原理其实就是分析epoll_create/epoll_ctl/epoll_wait三个核心函数究竟干了什么,下面逐个分析
epoll_create会创建一个eventpoll对象包括:
- 就绪列表:用于存放准备就绪的socket引用句柄
- 监听事件列表:用于存放epoll需要监听的socket引用句柄
- 等待队列:用于存放调用了epoll_wait的进程
将需要监听的socket添加到epoll_event的监听事件列表中
-
当进程A调用epoll_wait时会首先检测eventpoll的就绪列表中有误数据,如果有数据之间返回;如果没有数据则将进程中变为阻塞状态,从运行队列中取出来,添加到eventpoll的等待队列中,并且将event_poll添加到监听事件列表中所有socket的等待队列中(此处注意两点1.相对于selector的遍历rset看是否有socket就绪,epoll无需遍历之间检测就绪队列是否有数据即可,在高并发时可以提升性能;2.相对于selector进程添加到各个socket的等待队列,epoll将进程A添加到event_poll的等待队列,方便后面的释放) -
当client1对应客户端发来消息时,消息到达了网卡时触发网卡硬件中断,中断程序首先将消息copy到client1对应socket的读缓存区里,然后通过等待队列中的event_poll引用找到event_poll,为event_poll的就绪队列里添加自己的引用,并且移除等待队列中的event_poll同时移除event_poll等待队列中的进程A -
进程A回到运行队列分配到cup,获取到就行列表中的socket引用,遍历进行消息读取(遍历的全都是活跃连接,都是有效遍历)
3.3.3 epoll 相对于selector的优势总结
- 不用每次查询(select/epoll_wait)时向内核copy全部socket引用句柄(rset)【少copy数据】
- 在内核中不用循环rset判断是否有socket就绪,只要判断就绪列表是否为空即可【减少遍历】
- 进程阻塞时添加到eventpoll的等待队列中而不是全部socket,网卡中断触发后不用遍历socket是否进程,只要eventpoll释放进程即可【减少遍历】
- 进程获取查询(select/epoll_wait)结果后selector还需要遍历全部socket进行数据读取,epoll只需遍历就绪列表中的有效连接进行数据读取【减少遍历】
注:epoll的优势就是不会随着socket的增加而性能下降。但事情无绝对,并不是全部场景都推荐使用epoll。在并发量小,并且都是活跃连接的情况下selector反而更合适一些;通过上面的原理分析不难看出epoll设计相对于selector的无脑遍历更复杂一些,类似于空间换时间,自然会有一些额外消耗,只有在连接高到一定数量的情况下,epoll的额外消耗才能抵消selector的遍历,在高并发下才能显现他的优势
|