文章目录
IP
1、速率
速率:网络技术中的速率指的是连接在计算机网络上的主机在数字信道上传数据的速率,它也称为数据率(data rate)或比特率(bitrate)。
速率是计算机网络中最重要的一个性能指标。
速率的单位是 b/s,bit/s,bps。
当数据率较高时,就可以用 kb/s(103),Mb/s(106),Gb/s9,Tb/s(1012)
上面所说的速率往往是指额定速率或标称速率
2、IP 地址
网络层功能:寻址和路由选择
计算机网络层的主要目的是实现两个系统之间的数据透明传送,具体功能包括寻址和路由选择,连接的建立,保持和终止等。而实现点对点通信的基础,就是每个节点必须有一个IP地址,数据从哪来(源节点),到哪去(目的节点)。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2Z305JAe-1626960266865)(https://i.loli.net/2021/04/13/L59aXbUVOMDWt8I.png)]
3、IP 地址的概念及产生
1、MAC地址
物理地址(48位),每台机器出厂时规定的唯一地址。如果根据物理地址来判断某台主机,数据十分庞大且不利于管理。
2、IP 地址
逻辑地址(32位),可以由用户根据规定进行更改和设置。
这两类地址,需要解析协议ARP 与逆地址解析协议 RARP 来进行翻译。
由于通过各种物理网络存在异构性,利用 IP 协议就可以使这些性能各异的网络用户看起来好像一个统一的,抽象的逻辑互联网络,称之为 虚拟互联网。
IP 地址就是给每个连接在因特网上的主机(或路由器)分配一个在全世界范围是唯一的 32 位的标识符,它是一种分等级的地址结构,由因特网名字与号码指派公司 ICANN 进行分配。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LXJRWLSY-1626960266874)(https://i.loli.net/2021/04/13/Te42fwm9sKgYcX1.png)]
4、IP 地址的格式
每一类地址都由两个固定长度的字段组成,
- 网络号 net-id:它标志主机 (或路由器) 所连接到的网络
- 主机号 host-id:它标志该主机(或路由器)。
IP 地址通常由点分十进制的方式来表示。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hrXWC0Y8-1626960266877)(https://i.loli.net/2021/04/13/HKPd2mbz7Tk1MWe.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oaKmTUB3-1626960266880)(https://i.loli.net/2021/04/13/LwFgrCBW31olibm.png)]
5、地址的分类
根据网络号和主机号的不同,分为 A,B,C,D,E 类。
A类网络号少,网络内主机号多,通常是比较大的网络,如一个国家或地区的网络。
B类或C类,网络号逐渐增多,但每个网络内的主机号逐渐减少。比如我们学校或者公司的网络,可以容纳的数量有限,通常使用的C类网络。
D类和E类属于比较特殊的网络,分别用于多播和备用。
6、子类划分
两级划分有以下缺点:
- IP 地址空间的利用率低
- 给每一个物理网络分配一个网络号会使路由表变得太大
- 两级的 IP 地址不够灵活。
因此,需要子网划分:
- 在 IP 地址中增加了一个 “子网号字段”,使两级的 IP 地址变为三级的 IP 地址。
- 从主机号借用若干个位作为子网号 subnet-id,而主机号 host-id 也就相当减少了若干个位。
- 通过子网掩码与 IP 地址相与,可以求得子网号。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-S5mh42yK-1626960266882)(https://i.loli.net/2021/04/13/ta2gUmpcsDnJ9S3.png)]
7、子网划分结果
子网划分后,虽然从外界转入的路由器地址不变,但通过路由器转入的数据,可以通过子网掩码来进行分发。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YAmH5ujz-1626960266883)(https://i.loli.net/2021/04/13/LCrfR4y5cqsadHV.png)]
8、总结
IP 地址划分为 A,B,C,D,E 五类
1、A 类地址
- A 类地址第 1 字节为网络地址,其它3个字节为主机地址。
- A 类地址范围:1.0.0.0—127.255.255.255
- A 类地址中的私有地址和保留地址:
- 10.0.0.0—255.255.255.255 是私有地址(所谓的私有地址就是在互联网上不使用而被用在局域网络中的地址)
- 127.0.0.0—127.255.255.255 是保留地址,用做循环测试用的。
2、B 类地址
- B 类地址第 1 字节和第 2 字节为网络地址,其它 2 个字节为主机地址。
- B 类地址范围:128.0.0.0—191.255.255.255
- B 类地址的私有地址和保留地址
- 172.16.0.0—172.31.255.255 是私有地址
- 169.254.0.0—169.254.255.255 是保留地址。如果你的 IP 地址是自动获取 IP 地址,而你在网络上又没有找到可用的 DHCP 服务器。就会得到其中一个 IP 。
3、C 类网络
- C 类地址第 1 字节,第 2 字节和第 3 个字节为网络地址,第 4 个字节为主机地址。另外第 1 个字节的前三位固定为 110。
- C 类地址范围:192.0.0.1—255.255.255.254
- C 类地址中的私有地址:192.168.0.0—192.168.255.255 是私有地址。
4、D 类地址
- D 类地址部分网络和主机地址,它的第 1 个字节点的前四位固定为 1110 。
- D 类地址范围:224.0.0.0—239.255.255.255
组播报文的目的地址使用 D 类 IP 地址
范围是从 224.0.0.0 到 239.255.255.255 。D 类地址不能出现在 IP 报文的源 IP 地址字段。
224.0.0.0—224.0.0.255 为预留的组播地址(永久组地址)只能用于局域网中,路由器是不会转发的地址。
224.0.1.0—238.255.255.255 为用户可用的组播地址(临时组地址),可以用于 Internet 上的。
239.0.0.0—239.255.255.255 为本地管理组播地址,仅在特定的本地范围内有效。
5、E 类地址
- E 类地址也不分网络地址和主机地址,它的第1个字节的前五位固定为 1110
- E 类地址范围:240.0.0.0—255.255.255.255 。
6、计算掩码的方法
通过子网数来算
例如:需要将B 类 IP 地址 167.194.0.0 划分成 28 个子网:255.255.0.0
- (28)10 = (11100)2; 255.255.0.0
- 此二进制的位数是 5,则 N = 5;
- 此 IP 地址为 B 类地址,而 B 类地址的子网掩码是 255.255.0.0,且 B 类地址的主机地址是后 2 位(即0—255.1-254)。于是将子网掩码 255.255.0.0 中的主机地址前 5 位全部置 1 ,就可得到 255.255.248.0 ,而这组数据就是划分成 28 个子网的 B 类 IP 地址 167.194.0.0 的子网掩码。
利用主机数来计算
例如:需将 B 类 IP 地址 167.194.0.0 划分成若干个子网,每个子网内有主机 500 台:
- (500)10 = (111110100)2;
- 此二进制的位数是 9,则 N = 9
- 将该 B 类地址的子网掩码 255.255.0.0 的主机地址置 1,得到 255.255.255.255 。然后在从后向前将后 9 位置为 0 ,可得 11111111.11111111.11111110.00000000 即 255.255.254.0 。这组数值就是划分成主机为 500 台的B 类 IP 地址 167.194.0.0 的子网掩码
利用子网掩码计算最大有效子网数
A 类 IP 地址,子网掩码为 255.252.0.0,将它划分成若干子网络,每个子网络中可用主机数有多少?
1. 将子网掩码转换成二进制表示 11111111.11111100.0000000.00000000
2. 统计一下它的主机位共有 18 位
3. 最大可用主机数就是 2 的 18 次方减 2(除去全是 0 的网络地址和去拿时 1 广播地址),即每个子网络最多有 262142 台主机可用。
利用子网掩码确定子网络的起止地址
B 类 IP 地址 172.16.0.0,子网掩码为 255.255. 192.0,它所能划分的子网络起止地址是多少?
- 利用子网掩码计算,最多可以划分 4 个子网络
- 利用子网掩码计算,每个子网络可容纳 16384 台主机(包括网络地址和广播地址)
- 用 16384 除以 256 (网段包括网络地址和广播地址的全部主机数),结果是 64
- 具体划分网络起止方法如下:
172.16.0.0~172.16.63.255
172.16.64.0~172.16.127.255
172.16.128.0~172.16.191.255
172.16.192.0~172.16.255.255
网络基础
1、OSI 七层模型
描述一个网络中各个协议层的常用方法是使用国际标准化组织 (ISO)的计算机开放系统互连(OSI)模型,这是一个七层模型,如下图所示,图中同时给出了它与网络协议族的近似映射。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SF0kOa9c-1626960266893)(https://i.loli.net/2021/04/13/zhx68lOyDYugkQV.jpg)]
1、物理层
主要定义物理设备标准,如网线的接口类型,光纤的接口类型,各种传输介质的传输速率。它的主要作用是传输比特流(就是 1,0 转化为电流强弱来进行传输,到达目的地后再转化为 1,0,也就是说我们常说的数模转换与模数转换)。这一层的护具叫做比特。
2、数据链路层
定义了如何让格式化数据以帧为单位进行传输,以及如何控制对物理介质的访问。这一层还提供错误检测和纠正,以确保数据的可靠传输。如:串口通信中使用到的 115200,8,N,1
3、网络层(负责传送)
在位于不同地理位置的网络的两个主机系统之间提供连接和路径选择。Internet 的发展使得从世界各站点访问信息的用户数大大增加,而网络层正是管理这种连接的层。
4、传输层(负责打包和拆包)
定义了一些传输数据的协议和端口号(WWW端口80等),如:TCP(传输控制协议,传输效率低,可靠性强,用于传输可靠性要求高,数据量大的数据),UDP(用户数据协议,与TCP 特性恰恰相反,用于传输可靠性要求不高,数据量小的数据,如QQ聊天数据就是通过这种方式传输的)。主要是将从下层接收的数据进行分段和传输,到达目的地地址后再进行重组。常常把这一层数据叫做段。
5、会话层
通过传输层(端口号:传输端口与接收端口)建立数据传输的通路。主要在你的系统之间发起会话或者接受会话请求(设备之间需要互相认识可以是 IP 也可以是 MAC 或者是主机名)。
6、表示层
可确保一个系统的应用层所发送的信息可以被另一个系统的应用层读取,例如, PC 程序与另一台计算机进行通信,其中一台计算机使用扩展二一十进制交换吗(EBCDIC),而另一台则使用美国信息交换码(ASCII)来表示相同的字符,如有必要,表示层会通过使用一种通用格式来实现多种数据格式之间的转换。
7、应用层:
是最靠近用户的 OSI 层。这一层为用户的应用程序(例如电子邮件,文件传输和终端仿真)提供网络服务。
2、TCP/IP 四层模型
TCP/IP 协议族是一个四层协议系统,自底而上分别是数据链路层,网络层,传输层和应用层。每一层完成不同的功能,且通过若干协议来实现,上层协议使用下层协议提供的服务。
对应OSI 七层模型:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-a4uYjPfp-1626960266895)(…/MDFile/Linux 网络编程/Untitled.assets/0-1614566937798.jpg)]
1、数据链路层
数据链路层实现了网卡接口的网络驱动程序,以处理数据在物理媒介(比如以太网,领牌环等)上的传输。
数据链路层两个常用的协议是 ARP 协议(地址解析协议)和 RARP 协议(逆地址解析协议)。它们实现了 IP 地址和机器物理地址(MAC地址)之间的相互转换。
网络层使用 IP 地址寻址一台机器,而数据链路层使用物理地址寻址一台机器,因此网络层必须先将目标机器的 IP 地址转化成其物理地址,才能使用数据链路层提供的服务,这就是 ARP 协议的用途。
RARP 协议仅用于网络上的某些无盘工作站。因为缺乏存储设备,无盘工作栈无法记住自己的 IP 地址,但它们可以利用网卡上的物理地址来向网络管理者(服务器或网络管理软件)查询自身的 IP 地址。运行 RARP 服务的网络管理者通常存有该网络上所有机器的物理地址到 IP 地址的映射。
2、网络层
网路层实际数据包的选路和转发。
WAN(广域网) 通常使用众多分级的路由器来连接分散的主机或 LAN(局域网),因此,通信的两台主机一般不是直接相连的,而是通过多个中间节点(路由器)连接的。网路层的任务就是选择这些中间节点,以确定两台主机之间的通信路径。同时,网络层对上层协议隐藏了网络拓扑连接细节,使得在传输层和网络应用程序看来,通信的双方是直接相连的。
网络层最核心的协议是 IP 协议(因特网协议)。IP 协议根据数据包的目的 IP 地址来决定如何投递它。如果数据报不能直接发送给目标主机,那么 IP 协议就为它虚招一个合适的下一跳路由器,并将数据报交付给该路由器来转发。多次重复这一过程,数据报最终到达目标主机,或者由于发动失败而被丢弃。可以, IP 协议使用 逐跳的方式确定通信路径。
网路层另外一个重要的协议是 ICMP 协议(因特网控制报文协议)。它是 IP 协议的重要补充,主要用于检测网络连接。
8 位类型字段用于区分报文类型。它将 ICMP 报文分为两大类
差错报文,这类报文主要用来回应网络错误,比如目标不可到达(类型指为 3) 和重定向(类型值为 5):
查询报文,这类报文用来查询信息,比如 ping 程序就是使用 ICMP 报文查看目标是否可到达(类型值为8)的。
有的 ICMP 报文还是用 8 位代码字段来进一步细分不同的条件。比如重定向报文使用代码值 0 表示对网络重定向,代码值 1 表示对主机重定向。
ICMP 报文使用 16 位校验和字段对整个报文(包括头部和内容部分)进行循环冗余校验(CRC),以检验报文在传输过程中是否损坏。不同的 ICMP 报文类型具有不同的正文内容。
3、传输层
传输层为两台主机上的应用程序提供端到端的通信。与网络层使用的逐跳通信方式不同,传输层只关心通信的起始端和目的端,而不在乎数据包的中转过程。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VC0CrBy5-1626960266898)(https://i.loli.net/2021/04/13/VKC6NyPz2jYevmh.jpg)]
垂直的实线箭头表示 TCP/IP 协议族各层之间的实体通信(数据包确实是沿着这些线路传递的),而水平的虚线箭头表示逻辑通信线路。该图中还附带描述了不同物理网络的连接方法。可见。
数据链路层(驱动程序) 封装了物理网络的电气细节;网络层封装了网咯连接的细节;传输层则为应用程序封装了一条端到端的逻辑通信链路,它负责收集的收发,链路的超时重传等。
传输层协议:TCP 协议,UDP 协议
特性 | TCP 协议 | UDP 协议 |
---|---|---|
服务 | 可靠的,面向对象的,基于流 | 不可靠,无连接,基于数据报 |
超时重转 | 有 | 无 |
4、应用层
应用层负责处理应用程序的逻辑。
数据链路层,网络层和传输层负责处理网络通信细节,这部分必须既稳定又高效,因此它们都在内核空间中实现。而应用程则在用户空间中实现,因为它负责处理众多逻辑,比如文件传输,文件查询和网络管理等。如果应用层也在内核中实现,则会使内核变得非常庞大。当然,也有少数服务器程序时在内核中实现的,这样代码就能无须在用户空间和内核空间来回切换(主要是数据的复制),极大地提高了工作效率。不过这种代码实现起来较复杂,不够灵活,且不便于移植。
ping是应用程序,而不是协议,它利用 ICMP 报文检测网络连接,是调试网络环境的必备工具。
telnet 协议是一张远程登录协议,它使我们能在本地完成远程任务。
OSPF(开放最短路径优先)协议是一种动态路由更新协议,用于路由器之间的通信,以告诉对方各自的路由信息。
DNS(域名服务) 协议提供机器域名到IP 地址的转换。
应用层协议(或程序)可能跳过传输层直接使用网络层提供的服务,比如 ping 程序和 OSPF 协议。应用层协议(或程序)通常既可以使用 TCP 服务,又可以使用 UDP 服务,比如 DNS 协议。我们可以通过 /etc/services 文件查看所有知名的应用层协议,以及他们都能使用哪些传输层服务。
四层协议背后的思想:上层屏蔽下层细节,只是用其提供的服务。高内聚低耦合,每层专注于其功能,各层之间的关系依赖不大。
数据包在每层有不同的格式,从上到下依次叫 段,数据报,帧,数据从应用层通过协议栈向下专递,每经过一层加上对应层协议的报头,最后封装成帧发送到传输介质上,到达路由器或者目的主机剥掉头部,交付给上层需要者。这一过程称为封装,传输,分离,分用。
3、网络协议的概念
网络协议为计算机网络中进行数据交换而建立的规则,标准或约定的集合。
4、常见的网络协议
1、应用层
1、HTTP(超文本传输协议)
是一种用于分布式,协作式和超媒体信息系统的应用层协议。简单来说就是一种发布和接收 HTML 页面的方法,被用于在 Web 浏览器和网站服务器之间传递信息(80端口)。
2、HTTPS(超文本传输安全协议)
是一种透过计算机网络进行安全通信的传输协议。HTTPS 经由 HTTP 进行通信,但利用 SSL/TLS 来加密数据包。HTTPS 开发的主要目的,是提供对网站服务器的身份认证,保护交换数据的隐私和完整性(443端口)。
3、FTP(文件传输协议)
是用于在网络上进行文件传输的一套标准协议,它工作在 OSI 模型的第七层,TCP 模型的第四层,即应用层,使用TCP 传输而不是 UDP,客户在和服务器建立连接前要经过一个 “三次握手” 的过程,保证客户与服务器之间的连接时可靠的,而且是面向连接,为数据传输提供可靠保证。(21端口)。
4、SMTP(简单邮件传输协议)
是一种提供可靠且有效的电子邮件传输的协议。SMTP 是建立在 FTP 文件传输服务上的一种邮件服务,主要用于系统之间的邮件信息传递,并提供有关来信的通知。SMTP独立于特定的传输子系统,且只需要可靠有效的数据量信息支持,SMTP 的重要特性之一是其能跨越网络传输邮件,即 “SMTP邮件中继“。使用 SMTP,可实现相同网络处理进程之间的邮件传输,也可以通过中继器或网关实现某处理进程与其他网络之间的邮件传输(25端口)。
5、POP3(有据协议版本3)
是 TCP/IP 协议族中的一员,有 RFC1939 定义。本协议主要用于支持使用客户端远程管理在服务器上的电子邮件。提供了 SSL 加密的 POP3 协议被称为 POP3S(110端口)。
6、IMAP(因特昂邮件访问协议)
IMAP 是斯坦福大学在 1986 年开发的一种邮件获取协议。它的主要作用是邮件客户端可以通过这种协议从邮件服务器上获取邮件的信息,下载邮件等。当前的全为定义是 RFC3501。IMAP 协议运行在 TCP/IP 协议智商,使用的端口是 143.它与 POP3 协议的主要区别是用户可以不用把所有的邮件全部下载,可以通过客户端直接对服务器上的邮件进行操作(143端口)。
7、DNS(域名系统)
是一种分布式网络目录服务,主要用于域名与 IP 地址的相互转换,以及控制因特网的电子邮件的发送(53端口)。
2、传输层
1、TCP(传输控制协议)
TCP提供 IP 环境下的数据可靠传输,它提供的服务包括数据流传送,可靠性,有效流控,全双工操作和多路复用。通过面向连接,端到端和可靠的数据包发送。通俗说,它是事先为所发送的数据开辟出连接好的通道,然后在进行数据发送;而 UDP 则不为 IP 提供可靠性,流控或差错恢复功能。一般来说,TCP 对应的是可靠性要求高的应用,而 UDP 对应的则是可靠性要求低,传输经济的应用。
2、UDP(用户数据报协议)
UDP 为应用程序提供了一种无需建立连接就可以发送封装的 IP 数据包的方法。
3、网络层
1、IP(因特网协议)
是 TCP/IP 体系中的网络层协议。设计 IP 的目的是提高网络的可扩展性:一是解决互联网问题,实现大规模,异构网络的互联互通;二是分割顶层网络应用和底层网络技术之间的耦合关系,以利于两者的独立发展。根据端到端的设计原则,IP 只为主机提供一种无连接,不可靠的,尽力而为的数据报传输服务。
2、ICMP(控制消息协议)
用来给 IP 协议提供控制服务,允许路由器或目标主机给数据的发送方提供反馈信息。需要发送反馈信息的情况包括:数据包不能被发送到目标主机,路由器缓冲区溢出导致数据包被删除,路由器想要把流量重定向到另外一个更短的路由上等。
3、IGMP(互联网组管理协议)
是一种互联网协议,提供这样一种方法,使得互联网上的主机向邻近路由器报告它的广播组成员。广播使得互联网上的一个主机向网上确认对于源主机发送内容感兴趣的计算机发送信息。
4、RIP(路由信息协议)
是基于距离矢量算法的路由协议,利用跳数来作为计量标准。
5、OSPF(开放式最短路径优先)
是一个内部网关协议(IGP),用于在单一自治系统(AS)内决策路由。是对链路状态路由协议的一种实现,隶属内部网关协议(IGP),故运作于自治系统内部。著名的迪克斯加算法被用来计算最单路径树。OSPF支持负载均衡和基于服务类型的选路,也支持多种路由形式,如特定主机路由和子网路由等
6、ARP(地址解析协议)
可以把一个 IP 地址映射为 MAC 地址,RARP 是反向地址转换协议,通过 MAC 地址确定 IP 地址。
4、链路层
MAC:媒体访问控制
CSMA:载波侦探多路访问
CSMA/CD:载波侦听多路访问和碰撞检测
CSMA/CA:载波侦听多路访问/冲突避免
TCP和UDP区别
1、是否基于连接
- TCP是面向连接的协议
- UDP是无连接的协议
2、可靠性和有序性
- TCP可靠有序
- UDP不可靠,无序
3、实时性
- UDP实时性好
- TCP实时性差
4、协议首部大小
- TCP首部20字节
- UDP首部8字节
5、运行速度
- TCP运行速度慢
- UDP运行速度快
6、拥塞机制
- TCP有拥塞机制
- UDP没有拥塞机制
7、流模式(TCP)与数据报模式
- TCP视数据为字节流
- UDP视数据为报文
8、资源占用
- TCP占用资源多
- UDP占用资源少
协议格式
数据包封装
传输层及其以下的机制由内核提供,应用层由用户进程提供(后面将介绍如何使用 socket API 编写应用程序),应用程序对通讯数据的含义进行解释,而传输层及其以下处理通讯的细节,将数据从一台计算机通过一定的路径发送到另一台计算机。应用层数据通过协议栈发送到网络上时,每层协议都要加上一个数据首部(header),称为封装(Encapsulation),如下图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0eAey7AK-1626960266899)(https://i.loli.net/2021/04/13/RsvQrEuSKXfiUP1.jpg)]
不同的协议层对数据包有不同的称谓,在传输层叫做段(segment),在网络层叫做数据报(datagram),在链路层叫做帧(frame)。数据封装成帧后发到传输介质上,到达目的主机后每层协议再剥掉相应的首部,最后将应用成数据交给应用程序处理。
1、以太网帧格式
以太网的帧格式如下所示:
其中的源地址和目的地址是指网卡的硬件地址(也叫MAC地址),长度是48位,是在网卡出厂时固化的。可在 shell 中使用 ifconfig 命令查看,“HWaddr 00:15:F2:14:9E:3F” 部分就是硬件地址。协议字段有是三种值,分别对应 IP,ARP,RAPR。帧尾是 CRC 校验码。
以太网帧中的数据长度规定最小 46 字节,最大 1500 字节, ARP 和 RARP 数据包的长度不够 46 字节,要在后面不填充位。最大值1500称为以太网的最大传输单元(MTU),不同的网络类型有不同的 MTU,如果一个数据包从以太网路由到拔号链路上,数据包长度大于拨号链路的 MTU,则需要对数据包进行分片(fragmentation)。ifconfig 命令输出中也有 “MTU:1500”。注意,MTU这个概念指数据帧中有效荷载的最大长度,不包括帧头长度。
2、ARP 数据报格式
在网络通讯时,源主机的应用程序知道目的主机的 IP 地址和端口号,却不知道目的主机的硬件地址,而数据包首先是被网卡接收到再去处理上层协议的,如果接收到的数据包的硬件地址与本机不符,则直接丢弃。因此在通讯前必须获得目的主机的硬件地址。ARP 协议就起到这个作用。源主机发出 ARP 请求,询问 “IP 地址是192.168.0.1的主机的硬件地址是多少”,并将这个请求广播到本地网段(以太网帧首部的硬件地址填 FF:FF:FF:FF:FF:FF 表示广播),目的主机接收到广播的 ARP 请求,发现其中的 IP 地址与本机相符,则发送一个 ARP 应答数据包给源主机,将自己的硬件地址填写在应答包中。
每台主机都维护一个 ARP 缓存表,可以用 arp -a 命令查看。缓存表中的表项有过期时间(一般时间为20 分钟),如果 20 分钟内没有再次使用某个表项,则该表项失效,下次还要发 ARP 请求来获得目的主机的硬件。1100 0000 1010 1000 0000 0001 0000 0011 c0 a8 01 03
ARP 数据报的格式如下所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KLrpMUQY-1626960266901)(https://i.loli.net/2021/04/13/D5EBfkAxuW1p87z.png)]
源MAC地址,目的MAC地址在以太网首部和 ARP 请求中各出现一次,对于链路层为以太网的情况是多余的,但如果链路层是其它类型的网络则有可能是必要的。硬件类型指链路层网络类型,1 为以太网,协议类型指要转换的地址类型,0x0800 为 IP 地址,后面两个地址长度对于以太网地址和 IP 地址分别为 6 和 4(字节),op字段为 1 表示 ARP 请求,op 字段为 2 表示 ARP 应答。
3、IP 数据报格式
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QfGFnXWH-1626960266902)(https://i.loli.net/2021/04/13/FlsPHqjzUOic2Kb.jpg)]
IP 数据报的首部长度和数据长度都是可变长的,但总是4字节的整数倍。对于 IPv4,4 位版本字段是 4。4位首部长度的数值是以 4 字节为单位的,最小值为 5,也就是说首部长度最小是 4*5=20 字节,也就是不带任何选项的 IP 首部,4 为能表示的最大值是 15,也就是说首部长度最大是 60 字节。8 位 TOS 字段有 3 个位用来指定 IP 数据报的优先级(目前已经放弃不用),还有 4 个位表示可选的服务类型(最小延迟,最大吞吐量,最大可靠性,最小成本),还有一个位总是 0.总长度是整个数据报(包括 IP 首部和 IP 层 payload)的字节数。每传一个 IP 数据报,16 位的标识加 1,可用于分片和重新组装数据报。3 位标志和 13 位片偏移用于分片。TTL(Time to live) 是这样用的:源主机为数据包设定一个生存时间,比如 64,每过一个路由器就把该值减 1,如果减到 0 就表示路由已经太长了仍然找不到目的主机的网络,就丢弃该包,因此这个生存周期的单位不是秒,而是跳(hop)。==协议字段指示上层协议是 TCP,UDP,ICMP还是 IGMP。然后是校验位,只校验首部,数据的校验由更高协议负责。IPv4 的 IP 地址长度为 32 位。
4、UDP 数据报格式
5、TCP 格式
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-l7yzEQ3G-1626960266903)(https://i.loli.net/2021/04/13/rTbd4hKVgCLJ5ev.jpg)]
与UDP协议一样也有源端口号和目的端口号,通讯双方由 IP 地址和端口号标识。32 位序号,32位确认序号。4 位首部长度和 IP 协议头类似,表示 TCP 协议头的长度,以 4 字节为单位,因此 TCP 协议头最长可以是 4*15=60 字节,如果没有选项字段,TCP 协议头最短 20 字节。URG,ACK,PSH,RST,SYN,FIN 是六个控制位。16 位校验和将 TCP 协议头和数据都极端在内。
TCP 协议
1、TCP 协议
1、简介
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lglAsnhu-1626960266904)(https://i.loli.net/2021/04/13/wIUAzgbuy9cOVsj.png)]
1、序列号seq
占 4 个字节,用来标记数据段的顺序,TCP 把连接中发送的所有数据字节都编上一个序号,第一个字节的编号由本地随机产生;给字节编上序号后,就给每一个报文段指派一个序号;序列号seq 就是这个报文段中的第一个字节的数据编号。
2、确认号ack
占4个字节,期待收到对方下一个报文段的第一个数据字节的序号;序列号表示报文段携带数据的第一个字节的编号;而确认号指的是期望接收到下一个字节的编号;因此当前报文段最后一个字节的编号+1即为确认号。
3、确认ACK
占1位,仅当 ACK = 1时,确认号字段才有效。ACK = 0 时,确认号无效。
4、同步SYN
连接建立时用于同步序号。当 SYN = 1,ACK = 0 时表示:这是一个连接请求报文段。若同意连接,则在响应报文段中使得SYN=1,ACK=1。因此,SYN = 1 表示这是一个连接请求,或连接接受报文。SYN 这个标志位只有在 TCP 建立连接时才会被置 1,握手完成后SYN 标志位被置0.
5、终止FIN
用来释放一个连接。FIN = 1 表示:此报文段的发送方的数据已经发送完毕,并要求释放运输连接
PS:ACK,SYN,FIN这些大写的单词表示标志位,其值要么是 1,要么是 0;ack,seq 小写的单词表示序号。
字段 | 含义 |
---|---|
CWR | 拥塞窗口(发送方降低它的发送速率) |
ECE | ECE回显(发送方接收到了一个更早的拥塞通告) |
URG | 紧急指针是否有效。为1,表示某一位需要被优先处理 |
ACK | 确认号是否有效,一般置为1 |
PSH | 提示接收端应用程序立即从TCP缓冲区把数据独奏 |
PST | 对方要求重新建立连接,复位 |
SYN | 请求建立连接,并在其序列号的字段进行序列号的初始值设定。建立连接,设置为1 |
FIN | 希望断开连接 |
2、TCP 选项
每一个SYN可以含有多个TCP 选项。下面是常用的TCP选项。
1、MSS选项
发送 SYN 的 TCP 一端使用本地选项通告对端它的最大分节大小(MSS),也就是它在本连接的每个 TCP 分节中愿意接受的最大数据量。发送端 TCP 使用接收端的 MSS 值作为多发送分节的最大大小。
2、窗口规模选项
TCP 连接任何一端能够通告对端的最大窗口大小是 65535,因此在 TCP 首部中相应的字段占 16 位。然而当今因特网的飞速发展,要求有更大的窗口以获得尽可能大的吞吐量。这个新选项指定 TCP 首部中的通告窗口必须扩大(即左移)的位数(0~14),因此所提供的最大窗口接近 1 GB(65535*214)。在一个 TCP 连接上使用窗口规模的前提是它的两个端系统必须支持这个选项。
为提供与不支持这个徐哪像的较早实现间的互操作性,需应用如下规则。TCP 可以作为主动打开的部分内容随它的 SYN 发送该选项,但是只在对端也随它的 SYN 发送该选项的前提下,它才能扩大自己窗口的规模。类似地,服务器的 TCP 只有接收到随客户的 SYN 到达的该选项时,才能发送该选项。本逻辑假定实现忽略它们不理解的选项,如此忽略是必需的要求,也已普遍满足,但无法保证所有实现都满足此要求。
3、时间戳选项
这个选项对于高速网络连接时必要的,它可以防止由失而复现的分组可能造成的数据损坏。它是一个较新的选项,也以类似于窗口规模选项的方式协商处理。作为网络编程人员,我们无需考虑这个选项。
TCP 的大多数实现都支持这些常用选项。后两个选项有时称为 “RFC1323选项”,因为它们是在 RFC 1323 中说明的。既然高带宽或长延迟的网络被称为 ”长胖管道“,这两个选项也成为 “长胖管道选项”。
3、TCP 通信时序
下图试一次 TCP 通讯的时序图。TCP 连接建立断开。包含大家熟知的三次握手和四次挥手。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hV7PRwxf-1626960266905)(https://i.loli.net/2021/04/13/hUSJaIG7YwgOnop.png)]
在这个例子中,首先客户端主动发起连接,发送请求,然后服务器端相应请求,然后客户端主动关闭连接。两条数显表示通讯的两端,从上到下表示时间的先后顺序,注意,数据从一段传到网络的另一端也需要时间,所以图中的箭头都是斜的。双方发送的段按时间顺序编号为 1-10,各段中的主要信息也在箭头上标出,例如段 2 的箭头标着 SYN,8000(0),ACK1001,表示该段中的 SYN 位置1,32位序号是 8000,该段不携带有效载荷(数据字节数为0),ACK位置1,32位确认序号是1001,带有一个mss(最大报文长度)选项值为1024。
1、建立连接(三次握手)的过程
2、数据传输的过程
-
客户端发出段4,包含从序号1001开始的20个字节数据。
-
服务器发出段 5,确认序号为 1021,对序号为 1001-1020 的数据表示确认收到,同时请求发送序号1021开始的数据,服务器在应答的同时也向客户端发送序号 8001 开始的 10 个字节数据,这称为 piggyback。
-
客户端发出段 6 ,对服务器发来的序号为 8001-8010 的数据表示确认收到,请求发送序号 8011 开始的数据。
在数传授过程中,ACK和确认序号非常重要,应用程序交给 TCP 协议发送的数据会暂存在 TCP 层的发送缓冲区中,发送数据包给对方之后,只有收到对方应答的 ACK 段才知道该数据包确实发到了对方,可以从发送缓冲区中释放掉,如果因为网络故障丢失了数据包或者丢失了对方发回的 ACK 段,经过等待超时后 TCP 协议自动发送缓冲区中的数据包重发。
3、关闭连接(四次握手)的过程
4、滑动窗口(TCP 流量控制)
提出原因
如果发送端发送的速度较快,接收端接收到数据后处理的速度较慢,而接收缓冲区的大小是固定的,就会丢失数据。
解决方案
TCP 协议通过 ”滑动窗口“机制解决这一问题。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VihyyR14-1626960266906)(https://i.loli.net/2021/04/13/sE2SIeop63ZjYhR.png)]
-
发送端发起连接,声明最大段尺寸是 1460,初始序号是 0,窗口大小是 4k,表示 “我的接收缓冲区还有4k字节空闲,你发的数据不要超过 4k“。接收端应答连接请求,声明最大段尺寸是 1024,初始序号是 8000,窗口大小是 6k。发送端应答,三方握手结束。
-
发送端发出段 4-9,每个段带 1k 的数据,发送端根据窗口大小知道接收端的缓冲区满了,因此停止发送数据。
-
接收端的应用程序提走 2k 数据,接收缓冲区又有了 2k 空闲,接收端发出段 10,在应答已收到 6k 数据的同时声明窗口大小为 2k。
-
接收端的应用程序有提走 2k 数据,接收缓冲区有 4k 空闲,接收端发出段 11,重新声明窗口大小为 4k。
-
发送端发出段 12-13,每个段带 2k 数据,段 13 同时还包含 FIN 位。
-
接收端应答接收到的 2k 数据 (6145-8192),再加上 FIN 位占一个序号 8193,因此应答序号是 8194,连接处于半关闭状态,接收端重新声明窗口大小为 2k。
-
接收端的应用程序提走 2k数据,接收端重新声明窗口大小为 4k。
-
接收端的应用程序提走剩下的 2k 数据,接收缓冲区全空,接收端重新声明窗口大小为 6k。
-
接收端的应用程序在提走全部数据后,决定关闭连接,发出段 17 包含 FIN 位,发送端应答,连接完全关闭。
上图在接收端用小方块表示 1k 数据,实心的小方块表示已接收到的数据,虚线框表示接收缓冲区,因此套在虚线框中的空心小方块表示窗口大小,从图中可以看出,随着应用程序提走数据,虚线是向右滑动的,因此称为滑动窗口。
从这个例子还可以看出,发送端是按 1k 发送数据,而接收端的应用程序可以 2k 地提走数据,方然也有可能一次提走 3k 或 6k 数据,或者一次只提走几个字节的数据。也就是说,应用程序所看的数据是一个整体,或者说是一个流(stream),在底层通讯中这些数据可能被拆成很多数据包来发送,但是一个数据包有多少字节对应用程序是不可见的,因此 TCP 协议 是面向流的协议。而 UDP 是面向消息的协议,每个 UDP 段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,这一点和 TCP 是很不同的。
5、状态转换总体概述
TCP 涉及连接建立和连接终止的操作可以用状态转换图来说明。
TCP 为一个连接定义了 11 种状态,并且 TCP 规则规定如何基于当前状态及在该状态下所接收的分节从一个转换到另一个状态。举例来说,当某个应用进程在 CLOSED 转态下执行主动打开时,TCP 将发送一个 SYN,且新的状态时 SYN_SENT。如果这个 TCP 接着接收到一个带 ACK 的 SYN,它将发送一个 ACK,且新的状态是 ESTABLISHED。这个最终状态是绝大多数数据传送发生的状态。
如果某个应用进程在接收到一个 FIN 之前调用 close(主动关闭),那就转换到 FIN_WAIT_1 状态,但如果某个应用进程在 ESTABLISHED 状态期间接收到一个 FIN(被动关闭),那就转换到 CLOSE_WAIT 状态。
注意:还有两个我么未曾讨论的转换:一个位同时打开,发生在两端几乎同时发送 SYN 并且这两个 SYN 在网络交错的情形下,另一个同时关闭,发生在两端几乎同时发送 FIN 的情形下,他们是可能发生的,不过非常罕见。
1、CLOSED:表示初始状态
LISTEN:该状态表示服务器端的某个 SOCJET 处于监听状态,可以接受连接。
2、SYN_SENT
这个状态与 SYN_RCVD 遥相呼应,当客户端 SOCKET 执行 CONNECT 连接时,它首先发送 SYN 报文,随即进入到了 SYN_SENT 状态,并等待服务端的发送三次握手中的第2个报文。SYN_SENT 状态表示客户端已发送 SYN 报文。
3、SYN_RCVD
该状态表示接收到 SYN 报文,在正常情况下,这个状态是服务端的 SOCKET 在建立 TCP 连接时的三次握手会话过程中的一个中间状态,很短暂。此种状态时,当收到客户端的 ACK 报文后,会进入 ESTABLISHED 状态。
4、ESTABLISHED:表示连接已经建立
5、FIN_WAIT_1和FIN_WAIT_2
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 收到 ACK 以后进入该状态。称之为半连接或半关闭状态。该状态下的 socket 只能接收数据,不能发。
6、TIME_WAIT
表示收到了对方的 FIN 报文,并发送了 ACK 报文,等 2MSL 后即可回到 CLOSED 可用状态。如果 FIN_WAIT_1状态下,收到对方同时带 FIN 标志和 ACK 标志的报文时,可以直接进入到 TIME_WAIT 转态,而无须经过 FIN_WAIT_2 状态。
7、CLOSING
这种状态比较特殊,属于一种较罕见的状态。正常情况下,当你发送 FIN 报文后,按理来说是应该先收到(或同时收到)对方的 ACK 报文,再收到对方的 FIN 报文。但是 CLOSING 状态表示你发送 FIN 报文后,并没有接收到对方的 ACK 报文,反而却收到了对方的 FIN 报文。什么情况下会出现这种情况呢?如果双方几乎在同时 close 一个 SOCKET 的话,那么就出现了双方同时发送 FIN 报文的情况,也即会出现 CLOSING 状态,表示双方都正在关闭 SOCKET 连接。
8、CLOSE_WAIT
此种状态表示在等待关闭。当双方关闭一个 SOCKET 后发送 FIN 报文给自己,系统会回应一个 ACK 报文给对方,此时则进入到 CLOSE_WAIT 状态。接下来呢,察看是否还有数据发送给对方,如果没有可以 close 这个 SOCKET,发送 FIN 报文给对方,即关闭连接,所以在 CLOSE_WAIT 状态下,需要关闭连接。
9、LAST_ACK
该状态是被动关闭一方在发送 FIN 报文后,最后等待对方的 ACK 报文。当收到 ACK 报文后,即可以进入到 CLOSED 可用状态。
2、TCP 建立连接—三次握手状态转换
TCP 建立连接三次握手,必须确认过眼神(SYN),确认肯定对方喜欢自己(ACK),然后才可以建立连接(ESTABLISHED),传输数据。
TCP 建立连接三次握手的理解:
1、状态解释
1、CLOSED
虚拟出来的状态,实际不存在,你在 netstat-anp|grep 端口的时候是找不到的。
2、LISTEN
表示 Server 大门已开,随时准备有 Client 前来连线。
3、SYN_SENT
只在 Client 端出现,表示 Client 发送过 SYN 了,正在焦急等待 Server 的 ACK。
4、SYN_RCVD
只在 Server 端出现,表示 Server 收到 Client 的 SYN 了,并且已经发给 Client 自己的 ACK 和 SYN 了,正在焦急地等待 Client 的 ACK。
5、ESTABLISHED
在 Client 端出现表示 Client 把自己的 ACK (第3次握手)发出去了,Client 已经就绪,在 Server 端出现表示 Server 已经收到 Client 的 ACK(第3次握手)了,Server 已经就绪。
注:只有在 Client 和 Server 同时为 ESTABLISHED 时,即同时就绪时才可以进行数据传输。
2、Client 端口状态转换
- 应用层启动侦听,端口进入 LISTEN 状态。
- 接收到 Client 发来的 SYN,发送自己的 ACK 和 SYN,进入 SYN_RCVD 状态,等待 Client 的 ACK。
- 等待 Client 的 ACK 到来,接收到 ACK 后,进入 ESTABLISHED,等待超时,进入 CLOSED。
3、TCP 断开连接—四次挥手状态转换
TCP 通讯双方,不管是 Client 还是 Server,都可以主动断开连接,所以下面我们只以主动方和被动方为标注。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Jdc1BKqg-1626960266908)(https://i.loli.net/2021/04/13/bTv8fDU75clpGSK.jpg)]
1、状态解释
1、FIN_WAIT_1
进出线在主动方,表示主动方想要断开连接,已经关闭了写通道,并向端发送了 FIN,等待对方的 ACK 到来
2、CLOSED_WAIT
仅出现在被动方,表示被动收到 FIN 后,已经恢复 ACK,正在等自己的应用层调用 close 方法关闭通道,在 CLOSE_WAIT 状态下,自己只能发送数据,但不能接收数据。
3、FIN_WAIT_2
仅出现在主动方,表示主动方已收到对端的 ACK,等待对端的 FIN,此时无法再发送数据,但是可以接收数据。
4、TIME_WAIT
仅出现在主动方,表示主动方已经发出 ACK 了,本次通讯完事了,双方都不能再读写了,但是主动方不确定对方能不能收到最后一个 ACK,为了保证这个端口释放后,不被后来的连接马上使用被当成是新连接,通俗地讲,为了不乱套,这个状态会一直等待,等待 2*MSL 个时间,这个 MSL是os配置的,有默认参数,可以改。
2、主动方端口状态转换
- 应用层调用 close 方法发起关闭连接。
- 发送 FIN 到对端,关闭写通道,端口进入 FIN_WAIT 转态。
- 等待对端的确认 ACK 到来,接收到 ACK 后进入 FIN_WAIT_2 状态;如果在超时间内没有收到 ACK 则直接进入 CLOSED 状态。
- 如果在 FIN_WAIT_1 状态时收到了对端的 FIN,则进入 CLOSING 状态。
- 如果在 FIN_WAIT_2 状态时收到了对端的 FIN,则进入 WIME_WAIT 状态,如果在超时时间内没有收到这个 FIN 则直接进入 CLOSED 状态。
- 在 TIME_WAIT 状态等待 2*MSL(报文最长存活周期)后进入 CLOSED 状态。
3、被动端口状态转换
-
收到对端的 FIN 后,关闭读通道,自己进入 CLOSED_WAIT 状态。
-
在 CLOSED_WAIT 状态等待应用层调用 close 方法关闭 socket 连接
-
如果在超时时间内没调用 close,则直接进入 CLOSED 状态。
-
如果在超时时间内调用了 close,则向对端发送 FIN,自己进入 LAST_ACK 状态,等待对端的 ACK。
-
等待对端的 ACK,如果在超时时间内收到了 ACK 则直接进入 CLOSED 状态,否则超时后进入 CLOSED 状态。
-
调优
上边已经提到了,服务器有时也会关闭连接,应该是为了避免有些不地道的 CLient 在数据传输完之后不关闭 socket 连接,一直占用服务器资源,所以有些服务器也会主动关闭连接。
在高并发场景下,服务器主动主动关闭连接的情况,在服务端会出现大量的 TIME_WAIT 状态的端口,会占用os 文件句柄资源,导致新的连接可能无法建立,影响高并发性能,而关于 TCP 参数的调优中,有一大堆,但有一些参数调优对业务有风险。
在 Linux 服务器上,/ect/sysctl.conf 文件中修改 time_wait 缺省值(≈60s):net.ipv4.tcp_fin_timeout = 50
注:记得用 root 修改,修改后执行 sysctl-p 使参数立即生效。
netstat-apn 命令简单说明
-a:显示所有
-n:以网络 IP 地址代替名称,显示网络连接情形
-p:显示进程号和进程名
4、半关闭
当 TCP 连接中 A 发送 FIN 请求关闭,B 端回应 ACK 后(A 端进入FIN_WAIT_2 状态),B 没有立即发送 FIN 给 A 时,A 方处在半连接状态,此时 A 可以接收 B 发送的数据,但是 A 已不能再向 B 发送数据。
从程序的角度,可以使用 API 来控制实现半连接状态。
#include<sys/socket.h>
int shutdown(int sockfd,int how);
/*
socked:需要关闭的 socket 的描述符
how:允许为 shutdown 操作选择以下几种方式:
SHUT_RD(0):关闭 sockfd 的读功能,此选项将不允许 sockfd 进行读操作。该套接字不再接收数据,任何当前在套接字接收缓冲区的数据将被无声的丢弃掉。
SHUT_WR(1):关闭 sockfd 的写功能。此选项将不允许 sockfd 进行写操作。进程不能在对此套接字发出写操作。
SHUT_WR(2):关闭sockfd的读写功能。相当于调用 shutdown 两次:首先是以 SHUT_RD(1),然后以 SHUT_WR(2)
*/
使用 close 中止一个连接,但它只是减少描述符的引用计数,并不直接关闭连接,只有当描述符的引用计数为 0 时 才关闭连接。
shutdown 不考虑描述符的引用计数,直接关闭描述符。也可以选择中止一个方向的连接,只中止读或只中止写。
注:
- 如果有多个进程共享一个套接字,close 没被调用一次,计数减1,直到计数为 0 时,也就是所有进程都调用了 close,套接字将被释放。
- 在多进程中如果一个进程调用了 shutdown(sfd,SHUT_RDWR) 后,其他的进程将无法进行通信。但,如果一个进程 close(sfd) 将不会影响其他进程。
5、TIME_WAIT状态
TCP 中有关网络编程最不容易理解的是它的 TIME_WAIT 状态。该端点停留在这个状态的持续时间是最长分节生命期(MSL)的两倍,即2*MSL。
MSL 是任何 IP 数据报能够在因特网中存活的最长时间。我们知道这个时间是有限的,因此每个数据报含有一个称为(hop limit)的8位字段,它的最大值为 255。尽管这是一个跳数限制而不是真正的时间限制,我们仍假设:具有最大条线(255)的分组在网络中存活的时间不可能超过 MSL 秒。
分组在网络中“迷途”通常是路由异常的结果。某个路由器崩溃或某两个路由器之间的链路断开后,路由协议需花数秒钟到数分钟才能稳定并找出另一条通路。在这段时间内有可能发生路由循环(A->B->C->A),我们关心的分组可能就此陷入这样的循环。假设迷途的分组是一个 TCP 分节,在它迷途期间,发送端 TCP 超时并重传该分组,而重传的分组却通过某条候选路径到达最终目的地。然而不久后路由循环恢复或漫游的重复分组。TCP 必须正确处理这些重复的分组。
TIM_WAIT状态存在原因
- 可靠地实现 TCP 全双工连接的终止。
- 允许老的重复分节在网络中消逝。
4、拥塞控制
三次握手
1、客户端:我能连接你么?
- 客户端发送一个带 SYN 标志的 TCP 报文到服务器。这是三次握手过程中的段 1
客户端发出段 1,SYN 位表示连接请求。序号是 1000,这个序号在网络通讯中用作临时的地址,每发一个数据字节,这个序号要加 1,这样在接收端可以根据序号排出数据包的正确顺序,也可以发现丢包的情况,也可以发现丢包的情况,另外,规定 SYN 位和 FIN 位也要占一个序号,这次虽然没法数据,但是由于发了 SYN 位,因此下次再发送应该用序号 1001。mss表示最大段尺寸,如果一个段太大,封装成帧后超过了链路层的最大帧长度,就必须在 IP 层分片,为了避免这种弄个情况,客户端声明自己的最大段尺寸,建议服务器端发来的段不要超过这个长度。
2、服务器:你可以连接我
- 服务器端回应客户端,是三次握手中的第2个报文段,同时带 ACK 标志和 SYN 标志。它表示对刚才客户端SYN 的回应;同时又发送给客户端,询问客户端是否准备好进行数据通讯。
服务器发出段 2,也带有 SYN 位,同时置 ACK 位表示确认,确认序号是 1001,表示“我接收到序号1000及其以前所有的段,请你下次发送序号为1001的段”,也就是应答了客户端的连接请求,同时也给客户端发送出一个连接请求,同时声明最大尺寸为 1024。
3、客户端:我收到你的批准了
- 客户端必须再次回应服务器端一个 ACK 报文,这是报文段 3
客户端发出段3,对服务器的连接请求进行应答,确认序号是 8001.在这个过程中,客户端和服务器分别给对方发了连接请求,也应答率对方的来接请求,其中服务器的请求和应答在一个段中发出,因此一共有三个段用于建立连接,称为"三方握手"。在建立的同时,双方协商了一些信息,例如双方发送序号的初始值,最大段尺寸等。
在 TCP 通讯中,如果一方收到另一方发来的段,读出其中的目的端口,发现本机并没有任何进程使用这个端口,就会应答一个包含 RST 位 的段给另一方。例如,服务器并没有任何进程使用 8080 端口,我们却用 telnet 客户端去连接它,服务器收到客户端发来的 SYN 段就会应答一个 RST 段,he护短的 telnet 程序接收到 RST 段后就报告错误。
四次挥手
由于 TCP 连接时全双工的,因此每个方向都必须单独进行关闭。这原则是当一方完成它的数据发送任务后就能发送一个 FIN 来终止这个方向的连接。收到一个 FIN 只意味着这一方上没有数据流动,一个 TCP 连接在收到一个 FIN 后仍能发送数据。首先进行关闭的一方将执行主动关闭,而另一方执行被动关闭。
1、四次握手全过程
-
客户出段7,FIN 位表示关闭连接的请求。
-
服务器发出段8,应答客户端的关闭连接请求ACK。
-
服务器发出段9,其中也包含 FIN 位,向客户端发送关闭连接请求。
-
客户端发出段10,应答服务器的关闭连接请求ACK。
2、为什么关闭的时候是四次?
个人理解:客户端发送FIN表示我这边传送完成了,请求断开连接。服务器收到请求后回一个ACK,表示我收到了你的请求,但是这并不代表服务器这边传输完成了(万一没传完)。当服务器没有传输或传输完成后,才发送 FIN+ACK ,表示我这也传完了,你可以关闭了。
正式理解:因为当Server端收到Client端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当Server端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉Client端,“你发的FIN报文我收到了”。只有等到我Server端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四步握手。
3、为什么TIME_WAIT状态需要经过2MSL(最大报文段生存时间)才能返回到CLOSE状态?
个人理解:最后一次发送的 ACK 若中途丢了,服务器会一直发送第三次挥手信息,客户端得保持 TIME_WIT 来重新发送 ACK
正式理解:虽然按道理,四个报文都发送完毕,我们可以直接进入CLOSE状态了,但是我们必须假象网络是不可靠的,有可以最后一个ACK丢失。所以TIME_WAIT状态就是用来重发可能丢失的ACK报文。在Client发送出最后的ACK回复,但该ACK可能丢失。Server如果没有收到ACK,将不断重复发送FIN片段。所以Client不能立即关闭,它必须确认Server接收到了该ACK。Client会在发送出ACK之后进入到TIME_WAIT状态。Client会设置一个计时器,等待2MSL的时间。如果在该时间内再次收到FIN,那么Client会重发ACK并再次等待2MSL。所谓的2MSL是两倍的MSL(Maximum Segment Lifetime)。MSL指一个片段在网络中最大的存活时间,2MSL就是一个发送和一个回复所需的最大时间。如果直到2MSL,Client都没有再次收到FIN,那么Client推断ACK已经被成功接收,则结束TCP连接。
4、为什么不能用两次握手进行连接?
个人理解:易产生死锁。
正式理解:3次握手完成两个重要的功能,既要双方做好发送数据的准备工作(双方都知道彼此已准备好),也要允许双方就初始序列号进行协商,这个序列号在握手过程中被发送和确认。
现在把三次握手改成仅需要两次握手,死锁是可能发生的。作为例子,考虑计算机S和C之间的通信,假定C给S发送一个连接请求分组,S收到了这个分组,并发 送了确认应答分组。按照两次握手的协定,S认为连接已经成功地建立了,可以开始发送数据分组。可是,C在S的应答分组在传输中被丢失的情况下,将不知道S 是否已准备好,不知道S建立什么样的序列号,C甚至怀疑S是否收到自己的连接请求分组。在这种情况下,C认为连接还未建立成功,将忽略S发来的任何数据分 组,只等待连接确认应答分组。而S在发出的分组超时后,重复发送同样的分组。这样就形成了死锁。
5、如果已经建立了连接,但是客户端突然出现故障了怎么办?
个人理解:TCP 设有一个计时器,75min如果没反应,就会发送探测报文(一共10次),对方没反应就关闭连接。
正式理解:TCP还设有一个保活计时器,显然,客户端如果出现故障,服务器不能一直等下去,白白浪费资源。服务器每收到一次客户端的请求后都会重新复位这个计时器,时间通常是设置为2小时,若两小时还没有收到客户端的任何数据,服务器就会发送一个探测报文段,以后每隔75秒钟发送一次。若一连发送10个探测报文仍然没反应,服务器就认为客户端出了故障,接着就关闭连接。
拥塞控制
拥塞控制:发送端主动控制 cwnd,由慢启动(从 cwnd 初始为 1 开始启动,指数启动),拥塞避免(到达 ssthresh 后,为了避免拥塞开始尝试线性增长),快重传(接收方每收到一个报文段都要回复一个当前最大连续位置的确认,发送发只要一连收到三个重复确认就知道接收方丢包了,快速重传丢包的报文,并 TCP 马上把拥塞窗口 cwnd 减小),快恢复(直接从 ssthresh 线性增长)。
如果网络上的延时突然增加,那么 TCP 对这个事作出的应对只有重传数据,但是重传会导致网络的负担更重,于是会导致更大的延迟以及更多的丢包,于是这个情况就会进入恶心循环被不断地放大。试想一下,如果一个网络内有成千上万的 TCP 连接都这么行事,那么马上就会形成“网络风暴”,TCP 这个协议就会拖垮整个网络。所以 TCP 不能忽略网络上发生的事情,而无脑地一个劲重发数据,会对网络造成更大的伤害。对此 TCP 的设计理念是: TCP 不是一个自私的协议,当拥塞发生的时候,要做自我牺牲。就像交通阻塞一样,每个车都应该把路让出来,而不要再去抢路了。
1、慢启动
只有在 TCP 连接建立和网络出现超时时才使用。每经过一个传输轮次,拥塞窗口 cwnd 就加倍。一个传输轮次所经历的时间其实就是往返时间 RTT。不过“传输轮次”更加强调:把拥塞窗口 cwnd 所允许发送的报文段都连续发送出去,并收到了对已发送的最后一个字节的确认。另外,慢开始的“慢”并不是指 cwnd 的增长速率慢,而是值在 TCP 开始发送报文段时先设置 cwnd = 1,使得发送方在开始时只发送一个报文段(目的是试探一下网络的拥塞情况),然后再逐渐增大 cwnd。
为了防止拥塞窗口 cwnd 增长过大引起网络拥塞,还需要一个慢开始门限 ssthresh 状态变量。慢开始门限 ssthresh用法如下:
当 cwnd < ssthresh 时,使用上述的慢开始算法
当 cwnd > ssthresh 时,停止使用慢开始算法转而改用拥塞避免算法
当 cwnd = ssthresh 时,慢算法和拥塞避免算法都可以
2、拥塞避免
拥塞避免算法:让拥塞窗口 cwnd 缓慢地增大,即每经过一个往返时间 RTT 就把发送方的拥塞窗口 cwnd+1,而不是加倍。这样拥塞窗口 cwnd 按线性规律缓慢增长,比慢开始算法的拥塞窗口增长速率缓慢得多。
无论在慢开始阶段还是在拥塞避免阶段,只要发送方判断网络出现拥塞(其根据就是没有收到确认),就要把慢开始门限 ssthresh 设置为出现拥塞时的拥塞窗口的一半(但不能小于2)。然后把拥塞窗口 cwnd 重新设置为 1,执行慢开始算法。这样做的目的就是要迅速减少主机发送到网络中的分组数,使得发生拥塞的路由器有足够时间把队列中中积压的分组处理完毕。
当 TCP 连接进行初始化时,把拥塞窗口 cwnd 置为 1.前面已说过,为了便于理解,图中的窗口单位不使用字节而使用报文段的个数。慢开始门限的初始值设置为 16 个报文段,即cwnd = 16。
在执行慢开始算法时,拥塞窗口 cwnd 的初始值为 1。以后发送方每收到一个对新报文段的确认 ACK,就把拥塞窗口值置1,然后开始下一轮的传输(图中很坐标为传输轮次)。因此拥塞窗口 cwnd 随着传输轮次按指数规律增长。当拥塞窗口 cwnd 增长到慢开始门限值 ssthresh 时(即 cwnd=16),就改为执行拥塞控制算法,拥塞窗口按线性规律增长。
假定拥塞窗口的数值增长到 24 时,网络出现超时(这很可能即使网络发生拥塞了)。更新后的 ssthresh 值变为 12(即变为出现超时的拥塞窗口数值24 的一半),拥塞窗口再重新设置为1,并执行慢开始算法。当 cwnd = ssthresh = 12 时改为执行拥塞避免算法,拥塞窗口按线性规律增长,每经过一个往返时间增加一个 MSS 的大小。
强调:“拥塞避免”并非指完全能够避免拥塞。利用以上的措施要完全避免网络拥塞还是不可能的。“拥塞避免” 是说在拥塞避免阶段将拥塞窗口控制为按线性规律增长,使网络比较不容易出现拥塞。
3、快重传和快恢复
如果发送方设置的超时计时器时限已到但还没有收到确认,那么很可能是网络出现了拥塞,致使报文段在网咯中的某处被丢弃。这时,TCP 马上把拥塞窗口 cwnd 减小到 1,并执行慢开始算法,同时把慢开始门限值 ssthresh 减半。这是不使用快重传的情况。快重传算法首先要求接受方每收到一个失序的报文段后就立即发出重复确认(为的是使发送方及早知道报文段没有到达对方)而不要等到自己发送数据时才进行捎带确认。
接收方收到了 M1 和 M2 后都分别发出了确认。现在假定接收方没有收到 M3 但接着收到了 M4 。显然,接收方不能确认 M4 ,因为 M4 是收到的失序报文段。根据可靠传输原理,接收方可以什么都不做,也可以在适当时机发送一次对 M2 的确认。但按照快重传算法的规定,接收方应及时发送对 M2 的重复确认,这样做可以让发送方及早知道报文段 M3 没有到达接收方。发送方接着发送了 M5 和 M6 。接收方收到这两个报文后,也还要再次发出对 M2 的重复确认。这样,发送方共收到了接收方的四个对 M2 的确认,其中后三个重传对方尚未收到的报文段 M3 ,而不必继续等待 M3 设置的重传计时器到期。由于发送方今早重传未被确认的报文段,因此,采用快重传后可以使真个网络吞吐量提高约20%。
与快重传配合使用的还有恢复算法,其过程有以下两个要点:
当发送方连续收到三个重复确认,就执行 “惩罚减小”算法,把慢启动门限 ssthresh 减半。这是为了预防网络发生拥塞。请注意:接下去不执行慢开始算法。
由于发送方现在认为网络很可能没有发生拥塞,因此与慢开始不同之处是不执行慢开始算法(即拥塞窗口 cwnd 选择不设置为 1),而是把 cwnd 值设置为慢开始门限 ssthresh 减半后的数值,然后开始执行拥塞避免算法(“加法增大”),使拥塞窗口缓慢地线性增大。
上图给出了快重传和怀恢复的示意图,并表明了 “TCP Reno” 版本。区别:新的 TCP Reno 版本在快重传之后采用快恢复算法而不是采用慢开始算法。
发送方窗口的上下值 = Min[rwnd,cwnd]
当 rwnd < cwnd 时,是接收方的接收能力限制发送方窗口的最大值。
当 rwnd > cwnd 时,则是网络的拥塞限制发送方窗口的最大值。
Socket编程
1、套接字概念
Socket 本身有 "插座"的意思,在 Linux 环境下,用于表示进程网络通信的特殊文件类型。本质为内核借助缓冲区形成的伪文件。
既然是文件,那么理所当然的,我们可以使用文件描述符引用套接字。与管道类似的,Linux 系统将其封装成文件的目的是为了统一接口,使得读写套接字和读写文件的操作一致。区别是管道主要应用于本地进程间通信,而套接字多应用于网络进程间数据的传递。
在 TCP/IP 协议中,"IP地址+TCP或UDP端口"唯一标识网络通讯中的一进程。”IP地址+端口号“ 就对应一个 socket。欲建立连接的两个进程各自有一个socket 来标识,那么这两个 socket 组成的 socket pair 就唯一标识一个连接。因此可以用 Socket 来描述网络连接的一对一关系。
套接字通信原理如下图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Az4YtuY7-1626960266913)(https://i.loli.net/2021/04/14/YKG63hOAMxi5wqm.jpg)]
在网络通信中,套接字一定是成对出现的。一端的发送缓冲区对应另一端的接收缓冲区。我们使用同一个文件描述符索引发送缓冲区和接收缓冲区。
TCP/IP 协议最早在 BSD UNIX 上实现,为 TCP/IP 协议设计的应用层编程接口称为 socket API。本章的主要内容是 socketAPI,主要介绍 TCP 协议的函数接口。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xOgMrlKS-1626960266914)(https://i.loli.net/2021/04/14/Ei3MNQThlpqrgUA.jpg)]
2、网络字节序
我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分,磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分。网络数据流通样有大端小端之分,那么如何定义网络数据流的地址呢?发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出,接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存,因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址。
TCP/IP 协议规定,网络数据流应采用大端字节序,即低地址高字节。
为使网络程序具有可移植性,使同样的 C 代码在大端和小端计算机上编译后能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。
#include<arpa/inet.h>
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
h表示host,n表示network,l表示32位长整数,s表示16位短整数。
如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回,如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。
1、IP 地址转换函数
#include<arpa/inet.h>
int inet_pton(int af,const char *src,void *dst);
const char *inet_ntop(int af,const void *src,char *dst,socklen_t size);
其中 inet_pton 和 inet_ntop 不仅可以转换 IPv4 的 in_addr,还可以转换 IPv6的 in6_addr。因此函数接口是 void *addrptr。
2、socketaddr 数据结构
strcutsockaddr 很多网络编程函数诞生早于 IPv4 协议,那时候都使用的是 sockaddr 结构体,为了向前兼容,现在 sockaddr 退化成了 (void*) 的作用,传递一个地址给函数,至于这个函数是 sockaddr_in 还是 socaddr_in6,由地址族确定,然后函数内部再强制类型转化为所需的地址类型。
struct sockaddr{
sa_family_tsa_family;
charsa_data[14];
}
struct sockaddr_in{
__kernel_sa_family_tsin_family; //地址结构类型
__be16sin_port; //端口号
structin_addr sin_addr; //IP 地址
unsignedchar __pad[__SOCK_SIZE__ - sizeof(short int) - sizeof(unsignedshort int) - sizeof(struct in_addr)];
};
struct in_addr{
__be32s_addr;
}
strcut sockaddr_in6{
unsignedshort int sin6_family;
__be16sin6_port;
};
struct in6_addr{
union{
__u8 u6_addr8[16];
__be16 u6_addr16[8];
__be32 u6_addr32[4];
}in6_u;
#defines6_addr in6_u.u6_addr8
#defines6_addr16 in6_u6_addr16
#defines6_addr32 in6_u.u6_addr32
};
#define UNIX_PATH_MAX 108
structsockaddr_un{
__kernel_sa_family_tsun_family;
charsun_path[UNIX_PATH_MAX];
};
IPv4 和 IPv6 的地址格式定义在 netinet/in.h 中, IPv4 地址用 sockaddr_in 结构体表示,包括 16 位端口号和 32 位 IP 地址,IPv6 地址用 sockaddr_in6 结构体表示,包括 16 为端口号,128 位 IP 地址和一些控制手段。UNixDomain Socket 的地址格式定义在 sys/un.h 中,用 sockaddr_un 结构体表示。各种 socket 地址结构体的开头都是相同的,前 16 位表示整个结构体的长度(并不是所有UNIX的实现都有长度字段,如 Linux就没有),后 16 位表示地址类型。IPv4,IPv6和Unix Domain Socket 的地址类型分别定义为常数 AF-INET,AF_INET6,AF_UNIX。这样,只要取得某种 sockaddr结构体的首地址,不需要知道具体是哪种类型的 socketaddr 结构体,就可以根据地址类型字段确定结构体中的内容。因此,socket API 可以接受各种类型的 sockaddr 结构体指针做参数,例如 bind,accept,connect 等函数,这些函数的参数应该设计成 void* 类型以便接受各种类型的指针,但是 sock API 的实现早于ANSI C 标准化,那时还没有 void* 类型,因此这些函数的参数都用 struct sockaddr* 类型表示,在传递参数之前要强制类型转换一下,例如:
struct sockaddr_in servaddr;
bind(listen_fd,(struct sockaddr*)&servaddr,sizeof(servaddr));
3、网络套接字函数
1、socket模型创建流程图
![img](https://i-blog.csdnimg.cn/blog_migrate/3f59b5aac1e1e59670449ff8afc7a528.jpeg)
2、socket 函数:创建套接字
#include<sys/types.h>
#include<sys/socket.h>
int socket(int domain,int type,int protocol);
/*
domain:
AF_INET:这是大多数用来产生 socket 的协议,使用 TCP 或 UDP 老传输,用 IPv4 的地址
AF_INET6:与上面类似,不过是用 IPv6 的地址
AF_UNIX:本地协议,使用在 Unix 和 Linux 系统上,一般都是当客户和服务器在同一台及其上的时候使用
type:
SOCK_STREAM:这个协议是按照顺序地,可靠的,数据完整的基于字节流的连接,这是一个使用最多的 socket 类型,这个 socket 是使用 TCP 来进行传输。
SOCK_DGRAW:这个协议是无连接的,固定长度的传输调用。该协议是不可靠的,使用 UDP 来进行它的连接。
SOCK_SEQPACKET:该协议是双线路的,可靠的连接,发送规定长度的数据包进行传输。必须把这个包完整的接收才能进行读取。
SOCK_RAWsocket:类型提供单一的网络访问,这个 socket 类型使用 ICMP 公共协议。(ping,traceroute 使用该协议)
SOCK_RDM:这个类型是很少使用的,在大部分的操作系统上没有实现,它是提供给数据链路层使用,不保证数据包的顺序。
protocol:
传 0 表示使用默认协议
返回值:
成功:返回指向新创建的 socket 的文件描述符
失败:返回 -1,设置 errno
*/
socket() 打开一个网络通讯端口,如果成功的话,就像 open() 一样返回一个文件描述符,应用程序可以像读文件一样用 read/write 在网络上收发数据,如果 socket() 调用出错则返回 -1。==对于IPv4,domain 参数指定为 AF_INET。对于 TCP 协议,type 参数指定为 SOCK_STREAM,表示面向流的传输协议。如果 UDP 协议,则 type 参数指定为 SOCK_STREAM,表示面向流的传输协议。如果是 UDP 协议,则 type 参数指定为 SOCK_DGRAM,表示面向数据报的传输协议。protocol 参数的介绍从略,指定为 0 即可。
3、bind 函数:绑定ip和port
#include<sys/types.h>
#include<sys/socket.h>
int bind(int sockfd,const structsockaddr *addr,socklen_t addrlen);
/*
sockdf:
socket文件描述符
addr:
构造出 IP 地址加端口号
addrlen:
sizeof(addr)长度
返回值:
成功:返回0
失败:返回 -1,设置 errno
*/
服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号就可以向服务器发起连接,因此服务器需要调用 bind 绑定一个固定的网络地址和端口号。
bind() 的作用是将参数 sockfd和addr绑定在一起,使用 sockfd 这个用于网络通讯的文件描述符监听 addr 所描述的地址和端口号。前面讲过, struct sockaddr* 是一个通用指针类型,addr 参数实际上可以接受多种协议的 sockaddr 结构体,而它们的长度各不相同,所以需要第三个参数 addrlen 指定结构体的长度,如:
struct sockaddr_in servaddr;
bzero(&servaddr,sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(6666);
首先将整个结构体清零,然后设置地址类型为 AF_INET,网络地址为INADDR_ANY,这个宏表示本地的任意 IP 地址,因为服务器可能有多个网卡,每个网卡也可能绑定多个 IP 地址,这样设置可以在所有的 IP 地址上监听,直到与某个客户建立了连接时才确定下来到底用哪个 IP 地址,端口号为 6666。
4、listen 函数:将主动socket转换为被动socket
#include<sys/types.h>
#include<sys/socket.h>
int listen(int sockfd,int backlog);
/*
sockfd:
socket文件描述符
backlog:
排队建立 3 次握手队列和刚刚建立 3 次 捂手队列的链接数和查
*/
看系统默认 backing
cat /proc/sys/net/ipv4/tcp_max_syn_backlog
典型的服务器程序可以同时服务于多个客户端,当有客户端发起连接时,服务器调用的 accept() 返回并接受这个连接,如果有大量的客户端发起连接而服务器来不及处理,尚未 accpet 的客户端就处于连接等待状态,listen() 声明 sockfd 处于监听状态,并且最多允许有backlog 个客户端处于连接状态,如果接收到更多的连接请求就忽略。listen() 成功返回0,失败返回 -1
5、accpet 函数:接收客户端的请求
#include<sys/types.h>
#include<sys/socket.h>
int accept(int sockfd,struct sockaddr* addr,socklen_t* addrlen);
/*
sockfd:
socket文件描述符
addr:
传出参数,返回连接客户端地址信息,含 IP 地址和端口号
addrlen:
传入传出参数,传入 sizeof(addr)大小,函数返回时返回真正接收到地址结构体的大小
返回值:
成功返回一个新的 socket 文件描述符,用于和客户端通信
失败:返回-1,设置errno
*/
三方握手完成后,服务器调用 accept() 接收连接,如果服务器调用 accpet() 时还没有客户端的连接请求,就阻塞等待直到有客户连接上来。addr是一个传出参数,accpet() 返回时传出客户端的地址和端口号。addrlen 参数是一个传入传出参数(value-resultargument),传入的是调用提供的缓冲区 addr 的长度以避免缓冲区溢出问题,传出的客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区)。如果给 addr 参数传 NULL,表示不关心客户端的地址。
我们的服务器程序结构是这样的:
while(1){
cliaddr_len = sizeof(cliaddr);
connfd = accept(listenfd,(struct sockaddr*) &cliaddr,&cliaddr_len);
n = read(connfd,buf,MAXLINE);
......
close(connfg);
}
整个是一个 while 死循环,每次循环处理一个客户端连接。由于 cliaddr_len 是传入传出参数,每次调用 accept() 之前应该重新赋初值。accept() 的参数 listenfd 是先前的监听文件描述符,而 accept() 的返回值是另外一个文件描述符 connfd,之后与客户端之间就通过这个 connfd 通讯,最后关闭 connfd 断开连接,而不关闭 listenfd,再次回到循环开头 listenfd 仍然用作accept 的参数。accept() 成功返回一个文件描述符,出错返回 -1。
6、connect 函数:客户端连接服务器
#include<sys/types.h>
#include<sys/socket.h>
int connect(int sockfd,conststruct sockaddr * addr,socklen_t addrlen);
/*
sockdf:socket 文件描述符
addr:传入参数,指定服务器端地址信息,含 IP 地址和端口号
addrlen:传入参数,传入sizeof(addr)大小
返回值:
成功:返回0
失败:返回 -1,设置 errno
*/
客户端需要调用 connect() 连接服务器,connect和bind的参数形式一致,区别在于 bind 的参数是自己的地址,而 connect 的参数是对方的地址。connect() 的参数是对方的地址。connect() 成功返回 0,出错返回 -1。
2、c/s 模型-TCP
下图是基于 TCP 协议的客户端/服务器程序的一般流程:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Wi0WTGas-1626960266916)(https://i.loli.net/2021/04/14/FbGrI4nTizHVO2w.png)]
服务器调用 socket(),bind(),listen() 完成初始化后,调用 accept() 阻塞等待,处于监听的状态,客户端调用 socket() 初始化后,调用 connect() 发出 SYN 段并阻塞等待服务器应答,服务器应答一个 SYN-ACK段,客户端接收后从 connect() 返回,同同时应答一个 ACK 段,服务器收到后 从 accept() 返回。
数据传输的过程
建立连接后,TCP 协议提供双全工的通信服务,但是一般的客服端/服务器程序的流程是由客户端主动发起请求,服务器被动处理请求,一问一答的方式。因此,服务器从 accept() 返回后立刻调用 read(),读 socket 就像读管道一样,如果没有数据到达就阻塞等待,这时客户端调用write() 发送请求给服务器,服务器收到后从 read() 返回,对客户端的请求进行处理,在此期间客户端调用
Socket详解
1、网络套接字函数
1、socket模型创建流程图
![0](https://gitee.com/peng-yang97/pic-bed/raw/master/0-1614498409610.jpg)
2、socket 函数:创建套接字
#include<sys/types.h>
#include<sys/socket.h>
int socket(int domain,int type,int protocol);
/*
domain:
AF_INET:这是大多数用来产生 socket 的协议,使用 TCP 或 UDP 老传输,用 IPv4 的地址
AF_INET6:与上面类似,不过是用 IPv6 的地址
AF_UNIX:本地协议,使用在 Unix 和 Linux 系统上,一般都是当客户和服务器在同一台及其上的时候使用
type:
SOCK_STREAM:这个协议是按照顺序地,可靠的,数据完整的基于字节流的连接,这是一个使用最多的 socket 类型,这个 socket 是使用 TCP 来进行传输。
SOCK_DGRAW:这个协议是无连接的,固定长度的传输调用。该协议是不可靠的,使用 UDP 来进行它的连接。
SOCK_SEQPACKET:该协议是双线路的,可靠的连接,发送规定长度的数据包进行传输。必须把这个包完整的接收才能进行读取。
SOCK_RAWsocket:类型提供单一的网络访问,这个 socket 类型使用 ICMP 公共协议。(ping,traceroute 使用该协议)
SOCK_RDM:这个类型是很少使用的,在大部分的操作系统上没有实现,它是提供给数据链路层使用,不保证数据包的顺序。
protocol:
传 0 表示使用默认协议
返回值:
成功:返回指向新创建的 socket 的文件描述符
失败:返回 -1,设置 errno
*/
socket() 打开一个网络通讯端口,如果成功的话,就像 open() 一样返回一个文件描述符,应用程序可以像读文件一样用 read/write 在网络上收发数据,如果 socket() 调用出错则返回 -1。==对于IPv4,domain 参数指定为 AF_INET。对于 TCP 协议,type 参数指定为 SOCK_STREAM,表示面向流的传输协议。如果 UDP 协议,则 type 参数指定为 SOCK_STREAM,表示面向流的传输协议。如果是 UDP 协议,则 type 参数指定为 SOCK_DGRAM,表示面向数据报的传输协议。protocol 参数的介绍从略,指定为 0 即可。
3、bind 函数:绑定ip和port
#include<sys/types.h>
#include<sys/socket.h>
int bind(int sockfd,const structsockaddr *addr,socklen_t addrlen);
/*
sockdf:
socket文件描述符
addr:
构造出 IP 地址加端口号
addrlen:
sizeof(addr)长度
返回值:
成功:返回0
失败:返回 -1,设置 errno
*/
服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号就可以向服务器发起连接,因此服务器需要调用 bind 绑定一个固定的网络地址和端口号。
bind() 的作用是将参数 sockfd和addr绑定在一起,使用 sockfd 这个用于网络通讯的文件描述符监听 addr 所描述的地址和端口号。前面讲过, struct sockaddr* 是一个通用指针类型,addr 参数实际上可以接受多种协议的 sockaddr 结构体,而它们的长度各不相同,所以需要第三个参数 addrlen 指定结构体的长度,如:
struct sockaddr_in servaddr;
bzero(&servaddr,sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(6666);
首先将整个结构体清零,然后设置地址类型为 AF_INET,网络地址为INADDR_ANY,这个宏表示本地的任意 IP 地址,因为服务器可能有多个网卡,每个网卡也可能绑定多个 IP 地址,这样设置可以在所有的 IP 地址上监听,直到与某个客户建立了连接时才确定下来到底用哪个 IP 地址,端口号为 6666。
4、listen 函数:将主动socket转换为被动socket
#include<sys/types.h>
#include<sys/socket.h>
int listen(int sockfd,int backlog);
/*
sockfd:
socket文件描述符
backlog:
排队建立 3 次握手队列和刚刚建立 3 次 捂手队列的链接数和查
*/
看系统默认 backing
cat /proc/sys/net/ipv4/tcp_max_syn_backlog
典型的服务器程序可以同时服务于多个客户端,当有客户端发起连接时,服务器调用的 accept() 返回并接受这个连接,如果有大量的客户端发起连接而服务器来不及处理,尚未 accpet 的客户端就处于连接等待状态,listen() 声明 sockfd 处于监听状态,并且最多允许有backlog 个客户端处于连接状态,如果接收到更多的连接请求就忽略。listen() 成功返回0,失败返回 -1
主动套接字转换为被动套接字时,内核会为被动套接字提供两个队列,一个是半连接队列,一个是全连接队列。
半连接队列
三次握手进行到前两步时会将该链接入半连接队列中。存的是一种数据结构,该数据结构说明是半连接
全连接队列
三次握手进行完成时将半连接队列的链接放到全连接队列中
5、accpet 函数:接收客户端的请求
#include<sys/types.h>
#include<sys/socket.h>
int accept(int sockfd,struct sockaddr* addr,socklen_t* addrlen);
/*
sockfd:
socket文件描述符
addr:
传出参数,返回连接客户端地址信息,含 IP 地址和端口号
addrlen:
传入传出参数,传入 sizeof(addr)大小,函数返回时返回真正接收到地址结构体的大小
返回值:
成功返回一个新的 socket 文件描述符,用于和客户端通信
失败:返回-1,设置errno
*/
三方握手完成后,服务器调用 accept() 接收连接,如果服务器调用 accpet() 时还没有客户端的连接请求,就阻塞等待直到有客户连接上来。addr是一个传出参数,accpet() 返回时传出客户端的地址和端口号。addrlen 参数是一个传入传出参数(value-resultargument),传入的是调用提供的缓冲区 addr 的长度以避免缓冲区溢出问题,传出的客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区)。如果给 addr 参数传 NULL,表示不关心客户端的地址。
我们的服务器程序结构是这样的:
while(1){
cliaddr_len = sizeof(cliaddr);
connfd = accept(listenfd,(struct sockaddr*) &cliaddr,&cliaddr_len);
n = read(connfd,buf,MAXLINE);
......
close(connfg);
}
整个是一个 while 死循环,每次循环处理一个客户端连接。由于 cliaddr_len 是传入传出参数,每次调用 accept() 之前应该重新赋初值。accept() 的参数 listenfd 是先前的监听文件描述符,而 accept() 的返回值是另外一个文件描述符 connfd,之后与客户端之间就通过这个 connfd 通讯,最后关闭 connfd 断开连接,而不关闭 listenfd,再次回到循环开头 listenfd 仍然用作accept 的参数。accept() 成功返回一个文件描述符,出错返回 -1。
6、connect 函数:客户端连接服务器
#include<sys/types.h>
#include<sys/socket.h>
int connect(int sockfd,conststruct sockaddr * addr,socklen_t addrlen);
/*
sockdf:socket 文件描述符
addr:传入参数,指定服务器端地址信息,含 IP 地址和端口号
addrlen:传入参数,传入sizeof(addr)大小
返回值:
成功:返回0
失败:返回 -1,设置 errno
*/
客户端需要调用 connect() 连接服务器,connect和bind的参数形式一致,区别在于 bind 的参数是自己的地址,而 connect 的参数是对方的地址。connect() 的参数是对方的地址。connect() 成功返回 0,出错返回 -1。
2、Socket 代码实现
//服务器端
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#define SER_PORT 8000
#define SER_IP "192.168.179.130"
int main(int argc,char* argv[])
{
char buf[1024];
//创建一个套接字
int lfd = socket(AF_INET,SOCK_STREAM,0);
//为套接字绑定地址结构信息
struct sockaddr_in ser_addr,cli_addr;
ser_addr.sin_family = AF_INET;
ser_addr.sin_port = htons(SER_PORT);
ser_addr.sin_addr.s_addr = htonl();
//将套接字和地址结构信息绑定在一起
bind(lfd,(struct sockaddr*)&ser_addr,sizeof(ser_addr));
//监听 将主动套接字转换为被动套接字,等别人跟被动套接字连接
listen(lfd,64);
socklen_t len = sizeof(cli_addr);
//accept:接收客户端的请求
int cfd = accept(lfd,(struct sockaddr*)&cli_addr,&len);
int rr = read(cfd,buf,1024);
write(1,buf,rr);
}
单个的回射服务器
1、服务器
//一个简单的回舍服务器,只能和一个客户端连接
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<errno.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#define SER_PORT 8000
#define SER_IP "192.168.179.130"
int main(int argc,char* argv[])
{
char buf[1024];
//创建一个套接字
int lfd = socket(AF_INET,SOCK_STREAM,0);
//为套接字绑定地址结构信息
struct sockaddr_in ser_addr,cli_addr;
ser_addr.sin_family = AF_INET;
ser_addr.sin_port = htons(SER_PORT);
ser_addr.sin_addr.s_addr = htonl(INADDR_ANY);
bind(lfd,(struct sockaddr*)&ser_addr,sizeof(ser_addr));
listen(lfd,64);
socklen_t len = sizeof(cli_addr);
int cfd = accept(lfd,(struct sockaddr*)&cli_addr,&len);
while(1)
{
int rr = read(cfd,buf,1024);
write(STDOUT_FILENO,buf,rr); //#define STDOUT_FILENO 1
write(cfd,buf,rr);
}
return 0;
}
2、客户端
//一个简单的回舍服务器,只能和一个客户端连接
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<errno.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#define SER_PORT 8000
#define SER_IP "192.168.179.130"
int main(int argc,char* argv[])
{
char buf[1024];
//创建一个套接字
int cfd = socket(AF_INET,SOCK_STREAM,0);
//为套接字绑定地址结构信息
struct sockaddr_in ser_addr;
ser_addr.sin_family = AF_INET;
ser_addr.sin_port = htons(SER_PORT);
int dst;
//本地字节序转网络字节序
inet_pton(AF_INET,SER_IP,(void*)&dst);
ser_addr.sin_addr.s_addr = dst;
connect(cfd,(struct sockaddr*)&ser_addr,sizeof(ser_addr));
while(1)
{
int rr = read(STDOUT_FILENO,buf,1024);
write(cfd,buf,rr);
}
return 0;
}
多进程服务器
1、服务端
//多进程服务器
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<errno.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#define SER_PORT 8000
#define SER_IP "192.168.179.130"
int main(int argc,char* argv[])
{
char buf[1024];
//创建一个套接字
int lfd = socket(AF_INET,SOCK_STREAM,0);
//为套接字绑定地址结构信息
struct sockaddr_in ser_addr,cli_addr;
ser_addr.sin_family = AF_INET;
ser_addr.sin_port = htons(SER_PORT);
ser_addr.sin_addr.s_addr = htonl(INADDR_ANY);
bind(lfd,(struct sockaddr*)&ser_addr,sizeof(ser_addr));
listen(lfd,64);
socklen_t len = sizeof(cli_addr);
while(1)
{
//父进程负责接收accpet请求
int cfd = accept(lfd,(struct sockaddr*)&cli_addr,&len);
char dst[64];
int src = cli_addr.sin_addr.s_addr;
printf("客户端连接成功:客户端 IP = %s,PORT = %d\n",dst,ntohs(cli_addr.sin_port));
int pid = fork();
if(pid == 0) //子进程专门负责读数据
{
close(lfd); //子进程不用lfd,关掉
while (1) //不加 while 读一次就会跳出if 子进程就不能再读,while使子进程只能一直读
{
int rr = read(cfd,buf,1024);
write(STDOUT_FILENO,buf,rr); //#define STDOUT_FILENO 1
write(cfd,buf,rr);
}
}
close(cfd); //父进程不用cfd 关掉
}
return 0;
}
2、客户端
1、阻塞与非阻塞
阻塞与非阻塞是socket的属性
阻塞与非阻塞针对的只是 socket,阻塞与非阻塞是socket的属性,只是在 调用read,accept时才体现
socket默认是阻塞的
阻塞与非阻塞
阻塞相当于cpu运行到此处就不执行了,因为数据没来,cpu处理其他事务。当数据来时,cpu再继续往下执行。
非阻塞相当于cpu一直运行,当没有数据来时,仍然执行。当数据来时,执行数据来时的程序,之后仍然执行。
1、fctl() 函数
作用:改变文件描述符阻塞属性
格式
//头文件
#include<fcntl.h>
//使用:将cfd设置为非阻塞
int flags = fctl(cfd,F_GETFL); //获取文件描述符属性返回给 flags
flags |= O_NONBLOCK; //将flags设置为 O_NONBLOCK
fctl(cfd,F_SETFL,flags); //设置文件描述符属性
实例
//服务器端,将cfd改为非阻塞
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<errno.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#define SER_PORT 8000
#define SER_IP "192.168.179.130"
int main(int argc,char* argv[])
{
char buf[1024];
//创建一个套接字
int lfd = socket(AF_INET,SOCK_STREAM,0);
//为套接字绑定地址结构信息
struct sockaddr_in ser_addr,cli_addr;
ser_addr.sin_family = AF_INET;
ser_addr.sin_port = htons(SER_PORT);
ser_addr.sin_addr.s_addr = htonl(INADDR_ANY);
bind(lfd,(struct sockaddr*)&ser_addr,sizeof(ser_addr));
listen(lfd,64);
socklen_t len = sizeof(cli_addr);
while(1)
{
int cfd = accept(lfd,(struct sockaddr*)&cli_addr,&len);
int flags = fcntl(cfd,F_GETFL);
flags |= O_NONBLOCK;
fcntl(cfd,F_SETFL,flags);
char dst[64];
int src = cli_addr.sin_addr.s_addr;
inet_ntop(AF_INET,(void*)&src,dst,sizeof(dst));
printf("客户端连接成功:客户端IP = %s,PORT = %d\n",dst,ntohs(cli_addr.sin_port));
int pid = fork();
if(pid==0)
{
while(1)
{
int rr = read(cfd,buf,1024);
if(rr<0)
{
if(errno == EAGAIN)
{
printf("在读取非阻塞套接字文件,且没有数据\n");
sleep(1);
continue;
}
}
write(STDOUT_FILENO,buf,rr); //#define STDOUT_FILENO 1
write(cfd,buf,rr);
}
}
}
return 0;
}
select函数
1、select运行机制
select() 的机制是提供了一个 fd_set的数据结构,实际上是一个 long 类型的数组,每一个数组元素都能与一打开的文件描述符建立联系。当调用 select() 时,由内核根据 IO 状态修改 fd_set 的内容,由此来通知执行了 是哪一个 socket 。
2、select优缺点
1、优点
- 跨平台:windows,linux,maxos
2、缺点
- 受限于 fd_set ,最多只能同时监听1024个文件描述符
- 底层监听方式是轮询
- 用户到内核空间的大量数据拷贝
- fd_set传入传出参数,每次调用select都要重新赋值
3、代码实现
1、select函数
//头文件
#include<sys/select.h>
//select 函数
int select(int nfds,fd_set *readfds,fd_set *writefds,fd_set *exceptfds,struct timeval *timeout);
/*
int nfds:最大的文件描述符+1
fd_set:是一个128字节的数组,这里我们视这数组为1024位位图
readfds:读事件
writefds:写事件
exceptfds:异常事件
struct timeval *timeout:
与时间相关,默认设为 NULL
设select为阻塞:timeout设为NULL
设select为非阻塞:timeout.tv_sec=0;
timeout.tv_usec=0;
超时返回:timeout.tv_sec >0
timeout.tv_usec >0
select返回的是所有被监听事件触发的个数和
*/
//操作 fd_set 位图的函数
void FD_CLR(int fd,fd_set *set); //将指定的文件描述符清零
int FD_ISSET(int fd,fd_set *set); //判断该文件描述符是否在位图上
void FD_SET(int fd,fd_set *set); //将指定的文件描述符置 1
void FD_ZERO(fd_set *set); //将所有的文件描述符置 0
由select() 函数可知,select() 函数创建了三个1024位的位图,分别监听读事件,写事件,异常事件,select函数返回的是被监听事件触发的个数和。
2、实例
//服务器端,select函数
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<errno.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<sys/select.h>
#define SER_PORT 8000
#define SER_IP "192.168.179.130"
int main(int argc,char* argv[])
{
char buf[1024];
//创建一个套接字
int lfd = socket(AF_INET,SOCK_STREAM,0);
//为套接字绑定地址结构信息
struct sockaddr_in ser_addr,cli_addr;
ser_addr.sin_family = AF_INET;
ser_addr.sin_port = htons(SER_PORT);
ser_addr.sin_addr.s_addr = htonl(INADDR_ANY);
bind(lfd,(struct sockaddr*)&ser_addr,sizeof(ser_addr));
listen(lfd,64);
socklen_t len = sizeof(cli_addr);
//select函数
int maxfd = lfd;
fd_set rset,aset; //rset作为参数传入select函数,aset记录所有监听的文件描述符
FD_ZERO(&aset);
FD_SET(lfd,&aset);
while(1)
{
rset = aset;
int count = select(maxfd +1,&rset,NULL,NULL,NULL);
if(FD_ISSET(lfd,&rset))
{
int cfd = accept(lfd,(struct sockaddr*)&cli_addr,&len);
char dst[64];
int src = cli_addr.sin_addr.s_addr;
inet_ntop(AF_INET,(void*)&src,dst,sizeof(dst));
printf("客户端连接成功:客户端IP = %s,PORT = %d\n",dst,ntohs(cli_addr.sin_port));
FD_SET(cfd,&aset);
if(cfd>maxfd)
{
maxfd = cfd;
}
if(--count ==0) //优化1
{
continue;
}
}
int i;
for(i = lfd+1;i<maxfd+1;i++) //只遍历cfd发生的事件,lfd默认是最小的,所有lfd+1在之后就都是cfd
{
if(FD_ISSET(i,&rset))
{
int rr = read(i,buf,1024);
write(STDOUT_FILENO,buf,rr); //#define STDOUT_FILENO 1
write(i,buf,rr);
if(--count == 0) //优化2
{
break;
}
}
}
}
return 0;
}
poll函数
1、poll运行机制
poll函数和select运行机制相同,只是select中将事件分类,建立三个位图。而poll是将文件描述符,事件类型,返回值存入在一个结构体中,再将这个结构体存入一个数组中,当相应的文件描述符对应的事件触发,返回值改变以示该事件触发。
2、select和poll区别
- poll突破了select只能监听1024个文件描述符的缺点
3、函数实现
//头文件
#include<poll>
//poll函数
int poll(struct pollfd *fds,nfds_t nfds,int timeout);
//struct pollfd 结构体 将文件描述符和监听类型放在一起,触发时返回 revents
struct pollfd{
int fd; //文件描述符
short events; //需要监听的事件类型
short revents; //返回值
}
/*
struct pollfd 结构体
nfd_t nfds:最大文件描述符对应的下标+1
int timeout:时间 单位毫秒
poll设置阻塞:timeout = -1
poll设置为非阻塞:timeout = 0
poll设置为超时返回:timeout > 0
*/
4、实例
//服务器端,poll函数
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<errno.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<poll.h>
#define SER_PORT 8000
#define SER_IP "192.168.179.130"
int main(int argc,char* argv[])
{
char buf[1024];
//创建一个套接字
int lfd = socket(AF_INET,SOCK_STREAM,0);
//为套接字绑定地址结构信息
struct sockaddr_in ser_addr,cli_addr;
ser_addr.sin_family = AF_INET;
ser_addr.sin_port = htons(SER_PORT);
ser_addr.sin_addr.s_addr = htonl(INADDR_ANY);
bind(lfd,(struct sockaddr*)&ser_addr,sizeof(ser_addr));
listen(lfd,64);
socklen_t len = sizeof(cli_addr);
//poll函数
struct pollfd fds[1025];
int i;
for(i = 0;i<1025;i++)
{
fds[i].fd = -1;
}
fds[0].fd = lfd;
fds[0].events = POLLIN;
int maxi = 0;
while(1)
{
int count = poll(fds,max+1,-1);
if(count <0)
{
perror("poll error");
exit(1);
}
else if(count == 0)
{
continue;
}
else
{
//是否是lfd发生
if(fds[0].revents & POLLIN)
{
int cfd = accept(lfd,(struct sockaddr*)&cli_addr,&len);
if(cfd<0)
{
perror("accept error");
exit(1);
}
char dst[64];
int src = cli_addr.sin_addr.s_addr;
inet_ntop(AF_INET,(void*)&src,dst,sizeof(dst));
printf("客户端连接成功:客户端IP = %s,PORT = %d\n",dst,ntohs(cli_addr.sin_port));
for(i = 0;i<1025;i++)
{
if(fds[i].fd==-1)
{
fds[i].fd = cfd;
fds[i].events = POLLIN;
break;
}
}
if(maxi<i)
{
maxi = i;
}
int j;
for(j=1;j<maxi+1;j++)
{
if(fds[j];j<maxi+1;j++)
{
int rr = read(fds[j].fd,buf,1024);
if(rr == 0)
{
printf("客户端断开链接,fd = %d\n",fds[j].fd);
close(fds[j].fd);
fds[j].fd = -1;
}
write(STDOUT_FILENO,buf,rr); //#define STDOUT_FILENO 1
write(fds[j].fd,buf,rr);
}
}
}
return 0;
}
epoll函数
epoll运行机制
-
select,poll底层都是轮询机制,epoll底层是通知机制。
-
epoll_creat 会创建一个文件系统,该文件系统创建在内核区,使用 epoll_ctl 函数将节点挂在红黑树上,此时,内核会帮助我们监听红黑树上的事件,若事件发生,会调用callback函数,将事件发生的结点拷贝到双链表中。此时双链表中存放的都是发生事件的文件描述符。
epoll_creat
epoll_creat 函数执行的操作是创建以下文件系统(该文件系统创建在内核区),该数据结构包含 红黑树和 双链表。
epoll_creat函数会返回一个文件描述符:epfd。用来描述该文件系统。
epoll_ctl
操作红黑树,将节点挂在红黑树上。
该红黑树节点存放的是一个结构体,该结构体包含文件描述符和该文件描述符对应的事件。例如,根节点:文件描述符为1,读事件。
epoll_wait
操作链表,将链表中结点取下来存入struct epoll_event *event 数组中,取完后链表为空。epoll_wait 函数返回的是链表节点的个数n。之后遍历 *event 数组的前n项便可知是哪些事件发生了。
2、函数实现
//头文件
#include<sys/epoll.h>
int epoll_creat(int size);
int epoll_ctl(int epfd,int op,int fd,struct epoll_event *event);
int epoll_wait(int epfd,struct epoll_event *event,int maxevents,int timeout);
3、实例
//服务器端,epoll函数
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<errno.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<sys/epoll.h>
#define SER_PORT 8000
#define SER_IP "192.168.179.130"
#define MAXFD 1025
#define MAXSIZE 1024
int main(int argc,char* argv[])
{
char buf[MAXSIZE];
char buf[1024];
//创建一个套接字
int lfd = socket(AF_INET,SOCK_STREAM,0);
//为套接字绑定地址结构信息
struct sockaddr_in ser_addr,cli_addr;
ser_addr.sin_family = AF_INET;
ser_addr.sin_port = htons(SER_PORT);
ser_addr.sin_addr.s_addr = htonl(INADDR_ANY);
bind(lfd,(struct sockaddr*)&ser_addr,sizeof(ser_addr));
listen(lfd,64);
socklen_t len = sizeof(cli_addr);
//epoll函数
struct epoll_event_cli,cli_arry[MAXFD];
cli.eventd = EPOLLIN;
cli.data.fd = lfd;
int epfd = epoll_create(MAXFD);
epoll_ctl(epfd,EPOLL_CTL_ADD,lfd,&cli);
while(1)
{
int count = epoll_wait(epfd,cli_arry,MAXFD,-1);
if(count<0)
{
perror("epoll_wait error");
exit(1);
}
else if(count == 0)
{
continue;
}
else
{
int i;
for(i = 0;i<count;i++)
{
if(cli_arry[i].events & EPOLLIN)
{
if(cli_arry[i].data.fd == lfd)
{
int cfd = accept(lfd,(struct sockaddr*)&cli_addr,&len);
if(cfd<0)
{
perror("accept error");
exit(1);
}
char dst[64];
int src = cli_addr.sin_addr.s_addr;
inet_ntop(AF_INET,(void*)&src,dst,sizeof(dst));
printf("客户端连接成功:客户端IP = %s,PORT=%d\n",dst,ntohs(cli_addr.sin_port));
cli.events = EPOLLIN;
cli.data.fd = cfd;
epoll_ctl(epfd,EPOLL_CTL_ADD,cfd,&cli);
}
else
{
int rr = read(cli_arry[i].data.fd,buf,1024);
if(rr==0)
{
printf("客户端断开连接,fd = %d\n",cli_arry[i].data.fd);
close(cli_arry[i].data.fd);
epoll_ctl(epfd,EPOLL_CTL_DEL,cli_arry[i].data.fd,NULL);
continue;
}
write(STDOUT_FILENO,buf,rr);
write(cli_arry[i].data.fd,buf,rr);
}
}
}
}
}
return 0;
}
//printf("timeout.tv_sec = %d,timeout.tv_usec = %d\n",timeout.tv_sec,timeout.tv_usec);
ET和LT模式
Linux 多路IO复用
1、I/O多路复用概述
I/O多路复用,I/O就是指的我们网络I/O,多路指多个TCP连接,复用指复用一个或少量线程。串起来就是多个网络I/O复用一个或少量的线程来处理这些连接。
多路复用的本质是同步非阻塞I/O,多路复用的优势并不是单个连接处理的更快,而是在于能处理更多的连接。
I/O多路复用技术通过把多个I/O的阻塞复用到同一个select阻塞上,一个进程监视多个描述符,一旦某个描述符就位,能够通知程序进行读写操作。因为多路复用本质上是同步I/O,都需要应用程序在读写事件就绪后自己负责读写。
I/O编程过程中,需要同时处理多个客户端接入请求时,可以利用多线程或者I/O多路复用技术进行处理。其最大的优势是系统开销小,不需要创建和维护额外线程或进程。
应用场景
- 服务器需要同时处理多个处于监听状态或者多个连接状态的套接字
- 需要同时处理多种网络协议的套接字
- 一个服务器处理多个服务或协议
目前支持多路复用的系统调用有 select,poll,epoll
一、select特点
-
支持水平触发
-
一个线程可以同时处理多个Socket请求
-
最大连接数有限制(32位系统最大支持1024个连接,64位系统最大支持2048个连接)
-
每次调用select,轮询所有Socket(活跃的Socket和非活跃的Socket)找到相应的数据
二、poll特点
-
支持水平触发
-
一个线程可以同时处理多个Socket请求
-
每次调用poll,轮询所有Socket(活跃的Socket和非活跃的Socket)找到相应的数据
-
无最大连接数限制
三、epoll特点
-
基于事件驱动
-
支持水平触发和边缘触发(高速)
-
一个线程可以同时处理多个Socket请求
-
无最大连接数限制
-
每次调用epoll,只遍历活跃的Socket
线程池
输入网址到网页显示的过程
-
在客户端浏览器中输入网址URL。
-
发送到DNS(域名服务器)获得域名对应的WEB服务器的IP地址。
-
客户端浏览器与WEB服务器建立TCP(传输控制协议)连接。
-
客户端浏览器向对应IP地址的WEB服务器发送相应的HTTP或HTTPS请求。
-
WEB服务器响应请求,返回指定的URL数据或错误信息;如果设定重定向,则重定向到新的URL地址。
-
客户端浏览器下载数据,解析HTML源文件,解析的过程中实现对页面的排版,解析完成后,在浏览器中显示基础的页面。
-
分析页面中的超链接,显示在当前页面,重复以上过程直至没有超链接需要发送,完成页面的全部显示。