• 8、Linux 网络编程


    1. 网络结构模式

    1.1 C/S 结构

    • 简介

    客户机 - 服务器,即 Client - Server(C/S)结构。C/S 结构通常采取两层结构。

    服务器负责数据的管理,客户机负责完成与用户的交互任务。
    客户机是因特网上访问别人信息的机器,服务器则是提供信息供人访问的计算机。

    客户机通过局域网与服务器相连,接受用户的请求,并通过网络向服务器提出请求,对数据库进行操作。

    服务器接受客户机的请求,将数据提交给客户机,客户机将数据进行计算并将结果呈现给用户。服务器还要提供完善安全保护及对数据完整性的处理等操作,并允许多个客户机同时访问服务器,这就对服务器的硬件处理数据能力提出了很高的要求。

    在C/S结构中,应用程序分为两部分:服务器部分和客户机部分。
    服务器部分是多个用户共享的信息与功能,执行后台服务,如控制共享数据库的操作等;
    客户机部分为用户所专有,负责执行前台功能,在出错提示、在线帮助等方面都有强大的功能,并且可以在子程序间自由切换。

    • 优点
    1. 能充分发挥客户端 PC 的处理能力,很多工作可以在客户端处理后再提交给服务器,所以 C/S 结构客户端响应速度快;
    2. 操作界面漂亮、形式多样,可以充分满足客户自身的个性化要求
    3. C/S 结构的管理信息系统具有较强的事务处理能力,能实现复杂的业务流程
    4. 安全性较高,C/S 一般面向相对固定的用户群,程序更加注重流程,它可以对权限进行多层次校验,提供了更安全的存取模式,对信息安全的控制能力很强,一般高度机密的信息系统采用 C/S 结构适宜。
    • 缺点
    1. 客户端需要安装专用的客户端软件。首先涉及到安装的工作量,其次任何一台电脑出问题,如病毒、硬件损坏,都需要进行安装或维护。系统软件升级时,每一台客户机需要重新安装,其维护和升级成本非常高;
    2. 对客户端的操作系统一般也会有限制,不能够跨平台

    1.2 B/S 结构

    • 简介

    B/S 结构(Browser/Server,浏览器/服务器模式),是 WEB 兴起后的一种网络结构模式,WEB浏览器是客户端最主要的应用软件。

    这种模式统一了客户端(浏览器),将系统功能实现的核心部分集中到服务器上,简化了系统的开发、维护和使用。

    客户机上只要安装一个浏览器,如 Firefox 或 Internet Explorer,服务器安装 SQL Server、Oracle、MySQL 等数据库。浏览器通过 Web Server 同数据库进行数据交互。

    • 优点

    B/S 架构最大的优点是总体拥有成本低、维护方便、 分布性强、开发简单,可以不用安装任何专门的软件就能实现在任何地方进行操作,客户端零维护,系统的扩展非常容易,只要有一台能上网的电脑就能使用。

    • 缺点
    1. 通信开销大、系统和数据的安全性较低
    2. 个性特点明显降低,无法实现具有个性化的功能要求;
    3. 协议一般是固定的:http / https,导致无法传输大数据量的文件
    4. 客户端服务器端的交互是请求-响应模式,通常动态刷新页面,响应速度明显降低

    2. MAC地址

    网卡是一块被设计用来允许计算机在计算机网络上进行通讯的计算机硬件,又称为网络适配器或网络接口卡NIC。其拥有 MAC 地址,属于 OSI 模型的第 2 层,它使得用户可以通过电缆或无线相互连接(以太网卡/无线网卡)。每一个网卡都有一个被称为MAC 地址的独一无二的 48 位串行号

    网卡的主要功能:1.数据的封装与解封装、2.链路管理、3.数据编码与译码。

    MAC 地址(Media Access Control Address),直译为媒体存取控制位址,也称为局域网地址、以太网地址、物理地址或硬件地址,它是一个用来确认网络设备位置的位址,由网络设备制造商生产时烧录在网卡中。

    在 OSI 模型中,第三层网络层负责 IP 地址,第二层数据链路层则负责 MAC位址 。MAC 地址用于在网络中唯一标识一个网卡,一台设备若有一或多个网卡,则每个网卡都需要并会有一个唯一的 MAC 地址。

    MAC 地址的长度为 48 位(6个字节),通常表示为 12 个 16 进制数,如:00-16-EA-AE-3C-40 就是一个MAC 地址,其中前 3 个字节,16 进制数 00-16-EA 代表网络硬件制造商的编号,它由IEEE(电气与电子工程师协会)分配;而后 3 个字节,16进制数 AE-3C-40 代表该制造商所制造的某个网络产品(如网卡)的系列号。只要不更改自己的 MAC 地址,MAC 地址在世界是唯一的。形象地说,MAC 地址就如同身份证上的身份证号码,具有唯一性。(ifconfig 命令查看)

    3. IP 地址

    3.1 简介

    IP 协议是为计算机网络相互连接进行通信而设计的协议。在因特网中,它是能使连接到网上的所有计算机网络实现相互通信的一套规则,规定了计算机在因特网上进行通信时应当遵守的规则。任何厂家生产的计算机系统,只要遵守 IP 协议就可以与因特网互连互通。

    各个厂家生产的网络系统和设备,如以太网、分组交换网等,它们相互之间不能互通,不能互通的主要原因是因为它们所传送数据的基本单元(技术上称之为“帧”)的格式不同。

    IP 协议实际上是一套由软件程序组成的协议软件,它把各种不同“帧”统一转换成“IP 数据报”格式,这种转换是因特网的一个最重要的特点,使所有各种计算机都能在因特网上实现互通,即具有“开放性”的特点。正是因为有了IP 协议,因特网才得以迅速发展成为世界上最大的、开放的计算机通信网络。因此,IP 协议也可以叫做“因特网协议”。

    IP 地址(Internet Protocol Address)是指网络协议地址,又译为网际协议地址。IP 地址是 IP 协议提供的一种统一的地址格式,它为互联网上的每一个网络和每一台主机分配一个逻辑地址(非真实物理),以此来屏蔽物理地址的差异

    IP 地址是一个 32 位的二进制数,通常被分割为 4 个“ 8 位二进制数”(也就是 4 个字节)。IP 地址通常用“点分十进制”表示成(a.b.c.d)的形式,其中,a,b,c,d都是 0~255 之间的十进制整数。
    例:点分十进IP地址(100.4.5.6),实际上是32 位二进制数
    (01100100.00000100.00000101.00000110)。

    3.2 IP地址编址方式

    最初设计互联网络时,为了便于寻址以及层次化构造网络,每个IP 地址包括两个标识码(ID),即网络ID 和主机ID。同一个物理网络上的所有主机都使用同一个网络ID,网络上的一个主机(包括网络上工作站,服务器和路由器等)有一个主机ID 与其对应。

    Internet 委员会定义了5 种IP 地址类型以适合不同容量的网络,即A 类~ E 类。
    其中 A、B、C 3类(如下表格)由 InternetNIC 在全球范围内统一分配,D、E 类为特殊地址。

    类别IP地址范围最大网络数单个网段最大主机数私有IP地址范围
    A1.0.0.1-
    126.255.255.254
    126(2^7-2)1677721410.0.0.0-
    10.255.255.255
    B128.0.0.1-
    191.255.255.254
    16384(2^14)65534172.16.0.0-
    172.31.255.255
    C192.0.0.1-
    223.255.255.254
    2097152(2^21)254192.168.0.0-
    192.168.255.255

    主机数 = 2 的主机位数次方 - 2,因为主机号全为 1 时表示该网络广播地址,全为 0 时表示该网络
    的网络号,这是两个特殊地址。

    • A类IP地址

    一个 A 类 IP 地址是指, 在 IP 地址的四段号码中,第一段号码为网络号码,剩下的三段号码为本地计算机的号码。如果用二进制表示 IP 地址的话,A 类 IP 地址就由 1 字节的网络地址和 3 字节主机地址组成,网络地址的最高位必须是“0”

    A 类IP 地址中网络的标识长度为8 位,主机标识的长度为24 位,A 类网络地址数量较少,有126 个网络,每个网络可以容纳主机数达1600 多万台。(一般用在广域网

    A 类 IP 地址 地址范围 1.0.0.1 - 126.255.255.254(二进制表示为:00000001 00000000 00000000 00000001 - 01111111 11111111 11111111 11111110)。最后一个是广播地址(255)。
    A 类 IP 地址的子网掩码为 255.0.0.0,每个网络支持的最大主机数为 256 的 3 次方 - 2 = 16777214 台。

    • B类IP地址

    一个 B 类 IP 地址是指,在 IP 地址的四段号码中,前两段号码为网络号码。如果用二进制表示 IP 地址的话,B 类 IP 地址就由 2 字节的网络地址和 2 字节主机地址组成,网络地址的最高位必须是“10”

    B 类 IP地址中网络的标识长度为 16 位,主机标识的长度为 16 位,B 类网络地址适用于中等规模的网络,有 16384 个网络,每个网络所能容纳的计算机数为 6 万多台。

    B 类 IP 地址地址范围 128.0.0.1 - 191.255.255.254 (二进制表示为:10000000 00000000 00000000 00000001 - 10111111 11111111 11111111 11111110)。 最后一个是广播地址。
    B 类 IP 地址的子网掩码为 255.255.0.0,每个网络支持的最大主机数为 256 的 2 次方 - 2 = 65534台。

    • C类IP地址

    一个 C 类 IP 地址是指,在 IP 地址的四段号码中,前三段号码为网络号码,剩下的一段号码为本地计算机的号码。如果用二进制表示 IP 地址的话,C 类 IP 地址就由 3 字节的网络地址和 1 字节主机地址组成,网络地址的最高位必须是“110”

    C 类IP 地址中网络的标识长度为24 位,主机标识的长度为8 位,C类网络地址数量较多,有209 万余个网络。适用于小规模的局域网络,每个网络最多只能包含254台计算机。

    C 类 IP 地址范围 192.0.0.1-223.255.255.254 (二进制表示为: 11000000 00000000 00000000 00000001 - 11011111 11111111 11111111 11111110)。C类IP地址的子网掩码为 255.255.255.0,每个网络支持的最大主机数为 256 - 2 = 254 台。 

    • D类IP地址

    D 类 IP 地址在历史上被叫做多播地址(multicast address),即组播地址。在以太网中,多播地址命名了一组应该在这个网络中应用接收到一个分组的站点。多播地址的最高位必须是 “1110”,范围从224.0.0.0 - 239.255.255.255。

    • 特殊的网址

    每一个字节都为 0 的地址( “0.0.0.0” )对应于当前主机

    IP 地址中的(主机地址)每一个字节都为 1的 IP 地址( “255.255.255.255” )是当前子网的广播地址

    IP 地址中凡是以 “11110” 开头的 E 类 IP 地址都保留用于将来和实验使用。

    IP地址中不能以十进制 “127” 作为开头,该类地址中数字 127.0.0.1 到 127.255.255.255 用于回路测试,如:127.0.0.1可以代表本机IP地址。(ping 127.0.0.1 测试本机能否进行网络通信)

    3.3 子网掩码

    子网掩码(subnet mask)又叫网络掩码、地址掩码、子网络遮罩,它是一种用来指明一个IP 地址的哪些位标识的是主机所在的子网,以及哪些位标识的是主机的位掩码。子网掩码不能单独存在,它必须结合IP 地址一起使用(按位与)。子网掩码只有一个作用,就是将某个IP 地址划分成网络地址和主机地址两部分

    子网掩码是一个 32 位地址,用于屏蔽 IP 地址的一部分以区别网络标识和主机标识,并说明该 IP地址是在局域网上,还是在广域网上。

    子网掩码是在 IPv4 地址资源紧缺的背景下为了解决 lP 地址分配而产生的虚拟 lP 技术,通过子网掩码将 A、B、C 三类地址划分为若干子网,从而显著提高了 IP 地址的分配效率,有效解决了 IP地址资源紧张的局面。另一方面,在企业内网中为了更好地管理网络,网管人员也利用子网掩码的作用,人为地将一个较大的企业内部网络划分为更多个小规模的子网,再利用三层交换机的路由功能实现子网互联,从而有效解决了网络广播风暴和网络病毒等诸多网络管理方面的问题。

    根据 RFC950 定义,子网掩码是一个 32 位的 2 进制数,其对应网络地址的所有位都置为 1(连续的1,位数不一定是8的整数倍),对应于主机地址的所有位置都为 0。子网掩码告知路由器,地址的哪一部分是网络地址,哪一部分是主机地址,使路由器正确判断任意 IP 地址是否是本网段的,从而正确地进行路由。网络上,数据从一个地方传到另外一个地方,是依靠 IP 寻址。从逻辑上来讲,是两步的。第一步,从 IP 中找到所属的网络,好比是去找这个人是哪个小区的;第二步,再从 IP 中找到主机在这个网络中的位置,好比是在小区里面找到这个人。

    IP地址用 192.168.100.10 / 21 表示 子网掩码前面21位都是 1

    4. 端口

    端口(port),可以认为是设备与外界通讯交流的出口

    端口可分为虚拟端口和物理端口,其中虚拟端口指计算机内部或交换机路由器内的端口,不可见,是特指TCP/IP协议中的端口,是逻辑意义上的端口。例如计算机中的 80 端口(80为端口号)。

    物理端口又称为接口,是可见端口,计算机背板的 RJ45 网口,交换机路由器集线器等 RJ45 端口。电话使用 RJ11 插口也属于物理端口的范畴。

    如果把 IP 地址比作一间房子,端口就是出入这间房子的门。真正的房子只有几个门,但是一个 IP地址的端口可以有65536端口号大小为两个子节,2^16)个之多!端口是通过端口号来标记的,端口号只有整数,范围是从0 到65535(2^16-1)。

    端口号是标记计算机网络通信中属于进程的唯一编号,用来指定与哪个进程通信。

    • 周知端口(Well Known Ports)

    周知端口是众所周知的端口号,也叫知名端口、公认端口或者常用端口,范围从 0 到 1023,它们紧密绑定于一些特定的服务。例如 80 端口分配给 WWW 服务,21 端口分配给 FTP 服务,23 端口分配给Telnet服务等等。我们在 IE 的地址栏里输入一个网址的时候是不必指定端口号的,因为在默认情况下WWW 服务的端口是 “80”。网络服务是可以使用其他端口号的,如果不是默认的端口号则应该在地址栏上指定端口号,方法是在地址后面加上冒号“ : ”(半角),再加上端口号。比如使用 “8080” 作为 WWW服务的端口,则需要在地址栏里输入“网址:8080”。但是有些系统协议使用固定的端口号,它是不能被改变的,比如 139 端口专门用于 NetBIOS 与 TCP/IP 之间的通信,不能手动改变。

    • 注册端口(Registered Ports)

    注册端口号从1024 到49151,它们松散地绑定于一些服务,分配给用户进程或应用程序,这些进程主要是用户选择安装的一些应用程序,而不是已经分配好了公认端口的常用程序。这些端口在没有被服务器资源占用的时候,可以用用户端动态选用为源端口。一个应用程序可以有多个端口

    • 动态端口 / 私有端口(Dynamic Ports / Private Ports)

    动态端口的范围是从 49152 到 65535。之所以称为动态端口,是因为它一般不固定分配某种服务,而是动态分配。

    5. 网络模型

    5.1 OSI 七层参考模型

    OSI七层模型,亦称OSI(Open System Interconnection)参考模型,即开放式系统互联。参考模型是国际标准化组织(ISO)制定的一个用于计算机或通信系统间互联的标准体系,一般称为OSI 参考模型或七层模型。
    它是一个七层的、抽象的模型体,不仅包括一系列抽象的术语或概念,也包括具体的协议。

    1 物理层主要定义物理设备标准,如网线的接口类型、光纤的接口类型、各种传输介质的传输速率等。它的主要作用是传输比特流(1、0 电平大小)。这一层的数据叫做比特。

    2. 数据链路层建立逻辑连接、进行硬件地址寻址MAC)、差错校验等功能。定义了如何让格式化数据以帧为单位进行传输,以及如何让控制对物理介质的访问。将比特组合成字节进而组合成帧,用MAC地址访问介质。(涉及网卡)

    3. 网络层进行逻辑地址寻址IP),在位于不同地理位置的网络中的两个主机系统之间提供连接和路径选择。Internet的发展使得从世界各站点访问信息的用户数大大增加,而网络层正是管理这种连接的层。

    4. 传输层定义了一些传输数据的协议和端口号(WWW 端口80 等),如:TCP(传输控制协议,传输效率低,可靠性强,用于传输可靠性要求高,数据量大的数据),UDP(用户数据报协议,与TCP 特性恰恰相反,用于传输可靠性要求不高,数据量小的数据,如QQ 聊天数据就是通过这种方式传输的)。主要是将从下层接收的数据进行分段和传输,到达目的地址后再进行重组。常常把这一层数据叫做段。

    5. 会话层通过传输层(端口号:传输端口与接收端口)建立数据传输的通路。主要在你的系统之间发起会话或者接受会话请求。

    6. 表示层:数据的表示、安全、压缩。主要是进行对接收的数据进行解释、加密与解密、压缩与解压缩等(也就是把计算机能够识别的东西转换成人能够能识别的东西(如图片、声音等)。

    7. 应用层网络服务与最终用户的一个接口。这一层为用户的应用程序提供网络服务(例如电子邮件、文件传输 和 终端仿真)。

     5.2 TCP / IP 四层模型

    现在Internet(因特网)使用的主流协议族是TCP/IP 协议族,它是一个分层、多协议的通信体系。

    TCP/IP协议族是一个四层协议系统,自底而上分别是数据链路层、网络层、传输层和应用层。每一层完成不同的功能,且通过若干协议来实现,上层协议使用下层协议提供的服务

    四层体系结构的 TCP/IP 协议对七层体系结构的 OSI 进行了简化、合并在实际的应用中效率更高,成本更低。

     1. 应用层直接为应用进程提供服务
    (1)对不同种类的应用程序它们会根据自己的需要来使用应用层的不同协议,邮件传输应用使用了 SMTP 协议、万维网应用使用了 HTTP 协议、远程登录服务应用使用了有 TELNET 协议。
    (2)应用层还能加密、解密、格式化数据
    (3)应用层可以建立或解除与其他节点的联系,这样可以充分节省网络资源。

    2. 传输层运输层在整个 TCP/IP 协议中起到了中流砥柱的作用。将从下层接收的数据进行分段和传输,到达目的地址后再进行重组

    3. 网络层:在 TCP/IP 协议中网络层可以进行网络连接的建立和终止以及 IP 地址的寻找等功能。

    4. 网络接口层由于网络接口层兼并了物理层和数据链路层,网络接口层既是传输数据的物理媒介,也可以为网络层提供一条准确无误的线路

     6. 网络协议

    网络协议(Internet Protocol)是通信计算机双方必须共同遵从的一组约定(规则)。如怎么样建立连接、怎么样互相识别等。只有遵守这个约定,计算机之间才能相互通信交流。它的三要素是:语法、语义、时序它最终体现在网络上传输的数据包的格式

    协议往往分成几个层次进行定义,分层定义是为了使某一层协议的改变不影响其他层次的协议。

    分层常见协议
    应用层

    FTP协议(File Transfer Protocol 文件传输协议)、

    HTTP协议(Hyper Text Transfer Protocol 超文本传输协议)、

    NFS(Network File System 网络文件系统)、SSH

    传输层

    TCP协议(Transmission Control Protocol 传输控制协议)、

    UDP协议(User Datagram Protocol 用户数据报协议)

    网络层

    IP 协议(Internet Protocol 因特网互联协议)、

    ICMP 协议(Internet Control Message Protocol 因特网控制报文协议)、

    IGMP 协议(Internet Group Management Protocol 因特网组管理协议)

    网络接口层

    ARP协议(Address Resolution Protocol 地址解析协议)、

    RARP协议(Reverse Address Resolution Protocol 反向地址解析协议)

    6.1 UDP 协议(传输层)

    1. 源端口号:发送方端口号
    2. 目的端口号:接收方端口号
    3. 长度:UDP用户数据报的长度,最小值是8(仅有首部)
    4. 校验和:检测UDP用户数据报在传输中是否有错,有错就丢弃

    6.2 TCP 协议(传输层)

    1. 源端口号:发送方端口号
    2. 目的端口号:接收方端口号
    3. 序列号 seq:本报文段的数据的第一个字节的序号
    4. 确认号 ack:期望收到对方下一个报文段的第一个数据字节的序号
    5. 首部长度(数据偏移):TCP 报文段的数据起始处距离 TCP 报文段的起始处有多远,即首部长度。单位:32位,即以 4 字节为计算单位
    6. 保留:占 6 位,保留为今后使用,目前应置为 0
    7. 紧急 URG :此位置 1 ,表明紧急指针字段有效,它告诉系统此报文段中有紧急数据,应尽快传送
    8. 确认 ACK仅当 ACK=1 时确认号字段 ack 才有效,TCP 规定,在连接建立后所有传达的报文段都必须把 ACK 置1
    9. 推送 PSH:当两个应用进程进行交互式的通信时,有时在一端的应用进程希望在键入一个命令后立即就能够收到对方的响应。在这种情况下,TCP 就可以使用推送(push)操作,这时,发送方TCP 把 PSH 置 1,并立即创建一个报文段发送出去,接收方收到 PSH = 1 的报文段,就尽快地
    (即“推送”向前)交付给接收应用进程,而不再等到整个缓存都填满后再向上交付
    10. 复位 RST:用于复位相应的 TCP 连接
    11. 同步 SYN:仅在三次握手建立 TCP 连接时有效。当 SYN = 1 而 ACK = 0 时,表明这是一个连接请求报文段;对方若同意建立连接,则应在相应的报文段中使用 SYN = 1 和 ACK = 1。因此,SYN 置1 就表示这是一个连接请求或连接接受报文
    12. 终止 FIN:用来释放一个连接。当 FIN = 1 时,表明此报文段的发送方的数据已经发送完毕,并要求释放运输连接
    13. 窗口:指发送本报文段的一方的接收窗口(而不是自己的发送窗口)
    14. 校验和:校验和字段检验的范围包括首部和数据两部分,在计算校验和时需要加上 12 字节的伪头部
    15. 紧急指针:仅在 URG = 1 时才有意义,它指出本报文段中的紧急数据的字节数(紧急数据结束后就 是普通数据),即指出了紧急数据的末尾在报文中的位置,注意:即使窗口为零时也可发送紧急数据
    16. 选项:长度可变,最长可达 40 字节,当没有使用选项时,TCP 首部长度是 20 字节

    6.3 IP 协议(网络层)

     1. 版本:IP 协议的版本。通信双方使用过的 IP 协议的版本必须一致,目前最广泛使用的 IP 协议版本号为 4(即IPv4)。
    2. 首部长度:单位是 32 位(4 字节)。
    3. 服务类型:一般不适用,取值为 0。
    4. 总长度:指首部加上数据的总长度,单位为字节。
    5. 标识(identification):IP 软件在存储器中维持一个计数器,每产生一个数据报,计数器就加1,并将此值赋给标识字段。
    6. 标志(flag):目前只有两位有意义。
    标志字段中的最低位记为 MF。MF = 1 即表示后面“还有分片”的数据报;MF = 0 表示这已是若干数据报片中的最后一个。
    标志字段中间的一位记为 DF,意思是“不能分片”,只有当 DF = 0 时才允许分片。
    7. 片偏移:指出较长的分组在分片后,某片在源分组中的相对位置,也就是说,相对于用户数据段的起点,该片从何处开始。片偏移以 8 字节为偏移单位。
    8. 生存时间:TTL,表明是数据报在网络中的寿命,即为“跳数限制”,由发出数据报的源点设置这个字段。路由器在转发数据之前就把 TTL 值减一,当 TTL 值减为零时,就丢弃这个数据报。
    9. 协议:指出此数据报携带的数据时使用何种协议,以便使目的主机的 IP 层知道应将数据部分上交给(传输层的)哪个处理过程,常用的 ICMP(1),IGMP(2),TCP(6),UDP(17),IPv6(41)
    10. 首部校验和:只校验数据报的首部,不包括数据部分。
    11. 源地址:发送方 IP 地址
    12. 目的地址:接收方 IP 地址

    6.4 ARP协议(网络接口层)

    ARP协议根据IP地址找到MAC地址;
    RARP协议根据MAC地址找到IP地址。

    当系统连接过某一个机器的时候,会缓存IP地址对应的MAC地址(arp -a 命令),访问时会通过缓存先进行查找,否则则通过ARP协议查找(广播形式发送ARP请求报文,收到请求的主机根据报文解析做出应答)。

    ARP报文长度一共28个字节。

    1. 硬件类型:1 表示 MAC 地址
    2. 协议类型:0x800 表示 IP 地址
    3. 硬件地址长度:6
    4. 协议地址长度:4
    5. 操作:1 表示 ARP 请求,2 表示 ARP 应答,3 表示 RARP 请求,4 表示 RARP 应答

    6.5 以太网帧协议

    3. 类型:0x800表示 IP、0x806表示 ARP、0x835表示 RARP

    7. 封装 与 分用

    7.1 封装

    上层协议是如何使用下层协议提供的服务的呢?其实这是通过封装(encapsulation)实现的。

    应用程序数据在发送到物理网络上之前,将沿着协议栈从上往下依次传递。每层协议都将在上层数据的基础上加上自己的头部信息(有时还包括尾部信息),以实现该层的功能,这个过程就称为封装

    7.2 分用

    当帧到达目的主机时,将沿着协议栈自底向上依次传递。各层协议依次处理帧中本层负责的头部数据,以获取所需的信息,并最终将处理后的帧交给目标应用程序。这个过程称为分用(demultiplexing,解封装)。分用是依靠头部信息中的类型字段实现的

    8. 字节序

    现代 CPU 的累加器一次都能装载(至少)4 字节(这里考虑 32 位机),即一个整数。那么这 4 字节在内存中排列的顺序将影响它被累加器装载成的整数的值,这就是字节序问题。在各种计算机体系结构中,对于字节、字等的存储机制有所不同,因而引发了计算机通信领域中一个很重要的问题,即通信双方交流的信息单元(比特、字节、字、双字等等)应该以什么样的顺序进行传送。如果不达成一致的规则,通信双方将无法进行正确的编码/译码从而导致通信失败。

    字节序,顾名思义字节的顺序,就是大于一个字节类型的数据在内存中的存放顺序。 

    字节序分为大端字节序(Big-Endian) 和小端字节序(Little-Endian)。

    大端字节序是指一个整数的最高位字节(23 ~ 31 bit)存储在内存的低地址处,低位字节(0 ~ 7 bit)存储在内存的高地址处;

               0x 01  02  03  04 
    字节的高位   ----->  字节的低位
    内存的低位   ----->  内存的高位

                    01  02  03  04 

    小端字节序则是指整数的高位字节存储在内存的高地址处,而低位字节则存储在内存的低地址处。

               0x 01  02  03  04 
    字节的高位   ----->  字节的低位
    内存的低位   ----->  内存的高位

                    04  03  02  01

    通常用联合体 union 判断系统的字节序:

    1. // 通过代码检测当前主机的字节序
    2. #include <stdio.h>
    3. int main() {
    4. union {
    5. short value; // 2字节
    6. char bytes[sizeof(short)]; // char[2]
    7. } test;
    8. test.value = 0x0102;
    9. if((test.bytes[0] == 1) && (test.bytes[1] == 2)) {
    10. printf("大端字节序\n");
    11. } else if((test.bytes[0] == 2) && (test.bytes[1] == 1)) {
    12. printf("小端字节序\n");
    13. } else {
    14. printf("未知\n");
    15. }
    16. return 0;
    17. }

    字节序转换函数 

    当格式化的数据在两台使用不同字节序的主机之间直接传递时,接收端必然错误的解释之。

    解决问题的方法是:发送端总是把要发送的数据转换成大端字节序数据后再发送,而接收端知道对方传送过来的数据总是采用大端字节序,所以接收端可以根据自身采用的字节序决定是否对接收到的数据进行转换(小端机转换,大端机不转换)。

    网络字节顺序是 TCP/IP 中规定好的一种数据表示格式,它与具体的 CPU 类型、操作系统等无关,从而可以保证数据在不同主机之间传输时能够被正确解释,网络字节顺序采用大端排序方式。 

    BSD Socket 提供了封装好的转换接口,方便程序员使用。
    主机字节序到网络字节序的转换函数:htonshtonl
    网络字节序到主机字节序的转换函数:ntohsntohl

    h   -   host 主机,主机字节序

    to  -   转换成什么 
    n   -   network  网络字节序 
    s   -   short unsigned short,用于端口号

    l    -   long   unsigned int,用于IP地址

    1. #include <arpa/inet.h>
    2. // 转换端口
    3. uint16_t htons(uint16_t hostshort); // 主机字节序 - 网络字节序
    4. uint16_t ntohs(uint16_t netshort); // 网络字节序 - 主机字节序
    5. // 转IP
    6. uint32_t htonl(uint32_t hostlong); // 主机字节序 - 网络字节序
    7. uint32_t ntohl(uint32_t netlong); // 网络字节序 - 主机字节序
    1. #include <stdio.h>
    2. #include <arpa/inet.h>
    3. int main() {
    4. // htons 转换端口
    5. unsigned short a = 0x0102; // 小端:02 01
    6. printf("a : %x\n", a);
    7. unsigned short b = htons(a); // 大端数据:01 02 小端表示:0201
    8. printf("b : %x\n", b);
    9. printf("=======================\n");
    10. // htonl 转换IP
    11. char buf[4] = {192, 168, 1, 100};
    12. int num = *(int *)buf;
    13. int sum = htonl(num);
    14. unsigned char *p = (char *)&sum; // 大端数据:192 168 1 100 小端表示:100 1 168 192
    15. printf("%d %d %d %d\n", *p, *(p+1), *(p+2), *(p+3));
    16. printf("=======================\n");
    17. // ntohl
    18. unsigned char buf1[4] = {1, 1, 168, 192};
    19. int num1 = *(int *)buf1;
    20. int sum1 = ntohl(num1);
    21. unsigned char *p1 = (unsigned char *)&sum1;
    22. printf("%d %d %d %d\n", *p1, *(p1+1), *(p1+2), *(p1+3));
    23. // ntohs
    24. return 0;
    25. }

    9. socket 套接字

    9.1 简介

    所谓 socket(套接字),就是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。一个套接字就是网络上进程通信的一端,提供了应用层进程利用网络协议交换数据的机制。从所处的地位来讲,套接字上联应用进程,下联网络协议栈是应用程序通过网络协议进行通信的接口,是应用程序与网络协议根进行交互的接口

    socket 可以看成是两个网络应用程序进行通信时,各自通信连接中的端点,这是一个逻辑上的概念。它是网络环境中进程间通信的 API,也是可以被命名和寻址的通信端点,使用中的每一个套接字都有其类型和一个与之相连进程。通信时其中一个网络应用程序将要传输的一段信息写入它所在主机的 socket 中,该 socket 通过与网络接口卡(NIC)相连的传输介质将这段信息送到另外一台主机的 socket 中,使对方能够接收到这段信息。socket 是由 IP 地址和端口结合的,提供向应用层进程传送数据包的机制。

    socket 本身有“插座”的意思,在 Linux 环境下,用于表示进程间网络通信的特殊文件类型。本质为借助内核缓冲区形成的伪文件。既然是文件,那么理所当然的,我们可以使用文件描述符引用套接字。与管道类似的,Linux 系统将其封装成文件的目的是为了统一接口,使得读写套接字和读写文件的操作一致。区别是管道主要应用于本地进程间通信,而套接字多应用于网络进程间数据的传递。

    套接字通信分两部分:
    - 服务器端被动接受连接,一般不会主动发起连接
    - 客户端主动向服务器发起连接

    9.2 socket 地址

    socket 地址其实是一个结构体封装端口号和IP等信息
    后面的socket相关的 API 中需要使用到这个socket地址。

    9.2.1 通用 socket 地址

    socket 网络编程接口中表示 socket 地址的是结构体 sockaddr,其定义如下:

    1. #include <bits/socket.h>
    2. typedef unsigned short int sa_family_t;
    3. struct sockaddr {
    4. sa_family_t sa_family;
    5. char sa_data[14];
    6. };

    sa_family 成员是地址族类型(sa_family_t)的变量。常见的协议族(protocol family,也称 domain)和对应的地址族入下所示:

    协议族 地址族描述
    PF_UNIXAF_UNIXUNIX本地域协议族
    PF_INETAF_INETTCP/IPv4协议族
    PF_INET6AF_INET6TCP/IPv6协议族

    地址族(AF)类型通常与协议族(PF)类型对应。宏 PF_* 和 AF_* 都定义在 bits/socket.h 头文件中,且后者与前者有完全相同的值,所以二者通常混用。

    sa_data 成员用于存放 socket 地址值。但是,不同的协议族的地址值具有不同的含义和长度,如下所示:

    协议族地址值含义和长度
    PF_UNIX文件的路径名,长度可达到108字节
    PF_INET16 bit 端口号和 32 bit IPv4 地址,共 6 字节
    PF_INET616 bit 端口号,32 bit 流标识,128 bit IPv6 地址,32 bit 范围 ID,共 26 字节

    由上表可知,14 字节的 sa_data 根本无法容纳多数协议族的地址值。因此,Linux 定义了下面这个新的通用的 socket 地址结构体,这个结构体不仅提供了足够大的空间用于存放地址值,而且是内存对齐的。

    1. #include <bits/socket.h>
    2. typedef unsigned short int sa_family_t;
    3. struct sockaddr_storage{
    4. sa_family_t sa_family;
    5. unsigned long int __ss_align;
    6. char__ss_padding[ 128 - sizeof(__ss_align)];
    7. };

    使用通用socket地址的话需要将端口号和地址进行一些字节处理操作存放到结构体中,一般比较麻烦,因此socket API 提供了专用 socket 地址进行使用。

    9.2.2 专用 socket 地址

    很多网络编程函数诞生早于 IPv4 协议,那时候都使用的是 struct sockaddr 结构体,为了向前兼容,现在sockaddr 退化成了对专用 socket 地址进行(void *)转换的作用,传递一个地址给函数,至于这个函数是 sockaddr_in 还是sockaddr_in6,由地址族确定,然后函数内部再强制类型转化为所需的地址类型。

    下面是通用和专用 socket 地址结构之间的对比:

     UNIX 本地域协议族使用如下专用的 socket 地址结构体:

    1. #include <sys/un.h>
    2. struct sockaddr_un{
    3. sa_family_t sin_family;
    4. char sun_path[108];
    5. };

    TCP/IP 协议族有 sockaddr_in 和 sockaddr_in6 两个专用的 socket 地址结构体,它们分别用于IPv4 和IPv6:

    1. #include <netinet/in.h>
    2. struct sockaddr_in{
    3. sa_family_t sin_family; /*__SOCKADDR_COMMON(sin_) */
    4. in_port_t sin_port; /* Port number.*/
    5. struct in_addr sin_addr; /* Internet address.*/
    6. /* Pad to size of `struct sockaddr'.*/
    7. unsigned char sin_zero[sizeof (struct sockaddr) -__SOCKADDR_COMMON_SIZE -
    8. sizeof (in_port_t) - sizeof (struct in_addr)];
    9. };
    10. struct in_addr{
    11. in_addr_t s_addr;
    12. };
    13. struct sockaddr_in6{
    14. sa_family_t sin6_family;
    15. in_port_t sin6_port; /* Transport layer port # */
    16. uint32_t sin6_flowinfo; /* IPv6 flow information */
    17. struct in6_addr sin6_addr; /* IPv6 address */
    18. uint32_t sin6_scope_id; /* IPv6 scope-id */
    19. };
    20. typedef unsigned short uint16_t;
    21. typedef unsigned int uint32_t;
    22. typedef uint16_t in_port_t;
    23. typedef uint32_t in_addr_t;
    24. #define __SOCKADDR_COMMON_SIZE (sizeof (unsigned short int))

    所有专用 socket 地址(以及 sockaddr_storage)类型的变量在实际使用时都需要转化为通用 socket 地址类型 sockaddr(强制转化即可),因为所有 socket 编程接口使用的地址参数类型都是sockaddr。

    9.3 IP 地址转换(字符串-整数转换、字节序转换)

    通常,人们习惯用可读性好的字符串来表示 IP 地址,比如用点分十进制字符串表示 IPv4 地址,以及用十六进制字符串表示 IPv6 地址。

    但编程中我们需要先把它们转化为整数(二进制数)方能使用。
    而记录日志时则相反,我们要把整数表示的 IP 地址转化为可读的字符串。

    下面 3 个函数可用于用点分十进制字符串表示的 IPv4 地址和用网络字节序整数表示的 IPv4 地址之间的转换(已经较少使用):

    1. #include <arpa/inet.h>
    2. in_addr_t inet_addr(const char *cp);
    3. int inet_aton(const char *cp, struct in_addr *inp); // address to network
    4. char *inet_ntoa(struct in_addr in); // network to address

    下面这对更新的函数也能完成前面 3 个函数同样的功能,并且它们同时适用 IPv4 地址和 IPv6 地址:

    1. #include <arpa/inet.h>
    2. // p:点分十进制的IP字符串,n:表示network,网络字节序的整数
    3. int inet_pton(int af, const char *src, void *dst);
    4. af:地址族: AF_INET AF_INET6
    5. src:需要转换的点分十进制的IP字符串
    6. dst:转换后的结果保存在这个里面
    7. // 将网络字节序的整数,转换成点分十进制的IP地址字符串
    8. const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
    9. af:地址族: AF_INET AF_INET6
    10. src: 要转换的ip的整数的地址
    11. dst: 转换成IP地址字符串保存的地方
    12. size:第三个参数的大小(数组的大小)
    13. 返回值:返回转换后的数据的地址(字符串),和 dst 是一样的
    1. #include <stdio.h>
    2. #include <arpa/inet.h>
    3. int main() {
    4. // 创建一个ip字符串,点分十进制的IP地址字符串
    5. char buf[] = "192.168.1.4";
    6. unsigned int num = 0;
    7. // 将点分十进制的IP字符串转换成网络字节序的整数
    8. inet_pton(AF_INET, buf, &num);
    9. unsigned char * p = (unsigned char *)&num;
    10. // num = 67217600 = Bin 0000 0100 | 0000 0001 | 1010 1000 | 1100 0000
    11. // = Dec 4 | 1 | 168 | 192 内存高位->低位
    12. printf("num : %d\n", num);
    13. printf("%d %d %d %d\n", *p, *(p+1), *(p+2), *(p+3));
    14. // 将网络字节序的IP整数转换成点分十进制的IP字符串
    15. char ip[16] = "";
    16. const char * str = inet_ntop(AF_INET, &num, ip, 16);
    17. printf("str : %s\n", str);
    18. printf("ip : %s\n", str);
    19. return 0;
    20. }

    9.4 套接字函数

    1. #include <sys/types.h>
    2. #include <sys/socket.h>
    3. #include <arpa/inet.h> // 包含了这个头文件,上面两个就可以省略
    4. int socket(int domain, int type, int protocol);
    5. - 功能:创建一个套接字
    6. - 参数:
    7. - domain: 协议族
    8. AF_INET : ipv4
    9. AF_INET6 : ipv6
    10. AF_UNIX, AF_LOCAL : 本地套接字通信(进程间通信)
    11. - type: 通信过程中使用的协议类型
    12. SOCK_STREAM : 流式协议
    13. SOCK_DGRAM : 报式协议
    14. - protocol : 具体的一个协议。一般写0
    15. - SOCK_STREAM 流式协议则默认使用 TCP
    16. - SOCK_DGRAM 报式协议则默认使用 UDP
    17. - 返回值:
    18. - 成功:返回文件描述符,操作的就是内核缓冲区。
    19. - 失败:-1
    20. int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); // socket命名
    21. - 功能:绑定,将fd 和本地的IP + 端口进行绑定
    22. - 参数:
    23. - sockfd : 通过socket函数得到的文件描述符
    24. - addr : 需要绑定的socket地址,这个地址封装了ip和端口号的信息
    25. - addrlen : 第二个参数结构体占的内存大小
    26. int listen(int sockfd, int backlog); // /proc/sys/net/core/somaxconn
    27. - 功能:监听这个socket上的连接
    28. - 参数:
    29. - sockfd : 通过socket()函数得到的文件描述符
    30. - backlog : 未连接的和已经连接的和的最大值, 5
    31. int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
    32. - 功能:接收客户端连接,默认是一个阻塞的函数,阻塞等待客户端连接
    33. - 参数:
    34. - sockfd : 用于监听的文件描述符
    35. - addr : 传出参数,记录了连接成功后客户端的地址信息(ip,port)
    36. - addrlen : 指定第二个参数的对应的内存大小
    37. - 返回值:
    38. - 成功 :用于通信的文件描述符
    39. - -1 : 失败
    40. int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
    41. - 功能: 客户端连接服务器
    42. - 参数:
    43. - sockfd : 用于通信的文件描述符
    44. - addr : 客户端要连接的服务器的地址信息
    45. - addrlen : 第二个参数的内存大小
    46. - 返回值:成功 0, 失败 -1
    47. ssize_t write(int fd, const void *buf, size_t count); // (发送)写数据
    48. ssize_t read(int fd, void *buf, size_t count); // (接收)读数据

    10. socket TCP 通信

    TCP 和 UDP 都是传输层的通信协议:
    UDP:用户数据报协议,面向无连接,可以单播、多播、广播, 面向数据报,不可靠
    TCP:传输控制协议,面向连接的,仅支持单播传输,基于字节流,可靠

    UDPTCP
    是否创建连接无连接面向连接
    是否可靠不可靠可靠,无差错,不丢失,不重复
    连接对象的个数一对一、一对多、多对一、多对多仅支持一对一
    传输的方式面向数据包面向字节流
    首部开销8个字节最少20个字节
    拥塞控制、流量控制有(滑动窗口)
    适用场景

    短消息、实时应用、拥有大量Client
    (视频会议、直播、聊天)

    [ 效率高,不需要检验 ]

    可靠性要求高的应用(文件传输)

    10.1 TCP 通信流程

     服务器端(被动接受连接的角色)

    1. 创建一个用于监听的套接字(本质为伪文件、文件描述符)
    2. 将监听文件描述符和本地的 IP 和端口(服务器的地址信息)绑定
    3. 设置监听,监听的 fd 开始工作,监听客户端发起的连接请求
    4. 阻塞等待,当有客户端发起连接,解除阻塞
    5. 接受客户端的连接,得到一个用于和客户端通信的套接字(fd)
    6. 通信(发送、接收数据)
    7. 通信结束,断开连接

    客户端(主动发起连接)

    1. 创建一个用于通信的套接字(fd),客户端随机绑定一个端口
    2. 连接服务器,需要指定服务器的 IP 和端口
    3. 连接成功,进行通信(发送、接收数据)
    4. 通行结束,断开连接
    1. /* 服务器端 server.c */
    2. #include <stdio.h>
    3. #include <arpa/inet.h>
    4. #include <string.h>
    5. #include <unistd.h>
    6. #include <stdlib.h>
    7. #include <ctype.h>
    8. int main(){
    9. // 1. 创建
    10. int lfd = socket(AF_INET, SOCK_STREAM, 0);
    11. if(lfd == -1){
    12. perror("socket");
    13. exit(-1);
    14. }
    15. // 2. 绑定 fd 和 地址信息
    16. struct sockaddr_in saddr;
    17. saddr.sin_family = AF_INET;
    18. inet_pton(AF_INET, "192.168.15.128", &saddr.sin_addr.s_addr); // 通过 IP 地址转换给结构体成员赋值
    19. // saddr.sin_addr.s_addr = INADDR_ANY; // = 0 = 0.0.0.0 系统存在多个IP(网卡)时,服务端都进行通信绑定
    20. saddr.sin_port = htons(9999); // 字节序转换
    21. int ret = bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));
    22. if(ret == -1){
    23. perror("bind");
    24. exit(-1);
    25. }
    26. // 3. 监听
    27. ret = listen(lfd, 8);
    28. if(ret == -1){
    29. perror("listen");
    30. exit(-1);
    31. }
    32. // 4. 等待接受(阻塞)
    33. struct sockaddr_in caddr;
    34. socklen_t caddrLen = sizeof(caddr);
    35. int cfd = accept(lfd, (struct sockaddr*)&caddr, &caddrLen); // 返回通信文件描述符
    36. if(cfd == -1){
    37. perror("accept");
    38. exit(-1);
    39. }
    40. // 5. 输出客户端信息 IP + 端口
    41. char clientIP[16]; // 255.255.255.255\0
    42. inet_ntop(AF_INET, &caddr.sin_addr.s_addr, clientIP, sizeof(clientIP));
    43. unsigned short clientPort = ntohs(caddr.sin_port); // 端口号 字节序转换
    44. printf("client IP: %s, port: %d\n", clientIP, clientPort);
    45. // 6. 通信
    46. char rBuf[1024] = {0};
    47. while(1){
    48. int len = read(cfd, rBuf, sizeof(rBuf));
    49. if(len == -1){
    50. perror("read");
    51. exit(-1);
    52. }else if(len > 0){
    53. printf("recv client data : %s\n", rBuf);
    54. for(int i = 0; i < len; ++i) {
    55. rBuf[i] = toupper(rBuf[i]);
    56. }
    57. write(cfd, rBuf, sizeof(rBuf)); // 转换大写 回复
    58. }else if(len == 0){
    59. printf("client closed..."); // len == 0 客户端关闭
    60. break;
    61. }
    62. }
    63. close(cfd);
    64. close(lfd);
    65. return 0;
    66. }
    1. /* 客户端 client.c */
    2. #include <unistd.h>
    3. #include <string.h>
    4. #include <arpa/inet.h>
    5. #include <stdio.h>
    6. #include <stdlib.h>
    7. int main(){
    8. // 1. 创建socket文件描述符
    9. int fd = socket(AF_INET, SOCK_STREAM, 0);
    10. // 2. 连接指定服务器的IP和端口
    11. struct sockaddr_in saddr;
    12. saddr.sin_family = AF_INET;
    13. saddr.sin_port = htons(9999);
    14. inet_pton(AF_INET, "192.168.15.128", &saddr.sin_addr.s_addr);
    15. int ret = connect(fd, (struct sockaddr*)&saddr, sizeof(saddr));
    16. if(ret == -1){
    17. perror("connect");
    18. exit(-1);
    19. }
    20. // 3. 通信
    21. char * data = "hello, i am client.";
    22. int n = 10;
    23. while(n--){
    24. write(fd, data, strlen(data));
    25. char rBuf[1024] = {0};
    26. int len = read(fd, rBuf, sizeof(rBuf));
    27. if(len == -1){
    28. perror("read");
    29. exit(-1);
    30. }else if(len > 0){
    31. printf("recv server data : %s\n", rBuf);
    32. }else if(len == 0){
    33. printf("server closed...");
    34. break;
    35. }
    36. sleep(1);
    37. }
    38. // 4. 断开
    39. close(fd);
    40. return 0;
    41. }

    10.2 TCP 三次握手

    TCP 是一种面向连接单播协议,在发送数据前,通信双方必须在彼此间建立一条连接。
    所谓连接,其实是客户端和服务器的内存里保存的一份关于对方的信息,如 IP 地址、端口号等。

    TCP 可以看成是一种字节流,它会处理 IP 层或以下的层的丢包、重复以及错误问题
    在连接的建立过程中,双方需要交换一些连接的参数,这些参数可以放在 TCP 头部。

    TCP 提供了一种可靠、面向连接、字节流、传输层的服务,
    采用三次握手建立一个连接,采用四次挥手来关闭一个连接。

    关于 TCP 三次握手和四次挥手,满分回答在此https://baijiahao.baidu.com/s?id=1693383134922615393&wfr=spider&for=pc三次握手的目的是保证双方互相之间建立了连接(确保双方都能够接受和发送)
    三次握手发生在客户端发起连接的时候,当调用connect(),底层会通过 TCP 协议进行三次握手。

    •  16 位端口号(port number):告知主机报文段是来自哪里(源端口)以及传给哪个上层协议或应用程序(目的端口)的。进行 TCP 通信时,客户端通常使用系统自动选择的临时端口号。
    • 32 位序号sequence number):一次 TCP 通信(从 TCP 连接建立到断开)过程中某一个传输方向上的字节流的每个字节的编号。假设主机 A 和主机 B 进行 TCP 通信,A 发送给 B 的第一个TCP 报文段中,序号值被系统初始化为某个随机值 ISN(Initial Sequence Number,初始序号值)。那么在该传输方向上(从 A 到 B),后续的 TCP 报文段中序号值将被系统设置成 ISN 加上该报文段所携带数据的第一个字节在整个字节流中的偏移。
    • 32 位确认号acknowledgement number):用作对另一方发送来的 TCP 报文段的响应。其值是收到的 TCP 报文段的序号值 + 标志位长度(SYN,FIN) + 数据长度 。
    • 4 位头部长度(head length):标识该 TCP 头部有多少个 32 bit (4 字节)。因为 4 位最大能表示15,所以 TCP 头部最长是60 字节。
    • 6 位标志位包含如下几项:
      • URG 标志,表示紧急指针(urgent pointer)是否有效。
      • ACK 标志,表示确认号 ack 是否有效。我们称携带 ACK 标志的 TCP 报文段为确认报文段
      • PSH 标志,提示接收端应用程序应该立即从 TCP 接收缓冲区中读走数据,为接收后续数据腾出空间(如果应用程序不将接收到的数据读走,它们就会一直停留在 TCP 接收缓冲区中)。
      • RST 标志,表示要求对方重新建立连接。我们称携带 RST 标志的 TCP 报文段为复位报文段。
      • SYN 标志,表示请求建立一个连接。我们称携带 SYN 标志的 TCP 报文段为同步报文段。
      • FIN 标志,表示通知对方本端要关闭连接了。我们称携带 FIN 标志的 TCP 报文段为结束报文段。
    • 16 位窗口大小(window size):是 TCP 流量控制的一个手段。这里说的窗口,指的是接收通告窗口(Receiver Window,RWND)。它告诉对方本端的 TCP 接收缓冲区还能容纳多少字节的数据,这样对方就可以控制发送数据的速度。
    • 16 位校验和(TCP checksum):由发送端填充,接收端对 TCP 报文段执行 CRC 算法以校验TCP 报文段在传输过程中是否损坏。注意,这个校验不仅包括 TCP 头部,也包括数据部分。这也是 TCP 可靠传输的一个重要保障。
    • 16 位紧急指针(urgent pointer):是一个正的偏移量。它和序号字段的值相加表示最后一个紧急数据的下一个字节的序号。因此,确切地说,这个字段是紧急指针相对当前序号的偏移,不妨称之为紧急偏移。TCP 的紧急指针是发送端向接收端发送紧急数据的方法。

    第一次握手:
    1. 客户端将SYN标志位置为1
    2. 生成一个随机的32位的序号 seq = J 


    第二次握手:
    1. 服务器端接收客户端的连接: ACK=1
    2. 服务器会回发一个确认序号: ack = 客户端的序号 + 数据长度 + SYN/FIN(按一个字节算)
    3. 服务器端会向客户端发起连接请求: SYN = 1
    4. 服务器会生成一个随机序号:seq = K


    第三次握手:
    1.客户端应答服务器的连接请求:ACK = 1 (SYN=0)
    2.客户端回复收到了服务器端的数据:ack = 服务端的序号 + 数据长度 + SYN/FIN(按一个字节算)

    三次握手才能确保双方都能够接受和发送;
    前两次握手是带不了消息数据的,最后一次握手是可以携带数据的,不过一般不考虑复杂的情况;
    序号 seq 和确认序号 ack 能够保证信息传输的完整性和有序性。

    不合法的标志位组合:
    1、所有标志位都为0。
    2、SYN和FIN同时被置1。
    3、SYN和RST同时被置1。
    4、FIN和RST同时被置1。
    5、FIN位被置1,但ACK位没有被置1。
    6、PSH位被置1,但ACK位没有被置1。
    7、URG位被置1,但ACK位没有被置1。

    10.3 TCP 滑动窗口

    滑动窗口(Sliding window)是一种流量控制技术。
    早期的网络通信中,通信双方不会考虑网络的拥挤情况直接发送数据。由于大家不知道网络拥塞状况,同时发送数据,导致中间节点阻塞掉包,谁也发不了数据,所以就有了滑动窗口机制来解决此问题。

    滑动窗口协议是用来改善吞吐量的一种技术,即容许发送方在接收任何应答之前传送附加的包。接收方告诉发送方在某一时刻能送多少包(称窗口尺寸)。

    TCP 中采用滑动窗口来进行传输控制,滑动窗口的大小意味着接收方还有多大的缓冲区可以用于接收数据。发送方可以通过滑动窗口的大小来确定应该发送多少字节的数据。当滑动窗口为 0时,发送方一般不能再发送数据报。

    滑动窗口是 TCP 中实现诸如 ACK 确认、流量控制、拥塞控制的承载结构。

    窗口理解为缓冲区的大小;
    滑动窗口的大小会随着发送数据和接收数据而变化;
    通信的双方都有发送缓冲区和接收数据的缓冲区。

    发送缓冲区:
    白色格子:空闲的空间
    灰色格子:数据已经被发送出去了,但是还没有被接收
    紫色格子:还没有发送出去的数据


    接收缓冲区:
    白色格子:空闲的空间
    紫色格子:已经接收到的数据

            # mss: Maximum Segment Size(一条数据的最大的数据量)
            # win: 滑动窗口

            # ACK 为确认序号,应为小写字母 ack

    1. 客户端向服务器发起连接,客户单的滑动窗口是4096,一次发送的最大数据量是1460
    2. 服务器接收连接情况,告诉客户端服务器的窗口大小是6144,一次发送的最大数据量是1024
    3. 第三次握手
    4. 4-9 客户端连续给服务器发送了6k的数据,每次发送1k
    5. 第10次,服务器告诉客户端:发送的6k数据以及接收到,存储在缓冲区中,缓冲区数据已经处理了2k,窗口大小是2k
    6. 第11次,服务器告诉客户端:发送的6k数据以及接收到,存储在缓冲区中,缓冲区数据已经处理了4k,窗口大小是4k
    7. 第12次,客户端给服务器发送了1k的数据
    8. 第13次,客户端主动请求和服务器断开连接,并且给服务器发送了1k的数据
    9. 第14次,服务器回复 ACK 8194,a:同意断开连接的请求;b:告诉客户端已经接受到方才发的2k的数据;c:滑动窗口2k
    10. 第15、16次,通知客户端滑动窗口的大小
    11. 第17次,第三次挥手,服务器端给客户端发送 FIN,请求断开连接
    12. 第18次,第四次回收,客户端同意了服务器端的断开请求

     10.4 TCP 四次挥手

    四次挥手发生在断开连接的时候,在程序中当调用了close()会使用TCP协议进行四次挥手。
    客户端和服务器端都可以主动发起断开连接,谁先调用close()谁就是发起,发起方需要在最后等待2MSL时间。
    TCP是全双工的,在TCP连接的时候,采用三次握手建立的时候是双向连接的,在断开的时候需要双向断开

    第2、3次挥手不合并(需要进行四次挥手)是因为被断开连接的一方也需要主动关闭连接才行;这是由于 TCP 的半关闭(half-close)特性造成的,当收到对方的 FIN 报文时,仅仅表示主动关闭方不再发送数据,但是还能接受数据,被动关闭方是否现在关闭数据发送通道需要上层应用决定,因此 ACK 和 FIN 一般分开发送。

    第一次挥手:
    Client 将 FIN 置为1,序号 seq=u,发送给 Server,进入FIN_WAIT_1状态。

    第二次挥手
    Server 收到后,将ACK置为1,ack=u+1,响应给Client,进入CLOSE_WAIT状态。
    Client 收到响应后,进入FIN_WAIT_2状态。

    此时的 TCP 处于半关闭状态,客户端到服务端的连接释放,服务端还能发送数据。

    第三次挥手
    Server在结束所有数据传输后,将 FIN 置为1,seq=w,发送给Client,进入LAST_ACK状态。

    第四次挥手
    Client 收到后,将ACK置为1,ack=w+1,响应给Server,进入TIME_WAIT状态,等待2MSL后,进入CLOSED状态;

    Server 收到后,进入CLOSED状态。

    10.5 TCP 通信并发

    要实现TCP通信服务器处理并发的任务,使用多线程或者多进程来解决。

    10.5.1 多进程实现TCP并发通信

    1. 一个父进程,多个子进程,接受一个客户端连接,就创建一个子进程用于通信。
    2. 父进程负责等待并接受客户端的连接;
    3. 子进程:完成通信。

    服务器端程序中:
    捕捉 SIGCHLD 信号产生中断调用来回收子进程资源;
    由于软中断终止的 accept() 阻塞会产生 EINTR 错误,从而导致父进程退出,无法接收新的连接,因此在错误判断中应该单独处理。

    1. /* server.c */
    2. #include <stdio.h>
    3. #include <arpa/inet.h>
    4. #include <unistd.h>
    5. #include <stdlib.h>
    6. #include <string.h>
    7. #include <signal.h>
    8. #include <wait.h>
    9. #include <errno.h>
    10. void recyleChild(int arg) { // 软中断
    11. while(1) {
    12. int ret = waitpid(-1, NULL, WNOHANG); // 非阻塞 回收子进程
    13. if(ret == -1) {
    14. // 所有的子进程都回收了
    15. break;
    16. }else if(ret == 0) {
    17. // 还有子进程活着
    18. break;
    19. } else if(ret > 0){
    20. // 被回收了
    21. printf("子进程 %d 被回收了\n", ret);
    22. }
    23. }
    24. }
    25. int main() {
    26. struct sigaction act;
    27. act.sa_flags = 0;
    28. sigemptyset(&act.sa_mask);
    29. act.sa_handler = recyleChild;
    30. // 注册信号捕捉
    31. sigaction(SIGCHLD, &act, NULL);
    32. // 创建socket
    33. int lfd = socket(PF_INET, SOCK_STREAM, 0);
    34. if(lfd == -1){
    35. perror("socket");
    36. exit(-1);
    37. }
    38. struct sockaddr_in saddr;
    39. saddr.sin_family = AF_INET;
    40. saddr.sin_port = htons(9999);
    41. saddr.sin_addr.s_addr = INADDR_ANY;
    42. // 绑定
    43. int ret = bind(lfd,(struct sockaddr *)&saddr, sizeof(saddr));
    44. if(ret == -1) {
    45. perror("bind");
    46. exit(-1);
    47. }
    48. // 监听
    49. ret = listen(lfd, 128);
    50. if(ret == -1) {
    51. perror("listen");
    52. exit(-1);
    53. }
    54. // 不断循环等待客户端连接
    55. while(1) {
    56. struct sockaddr_in cliaddr;
    57. socklen_t len = sizeof(cliaddr);
    58. // 接受连接
    59. int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &len); // 阻塞等待
    60. if(cfd == -1) {
    61. if(errno == EINTR) { // 当有一个客户端退出,需要暂停accept阻塞,跳到中断中回收一个子进程资源
    62. continue; // 回收完后,将往accept下方代码进行执行,此时accept被打断,会产生一个软中断的错误,返回-1
    63. } // 这个错误就是 EINTR,因此需要判断
    64. perror("accept");
    65. exit(-1); // 产生接收错误,父进程退出,无法连接新的客户端,子进程仍然在通信
    66. }
    67. // 每一个连接进来,创建一个子进程跟客户端通信
    68. pid_t pid = fork();
    69. if(pid == 0) {
    70. // 子进程
    71. // 获取客户端的信息
    72. char cliIp[16];
    73. inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, cliIp, sizeof(cliIp));
    74. unsigned short cliPort = ntohs(cliaddr.sin_port);
    75. printf("client ip is : %s, prot is %d\n", cliIp, cliPort);
    76. // 接收客户端发来的数据
    77. char recvBuf[1024];
    78. while(1) {
    79. int len = read(cfd, &recvBuf, sizeof(recvBuf));
    80. if(len == -1) {
    81. perror("read");
    82. exit(-1);
    83. }else if(len > 0) {
    84. printf("recv client : %s\n", recvBuf);
    85. } else if(len == 0) {
    86. printf("client closed....\n");
    87. break;
    88. }
    89. write(cfd, recvBuf, strlen(recvBuf) + 1);
    90. }
    91. close(cfd);
    92. exit(0); // 退出当前子进程
    93. }
    94. }
    95. close(lfd);
    96. return 0;
    97. }
    1. // client.c
    2. #include <stdio.h>
    3. #include <arpa/inet.h>
    4. #include <unistd.h>
    5. #include <string.h>
    6. #include <stdlib.h>
    7. int main() {
    8. // 1.创建套接字
    9. int fd = socket(AF_INET, SOCK_STREAM, 0);
    10. if(fd == -1) {
    11. perror("socket");
    12. exit(-1);
    13. }
    14. // 2.连接服务器端
    15. struct sockaddr_in serveraddr;
    16. serveraddr.sin_family = AF_INET;
    17. inet_pton(AF_INET, "192.168.15.128", &serveraddr.sin_addr.s_addr);
    18. serveraddr.sin_port = htons(9999);
    19. int ret = connect(fd, (struct sockaddr *)&serveraddr, sizeof(serveraddr));
    20. if(ret == -1) {
    21. perror("connect");
    22. exit(-1);
    23. }
    24. // 3. 通信
    25. char recvBuf[1024];
    26. int i = 0;
    27. while(1) {
    28. sprintf(recvBuf, "data : %d\n", i++);
    29. // 给服务器端发送数据
    30. write(fd, recvBuf, strlen(recvBuf)+1);
    31. int len = read(fd, recvBuf, sizeof(recvBuf));
    32. if(len == -1) {
    33. perror("read");
    34. exit(-1);
    35. } else if(len > 0) {
    36. printf("recv server : %s\n", recvBuf);
    37. } else if(len == 0) {
    38. // 表示服务器端断开连接
    39. printf("server closed...");
    40. break;
    41. }
    42. sleep(1);
    43. }
    44. // 关闭连接
    45. close(fd);
    46. return 0;
    47. }

    10.5.2 多线程实现TCP并发通信

    11. TCP 状态转换

     TCP通信过程包括三个步骤:建立TCP连接通道(三次握手)、数据传输、断开TCP连接通道(四次挥手),其中在连接和断开过程中涉及到主机TCP状态的转换。

    TCP状态转换图详解https://www.pianshen.com/article/6579280764/

    11.1 三次握手过程的状态变化

    1.  CLOSED:起始点,在超时或者连接关闭时候进入此状态,这并不是一个真正的状态,而是这个状态图的假想起点和终点。
    2. LISTEN服务器端(监听)等待连接的状态。服务器经过 socket -> bind -> listen 函数之后进入此状态,开始监听客户端发过来的连接请求。此称为应用程序被动打开(等到客户端连接请求)。
    3. SYN_SENT:第一次握手发生阶段,客户端发起连接。客户端调用 connect,发送 SYN 给服务器端,然后进入 SYN_SENT 状态,等待服务器端确认(三次握手中的第二个报文)。如果服务器端不能连接,则直接进入CLOSED状态。
    4. SYN_RCVD:第二次握手发生阶段,跟 3 对应,这里是服务器端接收到了客户端的 SYN,此时服务器由 LISTEN 进入 SYN_RCVD 状态,同时服务器端回应一个 ACK,然后再发送一个 SYN 即 SYN + ACK 给客户端。状态图中还描绘了这样一种情况,当客户端在发送 SYN 的同时也收到服务器端的 SYN请求,即两个同时发起连接请求,那么客户端就会从 SYN_SENT 转换到 SYN_REVD 状态。
    5. ESTABLISHED:第三次握手发生阶段,客户端接收到服务器端的 ACK 包(ACK,SYN)之后,客户端进入 ESTABLISHED 状态,表明客户端这边已经准备好。但TCP 需要两端都准备好才可以进行数据传输。因此客户端也会发送一个 ACK 确认包,服务器端收到后会从 SYN_RCVD 状态转移到 ESTABLISHED 状态,表明服务器端也准备好进行数据传输了。 ESTABLISHED 也可以说是一个数据传送状态。

    11.2 四次挥手过程的状态变化

    1. FIN_WAIT_1:第一次挥手。主动方(这里以客户端执行主动关闭为例)终止连接时,发送 FIN 给对方,然后等待对方返回 ACK 。调用 close() 第一次挥手就进入此状态。
    2. CLOSE_WAIT:接收到 FIN 之后,被动方进入此状态。具体动作是接收到 FIN,同时发送 ACK。之所以叫 CLOSE_WAIT 可以理解为被动关闭的一方此时正在等待上层应用程序发出关闭连接指令。前面已经说过,TCP关闭是全双工过程,这里客户端执行了主动关闭,被动方服务器端接收到 FIN 后也需要调用 close 关闭来实现双向断开,这个 CLOSE_WAIT 就是处于这个状态,等待发送 FIN,发送了FIN 则进入 LAST_ACK 状态。
    3. FIN_WAIT_2主动端(这里是客户端)先执行主动关闭发送 FIN,然后接收到被动方返回的 ACK 后进入此状态。
    4. LAST_ACK被动方(服务器端)发起 close()  关闭请求,具体动作是发送 FIN 给对方,由 CLOSE_WAIT 进入 LAST_ACK 状态;在接收到 ACK 时进入 CLOSED 状态。
    5. CLOSING:两边同时发起关闭请求(发送 FIN)时,会由 FIN_WAIT_1 进入 CLOSING 状态,等待对方返回 ACK 后进行 TIME_WAIT 状态。
    6. TIME_WAIT:从状态变迁图会看到,四次挥手操作最后都会经过这样一个状态,在经过 2MSL 后进入CLOSED 状态。共有三个状态会进入该状态:
      (1)FIN_WAIT_2 进入:这是非同时关闭的情况,主动方在完成自身发起的主动关闭请求后,接收到了对方发送过来的 FIN,然后回应 ACK,则进入 TIME_WAIT 状态。
      (2)FIN_WAIT_1 进入:发起关闭后,发送了 FIN,等待 ACK 的时候,正好被动方也发起关闭请求,发送了FIN,这时主动方接收到了 ACK + FIN,然后发送ACK(对对方FIN的回应),(跳过 FIN_WAIT_2)直接进入 TIME_WAIT 状态。
      (3)由 CLOSING 进入:同时发起关闭情况下,主动端发送了 FIN 后,先接收到 FIN,并发送 ACK 给被动方,此时为 CLOSING 状态,当主动端接收到 ACK 后,进入 TIME_WAIT 状态;

    11.3 2MSL

    MSL(Maximum Segment Lifetime,最长数据包生命周期)是任何 IP 数据报能够在因特网中存活的最长时间。2MSL 就是一个发送和一个回复所需的最大时间。

    如果直到 2MSL,主动关闭方都没有再次收到 FIN,那么就推断 ACK 已经被成功接收,则结束TCP连接;
    如果在 2MSL 内再次收到 FIN,那么主动方会重发 ACK 并再次等待 2MSL

    主动断开连接的一方,最后进入一个 TIME_WAIT 状态,等待 2MSL (1~4分钟) 后进入 CLOSED 状态。其作用主要有两个:

    (1)可靠的实现 TCP 全双工的终止

            如果主动关闭方最后发送的 ACK (应答)包因为某种原因丢失了,那么被动方总是会会重新发送FIN,这样因为主动方有 TIME_WAIT 的存在,会重新发送 ACK 给被动方,但是如果没有 TIME_WAIT 这个状态,那么无论被动方是否收到 ACK 包,主动方都已经关闭连接了,此时被动方重新发送 FIN,主动方给回的就不是 ACK 包,而是 RST (连接复位)包,从而使被动方没有完成正常的 4 次挥手,不友好,而且有可能造成数据包丢失。
            所以要实现TCP全双工连接的正常终止(两方都关闭连接),必须处理终止过程中四个分节任何一个分节的丢失情况,那么主动关闭连接的主动端必须维持TIME_WAIT状态,最后一个回应 ACK 的是主动执行关闭的那端。从变迁图可以看出,如果没有TIME_WAIT状态,我们将没有任何机制来保证最后一个 ACK 能够正常到达。前面的FIN,ACK正常到达均有相应的状态对应。

    (2)允许老的重复的TCP数据包在网络中消逝

            如果目前的通信双方都已经调用了 close(),都到达了CLOSED状态,而没有TIME_WAIT状态时,会出现这样一种情况,现在有一个新的连接被建立起来,使用的IP地址和端口和这个先前到达了 CLOSED 状态的完全相同,假定原先的连接中还有数据报残存在网络之中,这样新的连接建立以后传输的数据极有可能就是原先的连接的数据报。
            为了防止这一点,TCP不允许从处于TIME_WAIT 状态的 socket 建立一个连接。处于TIME_WAIT状态的 socket 在等待了两倍的 MSL 时间之后,将会转变为CLOSED状态。这里TIME_WAIT状态持续的时间是2MSL,足以让这两个方向上残存的数据包被丢弃(最长是2MSL)。通过实施这个规则,我们就能保证每成功建立一个TCP连接时,来自该连接先前化身的老的重复分组都已经在网络中消逝了。

  • 相关阅读:
    Linux-gitlab常用命令
    认识车载神器-Android Auto
    WSL2安装ubuntu及修改安装位置,设置Ubuntu开机启动链接ssh服务
    技能大赛训练题:登录安全加固
    .Net Framework、.Net Core和.Net Standard的区别
    测试开发路线大纲与总结
    全面整理!机器学习常用的回归预测模型
    Altium Designer21使用说明-更新中
    c++ qt五子棋联网对战游戏
    docker安装运行环境相关的容器
  • 原文地址:https://blog.csdn.net/qq_19887221/article/details/125425259