8、Linux 网络编程
时间:2022-09-27 14:30:05
1. 网络结构模式
1.1 C/S 结构
客户机 - 服务器,即 Client - Server(C/S)结构。C/S 结构通常采用两层结构。
服务器负责数据管理,客户机负责与用户完成。
客户机是因特网上访问他人信息的机器,服务器是提供信息供他人访问的计算机。
客户机通过局域网连接到服务器,接受用户的请求,并通过网络向服务器提出请求,操作数据库。
服务器接受客户机的要求,向客户机提交数据,客户机计算数据,并向用户呈现结果。服务器还提供完美的安全保护和数据完整性处理,允许多客户同时访问服务器,这对服务器的硬件处理数据能力提出了很高的要求。
在C/S应用程序分为服务器和客户机两部分。
服务器部分是由多个用户共享的信息和功能后台服务,例如,控制共享数据库的操作;
客户机部分为用户所专有,负责执行前台功能,在错误提示、在线帮助等方面都有很强的功能,并且可以在子程序之间自由切换。
- 优点
- 能充分发挥客户端 PC 的处理能力,很多工作可以在客户端处理后提交给服务器,所以 C/S 结构客户端响应速度快;
- 操作界面美观,形式多样,能充分满足客户自身的需求个性化要求;
- C/S 结构管理信息系统具有较强的事务处理能力,能够实现业务流程复杂;
- 安全性高,C/S 一般来说,对于相对固定的用户群体,程序更注重流程。它可以多层次验证权限,提供更安全的访问模式,具有很强的信息安全控制能力。一般采用高度机密的信息系统 C/S 结构适宜。
- 缺点
- 需要安装客户端专用客户端软件。首先,它涉及到安装的工作量。其次,任何计算机出现问题,如病毒和硬件损坏,都需要安装或维护。当系统软件升级时,每台客户机器都需要重新安装成本的维护和升级非常高;
- 一般对客户端的操作系统也有限制,不能跨平台。
1.2 B/S 结构
- 简介
B/S 结构(Browser/Server,浏览器/服务器模式),是 WEB 网络结构模式兴起后,WEB浏览器是客户端最重要的应用软件。
这种模式统一客户端(浏览器),将系统功能实现的核心部分集中在服务器上,简化了系统的开发、维护和使用。
只要在客户机上安装浏览器,比如 Firefox 或 Internet Explorer,服务器安装 SQL Server、Oracle、MySQL 等数据库。浏览器通过 Web Server 与数据库进行数据交互。
- 优点
B/S 架构最大的优点是整体拥有成本低,维护方便, 分布性强,开发简单,无需安装任何特殊软件即可在任何地方操作。客户端零维护,系统扩展非常容易,只要有一台可以上网的电脑。
- 缺点
- 通信开销大、系统和数据安全性较低;
- 个性特征明显降低,不能满足个性化功能要求;
- 协议通常是固定的:http / https,大数据量的文件无法传输
- 客户端服务器端的交互是请求-响应模式,通常动态刷新页面,响应速度显著降低。
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.实际上是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地址范围 |
---|---|---|---|---|
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地址
一个 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 判断系统的字节序:
// 通过代码检测当前主机的字节序
#include
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
// 转换端口
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
#include
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;
}
9. socket 套接字
9.1 简介
一个进程进行网络传输的过程可以分为两个阶段:
- 第一阶段:在用户态下,将需要用到的数据封装,调用相应的系统函数如write/read,陷入内核态;
- 第二阶段:在内核态下,内核根据拷贝至内核空间的数据依次运行TCP/IP协议栈,对应用数据进行封装(TCP/IP、IP、帧),而后将数据帧传给网卡,由网卡完成物理传输。
而在两个阶段之间进行衔接的则是一个被称为套接字——Socket的东西。简单来说,Socket获取内核态下 TCP/IP 协议栈所需要的必备信息,并为应用层的数据传输提供相应的接口。就是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。
一个套接字就是网络上进程通信的一端,提供了应用层进程利用网络协议交换数据的机制。从所处的地位来讲,套接字上联应用进程,下联网络协议栈,是应用程序通过网络协议进行通信的接口,是应用程序与网络协议根进行交互的接口。
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,其定义如下:
#include
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
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 地址进行使用。
9.2.2 专用 socket 地址
很多网络编程函数诞生早于 IPv4 协议,那时候都使用的是 struct sockaddr 结构体,为了向前兼容,现在sockaddr 退化成了对专用 socket 地址进行(void *)转换的作用,传递一个地址给函数,至于这个函数是 sockaddr_in 还是sockaddr_in6,由地址族确定,然后函数内部再强制类型转化为所需的地址类型。
下面是通用和专用 socket 地址结构之间的对比:
UNIX 本地域协议族使用如下专用的 socket 地址结构体:
#include
struct sockaddr_un{
sa_family_t sin_family;
char sun_path[108];
};
TCP/IP 协议族有 sockaddr_in 和 sockaddr_in6 两个专用的 socket 地址结构体,它们分别用于IPv4 和IPv6:
#include
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。
9.3 IP 地址转换(字符串-整数转换、字节序转换)
通常,人们习惯用可读性好的字符串来表示 IP 地址,比如用点分十进制字符串表示 IPv4 地址,以及用十六进制字符串表示 IPv6 地址。
但编程中我们需要先把它们转化为整数(二进制数)方能使用。
而记录日志时则相反,我们要把整数表示的 IP 地址转化为可读的字符串。
下面 3 个函数可用于用点分十进制字符串表示的 IPv4 地址和用网络字节序整数表示的 IPv4 地址之间的转换(已经较少使用):
#include
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
// 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
#include
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;
}
9.4 套接字函数
#include
#include
#include // 包含了这个头文件,上面两个就可以省略
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 send (int __fd, const void *__buf, size_t __n, int __flags); // flags 一般为0
ssize_t read(int fd, void *buf, size_t count); // (接收)读数据
ssize_t recv (int __fd, void *__buf, size_t __n, int __flags); // flags 一般为0
10. socket TCP 通信
TCP 和 UDP 都是传输层的通信协议:
UDP:用户数据报协议,面向无连接,可以单播、多播、广播, 面向数据报,不可靠
TCP:传输控制协议,面向连接的,仅支持单播传输,基于字节流,可靠
UDP | TCP | |
---|---|---|
是否创建连接 | 无连接 | 面向连接 |
是否可靠 | 不可靠 | 可靠,无差错,不丢失,不重复 |
连接对象的个数 | 一对一、一对多、多对一、多对多 | 仅支持一对一 |
传输的方式 | 面向数据包 | 面向字节流 |
首部开销 | 8个字节 | 最少20个字节 |
拥塞控制、流量控制 | 无 | 有(滑动窗口) |
适用场景 | 短消息、实时应用、拥有大量Client [ 效率高,不需要检验 ] |
可靠性要求高的应用(文件传输) |
10.1 TCP 通信流程
服务器端(被动接受连接的角色):
- 创建一个用于监听的套接字(本质为伪文件、文件描述符)
- 将监听文件描述符和本地的 IP 和端口(服务器的地址信息)绑定
- 设置监听,监听的 fd 开始工作,监听客户端发起的连接请求
- 阻塞等待,当有客户端发起连接,解除阻塞
- 接受客户端的连接,得到一个用于和客户端通信的套接字(fd)
- 通信(发送、接收数据)
- 通信结束,断开连接
客户端(主动发起连接):
- 创建一个用于通信的套接字(fd),客户端随机绑定一个端口
- 连接服务器,需要指定服务器的 IP 和端口
- 连接成功,进行通信(发送、接收数据)
- 通行结束,断开连接
/* 服务器端 server.c */
#include
#include
#include
#include
#include
#include
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
#include
#include
#include
#include
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){