第四章 Linux网络编程

4.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. 对客户端的操作系统一般也会有限制,不能够跨平台。

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. 客户端服务器端的交互是请求-响应模式,通常动态刷新页面,响应速度明显降低。

4.2 4.3 MAC地址,IP地址,端口

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 地址就如同身份证上的身份证号码,具有唯一性。

3. IP 地址

简介

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 地址编址方式

最初设计互联网络时,为了便于寻址以及层次化构造网络,每个 IP 地址包括两个标识码(ID),即网络ID 和主机 ID。同一个物理网络上的所有主机都使用同一个网络 ID,网络上的一个主机(包括网络上工作站,服务器和路由器等)有一个主机 ID 与其对应。Internet 委员会定义了 5 种 IP 地址类型以适合不同容量的网络,即 A 类~ E 类。


其中 A、B、C 3类(如下表格)由 InternetNIC 在全球范围内统一分配,D、E 类为特殊地址。

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 0000000000000001 - 01111111 11111111 11111111 11111110)。最后一个是广播地址。
        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 0000000000000001 - 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 0000000000000001 - 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地址。

子网掩码

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

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

4. 端口

简介

        端口” 是英文 port 的意译,可以认为是设备与外界通讯交流的出口。端口可分为虚拟端口和物理端口,其中虚拟端口指计算机内部或交换机路由器内的端口,不可见,是特指TCP/IP协议中的端口,是逻辑意义上的端口。例如计算机中的 80 端口、21 端口、23 端口等。物理端口又称为接
口,是可见端口,计算机背板的 RJ45 网口,交换机路由器集线器等 RJ45 端口。电话使用 RJ11 插口也属于物理端口的范畴。
        如果把 IP 地址比作一间房子,端口就是出入这间房子的门。真正的房子只有几个门,但是一个 IP地址的端口可以有 65536(即:2^16)个之多!端口是通过端口号来标记的,端口号只有整数,范围是从 0 到65535(2^16-1)。

端口类型

        1.周知端口(Well Known Ports)
周知端口是众所周知的端口号,也叫知名端口、公认端口或者常用端口,范围从 0 到 1023,它们紧密绑定于一些特定的服务。例如 80 端口分配给 WWW 服务,21 端口分配给 FTP 服务,23 端口分配给Telnet服务等等。我们在 IE 的地址栏里输入一个网址的时候是不必指定端口号的,因为在默认情况下WWW 服务的端口是 “80”。网络服务是可以使用其他端口号的,如果不是默认的端口号则应该在地址栏上指定端口号,方法是在地址后面加上冒号“:”(半角),再加上端口号。比如使用 “8080” 作为 WWW服务的端口,则需要在地址栏里输入“网址:8080”。但是有些系统协议使用固定的端口号,它是不能被改变的,比如 139 端口专门用于 NetBIOS 与 TCP/IP 之间的通信,不能手动改变。
        2.注册端口(Registered Ports)
端口号从 1024 到 49151,它们松散地绑定于一些服务,分配给用户进程或应用程序,这些进程主要是用户选择安装的一些应用程序,而不是已经分配好了公认端口的常用程序。这些端口在没有被服务器资源占用的时候,可以用用户端动态选用为源端口。
        3.动态端口 / 私有端口(Dynamic Ports / Private Ports)
动态端口的范围是从 49152 到 65535。之所以称为动态端口,是因为它一般不固定分配某种服务,而是动态分配。

4.4 网络模型

OSI 七层参考模型

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

        1. 物理层:主要定义物理设备标准,如网线的接口类型、光纤的接口类型、各种传输介质的传输速率等。它的主要作用是传输比特流(就是由1、0转化为电流强弱来进行传输,到达目的地后再转化为1、0,也就是我们常说的数模转换与模数转换)。这一层的数据叫做比特。
        2. 数据链路层:建立逻辑连接、进行硬件地址寻址、差错校验等功能。定义了如何让格式化数据以帧为单位进行传输,以及如何让控制对物理介质的访问。将比特组合成字节进而组合成帧,用MAC地址访问介质。
        3. 网络层:进行逻辑地址寻址,在位于不同地理位置的网络中的两个主机系统之间提供连接和路径选择。Internet的发展使得从世界各站点访问信息的用户数大大增加,而网络层正是管理这种连接的层。
        4. 传输层:定义了一些传输数据的协议和端口号( WWW 端口 80 等),如:TCP(传输控制协议,传输效率低,可靠性强,用于传输可靠性要求高,数据量大的数据),UDP(用户数据报协议,与TCP 特性恰恰相反,用于传输可靠性要求不高,数据量小的数据,如 QQ 聊天数据就是通过这种方式传输的)。 主要是将从下层接收的数据进行分段和传输,到达目的地址后再进行重组。常常把这一层数据叫做段。
        5. 会话层:通过传输层(端口号:传输端口与接收端口)建立数据传输的通路。主要在你的系统之间发起会话或者接受会话请求。
        6. 表示层:数据的表示、安全、压缩。主要是进行对接收的数据进行解释、加密与解密、压缩与解压缩等(也就是把计算机能够识别的东西转换成人能够能识别的东西(如图片、声音等)。
        7. 应用层:网络服务与最终用户的一个接口。这一层为用户的应用程序(例如电子邮件、文件传输和终端仿真)提供网络服务。 

TCP/IP 四层模型

简介

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

        TCP/IP 协议在一定程度上参考了 OSI 的体系结构。OSI 模型共有七层,从下到上分别是物理层、数据链路层、网络层、传输层、会话层、表示层和应用层。但是这显然是有些复杂的,所以在 TCP/IP 协议中,它们被简化为了四个层次。
        (1)应用层、表示层、会话层三个层次提供的服务相差不是很大,所以在 TCP/IP 协议中,它们被合并为应用层一个层次。
        (2)由于传输层和网络层在网络协议中的地位十分重要,所以在 TCP/IP 协议中它们被作为独立的两个层次。
        (3)因为数据链路层和物理层的内容相差不多,所以在 TCP/IP 协议中它们被归并在网络接口层一个层次里。只有四层体系结构的 TCP/IP 协议,与有七层体系结构的 OSI 相比要简单了不少,也正是这样,TCP/IP 协议在实际的应用中效率更高,成本更低。

四层介绍

         1. 应用层:应用层是 TCP/IP 协议的第一层,是直接为应用进程提供服务的。
(1)对不同种类的应用程序它们会根据自己的需要来使用应用层的不同协议,邮件传输应用使用
了 SMTP 协议、万维网应用使用了 HTTP 协议、远程登录服务应用使用了有 TELNET 协议。
(2)应用层还能加密、解密、格式化数据。
(3)应用层可以建立或解除与其他节点的联系,这样可以充分节省网络资源。
        2. 传输层:作为 TCP/IP 协议的第二层,运输层在整个 TCP/IP 协议中起到了中流砥柱的作用。且在运输层中, TCP 和 UDP 也同样起到了中流砥柱的作用。
        3. 网络层:网络层在 TCP/IP 协议中的位于第三层。在 TCP/IP 协议中网络层可以进行网络连接的建立和终止以及 IP 地址的寻找等功能。
        4. 网络接口层:在 TCP/IP 协议中,网络接口层位于第四层。由于网络接口层兼并了物理层和数据链路层所以,网络接口层既是传输数据的物理媒介,也可以为网络层提供一条准确无误的线路。

4.5 协议

简介

        协议,网络协议的简称,网络协议是通信计算机双方必须共同遵从的一组约定。如怎么样建立连接、怎么样互相识别等。只有遵守这个约定,计算机之间才能相互通信交流。它的三要素是:语法、语义、时序。
        为了使数据在网络上从源到达目的,网络通信的参与方必须遵循相同的规则,这套规则称为协议(protocol),它最终体现为在网络上传输的数据包的格式。
协议往往分成几个层次进行定义,分层定义是为了使某一层协议的改变不影响其他层次的协议。

常见协议

        应用层常见的协议有:FTP协议(File Transfer Protocol 文件传输协议)、HTTP协议(Hyper TextTransfer Protocol 超文本传输协议)、NFS(Network File System 网络文件系统)。
        传输层常见协议有:TCP协议(Transmission Control Protocol 传输控制协议)、UDP协议(UserDatagram Protocol 用户数据报协议)。
        网络层常见协议有:IP 协议(Internet Protocol 因特网互联协议)、ICMP 协议(Internet ControlMessage Protocol 因特网控制报文协议)、IGMP 协议(Internet Group Management Protocol 因特网组管理协议)。
        网络接口层常见协议有:ARP协议(Address Resolution Protocol 地址解析协议)、RARP协议(Reverse Address Resolution Protocol 反向地址解析协议)。

UDP协议

TCP协议


1. 源端口号:发送方端口号
2. 目的端口号:接收方端口号
3. 序列号:本报文段的数据的第一个字节的序号
4. 确认序号:期望收到对方下一个报文段的第一个数据字节的序号
5. 首部长度(数据偏移):TCP 报文段的数据起始处距离 TCP 报文段的起始处有多远,即首部长
度。单位:32位,即以 4 字节为计算单位
6. 保留:占 6 位,保留为今后使用,目前应置为 0
7. 紧急 URG :此位置 1 ,表明紧急指针字段有效,它告诉系统此报文段中有紧急数据,应尽快传送
8. 确认 ACK:仅当 ACK=1 时确认号字段才有效,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 就表示这是一个连接请求或连接接受报文。

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 地址

以太网帧协议

 

ARP协议

 

 

4.6 4.7 网络通信过程

 

4.8 socket介绍 

1. socket 介绍

        所谓 socket(套接字),就是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。一个套接字就是网络上进程通信的一端,提供了应用层进程利用网络协议交换数据的机制。从所处的地位来讲,套接字上联应用进程,下联网络协议栈,是应用程序通过网络协议进行通信的接口,是应用程序与网络协议根进行交互的接口。
        socket 可以看成是两个网络应用程序进行通信时,各自通信连接中的端点,这是一个逻辑上的概念。它是网络环境中进程间通信的 API,也是可以被命名和寻址的通信端点,使用中的每一个套接字都有其类型和一个与之相连进程。通信时其中一个网络应用程序将要传输的一段信息写入它所在主机的 socket 中,该 socket 通过与网络接口卡(NIC)相连的传输介质将这段信息送到另外一台主机的 socket 中,使对方能够接收到这段信息。socket 是由 IP 地址和端口结合的,提供向应用层进程传送数据包的机制。
        socket 本身有“插座”的意思,在 Linux 环境下,用于表示进程间网络通信的特殊文件类型。本质为内核借助缓冲区形成的伪文件。既然是文件,那么理所当然的,我们可以使用文件描述符引用套接字。与管道类似的,Linux 系统将其封装成文件的目的是为了统一接口,使得读写套接字和读写文件的操作一致。区别是管道主要应用于本地进程间通信,而套接字多应用于网络进程间数据的传递。

2. 字节序

简介

        现代 CPU 的累加器一次都能装载(至少)4 字节(这里考虑 32 位机),即一个整数。那么这 4字节在内存中排列的顺序将影响它被累加器装载成的整数的值,这就是字节序问题。在各种计算机体系结构中,对于字节、字等的存储机制有所不同,因而引发了计算机通信领域中一个很重要的问题,即通信双方交流的信息单元(比特、字节、字、双字等等)应该以什么样的顺序进行传送。如果不达成一致的规则,通信双方将无法进行正确的编码/译码从而导致通信失败。
        字节序,顾名思义字节的顺序,就是大于一个字节类型的数据在内存中的存放顺序(一个字节的数据当然就无需谈顺序的问题了)。
        字节序分为大端字节序(Big-Endian) 和小端字节序(Little-Endian)。大端字节序是指一个整数的最高位字节(23 ~ 31 bit)存储在内存的低地址处,低位字节(0 ~ 7 bit)存储在内存的高地址处;小端字节序则是指整数的高位字节存储在内存的高地址处,而低位字节则存储在内存的低地址处。

字节序举例

/*
    字节序:字节在内存中存储的顺序。
*/

//通过代码检测当前主机的字节序

#include <stdio.h>

int main(){

    union{
        short value; //2字节
        char bytes[sizeof(short)]; //2字节
    }test;

    test.value = 0X0102;
    if(test.bytes[0] == 1 && test.bytes[1] == 2){
        printf("大端字节序\n");
    }
    else{
        printf("小段字节序\n");
    }

    return 0;
}

 4.10 字节序转换函数

/*
    网络通信时,需要将主机字节序转换为网络字节序(大端),
    另一端收到数据后根据情况将网络字节序转换为主机字节序。

    #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;
    unsigned short b = htons(a);
    printf("a = %x, b = %x\n", a, b);

    printf("=========================\n");

    //htonl 转换IP
    char buf[4] = {192, 168, 1, 100};
    int num = *(int *)buf;
    int sum = htonl(num);

    unsigned char *p = (char *)&sum;
    printf("%d.%d.%d.%d\n", *p, *(p + 1), *(p + 2), *(p + 3));
    printf("==========================\n");

    //ntohs


    //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));
    return 0;
}

4.11 socket地址

  

4.12 ip地址转换函数 

/*
    #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字符串,点分十进制的字符串
    char buf[] = "192.168.1.4";
    unsigned int num = 0;
    //将点分十进制的ip字符串,转化为网络字节序的整数。
    inet_pton(AF_INET, buf, &num); 
    unsigned char *p = (unsigned char *)&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", ip);
    return 0;
}

 4.13 TCP通信流程

 

 4.14 socket函数

 4.15 TCP通信实现(服务器端)

//TCP 通信的服务端

#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

int main(){

    //1.创建socket(用于监听的套接字)
    int lfd = socket(AF_INET, SOCK_STREAM, 0);

    if(lfd == -1){
        perror("socket");
        exit(-1);
    }
    
    //2.绑定
    struct sockaddr_in saddr;
    saddr.sin_family = AF_INET;
    //inet_pton(AF_INET, "192.168.106.133", saddr.sin_addr.s_addr);
    saddr.sin_addr.s_addr = INADDR_ANY; //0.0.0.0服务端才可以这样写
    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 clientaddr;
    int len = sizeof(clientaddr);
    int cfd = accept(lfd, (struct sockaddr*)&clientaddr, &len);
    
    if(cfd == -1){
        perror("accpet");
        exit(-1);
    }

    //输出客户端的信息
    char clientIP[16];
    inet_ntop(AF_INET, &clientaddr.sin_addr.s_addr, clientIP, sizeof(clientIP));
    unsigned short clientPort = ntohs(clientaddr.sin_port);
    printf("client ip is %s, port is %d\n", clientIP, clientPort);

    //5.通信
    char recvBuf[1024] = {0};
    while(1){
        //获取客户端的数据
        int num = read(cfd, recvBuf, sizeof(recvBuf));
        if(num == -1){
            perror("read");
            exit(-1);
        }
        else if(num > 0){
            printf("recv client data : %s\n", recvBuf);
        }
        else if(num == 0){
            //表示客户端断开连接
            printf("client closed...\n");
            break;
        }
        //给客户端发送数据
        char *data = "hello, i am server";
        write(cfd, data, strlen(data));
        sleep(1);
    }

    //关闭文件描述符
    close(lfd);
    close(cfd);

    return 0;
}

4.16 TCP通信实现(客户端)

//TCP通信客户端

#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.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.106.133", &serveraddr.sin_addr.s_addr);
    serveraddr.sin_port = htons(9999);
    int ret = connect(fd,(struct sockaddr *)&serveraddr, sizeof(serveraddr));
    
    if(ret == -1){
        perror("connet");
        exit(-1);
    }

    //3.通信
    char recvBuf[1024] = {0};

    while(1){
        //给服务器发送数据
        char *data = "hello, i am client";
        write(fd, data, strlen(data));
        sleep(1);
        //读取服务器的数据
        int len = read(fd, recvBuf, sizeof(recvBuf));
        if(len == -1){
            perror("read");
            exit(-1);
        }
        else if(len > 0){
            printf("recv server data : %s\n", recvBuf);
        }
        else if(len == 0){
            //表示客户端断开连接
            printf("server closed...\n");
            break;
        }
    }
    //关闭连接
    close(fd);


    return 0;
}

 

 4.17 TCP三次握手

 

4.18 滑动窗口

4.19 TCP四次挥手

4.20 4.21 多进程实现并发服务器

#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <errno.h>
#include <wait.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 serveraddr;
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_port = htons(9999);
    serveraddr.sin_addr.s_addr = INADDR_ANY;
    int ret = bind(lfd, (struct sockaddr*)&serveraddr, sizeof(serveraddr));
    if(ret == - 1){
        perror("bind");
        exit(-1);
    }

    //监听
    ret = listen(lfd, 5);
    if(ret == -1){
        perror("listen");
        exit(-1);
    }

    //不断循环等待客户端连接
    while(1){

        //接受连接
        struct sockaddr_in clientaddr;
        int len = sizeof(clientaddr);
        int cfd = accept(lfd, (struct sockaddr*)&clientaddr, &len);
        if(cfd == -1){
            if(errno == EINTR) //这样设置可以继续重新连接
                continue;
            perror("accept");
            exit(-1);
        }

        //每一个连接进来,创建一个子进程跟客户端通信
        pid_t pid = fork();
        if(pid == 0){
            //子进程
            //获取客户端信息
            char clientIP[16];
            inet_ntop(AF_INET, &clientaddr.sin_addr.s_addr, clientIP, sizeof(clientIP));
            unsigned short clientPort = ntohs(clientaddr.sin_port);
            printf("client ip is %s, prot is %d\n", clientIP, clientPort);

            //接受客户端发来的数据
            char recvBuf[1024] = {0};
            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");
                }

                //向客户端发送数据
                write(cfd, recvBuf, strlen(recvBuf) + 1);
            }
            close(cfd);
            exit(0);//退出当前子进程
        }
    }

    close(lfd);
    return 0;
}
//TCP通信客户端

#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.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.106.133", &serveraddr.sin_addr.s_addr);
    serveraddr.sin_port = htons(9999);
    int ret = connect(fd,(struct sockaddr *)&serveraddr, sizeof(serveraddr));
    
    if(ret == -1){
        perror("connet");
        exit(-1);
    }

    //3.通信
    char recvBuf[1024] = {0};
    int i = 0;
    while(1){
        //给服务器发送数据
        sprintf(recvBuf, "data = %d\n", i++);
        write(fd, recvBuf, strlen(recvBuf) + 1);
        sleep(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...\n");
            break;
        }
    }
    //关闭连接
    close(fd);


    return 0;
}

4.22 多线程实现并发服务器

#define _POSIX_SOURCE
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>

struct sockInfo{
    int fd; //通信的文件描述符
    pthread_t tid; //线程号
    struct sockaddr_in addr;
};

struct sockInfo sockInfos[128];

void *working(void *arg){

    struct sockInfo *pinfo = (struct sockInfo*)arg;


    //子线程和客户端进行通信 cfd 客户端信息 线程号
    //获取客户端信息
    char clientIP[16];
    inet_ntop(AF_INET, &pinfo->addr.sin_addr.s_addr, clientIP, sizeof(clientIP));
    unsigned short clientPort = ntohs(pinfo->addr.sin_port);
    printf("client ip is %s, prot is %d\n", clientIP, clientPort);

    //接受客户端发来的数据
    char recvBuf[1024] = {0};
    while(1){
        int len = read(pinfo->fd, &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(pinfo->fd, recvBuf, strlen(recvBuf) + 1);
    }
    close(pinfo->fd);

    return NULL;
}

int main(){

    //创建socket
    int lfd = socket(PF_INET, SOCK_STREAM, 0);
    if(lfd == -1){
        perror("socket");
        exit(-1);
    }

    //绑定
    struct sockaddr_in serveraddr;
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_port = htons(9999);
    serveraddr.sin_addr.s_addr = INADDR_ANY;
    int ret = bind(lfd, (struct sockaddr*)&serveraddr, sizeof(serveraddr));
    if(ret == - 1){
        perror("bind");
        exit(-1);
    }

    //监听
    ret = listen(lfd, 5);
    if(ret == -1){
        perror("listen");
        exit(-1);
    }

    //初始化数据
    int max = sizeof(sockInfos) / sizeof(sockInfos[0]);
    for(int i = 0; i < max; ++i){
        bzero(&sockInfos[i], sizeof(sockInfos[i]));
        sockInfos[i].fd = -1;
        sockInfos[i].tid = -1;
    }
    
    //循环等待客户端连接,一旦一个客户端连接进来,就创建一个子线程进行通信
    while(1){

        //接受连接
        struct sockaddr_in clientaddr;
        int len = sizeof(clientaddr);
        int cfd = accept(lfd, (struct sockaddr*)&clientaddr, &len);

        struct sockInfo *pinfo;
        for(int i = 0; i < max; ++i){
            //从数组中找到一个可用的sockInfo元素
            if(sockInfos[i].fd == -1){
                pinfo = &sockInfos[i];
                break;
            }
            if(i == max - 1){
                sleep(1);
                i--;
            }
        }

        pinfo->fd = cfd;
        memcpy(&pinfo->addr, &clientaddr, len);

        //创建子线程
        pthread_create(&pinfo->tid, NULL, working, pinfo);

        pthread_detach(pinfo->tid);
        //close(cfd);
    }
    close(lfd);
    
    return 0;
}

4.23 TCP状态转换

 

 2.24 半关闭、端口复用

 

 出现这样错误bind: Address already in use

 输入 netstat -apn | grep 9999 

然后kill -9 7960

当启动server和client后

 杀死server

服务器处于FIN_WAIT2,客户端处于CLOSE_WAIT状态 

 杀死client

 服务器处于TIME_WAIT中,需要1分钟后才能关闭,此时在运行server会出现端口已使用的问题。

这时候就需要端口复用了。

#include <stdio.h>
#include <ctype.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>

int main(int argc, char *argv[]) {

    // 创建socket
    int lfd = socket(PF_INET, SOCK_STREAM, 0);

    if(lfd == -1) {
        perror("socket");
        return -1;
    }

    struct sockaddr_in saddr;
    saddr.sin_family = AF_INET;
    saddr.sin_addr.s_addr = INADDR_ANY;
    saddr.sin_port = htons(9999);
    
    //int optval = 1;
    //setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval));

    int optval = 1;
    setsockopt(lfd, SOL_SOCKET, SO_REUSEPORT, &optval, sizeof(optval));

    // 绑定
    int ret = bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));
    if(ret == -1) {
        perror("bind");
        return -1;
    }

    // 监听
    ret = listen(lfd, 8);
    if(ret == -1) {
        perror("listen");
        return -1;
    }

    // 接收客户端连接
    struct sockaddr_in cliaddr;
    socklen_t len = sizeof(cliaddr);
    int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);
    if(cfd == -1) {
        perror("accpet");
        return -1;
    }

    // 获取客户端信息
    char cliIp[16];
    inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, cliIp, sizeof(cliIp));
    unsigned short cliPort = ntohs(cliaddr.sin_port);

    // 输出客户端的信息
    printf("client's ip is %s, and port is %d\n", cliIp, cliPort );

    // 接收客户端发来的数据
    char recvBuf[1024] = {0};
    while(1) {
        int len = recv(cfd, recvBuf, sizeof(recvBuf), 0);
        if(len == -1) {
            perror("recv");
            return -1;
        } else if(len == 0) {
            printf("客户端已经断开连接...\n");
            break;
        } else if(len > 0) {
            printf("read buf = %s\n", recvBuf);
        }

        // 小写转大写
        for(int i = 0; i < len; ++i) {
            recvBuf[i] = toupper(recvBuf[i]);
        }

        printf("after buf = %s\n", recvBuf);

        // 大写字符串发给客户端
        ret = send(cfd, recvBuf, strlen(recvBuf) + 1, 0);
        if(ret == -1) {
            perror("send");
            return -1;
        }
    }
    
    close(cfd);
    close(lfd);

    return 0;
}
#include <stdio.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

int main() {

    // 创建socket
    int fd = socket(PF_INET, SOCK_STREAM, 0);
    if(fd == -1) {
        perror("socket");
        return -1;
    }

    struct sockaddr_in seraddr;
    inet_pton(AF_INET, "127.0.0.1", &seraddr.sin_addr.s_addr);
    seraddr.sin_family = AF_INET;
    seraddr.sin_port = htons(9999);

    // 连接服务器
    int ret = connect(fd, (struct sockaddr *)&seraddr, sizeof(seraddr));

    if(ret == -1){
        perror("connect");
        return -1;
    }

    while(1) {
        char sendBuf[1024] = {0};
        fgets(sendBuf, sizeof(sendBuf), stdin);

        write(fd, sendBuf, strlen(sendBuf) + 1);

        // 接收
        int len = read(fd, sendBuf, sizeof(sendBuf));
        if(len == -1) {
            perror("read");
            return -1;
        }else if(len > 0) {
            printf("read buf = %s\n", sendBuf);
        } else {
            printf("服务器已经断开连接...\n");
            break;
        }
    }

    close(fd);

    return 0;
}

4.25 IO多路复用简介

从文件中写入内存叫输入,从内存写到文件叫输出。不仅仅只是这。

1. I/O多路复用(I/O多路转接)

        I/O 多路复用使得程序能同时监听多个文件描述符,能够提高程序的性能,Linux 下实现 I/O 多路复用的系统调用主要有 select、poll 和 epoll。

 4.26 select API介绍

// sizeof(fd_set) = 128 1024
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
        fd_set *exceptfds, struct timeval *timeout);
    - 参数:
        - nfds : 委托内核检测的最大文件描述符的值 + 1
        - readfds : 要检测的文件描述符的读的集合,委托内核检测哪些文件描述符的读的属性
                    - 一般检测读操作
                    - 对应的是对方发送过来的数据,因为读是被动的接收数据,检测的就是读缓冲区
                    - 是一个传入传出参数
        - writefds : 要检测的文件描述符的写的集合,委托内核检测哪些文件描述符的写的属性
                    - 委托内核检测写缓冲区是不是还可以写数据(不满的就可以写)
        - exceptfds : 检测发生异常的文件描述符的集合
        - timeout : 设置的超时时间
            struct timeval {
                long tv_sec; /* seconds */
                long tv_usec; /* microseconds */
            };
            - NULL : 永久阻塞,直到检测到了文件描述符有变化
            - tv_sec = 0 tv_usec = 0, 不阻塞
            - tv_sec > 0 tv_usec > 0, 阻塞对应的时间
    - 返回值 :
        - -1 : 失败
        - >0(n) : 检测的集合中有n个文件描述符发生了变化
// 将参数文件描述符fd对应的标志位设置为0
void FD_CLR(int fd, fd_set *set);
// 判断fd对应的标志位是0还是1, 返回值 : fd对应的标志位的值,0,返回0, 1,返回1
int FD_ISSET(int fd, fd_set *set);
// 将参数文件描述符fd 对应的标志位,设置为1
void FD_SET(int fd, fd_set *set);
// fd_set一共有1024 bit, 全部初始化为0
void FD_ZERO(fd_set *set);

4.27 select代码编写 

#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/select.h>

int main(){

    //创建socket
    int lfd = socket(PF_INET, SOCK_STREAM, 0);
    struct sockaddr_in saddr;
    saddr.sin_family = PF_INET;
    saddr.sin_addr.s_addr = INADDR_ANY;
    saddr.sin_port = htons(9999);

    //绑定
    bind(lfd, (struct sockaddr*)&saddr, sizeof(saddr));

    //监听
    listen(lfd, 8);

    //创建一个fd_set 的集合,存放的是需要检测的文件描述符
    fd_set rdset, temp;
    FD_ZERO(&rdset);
    FD_SET(lfd, &rdset);
    int maxfd = lfd;

    while(1){

        temp = rdset;

        //调用select系统函数,让内核检测哪些文件描述符有数据
        int ret = select(maxfd + 1, &temp, NULL, NULL, NULL);
        if(ret == -1){
            perror("select");
            exit(-1);
        }
        else if(ret == 0){
            continue;
        }
        else if(ret > 0){
            //说明检测到了有文件描述符对应的缓冲区的数据发生了改变
            if(FD_ISSET(lfd, &temp)){
                //表示有新的客户端连接进来了
                struct sockaddr_in clientaddr;
                int len = sizeof(clientaddr);
                int cfd = accept(lfd, (struct sockaddr*)&clientaddr, &len);

                //将新的文件描述符加入集合中
                FD_SET(cfd, &rdset);
                
                //更新最大的文件描述符
                maxfd = cfd > maxfd ? cfd : maxfd;
            }
            for(int i = lfd + 1; i <= maxfd; ++i){
                if(FD_ISSET(i, &temp)){
                    //说明这个文件描述符对应的客户端发来了数据
                    char buf[1024] = {0};
                    int len = read(i, buf, sizeof(buf));
                    if(len == -1){
                        perror("read");
                        exit(-1);
                    }
                    else if(len == 0){
                        printf("client closed....\n");
                        close(i);
                        FD_CLR(i, &rdset);
                    }
                    else{
                        printf("read buf = %s\n", buf);
                        write(i, buf, strlen(buf) + 1);
                    }
                }
            }
        }
    }

    close(lfd);


    return 0;
}
#include <stdio.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

int main() {

    // 创建socket
    int fd = socket(PF_INET, SOCK_STREAM, 0);
    if(fd == -1) {
        perror("socket");
        return -1;
    }

    struct sockaddr_in seraddr;
    inet_pton(AF_INET, "127.0.0.1", &seraddr.sin_addr.s_addr);
    seraddr.sin_family = AF_INET;
    seraddr.sin_port = htons(9999);

    // 连接服务器
    int ret = connect(fd, (struct sockaddr *)&seraddr, sizeof(seraddr));

    if(ret == -1){
        perror("connect");
        return -1;
    }
    
    int num = 0;
    while(1) {
        char sendBuf[1024] = {0};
        //fgets(sendBuf, sizeof(sendBuf), stdin);
        sprintf(sendBuf, "send data %d", num++);

        write(fd, sendBuf, strlen(sendBuf) + 1);

        // 接收
        int len = read(fd, sendBuf, sizeof(sendBuf));
        if(len == -1) {
            perror("read");
            return -1;
        }else if(len > 0) {
            printf("read buf = %s\n", sendBuf);
        } else {
            printf("服务器已经断开连接...\n");
            break;
        }
        sleep(1);
    }

    close(fd);

    return 0;
}

4.28 poll API介绍及代码编写

 

#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/select.h>
#include <poll.h>

int main(){

    //创建socket
    int lfd = socket(PF_INET, SOCK_STREAM, 0);
    struct sockaddr_in saddr;
    saddr.sin_family = PF_INET;
    saddr.sin_addr.s_addr = INADDR_ANY;
    saddr.sin_port = htons(9999);

    //绑定
    bind(lfd, (struct sockaddr*)&saddr, sizeof(saddr));

    //监听
    listen(lfd, 8);

    //初始化检测的文件描述符数组
    struct pollfd fds[1024];
    for(int i = 0; i < 1024; ++i){
        fds[i].fd = -1;
        fds[i].events = POLLIN;
    }
    fds[0].fd = lfd;

    int nfds = 0;


    while(1){

        //调用poll系统函数,让内核检测哪些文件描述符有数据
        int ret = poll(fds, nfds + 1, -1);
        if(ret == -1){
            perror("select");
            exit(-1);
        }
        else if(ret == 0){
            continue;
        }
        else if(ret > 0){
            //说明检测到了有文件描述符对应的缓冲区的数据发生了改变
            if(fds[0].revents & POLLIN){
                //表示有新的客户端连接进来了
                struct sockaddr_in clientaddr;
                int len = sizeof(clientaddr);
                int cfd = accept(lfd, (struct sockaddr*)&clientaddr, &len);

                //将新的文件描述符加入集合中
                int i;
                for(i = 1; i < 1024; ++i){
                    if(fds[i].fd == -1){
                        fds[i].fd = cfd;
                        fds[i].events = POLLIN;
                        break;
                    }
                }
                
                //更新最大的文件描述符的索引
                nfds = i > nfds ? i : nfds;
            }
            for(int i = 1; i <= nfds; ++i){
                if(fds[i].revents & POLLIN){
                    //说明这个文件描述符对应的客户端发来了数据
                    char buf[1024] = {0};
                    int len = read(fds[i].fd, buf, sizeof(buf));
                    if(len == -1){
                        perror("read");
                        exit(-1);
                    }
                    else if(len == 0){
                        printf("client closed....\n");
                        close(fds[i].fd);
                        fds[i].fd = -1;
                    }
                    else{
                        printf("read buf = %s\n", buf);
                        write(fds[i].fd, buf, strlen(buf) + 1);
                    }
                }
            }
        }
    }

    close(lfd);


    return 0;
}

4.29 epoll API介绍 

        通过epoll_create(2000)在内核中创建一个eventpoll实例(相当于一个文件)函数返回值epfd就是这个实例的文件描述符,直接对内核态进行操作,不需要将数据从用户区拷贝到内核区。eventpoll中有两个成员一个为struct rb_root类型的rbr,是红黑树类型,存放的是我们要检测的文件描述符(之前我们是用数组存放的还需要遍历去检测那个需要检测),另个一个成员为struct list_head双俩表 类型的rdlist是检测到哪些文件描述符中的数据已经改变了。

        通过enpoll_ctl()函数可以向eventpoll中添加(删除修改)我们想要检测的信息。(添加到rbr)

        如果有文件描述符改变,通过enpoll_wait()中的传出参数就可以知道有哪几个改变。(传出rdlist)

#include <sys/epoll.h>

// 创建一个新的epoll实例。在内核中创建了一个数据,这个数据中有两个比较重要的数据,一个是需要检
测的文件描述符的信息(红黑树),还有一个是就绪列表,存放检测到数据发送改变的文件描述符信息(双向
链表)。
int epoll_create(int size);
    - 参数:
        size : 目前没有意义了。随便写一个数,必须大于0
    - 返回值:
        -1 : 失败
        > 0 : 文件描述符,操作epoll实例的
typedef union epoll_data {
    void *ptr;
    int fd;
    uint32_t u32;
    uint64_t u64;
} epoll_data_t;
struct epoll_event {
    uint32_t events; /* Epoll events */
    epoll_data_t data; /* User data variable */
};
常见的Epoll检测事件:
- EPOLLIN
- EPOLLOUT
- EPOLLERR

// 对epoll实例进行管理:添加文件描述符信息,删除信息,修改信息
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
    - 参数:
        - epfd : epoll实例对应的文件描述符
        - op : 要进行什么操作
            EPOLL_CTL_ADD: 添加
            EPOLL_CTL_MOD: 修改
            EPOLL_CTL_DEL: 删除
        - fd : 要检测的文件描述符
        - event : 检测文件描述符什么事情
// 检测函数
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
    - 参数:
        - epfd : epoll实例对应的文件描述符
        - events : 传出参数,保存了发送了变化的文件描述符的信息
        - maxevents : 第二个参数结构体数组的大小
        - timeout : 阻塞时间
            - 0 : 不阻塞
            - -1 : 阻塞,直到检测到fd数据发生变化,解除阻塞
            - > 0 : 阻塞的时长(毫秒)
    - 返回值:
        - 成功,返回发送变化的文件描述符的个数 > 0
        - 失败 -1

4.30 epoll 代码编写

#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/select.h>
#include <sys/epoll.h>

int main(){
    //创建socket
    int lfd = socket(PF_INET, SOCK_STREAM, 0);
    struct sockaddr_in saddr;
    saddr.sin_family = PF_INET;
    saddr.sin_addr.s_addr = INADDR_ANY;
    saddr.sin_port = htons(9999);

    //绑定
    bind(lfd, (struct sockaddr*)&saddr, sizeof(saddr));

    //监听
    listen(lfd, 8);

    //调用epoll_create()创建一个epoll实例
    int epfd = epoll_create(100);

    //将监听的文件描述符相关的检测信息添加到epoll实例中
    struct epoll_event epev;
    epev.events = EPOLLIN;
    epev.data.fd = lfd;
    epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &epev);

    struct epoll_event epevs[1024];

    while(1){

        int ret = epoll_wait(epfd, epevs, 1024, -1);
        if(ret == -1){
            perror("epoll_wait");
            exit(-1);
        }

        printf("ret = %d\n", ret);

        for(int i = 0; i < ret; ++i){
            if(epevs[i].data.fd == lfd){
                //监听的文件描述符有数据到达,有客户端连接
                //表示有新的客户端连接进来了
                struct sockaddr_in clientaddr;
                int len = sizeof(clientaddr);
                int cfd = accept(lfd, (struct sockaddr*)&clientaddr, &len);

                epev.events = EPOLLIN | EPOLLOUT;
                epev.data.fd = cfd;
                epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &epev);
            }
            else{
                // 有数据到达,需要通信
                //说明这个文件描述符对应的客户端发来了数据
                if(epevs[i].events & EPOLLOUT){
                    continue;
                }
                int curfd = epevs[i].data.fd;
                char buf[1024] = {0};
                int len = read(curfd, buf, sizeof(buf));
                if(len == -1){
                    perror("read");
                    exit(-1);
                }
                else if(len == 0){
                    printf("client closed....\n");
                    epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);
                    close(curfd);
                }
                else{
                    printf("read buf = %s\n", buf);
                    write(curfd, buf, strlen(buf) + 1);
                }
                
            }
        }
    }
    close(lfd);
    close(epfd);
    return 0;
}

4.31 epoll的两种工作模式

LT 模式 (水平触发)

        LT(level - triggered)是缺省的工作方式,并且同时支持 block 和 no-block socket。在这
种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的 fd 进行 IO 操作。如果你不作任何操作,内核还是会继续通知你的。

假设委托内核检测读事件 -> 检测fd的读缓冲区
读缓冲区有数据 - > epoll检测到了会给用户通知
        a.用户不读数据,数据一直在缓冲区,epoll 会一直通知
        b.用户只读了一部分数据,epoll会通知
        c.缓冲区的数据读完了,不通知

ET 模式(边沿触发)

        ET(edge - triggered)是高速工作方式,只支持 no-block socket。在这种模式下,当描述
符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了。但是请注意,如果一直不对这个 fd 作 IO 操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once)。
        ET 模式在很大程度上减少了 epoll 事件被重复触发的次数,因此效率要比 LT 模式高。epoll
工作在 ET 模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。

假设委托内核检测读事件 -> 检测fd的读缓冲区
读缓冲区有数据 - > epoll检测到了会给用户通知
        a.用户不读数据,数据一致在缓冲区中,epoll下次检测的时候就不通知了
        b.用户只读了一部分数据,epoll不通知
        c.缓冲区的数据读完了,不通知

LT模式 水平触发
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/select.h>
#include <sys/epoll.h>

int main(){
    //创建socket
    int lfd = socket(PF_INET, SOCK_STREAM, 0);
    struct sockaddr_in saddr;
    saddr.sin_family = AF_INET;
    saddr.sin_addr.s_addr = INADDR_ANY;
    saddr.sin_port = htons(9999);

    //绑定
    bind(lfd, (struct sockaddr*)&saddr, sizeof(saddr));

    //监听
    listen(lfd, 8);

    //调用epoll_create()创建一个epoll实例
    int epfd = epoll_create(100);

    //将监听的文件描述符相关的检测信息添加到epoll实例中
    struct epoll_event epev;
    epev.events = EPOLLIN;
    epev.data.fd = lfd;
    epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &epev);

    struct epoll_event epevs[1024];

    while(1){

        int ret = epoll_wait(epfd, epevs, 1024, -1);
        if(ret == -1){
            perror("epoll_wait");
            exit(-1);
        }

        printf("ret = %d\n", ret);

        for(int i = 0; i < ret; ++i){

            int curfd = epevs[i].data.fd;

            if(curfd == lfd){
                //监听的文件描述符有数据到达,有客户端连接
                //表示有新的客户端连接进来了
                struct sockaddr_in clientaddr;
                int len = sizeof(clientaddr);
                int cfd = accept(lfd, (struct sockaddr*)&clientaddr, &len);

                epev.events = EPOLLIN;
                epev.data.fd = cfd;
                epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &epev);
            }
            else{
                // 有数据到达,需要通信
                //说明这个文件描述符对应的客户端发来了数据
                if(epevs[i].events & EPOLLOUT){
                    continue;
                }
                //int curfd = epevs[i].data.fd;
                char buf[5] = {0};
                int len = read(curfd, buf, sizeof(buf));
                if(len == -1){
                    perror("read");
                    exit(-1);
                }
                else if(len == 0){
                    printf("client closed....\n");
                    epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);
                    close(curfd);
                }
                else{
                    printf("read buf = %s\n", buf);
                    write(curfd, buf, strlen(buf) + 1);
                }
                
            }
        }
    }
    close(lfd);
    close(epfd);
    return 0;
}
ET模式 边沿触发
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/select.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <errno.h>

int main(){
    //创建socket
    int lfd = socket(PF_INET, SOCK_STREAM, 0);
    struct sockaddr_in saddr;
    saddr.sin_family = PF_INET;
    saddr.sin_addr.s_addr = INADDR_ANY;
    saddr.sin_port = htons(9999);

    //绑定
    bind(lfd, (struct sockaddr*)&saddr, sizeof(saddr));

    //监听
    listen(lfd, 8);

    //调用epoll_create()创建一个epoll实例
    int epfd = epoll_create(100);

    //将监听的文件描述符相关的检测信息添加到epoll实例中
    struct epoll_event epev;
    epev.events = EPOLLIN;
    epev.data.fd = lfd;
    epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &epev);

    struct epoll_event epevs[1024];

    while(1){

        int ret = epoll_wait(epfd, epevs, 1024, -1);
        if(ret == -1){
            perror("epoll_wait");
            exit(-1);
        }

        printf("ret = %d\n", ret);

        for(int i = 0; i < ret; ++i){
            int curfd = epevs[i].data.fd;
            if(epevs[i].data.fd == lfd){
                //监听的文件描述符有数据到达,有客户端连接
                //表示有新的客户端连接进来了
                struct sockaddr_in clientaddr;
                int len = sizeof(clientaddr);
                int cfd = accept(lfd, (struct sockaddr*)&clientaddr, &len);

                //设置cfd属性非阻塞
                int flag = fcntl(cfd, F_GETFL);
                flag |= O_NONBLOCK;
                fcntl(cfd, F_SETFL, flag);


                epev.events = EPOLLIN | EPOLLET;
                epev.data.fd = cfd;
                epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &epev);
            }
            else{
                // 有数据到达,需要通信
                //说明这个文件描述符对应的客户端发来了数据
                if(epevs[i].events & EPOLLOUT){
                    continue;
                }
                
                //循环读取所有数据(循环)
                char buf[5];
                int len = 0;
                while((len = read(curfd, buf, sizeof(buf))) > 0){
                    //打印数据
                    printf("recv data : %s\n", buf);
                    write(curfd, buf, len);
                }
                if(len == 0){
                    printf("client closed...\n");
                }
                else if(len == -1){
                    if(errno == EAGAIN){  //当read设置为非阻塞要判断这个,数据已经读完还要继续读
                        printf("data over....\n");
                    }else{
                        perror("read");
                        exit(-1);
                    }
                    
                }
            }
        }
    }
    close(lfd);
    close(epfd);
    return 0;
}

4.32 UDP通信实现

#include <sys/types.h>
#include <sys/socket.h>
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
                const struct sockaddr *dest_addr, socklen_t addrlen);
    - 参数:
        - sockfd : 通信的fd
        - buf : 要发送的数据
        - len : 发送数据的长度
        - flags : 0
        - dest_addr : 通信的另外一端的地址信息
        - addrlen : 地址的内存大小
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                    struct sockaddr *src_addr, socklen_t *addrlen);
    - 参数:
        - sockfd : 通信的fd
        - buf : 接收数据的数组
        - len : 数组的大小
        - flags : 0
        - src_addr : 用来保存另外一端的地址信息,不需要可以指定为NULL
        - addrlen : 地址的内存大小
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <arpa/inet.h>

int main(){

    //1.创建一个通信的socket
    int fd = socket(PF_INET, SOCK_DGRAM, 0);
    if(fd == -1){
        perror("socket");
        exit(-1);
    }
    //2.绑定
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(9999);
    addr.sin_addr.s_addr = INADDR_ANY;
    int ret = bind(fd, (struct sockaddr*)&addr, sizeof(addr));
    if(ret == -1){
        perror("bind");
        exit(-1);
    }
    //3.通信
    while(1){

        //接受数据
        char recvbuf[128];
        struct sockaddr_in clientaddr;
        int len = sizeof(clientaddr);
        int num = recvfrom(fd, recvbuf, sizeof(recvbuf), 0, (struct sockaddr*)&clientaddr, &len);

        char IPbuf[16];
        inet_ntop(AF_INET, &clientaddr.sin_addr.s_addr, IPbuf, sizeof(IPbuf));
        printf("client IP :  %s, Port : %d\n", IPbuf, ntohs(clientaddr.sin_port));

        printf("server receive data = %s\n", recvbuf);

        //发送数据
        sendto(fd, recvbuf, strlen(recvbuf) + 1, 0,(struct sockaddr*)&clientaddr, sizeof(clientaddr));

    }
    close(fd);

    return 0;;
}
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <arpa/inet.h>

int main(){

    //1.创建一个通信的socket
    int fd = socket(PF_INET, SOCK_DGRAM, 0);
    if(fd == -1){
        perror("socket");
        exit(-1);
    }

    //服务器的地址信息
    struct sockaddr_in saddr;
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(9999);
    inet_pton(AF_INET, "127.0.0.1", &saddr.sin_addr.s_addr);
  
    int num = 0;
    //3.通信
    while(1){

        //发送数据
        char sendBuf[128];
        sprintf(sendBuf, "hello, i am client %d\n", num++);
        sendto(fd, sendBuf, strlen(sendBuf) + 1, 0,(struct sockaddr*)&saddr, sizeof(saddr));


        //接受数据
        struct sockaddr_in clientaddr;
        int num = recvfrom(fd, sendBuf, sizeof(sendBuf), 0, NULL, NULL);
        printf("client receive data = %s\n", sendBuf);
        sleep(1);
    }
    close(fd);

    return 0;;
}

4.33 广播

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <arpa/inet.h>

int main(){

    //1.创建一个通信的socket
    int fd = socket(PF_INET, SOCK_DGRAM, 0);
    if(fd == -1){
        perror("socket");
        exit(-1);
    }

    //2.设置广播属性
    int op = 1;
    setsockopt(fd, SOL_SOCKET, SO_BROADCAST, &op, sizeof(op));

    //3.创建一个广播的地址
    struct sockaddr_in clientaddr;
    clientaddr.sin_family = AF_INET;
    clientaddr.sin_port = htons(9999);
    clientaddr.sin_addr.s_addr = INADDR_ANY;
    inet_pton(AF_INET, "192.168.106.255", &clientaddr.sin_addr.s_addr);
   
    //3.通信
    int num = 0;
    while(1){
        
        char sendBuf[128];
        sprintf(sendBuf, "hello, client...%d\n", num++);

        //发送数据
        sendto(fd, sendBuf, strlen(sendBuf) + 1, 0,(struct sockaddr*)&clientaddr, sizeof(clientaddr));
        printf("广播的数据:%s\n", sendBuf);
        sleep(1);

    }
    close(fd);

    return 0;;
}

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <arpa/inet.h>

int main(){

    //1.创建一个通信的socket
    int fd = socket(PF_INET, SOCK_DGRAM, 0);
    if(fd == -1){
        perror("socket");
        exit(-1);
    }

    //2.客户端绑定本地的IP的端口
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(9999);
    addr.sin_addr.s_addr = INADDR_ANY;

    int ret = bind(fd, (struct sockaddr*)&addr, sizeof(addr));
    if(ret == -1){
        perror("bind");
        exit(-1);
    }
  
    int num = 0;
    //3.通信
    while(1){

        char buf[128];
        //接受数据
        int num = recvfrom(fd, buf, sizeof(buf), 0, NULL, NULL);
        printf("client receive data = %s\n", buf);

    }
    close(fd);

    return 0;;
}

4.34 组播

4.35 本地套接字通信

本地套接字的作用:本地的进程间通信
        有关系的进程间的通信
        没有关系的进程间的通信
本地套接字实现流程和网络套接字类似,一般呢采用TCP的通信流程

 

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/un.h>


int main(){

    unlink("server.sock");

    //1.创建监听的套接字
    int lfd = socket(AF_LOCAL, SOCK_STREAM, 0);
    if(lfd == -1){
        perror("socket");
        exit(-1);
    }

    struct sockaddr_un addr;
    addr.sun_family = AF_LOCAL;
    strcpy(addr.sun_path, "server.sock");

    //2.绑定本地套接字文件
    int ret = bind(lfd, (struct sockaddr *)&addr, sizeof(addr));
    if(ret == -1){
        perror("bind");
        exit(-1);
    }

    //3.监听
    ret = listen(lfd, 100);
    if(ret == -1){
        perror("listen");
        exit(-1);
    }

    //4.等待客户端连接
    struct sockaddr_un clientaddr;
    int len = sizeof(clientaddr);
    int cfd = accept(lfd, (struct sockaddr*)&clientaddr, &len);
    if(cfd == -1){
        perror("accept");
        exit(-1);
    }

    printf("client socket filename : %s\n", clientaddr.sun_path);

    //5.通信
    while(1){

        char buf[128];
        int len = recv(cfd, buf, sizeof(buf), 0);
        if(ret == -1){
            perror("recv");
            exit(-1);
        }
        else if(len == 0){
            printf("client closed....\n");
            break;
        }
        else{
            printf("client say : %s\n", buf);
            send(cfd, buf, len, 0);
        }
    }
    close(cfd);
    close(lfd);

    return 0;
}
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/un.h>


int main(){

    unlink("client.sock");

    //1.创建套接字
    int cfd = socket(AF_LOCAL, SOCK_STREAM, 0);
    if(cfd == -1){
        perror("socket");
        exit(-1);
    }

    struct sockaddr_un addr;
    addr.sun_family = AF_LOCAL;
    strcpy(addr.sun_path, "client.sock");

    //2.绑定本地套接字文件
    int ret = bind(cfd, (struct sockaddr *)&addr, sizeof(addr));
    if(ret == -1){
        perror("bind");
        exit(-1);
    }

    //3.连接服务器
    struct sockaddr_un serveraddr;
    serveraddr.sun_family = AF_LOCAL;
    strcpy(serveraddr.sun_path, "server.sock");
    ret = connect(cfd, (struct sockaddr*)&serveraddr, sizeof(serveraddr));
    if(ret == -1){
        perror("connect");
        exit(-1);
    }

    //4.通信
    int num = 0;
    while(1){

        //发送数据
        char buf[128];
        sprintf(buf, "hello, i am client %d\n", num++);
        send(cfd, buf, strlen(buf) + 1, 0);

        //接受数据
        int len = recv(cfd, buf, sizeof(buf), 0);

        if(ret == -1){
            perror("recv");
            exit(-1);
        }
        else if(len == 0){
            printf("server closed....\n");
            break;
        }
        else{
            printf("server say : %s\n", buf);
        }
        sleep(1);
    }
    close(cfd);


    return 0;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值