Linux高并发服务器开发-Linux网络编程

网络结构模式

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,服务器安装 SQLServer、Oracle、MySQL 等数据库。浏览器通过 Web Server 同数据库进行数据交互。

优点

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

缺点

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

MAC地址

网卡是一块被设计用来允许计算机在计算机网络上进行通讯的计算机硬件,又称为 网络适配器 或 网络接口卡NIC。

其拥有 MAC 地址,属于 OSI 模型的第 2 层,它使得用户可以通过电缆或无线相互连接。每一个网卡都有一个被称为 MAC 地址的独一无二的 48 位行号。

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

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

IP地址

IP 协议是为计算机网络相互连接进行通信而设计的协议。在因特网中,它是能使连接到网上的所有计算机网络实现相互通信的一套规则,规定了计算机在因特网上进行通信时应当遵守的规则。任何厂家生产的计算机系统,只要遵守 IP 协议就可以与因特网互连互通。各个厂家生产的网络系统和设备,如以太网、分组交换网等,它们相互之间不能互通,不能互通的主要原因是因为它们所传送数据的基本单元(技术上称之为“帧”)的格式不同

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

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

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

IP地址编制方式

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

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

Alt text

子网掩码

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

端口

“端口”是英文 port 的意译,可以认为是设备与外界通讯交流的出口。端口可分为虚拟端口和物理端口,其中虚拟端口指计算机内部或交换机路由器内的端口,不可见,是特指TCP/IP协议中的端口,是逻辑意义上的端口。例如计算机中的 80 端口、21 端口、23 端口等。物理端口又称为接口,是可见端口,计算机背板的 RJ45网口,交换机路由器集线器等 RJ45 端口。电话使用 RJ11 插口也属于物理端口的范畴。

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

网络模型

OSI七层参考模型

Alt text

TCP/IP四层模型

Alt text

协议

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

常见协议

应用层常见的协议有: FTP协议 (File Transfer Protocol 文件传输协议) 、 HTTP协议 (Hyper Text TransferProtocol 超文本传输协议)、 NFS (Network File System 网络文件系统)。

传输层常见协议有: TCP协议 (Transmission Control Protocol 传输控制协议)、 UDP协议(User DatagramProtocol 用户数据报协议)。

网络层常见协议有: IP协议(lnternet Protocol因特网互联协议)、 ICMP协议 (Internet Control MessageProtocol 因特网控制报文协议)、 IGMP协议 (Internet Group Management Protocol 因特网组管理协议)。

网络接口层常见协议有: ARP协议 (Address Resolution Protocl 地址解析协议)、RARP协议 (ReverseAddress Resolution Protocol 反向地址解析协议)。

UDP 协议

Alt text

TCP 协议

Alt text

IP 协议

Alt text

以太网帧 协议

Alt text

ARP 协议

Alt text

网络通信的过程

封装

Alt text

分用

Alt text
Alt text

socket通信基础

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

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

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

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

字节序

现代 CPU 的累加器一次都能装载(至少) 4字节(这里考虑 32 位机),即一个整数,那么这4字节在内存中排列的顺序将影响它被累加器装载成的整数的值,这就是字节序问题。

在各种计算机体系结构中,对于字节、字等的存储机制有所不同,因而引发了计算机通信领域中一个很重要的问题,即通信双方交流的信息单元(比特、字节、字、双字等等)应该以什么样的顺序进行传送。如果不达成一致的规则,通信双方将无法进行正确的编码/译码从而导致通信失败。

字节序,顾名思义字节的顺序,就是大于一个字节类型的数据在内存中的存放顺序(一个字节的数据当然就无需谈顺序的问题了)*。

字节序分为大端字节序(Big-Endian) 和小端字节序(Little-Endian) 。大端字节序是指一个整数的最高位字节 (23 ~ 31 bit)存储在内存的低地址处,低位字节 (0 ~ 7 bit) 存储在内存的高地址处,小端字节序则是指整数的高位字节存储在内存的高地址处,而低位字节则存储在内存的低地址处。

字节序转换函数

当格式化的数据在两台使用不同字节序的主机之间直接传递时,接收端必然错误的解释之。
解决问题的方法是:发送端总是把要发送的数据转换成大端字节序数据后再发送,而接收端知道对方传送过来的数据总是采用大端字节序,所以接收端可以根据自身采用的字节序决定是否对接收到的数据进行转换(小端机转换,大端机不转换)。

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

BSD Socket提供了封装好的转换接口,方便程序员使用,包括从主机字节序到网络字节序的转换函数: htons、htonl; 从网络字节序到主机字节序的转换函数: ntohs、ntohl。

#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);

socket地址

通用socket地址

socket地址其实是一个结构体,封装IP,端口号等信息。

#include <bits/socket.h>
struct sockaddr {
    sa_family_t sa_family;
    char        sa_data[14];
};

typedef unsigned short int sa_family_t:

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协议族

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

协议族 地址值含义和长度
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>
struct sockaddr_storage{
    sa_family_t sa_family;
    unsigned long int __ss_align;
    char __ ss_padding[ 128 - sizeof(_ss_align) ];
};

typedef unsigned short int sa_family_t;

专用socket地址

很多网络编程函数诞生早于 IPv4 协议,那时候都使用的是 struct sockaddr 结构体,为了向前兼容,现在sockaddr 退化成了 (void *) 的作用,传递一个地址给函数,至于这个函数是 sockaddr in 还是 sockaddr in6由地址族确定,然后函数内部再强制类型转化为所需的地址类型。
Alt text
所有专用 socket 地址(以及 sockaddr_storage) 类型的变量在实际使用时都需要转化为通用 socket 地址类型 sockaddr (强制转化即可),因为所有 socket 编程接口使用的地址参数类型都是 sockaddr。

IP地址转换函数

通常,人们习惯用可读性好的字符串来表示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);
char *inet_ntoa(struct in_addr in);

下面对上面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 是一样的

TCP通信流程

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

Alt text

TCP 通信的流程

// 服务器端(被动接受连接的角色)
1.创建一个用于监听的套接字
    - 监听:监听有客户端的连接
    - 套接字:这个套接字其实就是一个文件描述符
2.将这个监听文件描述符和本地的IP和端口绑定(IP和端口就是服务器的地址信息)
    - 客户端连接服务器的时候使用的就是这个IP和端口
3.设置监听,监听的fd开始工作
4.阻塞等待,当有客户端发起连接,解除阻塞,接受客户端的连接,会得到一个和客户端通信的套接字(fd)
5.通信
    -接收数据
    -发送数据
6.通信结束,断开连接
//客户端
1.创建一个用于通信的套接字(fd)
2.连接服务器,需要指定连接的服务器的 IP 和 端口
3.连接成功了,客户端可以直接和服务器通信
    -接收数据
    -发送数据
4.通信结束,断开连接

socket函数

#include <sys/types .h>
#include <sys/socket .h>
#include <arpa/inet.h> // 包含了这个头文件,上面两个就可以省略

socket ( )

//创建一个套接字
int socket(int domain, int type, int protocol) ;
    参数:
        - domain: 协议族
            -AF_INET : ipv4
            -AF_INET6 : ipv6
            -AF_UNIX,AF_LOCAL : 本地套接字通信(进程间通信)
        - type: 通信过程中使用的协议类型
            -SOCK_STREAM :流式协议  
            -SOCK_DGRAM:报式协议
        - protoco1 : 具体的一个协议。一般写0
            - SOCK_STREAM : 流式协议认使用 TCP
            - SOCK_DGRAM :报式协议认使用 UDP
    返回值:
        - 成功:返回文件描述符,操作的就是内核缓冲区。
        - 失败:-1

bind ( )

//绑定,将fd 和本地的IP + 端口进行绑定
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen); // socket命名
    -参数:
        - sockfd : 通过socket函数得到的文件描述符
        - addr : 需要绑定的socket地址,这个地址封装了ip和端口号的信息
        - addrlen :第二个参数结构体占的内存大小

listen ( )

//监听这个socket上的连接 
int listen(int sockfa, int backlog);    proc/sys /net/core/somaxconn
    -参数:
    -sockfd : 通过socketO函数得到的文件描述符
    -backlog : 未连接的和已经连接的和的最大值

accept ( )

//接收客户端连接,默认是一个阻塞的函数,阻塞等待客户端连接
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
    参数:
        - sockfd:用于监听的文件描述符
        - addr:传出参数,记录了连接成功后客户端的地址信息(ip,port)
        - addrlen : 指定第二个参数的对应的内存大小。
    返回值:
        -成功:用于通信的文件描述符
        -失败:-1

connect ( )

//客户端连接服务器
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 三次握手,四次挥手

三次握手
Alt text
四次挥手
Alt text

TCP 滑动窗口

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

滑动窗口协议是用来改善吞吐量的一种技术,即容许发送方在接收任何应答之前传送附加的包。接收方告诉发送方在某一时刻能送多少包(称窗口尺寸)。TCP 中采用滑动窗口来进行传输控制,滑动窗口的大小意味着接收方还有多大的缓冲区可以用于接收数据。发送方可以通过滑动窗口的大小来确定应该发送多少字节的数据。当滑动窗口为 0 时,发送方一般不能再发送数据报。

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

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

TCP状态转换

Alt text
Alt text
Alt text
Alt text
Alt text
半关闭
Alt text

#include <sys/socket .h>
int shutdown(int sockfd, int how) ;
sockfd: 需要关闭的socket的描述符
how:    允许为shutdown操作选择以下几种方式:
            SHUT_RD(0):关闭sockfd上的读功能,此选项将不允许sockfd进行读操作。
                        该套接字不再接收数据,任何当前在套接字接受缓冲区的数据将被无声的丢弃掉。
            SHUT_WR(1):关闭sockfd的写功能,此选项将不允许sockfd进行写操作。进程不能在对此套接字发出写操作。
            SHUT_RDWR(2):关闭sockfd的读写功能。相当于调用shutdown两次: 首先是以SHUT_RD,然局以SHUT_WR。

使用 close 中止一个连接,但它只是减少描述符的引用计数,并不直接关闭连接,只有当描述符的引用计数为0才关闭连接。shutdown 不考虑描述符的引用计数,直接关闭描述符。也可选择中止一个方向的连接,只中止读或只中止写。
注意:

  1. 如果有多个进程共享一个套接字,close 每被调用一次,计数减 1,直到计数为 0时,也就是所用进程都调用了 close,套接字将被释放。
  2. 在多进程中如果一个进程调用了 shutdown(sfd,SHUT_RDWR)后,其它的进程将无法进行通信。但如果一个进程 close(sfd)将不会影响到其它进程

端口复用

端口复用最常用的用途是:

  1. 防止服务器重启时之前绑定的端口还未释放
  2. 程序突然退出而系统没有释放端口
#include <sys/types .h>
#incTude <sys/socket .h>
//设置套接字的属性(不仅仅是端口复用)
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
    参数;
        -sockfd : 要操作的文件描述符
        -leve1 : 级别 - SOL_SOCKET(端口复用的级别)
        -optname :选项的名称
            - SO_REUSEADDR
            - SO_REUSEPORT
        -optva1 : 端口复用的值(整形)
            - 1 : 可以复用
            - 0 : 不可以复用
        -optlen : optval参数的大小

端口复用,设置的时机是在服务器绑定端口之前
setsockopt();
bind();

常看网络相关信息的命令
netstat
参数:
-a 所有的socket
-p 显示正在使用socket的程序的名称
-n 直接使用IP地址,而不通过域名服务器。

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

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

阻塞等待

好处: 不占用CPU宝贵的时间片。
缺点: 同一时刻只能处理一个操作,效率低。

多线程或者多进程解决 BIO模型
Alt text

非阻塞,忙轮询

优点: 提高了程序的执行效率。
缺点: 需要占用更多的CPU和系统资源。

  1. NIO模型
    Alt text

  2. 使用IO多路转接技术 select/poll/epoll解决

select

  1. 首先要构造一个关于文件描述符的列表,将要监听的文件描述符添加到该列表中。
  2. 调用一个系统函数,监听该列表中的文件描述符,直到这些描述符中的一个或者多个进行I/O操作时,该函数才返回。
    a. 这个函数是阻塞的
    b. 函数对文件描述符的检测的操作是由内核完成的
  3. 在返回时,它会告诉进程有多少(哪些)描述符进行 I / O 操作
#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 {
                1ong tv_sec;    / seconds */
                1ong 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);

缺点:
1每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
2.同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
3.select支持的文件描述符数量太小了默认是1024
4.fds集合不能重用,每次都需要重置

poll

#include <poll.h>
struct pollfd {
    int fd;         /*委托内核检测的文件描述符*/
    short events;   /*委托内核检测文件描述符的什么事*/
    short revents;  /*  文件描述符实际发生的事件*/
};

int poll(struct pollfd *fds, nfds_t nfds, int timeout)
    -参数:
        -fds :是一个struct po11fd 结构体数组,这是一个需要检测的文件描述符的集合
        -nfds : 这个是第一个参数数组中最后一个有效元素的下标 + 1
        -timeout : 阻塞时长
             0: 不阻塞
            -1: 阻塞,当检测到需要检测的文件描述符有变化,解除阻塞
            >0: 阻塞的时长
    -返回值:
        -1 :失败
        >0(n) :成功,n表示检测到集合中有n个文件描述符发生变化

Alt text

epoll

Alt text

#include <sys/epo11.h>

//创建一个新的epoll实例,在内核中创建
int epo11_create(int size);
    -参数:
        -size:目前没有意义,随便写一个>0
    -返回值:
        -1: 失败
        >0:  文件描述符,epoll实例



typedef union epoll_data{
    void        *ptr;
    int         fd;
    uint32_t    u32;
    uint64_t    u64;
} epo11_data_t;

struct epo11_event{
    uint32_t        events;     /* Epoll events */
    epo11_data_t    data;       /* User data variable */
};
常见的Epoll检测事件:
    -EPOLLIN
    -EPOLLOUT
    -EPOLLERR

//对epo11实例进行管理: 添加文件描述符信息,删除信息,修改信息
int epol1_ctl(int epfd, int op, int fd, struct epoll_event *event);
    -参数
        -epfd : epo11实例对应的文件描述符
        -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

Epoll工作模式
Alt text
Alt text

UDP通信

Alt text

#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)

广播

向子网中多台计算机发送消息,并且子网中所有的计算机都可以接收到发送方发送的消息,每个广播消息都包含一个特殊的IP地址,这个IP中子网内主机标志部分的二进制全部为1。
a.只能在局域网中使用。
b.客户端需要绑定服务器广播使用的端口,才可以接收到广播消息

同端口复用
setsockopt()
    -optname: SO_BROADCAST  
    -optval:    1表示允许

组播

单播地址标识单个IP 接口,广播地址标识某个子网的所有接口,多播地址标识一组IP接口。
单播和广播是寻址方案的两个极端(要么单个要么全部) ,多播则意在两者之间提供一种折中方案。多播数据报只应该由对它感兴趣的接口接收,也就是说由运行相应多播会话应用系统的主机上的接口接收。
另外,广播一般局限于局域网内使用,而多播则既可以用于局域网,也可以跨广域网使用。
a.组播既可以用于局域网,也可以用于广域网
b.客户端需要加入多播组,才能接收到多播的数据

组播地址
IP 多播通信必须依赖于 IP 多播地址,在IPV4 中它的范围从 224.0.0.0 到 239.255.255.255,并被划分为局部链接多播地址预留多播地址管理权限多播地址三类:
Alt text

setsockopt()

    服务器设置多播的信息,外出接口
        -leve1 : IPPROTO_IP
        -optname : IP_MULTICAST_IF
        -optval : struct in_addr
    
    客户端加入到多播组:
        - leve1 : IPPROTO_IP
        -optname : IP_ADD_MEMBERSHIP
        -optval : struct ip_mreq
    
    struct ip_mreq{
        // 组播组的IP地址
        struct in_addr imr_multiaddr ;
        // 本地某一网络设备接口的IP地址
        struct in_addr imr_address;

        int imr_ifindex; // 网卡编号
    };

    typedef uint32_t in_addr_t;
    struct in_addr{
        in_addr_t s_addr;
    };

本地套接字通信

本地套接字的作用: 本地的进程间通信
有关系的进程间的通信
没有关系的进程间的通信

服务器端

1.创建监听的套接字
    int lfd = socket(AF_UNIX/AF_LOCAL,SOCK_STREAM,0);
2 .监听的套接字绑定本地的套接字文件 -> server端
    struct sockaddr_un addr ;
    // 绑定成功之后,指定的sun_path中的套接字文件会自动生成
    bind(lfd,addr,len) ;
3. 监听
    listen(lfd,100);
4.等待并接受连接请求
    struct sockaddr_un cliaddr;
    int cfd = accept(lfd, &cliaddr , len) ;
5.通信
    接收数据: read/recv
    发送数据: write/send
6.关闭连接
    close();


客户端

1.创建通信的套接字
    int 1fd = socket(AF_UNIX/AF_LOCAL,SOCK_STREAM,0);
2 .绑定本地的套接字
    struct sockaddr_un addr ;
    // 绑定成功之后,指定的sun_path中的套接字文件会自动生成
    bind(lfd,addr,len) ;
3. 连接服务器
    struct sockaddr_un serveraddr;
    connect (fd, &serveraddr , sizeof(serveraddr)) ;
5.通信
    接收数据: read/recv
    发送数据: write/send
6.关闭连接
    close();
// 头文件: sys/un.h
#define UNIX_PATH_MAX 108
struct sockaddr_un{
    sa_family_t sun_family; // 地址族协议 af_local
    char sun_path[UNIX_PATH_MAX]; // 套接字文件的路径,这是一个伪文件,大小永远=0
};
posted @   玩世不恭xxh  阅读(118)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 25岁的心里话
点击右上角即可分享
微信分享提示