客户机 - 服务器,即 Client - Server(C/S)结构。C/S 结构通常采取两层结构。
服务器负责数据的管理,客户机负责完成与用户的交互任务。
客户机是因特网上访问别人信息的机器,服务器则是提供信息供人访问的计算机。
客户机通过局域网与服务器相连,接受用户的请求,并通过网络向服务器提出请求,对数据库进行操作。
服务器接受客户机的请求,将数据提交给客户机,客户机将数据进行计算并将结果呈现给用户。服务器还要提供完善安全保护及对数据完整性的处理等操作,并允许多个客户机同时访问服务器,这就对服务器的硬件处理数据能力提出了很高的要求。
在C/S结构中,应用程序分为两部分:服务器部分和客户机部分。
服务器部分是多个用户共享的信息与功能,执行后台服务,如控制共享数据库的操作等;
客户机部分为用户所专有,负责执行前台功能,在出错提示、在线帮助等方面都有强大的功能,并且可以在子程序间自由切换。
B/S 结构(Browser/Server,浏览器/服务器模式),是 WEB 兴起后的一种网络结构模式,WEB浏览器是客户端最主要的应用软件。
这种模式统一了客户端(浏览器),将系统功能实现的核心部分集中到服务器上,简化了系统的开发、维护和使用。
客户机上只要安装一个浏览器,如 Firefox 或 Internet Explorer,服务器安装 SQL Server、Oracle、MySQL 等数据库。浏览器通过 Web Server 同数据库进行数据交互。
B/S 架构最大的优点是总体拥有成本低、维护方便、 分布性强、开发简单,可以不用安装任何专门的软件就能实现在任何地方进行操作,客户端零维护,系统的扩展非常容易,只要有一台能上网的电脑就能使用。
网卡是一块被设计用来允许计算机在计算机网络上进行通讯的计算机硬件,又称为网络适配器或网络接口卡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 命令查看)
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)。
最初设计互联网络时,为了便于寻址以及层次化构造网络,每个IP 地址包括两个标识码(ID),即网络ID 和主机ID。同一个物理网络上的所有主机都使用同一个网络ID,网络上的一个主机(包括网络上工作站,服务器和路由器等)有一个主机ID 与其对应。
Internet 委员会定义了5 种IP 地址类型以适合不同容量的网络,即A 类~ E 类。
其中 A、B、C 3类(如下表格)由 InternetNIC 在全球范围内统一分配,D、E 类为特殊地址。
类别 | IP地址范围 | 最大网络数 | 单个网段最大主机数 | 私有IP地址范围 |
---|---|---|---|---|
A | 1.0.0.1- 126.255.255.254 | 126(2^7-2) | 16777214 | 10.0.0.0- 10.255.255.255 |
B | 128.0.0.1- 191.255.255.254 | 16384(2^14) | 65534 | 172.16.0.0- 172.31.255.255 |
C | 192.0.0.1- 223.255.255.254 | 2097152(2^21) | 254 | 192.168.0.0- 192.168.255.255 |
主机数 = 2 的主机位数次方 - 2,因为主机号全为 1 时表示该网络广播地址,全为 0 时表示该网络
的网络号,这是两个特殊地址。
一个 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 地址是指,在 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 地址是指,在 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 地址在历史上被叫做多播地址(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 测试本机能否进行网络通信)
子网掩码(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。
端口(port),可以认为是设备与外界通讯交流的出口。
端口可分为虚拟端口和物理端口,其中虚拟端口指计算机内部或交换机路由器内的端口,不可见,是特指TCP/IP协议中的端口,是逻辑意义上的端口。例如计算机中的 80 端口(80为端口号)。
物理端口又称为接口,是可见端口,计算机背板的 RJ45 网口,交换机路由器集线器等 RJ45 端口。电话使用 RJ11 插口也属于物理端口的范畴。
如果把 IP 地址比作一间房子,端口就是出入这间房子的门。真正的房子只有几个门,但是一个 IP地址的端口可以有65536(端口号大小为两个子节,2^16)个之多!端口是通过端口号来标记的,端口号只有整数,范围是从0 到65535(2^16-1)。
端口号是标记计算机网络通信中属于进程的唯一编号,用来指定与哪个进程通信。
周知端口是众所周知的端口号,也叫知名端口、公认端口或者常用端口,范围从 0 到 1023,它们紧密绑定于一些特定的服务。例如 80 端口分配给 WWW 服务,21 端口分配给 FTP 服务,23 端口分配给Telnet服务等等。我们在 IE 的地址栏里输入一个网址的时候是不必指定端口号的,因为在默认情况下WWW 服务的端口是 “80”。网络服务是可以使用其他端口号的,如果不是默认的端口号则应该在地址栏上指定端口号,方法是在地址后面加上冒号“ : ”(半角),再加上端口号。比如使用 “8080” 作为 WWW服务的端口,则需要在地址栏里输入“网址:8080”。但是有些系统协议使用固定的端口号,它是不能被改变的,比如 139 端口专门用于 NetBIOS 与 TCP/IP 之间的通信,不能手动改变。
注册端口号从1024 到49151,它们松散地绑定于一些服务,分配给用户进程或应用程序,这些进程主要是用户选择安装的一些应用程序,而不是已经分配好了公认端口的常用程序。这些端口在没有被服务器资源占用的时候,可以用用户端动态选用为源端口。一个应用程序可以有多个端口。
动态端口的范围是从 49152 到 65535。之所以称为动态端口,是因为它一般不固定分配某种服务,而是动态分配。
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. 应用层:网络服务与最终用户的一个接口。这一层为用户的应用程序提供网络服务(例如电子邮件、文件传输 和 终端仿真)。
现在Internet(因特网)使用的主流协议族是TCP/IP 协议族,它是一个分层、多协议的通信体系。
TCP/IP协议族是一个四层协议系统,自底而上分别是数据链路层、网络层、传输层和应用层。每一层完成不同的功能,且通过若干协议来实现,上层协议使用下层协议提供的服务。
四层体系结构的 TCP/IP 协议对七层体系结构的 OSI 进行了简化、合并在实际的应用中效率更高,成本更低。
1. 应用层:直接为应用进程提供服务。
(1)对不同种类的应用程序它们会根据自己的需要来使用应用层的不同协议,邮件传输应用使用了 SMTP 协议、万维网应用使用了 HTTP 协议、远程登录服务应用使用了有 TELNET 协议。
(2)应用层还能加密、解密、格式化数据。
(3)应用层可以建立或解除与其他节点的联系,这样可以充分节省网络资源。
2. 传输层:运输层在整个 TCP/IP 协议中起到了中流砥柱的作用。将从下层接收的数据进行分段和传输,到达目的地址后再进行重组。
3. 网络层:在 TCP/IP 协议中网络层可以进行网络连接的建立和终止以及 IP 地址的寻找等功能。
4. 网络接口层:由于网络接口层兼并了物理层和数据链路层,网络接口层既是传输数据的物理媒介,也可以为网络层提供一条准确无误的线路。
网络协议(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 反向地址解析协议) |
1. 源端口号:发送方端口号
2. 目的端口号:接收方端口号
3. 长度:UDP用户数据报的长度,最小值是8(仅有首部)
4. 校验和:检测UDP用户数据报在传输中是否有错,有错就丢弃
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 字节
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 地址
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 应答
3. 类型:0x800表示 IP、0x806表示 ARP、0x835表示 RARP。
上层协议是如何使用下层协议提供的服务的呢?其实这是通过封装(encapsulation)实现的。
应用程序数据在发送到物理网络上之前,将沿着协议栈从上往下依次传递。每层协议都将在上层数据的基础上加上自己的头部信息(有时还包括尾部信息),以实现该层的功能,这个过程就称为封装。
当帧到达目的主机时,将沿着协议栈自底向上依次传递。各层协议依次处理帧中本层负责的头部数据,以获取所需的信息,并最终将处理后的帧交给目标应用程序。这个过程称为分用(demultiplexing,解封装)。分用是依靠头部信息中的类型字段实现的。
现代 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 判断系统的字节序:
- // 通过代码检测当前主机的字节序
- #include <stdio.h>
-
- int main() {
- union {
- short value; // 2字节
- char bytes[sizeof(short)]; // char[2]
- } test;
-
- test.value = 0x0102;
- if((test.bytes[0] == 1) && (test.bytes[1] == 2)) {
- printf("大端字节序\n");
- } else if((test.bytes[0] == 2) && (test.bytes[1] == 1)) {
- printf("小端字节序\n");
- } else {
- printf("未知\n");
- }
- return 0;
- }
当格式化的数据在两台使用不同字节序的主机之间直接传递时,接收端必然错误的解释之。
解决问题的方法是:发送端总是把要发送的数据转换成大端字节序数据后再发送,而接收端知道对方传送过来的数据总是采用大端字节序,所以接收端可以根据自身采用的字节序决定是否对接收到的数据进行转换(小端机转换,大端机不转换)。
网络字节顺序是 TCP/IP 中规定好的一种数据表示格式,它与具体的 CPU 类型、操作系统等无关,从而可以保证数据在不同主机之间传输时能够被正确解释,网络字节顺序采用大端排序方式。
BSD Socket 提供了封装好的转换接口,方便程序员使用。
主机字节序到网络字节序的转换函数:htons、htonl;
网络字节序到主机字节序的转换函数:ntohs、ntohl。
h - host 主机,主机字节序
to - 转换成什么
n - network 网络字节序
s - short unsigned short,用于端口号l - long unsigned int,用于IP地址
- #include <arpa/inet.h>
- // 转换端口
- uint16_t htons(uint16_t hostshort); // 主机字节序 - 网络字节序
- uint16_t ntohs(uint16_t netshort); // 网络字节序 - 主机字节序
-
- // 转IP
- uint32_t htonl(uint32_t hostlong); // 主机字节序 - 网络字节序
- uint32_t ntohl(uint32_t netlong); // 网络字节序 - 主机字节序
- #include <stdio.h>
- #include <arpa/inet.h>
-
- int main() {
-
- // htons 转换端口
- unsigned short a = 0x0102; // 小端:02 01
- printf("a : %x\n", a);
- unsigned short b = htons(a); // 大端数据:01 02 小端表示:0201
- printf("b : %x\n", b);
-
- printf("=======================\n");
-
- // htonl 转换IP
- char buf[4] = {192, 168, 1, 100};
- int num = *(int *)buf;
- int sum = htonl(num);
- unsigned char *p = (char *)∑ // 大端数据:192 168 1 100 小端表示:100 1 168 192
- printf("%d %d %d %d\n", *p, *(p+1), *(p+2), *(p+3));
-
- printf("=======================\n");
-
- // ntohl
- unsigned char buf1[4] = {1, 1, 168, 192};
- int num1 = *(int *)buf1;
- int sum1 = ntohl(num1);
- unsigned char *p1 = (unsigned char *)&sum1;
- printf("%d %d %d %d\n", *p1, *(p1+1), *(p1+2), *(p1+3));
-
- // ntohs
-
- return 0;
- }
所谓 socket(套接字),就是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。一个套接字就是网络上进程通信的一端,提供了应用层进程利用网络协议交换数据的机制。从所处的地位来讲,套接字上联应用进程,下联网络协议栈,是应用程序通过网络协议进行通信的接口,是应用程序与网络协议根进行交互的接口。
socket 可以看成是两个网络应用程序进行通信时,各自通信连接中的端点,这是一个逻辑上的概念。它是网络环境中进程间通信的 API,也是可以被命名和寻址的通信端点,使用中的每一个套接字都有其类型和一个与之相连进程。通信时其中一个网络应用程序将要传输的一段信息写入它所在主机的 socket 中,该 socket 通过与网络接口卡(NIC)相连的传输介质将这段信息送到另外一台主机的 socket 中,使对方能够接收到这段信息。socket 是由 IP 地址和端口结合的,提供向应用层进程传送数据包的机制。
socket 本身有“插座”的意思,在 Linux 环境下,用于表示进程间网络通信的特殊文件类型。本质为借助内核缓冲区形成的伪文件。既然是文件,那么理所当然的,我们可以使用文件描述符引用套接字。与管道类似的,Linux 系统将其封装成文件的目的是为了统一接口,使得读写套接字和读写文件的操作一致。区别是管道主要应用于本地进程间通信,而套接字多应用于网络进程间数据的传递。
套接字通信分两部分:
- 服务器端:被动接受连接,一般不会主动发起连接
- 客户端:主动向服务器发起连接
socket 地址其实是一个结构体,封装端口号和IP等信息。
后面的socket相关的 API 中需要使用到这个socket地址。
socket 网络编程接口中表示 socket 地址的是结构体 sockaddr,其定义如下:
- #include <bits/socket.h>
-
- typedef unsigned short int sa_family_t;
-
- struct sockaddr {
- sa_family_t sa_family;
- char sa_data[14];
- };
sa_family 成员是地址族类型(sa_family_t)的变量。常见的协议族(protocol family,也称 domain)和对应的地址族入下所示:
协议族 | 地址族 | 描述 |
---|---|---|
PF_UNIX | AF_UNIX | UNIX本地域协议族 |
PF_INET | AF_INET | TCP/IPv4协议族 |
PF_INET6 | AF_INET6 | TCP/IPv6协议族 |
地址族(AF)类型通常与协议族(PF)类型对应。宏 PF_* 和 AF_* 都定义在 bits/socket.h 头文件中,且后者与前者有完全相同的值,所以二者通常混用。
sa_data 成员用于存放 socket 地址值。但是,不同的协议族的地址值具有不同的含义和长度,如下所示:
协议族 | 地址值含义和长度 |
---|---|
PF_UNIX | 文件的路径名,长度可达到108字节 |
PF_INET | 16 bit 端口号和 32 bit IPv4 地址,共 6 字节 |
PF_INET6 | 16 bit 端口号,32 bit 流标识,128 bit IPv6 地址,32 bit 范围 ID,共 26 字节 |
由上表可知,14 字节的 sa_data 根本无法容纳多数协议族的地址值。因此,Linux 定义了下面这个新的通用的 socket 地址结构体,这个结构体不仅提供了足够大的空间用于存放地址值,而且是内存对齐的。
- #include <bits/socket.h>
-
- typedef unsigned short int sa_family_t;
-
- struct sockaddr_storage{
- sa_family_t sa_family;
- unsigned long int __ss_align;
- char__ss_padding[ 128 - sizeof(__ss_align)];
- };
使用通用socket地址的话需要将端口号和地址进行一些字节处理操作存放到结构体中,一般比较麻烦,因此socket API 提供了专用 socket 地址进行使用。
很多网络编程函数诞生早于 IPv4 协议,那时候都使用的是 struct sockaddr 结构体,为了向前兼容,现在sockaddr 退化成了对专用 socket 地址进行(void *)转换的作用,传递一个地址给函数,至于这个函数是 sockaddr_in 还是sockaddr_in6,由地址族确定,然后函数内部再强制类型转化为所需的地址类型。
下面是通用和专用 socket 地址结构之间的对比:
UNIX 本地域协议族使用如下专用的 socket 地址结构体:
- #include <sys/un.h>
- struct sockaddr_un{
- sa_family_t sin_family;
- char sun_path[108];
- };
TCP/IP 协议族有 sockaddr_in 和 sockaddr_in6 两个专用的 socket 地址结构体,它们分别用于IPv4 和IPv6:
- #include <netinet/in.h>
- struct sockaddr_in{
- sa_family_t sin_family; /*__SOCKADDR_COMMON(sin_) */
- in_port_t sin_port; /* Port number.*/
- struct in_addr sin_addr; /* Internet address.*/
- /* Pad to size of `struct sockaddr'.*/
- unsigned char sin_zero[sizeof (struct sockaddr) -__SOCKADDR_COMMON_SIZE -
- sizeof (in_port_t) - sizeof (struct in_addr)];
- };
-
- struct in_addr{
- in_addr_t s_addr;
- };
-
- struct sockaddr_in6{
- sa_family_t sin6_family;
- in_port_t sin6_port; /* Transport layer port # */
- uint32_t sin6_flowinfo; /* IPv6 flow information */
- struct in6_addr sin6_addr; /* IPv6 address */
- uint32_t sin6_scope_id; /* IPv6 scope-id */
- };
-
- typedef unsigned short uint16_t;
- typedef unsigned int uint32_t;
- typedef uint16_t in_port_t;
- typedef uint32_t in_addr_t;
- #define __SOCKADDR_COMMON_SIZE (sizeof (unsigned short int))
所有专用 socket 地址(以及 sockaddr_storage)类型的变量在实际使用时都需要转化为通用 socket 地址类型 sockaddr(强制转化即可),因为所有 socket 编程接口使用的地址参数类型都是sockaddr。
通常,人们习惯用可读性好的字符串来表示 IP 地址,比如用点分十进制字符串表示 IPv4 地址,以及用十六进制字符串表示 IPv6 地址。
但编程中我们需要先把它们转化为整数(二进制数)方能使用。
而记录日志时则相反,我们要把整数表示的 IP 地址转化为可读的字符串。
下面 3 个函数可用于用点分十进制字符串表示的 IPv4 地址和用网络字节序整数表示的 IPv4 地址之间的转换(已经较少使用):
- #include <arpa/inet.h>
- in_addr_t inet_addr(const char *cp);
- int inet_aton(const char *cp, struct in_addr *inp); // address to network
- char *inet_ntoa(struct in_addr in); // network to address
下面这对更新的函数也能完成前面 3 个函数同样的功能,并且它们同时适用 IPv4 地址和 IPv6 地址:
- #include <arpa/inet.h>
- // p:点分十进制的IP字符串,n:表示network,网络字节序的整数
- int inet_pton(int af, const char *src, void *dst);
- af:地址族: AF_INET AF_INET6
- src:需要转换的点分十进制的IP字符串
- dst:转换后的结果保存在这个里面
-
- // 将网络字节序的整数,转换成点分十进制的IP地址字符串
- const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
- af:地址族: AF_INET AF_INET6
- src: 要转换的ip的整数的地址
- dst: 转换成IP地址字符串保存的地方
- size:第三个参数的大小(数组的大小)
- 返回值:返回转换后的数据的地址(字符串),和 dst 是一样的
- #include <stdio.h>
- #include <arpa/inet.h>
-
- int main() {
-
- // 创建一个ip字符串,点分十进制的IP地址字符串
- char buf[] = "192.168.1.4";
- unsigned int num = 0;
-
- // 将点分十进制的IP字符串转换成网络字节序的整数
- inet_pton(AF_INET, buf, &num);
- unsigned char * p = (unsigned char *)#
- // num = 67217600 = Bin 0000 0100 | 0000 0001 | 1010 1000 | 1100 0000
- // = Dec 4 | 1 | 168 | 192 内存高位->低位
- printf("num : %d\n", num);
- printf("%d %d %d %d\n", *p, *(p+1), *(p+2), *(p+3));
-
- // 将网络字节序的IP整数转换成点分十进制的IP字符串
- char ip[16] = "";
- const char * str = inet_ntop(AF_INET, &num, ip, 16);
- printf("str : %s\n", str);
- printf("ip : %s\n", str);
-
- return 0;
- }
- #include <sys/types.h>
- #include <sys/socket.h>
- #include <arpa/inet.h> // 包含了这个头文件,上面两个就可以省略
-
- int socket(int domain, int type, int protocol);
- - 功能:创建一个套接字
- - 参数:
- - domain: 协议族
- AF_INET : ipv4
- AF_INET6 : ipv6
- AF_UNIX, AF_LOCAL : 本地套接字通信(进程间通信)
- - type: 通信过程中使用的协议类型
- SOCK_STREAM : 流式协议
- SOCK_DGRAM : 报式协议
- - protocol : 具体的一个协议。一般写0
- - SOCK_STREAM 流式协议则默认使用 TCP
- - SOCK_DGRAM 报式协议则默认使用 UDP
- - 返回值:
- - 成功:返回文件描述符,操作的就是内核缓冲区。
- - 失败:-1
-
- int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); // socket命名
- - 功能:绑定,将fd 和本地的IP + 端口进行绑定
- - 参数:
- - sockfd : 通过socket函数得到的文件描述符
- - addr : 需要绑定的socket地址,这个地址封装了ip和端口号的信息
- - addrlen : 第二个参数结构体占的内存大小
-
- int listen(int sockfd, int backlog); // /proc/sys/net/core/somaxconn
- - 功能:监听这个socket上的连接
- - 参数:
- - sockfd : 通过socket()函数得到的文件描述符
- - backlog : 未连接的和已经连接的和的最大值, 5
-
- int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
- - 功能:接收客户端连接,默认是一个阻塞的函数,阻塞等待客户端连接
- - 参数:
- - sockfd : 用于监听的文件描述符
- - addr : 传出参数,记录了连接成功后客户端的地址信息(ip,port)
- - addrlen : 指定第二个参数的对应的内存大小
- - 返回值:
- - 成功 :用于通信的文件描述符
- - -1 : 失败
-
- int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- - 功能: 客户端连接服务器
- - 参数:
- - sockfd : 用于通信的文件描述符
- - addr : 客户端要连接的服务器的地址信息
- - addrlen : 第二个参数的内存大小
- - 返回值:成功 0, 失败 -1
-
- ssize_t write(int fd, const void *buf, size_t count); // (发送)写数据
-
- ssize_t read(int fd, void *buf, size_t count); // (接收)读数据
TCP 和 UDP 都是传输层的通信协议:
UDP:用户数据报协议,面向无连接,可以单播、多播、广播, 面向数据报,不可靠
TCP:传输控制协议,面向连接的,仅支持单播传输,基于字节流,可靠
UDP | TCP | |
---|---|---|
是否创建连接 | 无连接 | 面向连接 |
是否可靠 | 不可靠 | 可靠,无差错,不丢失,不重复 |
连接对象的个数 | 一对一、一对多、多对一、多对多 | 仅支持一对一 |
传输的方式 | 面向数据包 | 面向字节流 |
首部开销 | 8个字节 | 最少20个字节 |
拥塞控制、流量控制 | 无 | 有(滑动窗口) |
适用场景 | 短消息、实时应用、拥有大量Client [ 效率高,不需要检验 ] | 可靠性要求高的应用(文件传输) |
服务器端(被动接受连接的角色):
客户端(主动发起连接):
- /* 服务器端 server.c */
- #include <stdio.h>
- #include <arpa/inet.h>
- #include <string.h>
- #include <unistd.h>
- #include <stdlib.h>
- #include <ctype.h>
-
- int main(){
-
- // 1. 创建
- int lfd = socket(AF_INET, SOCK_STREAM, 0);
- if(lfd == -1){
- perror("socket");
- exit(-1);
- }
-
- // 2. 绑定 fd 和 地址信息
- struct sockaddr_in saddr;
- saddr.sin_family = AF_INET;
- inet_pton(AF_INET, "192.168.15.128", &saddr.sin_addr.s_addr); // 通过 IP 地址转换给结构体成员赋值
- // saddr.sin_addr.s_addr = INADDR_ANY; // = 0 = 0.0.0.0 系统存在多个IP(网卡)时,服务端都进行通信绑定
- saddr.sin_port = htons(9999); // 字节序转换
- int ret = bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));
- if(ret == -1){
- perror("bind");
- exit(-1);
- }
-
- // 3. 监听
- ret = listen(lfd, 8);
- if(ret == -1){
- perror("listen");
- exit(-1);
- }
-
- // 4. 等待接受(阻塞)
- struct sockaddr_in caddr;
- socklen_t caddrLen = sizeof(caddr);
- int cfd = accept(lfd, (struct sockaddr*)&caddr, &caddrLen); // 返回通信文件描述符
- if(cfd == -1){
- perror("accept");
- exit(-1);
- }
-
- // 5. 输出客户端信息 IP + 端口
- char clientIP[16]; // 255.255.255.255\0
- inet_ntop(AF_INET, &caddr.sin_addr.s_addr, clientIP, sizeof(clientIP));
- unsigned short clientPort = ntohs(caddr.sin_port); // 端口号 字节序转换
- printf("client IP: %s, port: %d\n", clientIP, clientPort);
-
- // 6. 通信
- char rBuf[1024] = {0};
- while(1){
- int len = read(cfd, rBuf, sizeof(rBuf));
- if(len == -1){
- perror("read");
- exit(-1);
- }else if(len > 0){
- printf("recv client data : %s\n", rBuf);
- for(int i = 0; i < len; ++i) {
- rBuf[i] = toupper(rBuf[i]);
- }
- write(cfd, rBuf, sizeof(rBuf)); // 转换大写 回复
- }else if(len == 0){
- printf("client closed..."); // len == 0 客户端关闭
- break;
- }
- }
-
- close(cfd);
- close(lfd);
- return 0;
- }
- /* 客户端 client.c */
- #include <unistd.h>
- #include <string.h>
- #include <arpa/inet.h>
- #include <stdio.h>
- #include <stdlib.h>
- int main(){
-
- // 1. 创建socket文件描述符
- int fd = socket(AF_INET, SOCK_STREAM, 0);
-
- // 2. 连接指定服务器的IP和端口
- struct sockaddr_in saddr;
- saddr.sin_family = AF_INET;
- saddr.sin_port = htons(9999);
- inet_pton(AF_INET, "192.168.15.128", &saddr.sin_addr.s_addr);
- int ret = connect(fd, (struct sockaddr*)&saddr, sizeof(saddr));
- if(ret == -1){
- perror("connect");
- exit(-1);
- }
-
- // 3. 通信
- char * data = "hello, i am client.";
- int n = 10;
- while(n--){
- write(fd, data, strlen(data));
- char rBuf[1024] = {0};
- int len = read(fd, rBuf, sizeof(rBuf));
- if(len == -1){
- perror("read");
- exit(-1);
- }else if(len > 0){
- printf("recv server data : %s\n", rBuf);
- }else if(len == 0){
- printf("server closed...");
- break;
- }
- sleep(1);
- }
-
- // 4. 断开
- close(fd);
- return 0;
- }
TCP 是一种面向连接的单播协议,在发送数据前,通信双方必须在彼此间建立一条连接。
所谓连接,其实是客户端和服务器的内存里保存的一份关于对方的信息,如 IP 地址、端口号等。
TCP 可以看成是一种字节流,它会处理 IP 层或以下的层的丢包、重复以及错误问题。
在连接的建立过程中,双方需要交换一些连接的参数,这些参数可以放在 TCP 头部。
TCP 提供了一种可靠、面向连接、字节流、传输层的服务,
采用三次握手建立一个连接,采用四次挥手来关闭一个连接。
关于 TCP 三次握手和四次挥手,满分回答在此https://baijiahao.baidu.com/s?id=1693383134922615393&wfr=spider&for=pc三次握手的目的是保证双方互相之间建立了连接(确保双方都能够接受和发送)。
三次握手发生在客户端发起连接的时候,当调用connect(),底层会通过 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。
滑动窗口(Sliding window)是一种流量控制技术。
早期的网络通信中,通信双方不会考虑网络的拥挤情况直接发送数据。由于大家不知道网络拥塞状况,同时发送数据,导致中间节点阻塞掉包,谁也发不了数据,所以就有了滑动窗口机制来解决此问题。
滑动窗口协议是用来改善吞吐量的一种技术,即容许发送方在接收任何应答之前传送附加的包。接收方告诉发送方在某一时刻能送多少包(称窗口尺寸)。
TCP 中采用滑动窗口来进行传输控制,滑动窗口的大小意味着接收方还有多大的缓冲区可以用于接收数据。发送方可以通过滑动窗口的大小来确定应该发送多少字节的数据。当滑动窗口为 0时,发送方一般不能再发送数据报。
滑动窗口是 TCP 中实现诸如 ACK 确认、流量控制、拥塞控制的承载结构。
窗口理解为缓冲区的大小;
滑动窗口的大小会随着发送数据和接收数据而变化;
通信的双方都有发送缓冲区和接收数据的缓冲区。发送缓冲区:
白色格子:空闲的空间
灰色格子:数据已经被发送出去了,但是还没有被接收
紫色格子:还没有发送出去的数据
接收缓冲区:
白色格子:空闲的空间
紫色格子:已经接收到的数据
# mss: Maximum Segment Size(一条数据的最大的数据量)
# win: 滑动窗口# ACK 为确认序号,应为小写字母 ack
- 客户端向服务器发起连接,客户单的滑动窗口是4096,一次发送的最大数据量是1460
- 服务器接收连接情况,告诉客户端服务器的窗口大小是6144,一次发送的最大数据量是1024
- 第三次握手
- 4-9 客户端连续给服务器发送了6k的数据,每次发送1k
- 第10次,服务器告诉客户端:发送的6k数据以及接收到,存储在缓冲区中,缓冲区数据已经处理了2k,窗口大小是2k
- 第11次,服务器告诉客户端:发送的6k数据以及接收到,存储在缓冲区中,缓冲区数据已经处理了4k,窗口大小是4k
- 第12次,客户端给服务器发送了1k的数据
- 第13次,客户端主动请求和服务器断开连接,并且给服务器发送了1k的数据
- 第14次,服务器回复 ACK 8194,a:同意断开连接的请求;b:告诉客户端已经接受到方才发的2k的数据;c:滑动窗口2k
- 第15、16次,通知客户端滑动窗口的大小
- 第17次,第三次挥手,服务器端给客户端发送 FIN,请求断开连接
- 第18次,第四次回收,客户端同意了服务器端的断开请求
四次挥手发生在断开连接的时候,在程序中当调用了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状态。
要实现TCP通信服务器处理并发的任务,使用多线程或者多进程来解决。
1. 一个父进程,多个子进程,接受一个客户端连接,就创建一个子进程用于通信。
2. 父进程负责等待并接受客户端的连接;
3. 子进程:完成通信。
服务器端程序中:
捕捉 SIGCHLD 信号产生中断调用来回收子进程资源;
由于软中断终止的 accept() 阻塞会产生 EINTR 错误,从而导致父进程退出,无法接收新的连接,因此在错误判断中应该单独处理。
- /* server.c */
- #include <stdio.h>
- #include <arpa/inet.h>
- #include <unistd.h>
- #include <stdlib.h>
- #include <string.h>
- #include <signal.h>
- #include <wait.h>
- #include <errno.h>
-
- void recyleChild(int arg) { // 软中断
- while(1) {
- int ret = waitpid(-1, NULL, WNOHANG); // 非阻塞 回收子进程
- if(ret == -1) {
- // 所有的子进程都回收了
- break;
- }else if(ret == 0) {
- // 还有子进程活着
- break;
- } else if(ret > 0){
- // 被回收了
- printf("子进程 %d 被回收了\n", ret);
- }
- }
- }
-
- int main() {
- struct sigaction act;
- act.sa_flags = 0;
- sigemptyset(&act.sa_mask);
- act.sa_handler = recyleChild;
- // 注册信号捕捉
- sigaction(SIGCHLD, &act, NULL);
- // 创建socket
- int lfd = socket(PF_INET, SOCK_STREAM, 0);
- if(lfd == -1){
- perror("socket");
- exit(-1);
- }
-
- struct sockaddr_in saddr;
- saddr.sin_family = AF_INET;
- saddr.sin_port = htons(9999);
- saddr.sin_addr.s_addr = INADDR_ANY;
- // 绑定
- int ret = bind(lfd,(struct sockaddr *)&saddr, sizeof(saddr));
- if(ret == -1) {
- perror("bind");
- exit(-1);
- }
- // 监听
- ret = listen(lfd, 128);
- if(ret == -1) {
- perror("listen");
- exit(-1);
- }
- // 不断循环等待客户端连接
- while(1) {
- struct sockaddr_in cliaddr;
- socklen_t len = sizeof(cliaddr);
- // 接受连接
- int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &len); // 阻塞等待
- if(cfd == -1) {
- if(errno == EINTR) { // 当有一个客户端退出,需要暂停accept阻塞,跳到中断中回收一个子进程资源
- continue; // 回收完后,将往accept下方代码进行执行,此时accept被打断,会产生一个软中断的错误,返回-1
- } // 这个错误就是 EINTR,因此需要判断
- perror("accept");
- exit(-1); // 产生接收错误,父进程退出,无法连接新的客户端,子进程仍然在通信
- }
- // 每一个连接进来,创建一个子进程跟客户端通信
- pid_t pid = fork();
- if(pid == 0) {
- // 子进程
- // 获取客户端的信息
- char cliIp[16];
- inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, cliIp, sizeof(cliIp));
- unsigned short cliPort = ntohs(cliaddr.sin_port);
- printf("client ip is : %s, prot is %d\n", cliIp, cliPort);
-
- // 接收客户端发来的数据
- char recvBuf[1024];
- while(1) {
- int len = read(cfd, &recvBuf, sizeof(recvBuf));
- if(len == -1) {
- perror("read");
- exit(-1);
- }else if(len > 0) {
- printf("recv client : %s\n", recvBuf);
- } else if(len == 0) {
- printf("client closed....\n");
- break;
- }
- write(cfd, recvBuf, strlen(recvBuf) + 1);
- }
- close(cfd);
- exit(0); // 退出当前子进程
- }
- }
- close(lfd);
- return 0;
- }
- // client.c
- #include <stdio.h>
- #include <arpa/inet.h>
- #include <unistd.h>
- #include <string.h>
- #include <stdlib.h>
-
- int main() {
-
- // 1.创建套接字
- int fd = socket(AF_INET, SOCK_STREAM, 0);
- if(fd == -1) {
- perror("socket");
- exit(-1);
- }
-
- // 2.连接服务器端
- struct sockaddr_in serveraddr;
- serveraddr.sin_family = AF_INET;
- inet_pton(AF_INET, "192.168.15.128", &serveraddr.sin_addr.s_addr);
- serveraddr.sin_port = htons(9999);
- int ret = connect(fd, (struct sockaddr *)&serveraddr, sizeof(serveraddr));
- if(ret == -1) {
- perror("connect");
- exit(-1);
- }
-
- // 3. 通信
- char recvBuf[1024];
- int i = 0;
- while(1) {
- sprintf(recvBuf, "data : %d\n", i++);
- // 给服务器端发送数据
- write(fd, recvBuf, strlen(recvBuf)+1);
- int len = read(fd, recvBuf, sizeof(recvBuf));
- if(len == -1) {
- perror("read");
- exit(-1);
- } else if(len > 0) {
- printf("recv server : %s\n", recvBuf);
- } else if(len == 0) {
- // 表示服务器端断开连接
- printf("server closed...");
- break;
- }
- sleep(1);
- }
-
- // 关闭连接
- close(fd);
- return 0;
- }
TCP通信过程包括三个步骤:建立TCP连接通道(三次握手)、数据传输、断开TCP连接通道(四次挥手),其中在连接和断开过程中涉及到主机TCP状态的转换。
TCP状态转换图详解https://www.pianshen.com/article/6579280764/
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连接时,来自该连接先前化身的老的重复分组都已经在网络中消逝了。