文章目录
网络结构模式
- C/S结构
-
B/S 结构(Browser/Server,浏览器/服务器模式)
协议一般是固定的:
http/https
MAC地址、IP地址、端口
-
MAC地址(OSI模型的第二层 数据链路层)
-
每一个网卡都有一个被称为 MAC 地址的独一无二的
48 位
串行号。 -
网卡的
主要功能
:1.数据的封装与解封装、2.链路管理、3.数据编码与译码。 -
MAC 地址的长度为 48 位(6个字节),通常表示为 12 个 16 进制数,如:
00-16-EA-AE-3C-40
就是一个MAC 地址,其中前 3 个字节,16 进制数 00-16-EA 代表网络硬件制造商的编号,它由
IEEE(电气与电子工程师协会)分配,而后 3 个字节,16进制数 AE-3C-40 代表该制造商所制造的某个网络产品(如网卡)的系列号。
-
IP(OSI :第三层网络层)
-
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 地址是一个 32 位的二进制数,通常被分割为
-
ABCD特殊 类网络介绍
A类网路:第一个字节是网络地址,其最高位必须为‘0’B类网络:第一、二个字节是网络地址,其从‘10’开始
C类网络:第一、二、三个字节是网络地址,其从’110‘开始
D类网络: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地址。 -
网络数和单个网段数表格
单个网段最大主机数减去2
是因为有 一个网关地址和一个广播地址
-
子网掩码subnet mask
又叫网络掩码、地址掩码、子网络遮罩,它是一种用来指明一个 IP 地
址的哪些位
标识
的是主机所在的子网,以及哪些位标识的是主机的位掩码。不能单独存在。
-
端口 (port)
虚拟端口:指计算机内部或交换机路由器内的端口,不可见,是特指TCP/IP协议中的端口。
物理端口:也称为接口,是可见端口,计算机背板的 RJ45 网口等
-
端口类型
-
周知端口(Well Known Ports)
范围从 0 到 1023,它们紧密绑定于一些特定的服务。例如
80 端口分配给 WWW 服务
,21 端口分配给 FTP 服务,23 端口分配给Telnet服务等等。 - 注册端口(Registered Ports)
- 动态端口 / 私有端口(Dynamic Ports / Private Ports)
-
周知端口(Well Known Ports)
网络七层模型
TCP/IP 四层模型
协议
封装与分用(传输用到的)
- 网络通信过程
-
ARP请求例子
简述:整个过程就是当前A主机向整个局域网的主机发送ARP请求,只有ARP请求所包含的目标IP地址与B主机IP地址相同,B主机返回一个ARP应答。
Socket通信
概述
- Socket可以抽象成网络中不同的主机之间双向通信的端点。
- socket 是由 IP 地址和端口结合的,提供向应用层进程传送数据包的机制。
- 在 Linux 环境下,用于表示进程间网络通信的特殊文件类型。本质为内核借助缓冲区形成的伪文件。
- 与管道的区别在于管道是负责主机内部的进程间通信,而socket是负责不同主机的进程间通信。
字节序
注
:在传输数据时都是转成大端字节序,若接收数据的主机是小端字节序则会通过API转换为小端字节序。
字节序转换函数
-
主机字节序转网络字节序:
htons、htonl
函数(host to net 无符号short/int) -
网络字节序转主机字节序:
ntohs、ntohl
(net to host 无符号short/int)
端口一般用 htons 、ntohs
#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地址其实是一个结构体,封装端口号和IP等信息。后面的socket相关的api中需要使用到这个 socket地址。
-
Linux 定义的 socket 地址结构体兼容PF_UNIX、PF_INET、PF_INET6
协议族
#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,由地址族确定,然后函数内部再
强制类型转化
为所需的地址类型。
- UNIX 本地域协议族使用专用的 socket 地址结构体
-
TCP/IP 协议族有
sockaddr_in
和
sockaddr_in6
两个专用的 socket 地址结构体。
所有专用 socket 地址(以及 sockaddr_storage)类型的变量在实际使用时都需要转化为通用 socket 地址类型 sockaddr(强制转化即可),因为所有 socket 编程接口使用的地址参数类型都是 sockaddr。
IP地址转换(字符串ip-整数 ,主机、网络字节序的转换)
-
点分十进制字符串
表示 IPv4 地址,以及用
十六进制字符串
表示 IPv6 地址 - 编程中我们需要先把它们转化为整数(二进制数)方能使用,而记录日志时则相反。
-
3 个函数可用于用
点分十进制字符串表示的 IPv4或者6 地址
和用
网络字节序整数表示的 IPv4或者6 地址
之间的转换
#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:转换后的结果保存在这个里面
-
返回值
为0 表示成功!
#include <arpa/inet.h>
将网络字节序的整数,转换成点分十进制的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对比
通信流程
-
服务器端 (被动接受连接的角色)
-
创建一个
用于监听的套接字
监听:监听有客户端的连接 – 套接字:这个套接字其实就是一个文件描述符 -
将这个监听文件描述符和本地的IP和端口绑定
(IP和端口就是服务器的地址信息)
客户端连接服务器的时候使用的就是这个IP和端口 -
设置监听
,监听的fd开始工作 -
阻塞等待
,当有客户端发起连接,解除阻塞,接受客户端的连接,会得到一个和客户端通信的套接字 (fd) -
通信
接收数据
发送数据 -
通信结束
,断开连接
-
客户端
-
创建一个用于
通信的套接字
(fd) -
连接服务器
,需要指定连接的服务器的 IP 和 端口 -
连接成功了
,客户端可以直接和服务器通信
接收数据
发送数据 -
通信结束
,断开连接
socket(套接字)函数
TCP三次握手、四次挥手
三次握手
TCP 提供了一种可靠、面向连接、字节流、传输层的服务,采用三次握手建立一个连接。采用 四次挥手
来关闭一个连接。
-
第一次握手
1.客户端将SYN标志位置为1
2. 客户端发送生成的32位随机序号seq =J,后面可以加数据信息。 -
第二次握手
1.服务端收到客户端请求,ACK置为1
2. 服务端会回发一个确认序号 ack = 客户端序号+数据长度 + SYN/FIN(按一个字节算)
3. 服务端向客户端发起连接请求 ,SYN = 1
4. 服务器会生成一个随机序列号发送给客户端 seq = K -
第三次握手
1.客户端应答服务器请求 ACK = 1
2. 客户端向服务器发送 ack = 服务端的序号 + 数据长度 + SYN/FIN(按一个字节算)
四次挥手
- 四次挥手发生在断开连接的时候,在程序中当调用了close()会使用TCP协议进行四次挥手。
- 客户端和服务器端都可以主动发起断开连接,谁先调用close()谁就是发起。
- 因为在TCP连接的时候,采用三次握手建立的的连接是双向的,在断开的时候需要双向断开。
TCP滑动窗口
-
滑动窗口
是 TCP 中实现诸如
ACK 确认、流量控制、拥塞控制的承载结构
-
图解
多进程实现并发服务器
-
服务端案例代码
- 父进程创建socket并绑定和设置监听
- 父进程不断循环等待连接accept
- 定义struct sigaction act,注册信号捕捉SIGHLD(子进程退出会产生的信号)
- 在父进程中创建子进程,由子进程进行TCP通信
- 关闭cfd并回收子进程
- 关闭lfd
- 注意
-
write(cfd,revbuf,strlen(revbuf)+1);
//加1是为了把 字符串终止符发送过去 以免产生bug(该bug跟字符串长短相关) - 信号捕捉流程(子进程回收)复习
//多进程服务端TCP通信
#include<stdio.h>
//字节序
#include<arpa/inet.h>
//socket通信
#include <sys/types.h>
#include <sys/socket.h>
//exit
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
//信号捕捉,子进程回收
#include<errno.h>
#include <signal.h>
#include <sys/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() {
//定义act相关参数
struct sigaction act;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
act.sa_handler = recyleChild;
//注册信号捕捉
sigaction(SIGCHLD,&act,NULL);
//创建socket
int lfd = socket(AF_INET,SOCK_STREAM,0);
if(lfd == -1) {
perror("socket");
exit(-1);
}
//绑定本机ip地址和端口
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(9999);
saddr.sin_addr.s_addr = INADDR_ANY;
int ret = bind(lfd,(struct sockaddr*)&saddr,sizeof(saddr));
if(ret == -1) {
perror("bind");
exit(-1);
}
//监听连接
ret = listen(lfd,8);
if(ret == -1) {
perror("listen");
exit(-1);
}
//循环接收客户端连接
while(1) {
struct sockaddr_in caddr;
int len = sizeof(caddr);
int cfd = accept(lfd,(struct sockaddr*)&caddr,&len);
if(cfd == -1) {
if(errno == EINTR) continue;
perror("accept");
exit(-1);
}
//创建子进程,输出客户端信息并进行通信
pid_t spid = fork();
if(spid == 0) {
//子进程
//输出客户端ip 和端口号
char cip[16];
inet_ntop(AF_INET,&caddr.sin_addr.s_addr,cip,strlen(cip));
unsigned short cport = ntohs(caddr.sin_port);
printf("Client ip is %s and port is %d\n",cip,cport);
//创建接收缓冲区
char revbuf[1024];
while(1) {
//接收客户端信息
int rlen = read(cfd,revbuf,sizeof(revbuf));
if(rlen == -1) {
perror("read");
exit(-1);
} else if(rlen > 0) {
printf("Sever have recieved :%s\n",revbuf);
} else if(rlen == 0) {
printf("client have closed..\n");
break;
}
sleep(1);
//发送信息给客户端
write(cfd,revbuf,strlen(revbuf)+1);//加1是为了把 字符串终止符发送过去 以免产生bug(该bug跟字符串长短相关)
}
//关闭客户端文件描述符
close(cfd);
//退出当前子进程
exit(0);
}
}
//关闭监听描述符
close(lfd);
return 0;
}
-
客户端案例代码
- 创建socket并绑定,连接服务端
- 通信
- 关闭fd
-
注意
write(cfd,revbuf,strlen(revbuf)+1);
//加1是为了把 字符串终止符发送过去 以免产生bug(该bug跟字符串长短相关)
//TCP通信的客户端(无多进程)
#include<stdio.h>
#include<arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
int main() {
//1创建socket
int cfd = socket(AF_INET,SOCK_STREAM,0);
if(cfd == -1) {
perror("socket");
exit(-1);
}
//2与服务端连接
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(9999);
inet_pton(AF_INET,"172.26.4.132",&saddr.sin_addr.s_addr);
int ret = connect(cfd,(struct sockaddr *)&saddr,sizeof(saddr));
if(ret == -1) {
perror("connect");
exit(-1);
}
//3通信
char revbuf[1024];
int i = 0;
while(1) {
//发送信息给服务端
sprintf(revbuf,"hello ! I am Client:%d\n",i++);
//sprintf(revbuf, "data : %d\n", i++);
write(cfd,revbuf,strlen(revbuf)+1);//加1是为了把 字符串终止符发送过去 以免产生bug(该bug跟字符串长短相关)
//i++;
//接收服务端信息
int len = read(cfd,revbuf,sizeof(revbuf));
if(len == -1) {
perror("read");
exit(-1);
} else if(len > 0) {
printf("Client have recieved :%s\n",revbuf);
} else if(len == 0) {
printf("Sever have closed..");
break;
}
}
//4关闭
close(cfd);
return 0;
}
多线程实现并发服务器
- 服务端案例代码(客户端案例代码同上此处‘’略‘’)
-
注意
1.创建sockInfo 结构体的原因、如何初始化,如何获取参数(
指针
)和分配结构体空闲元素(
for循环检测-1,倒一等待1s
)。
2. 线程创建的流程及void * (working)(void*)
3. 回收线程运用 pthread_detach(非阻塞)而不运用 pthread_join(阻塞)。
//多线程服务端TCP通信
#include<stdio.h>
//字节序
#include<arpa/inet.h>
//socket通信
#include <sys/types.h>
#include <sys/socket.h>
//exit
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
#include<pthread.h>
//创建结构体的原因: 线程处理需要获取多个参数,那么pthread_creat 的第四个参数可以作为传入,
// 但只能传入一个,所以只需传入一个结构体指针即可获得三个参数。
struct sockInfo {
int fd; // 通信的文件描述符
struct sockaddr_in addr;
pthread_t tid; // 线程号
};
struct sockInfo sockinfos[128];
//——————————————————————创建线程后执行的区域——————————————————————————————//
void * working(void* arg) {
// 子线程和客户端通信 cfd 客户端的信息 线程号
struct sockInfo * pinfo = (struct sockInfo *)arg;
//输出客户端ip 和端口号
char cip[16];
inet_ntop(AF_INET,&pinfo->addr.sin_addr.s_addr,cip,strlen(cip));
unsigned short cport = ntohs(pinfo->addr.sin_port);
printf("Client ip is %s and port is %d\n",cip,cport);
//创建接收缓冲区
char revbuf[1024];
while(1) {
//接收客户端信息
int rlen = read(pinfo->fd,revbuf,sizeof(revbuf));
if(rlen == -1) {
perror("read");
exit(-1);
} else if(rlen > 0) {
printf("Sever have recieved :%s\n",revbuf);
} else if(rlen == 0) {
printf("client have closed..\n");
break;
}
sleep(1);
//发送信息给客户端
write(pinfo->fd,revbuf,strlen(revbuf)+1);//加1是为了把 字符串终止符发送过去 以免产生bug(该bug跟字符串长短相关)
}
//关闭客户端文件描述符
close(pinfo->fd);
return NULL;
}
//——————————————————————创建线程后执行的区域——————————————————————————————//
int main() {
//创建socket
int lfd = socket(AF_INET,SOCK_STREAM,0);
if(lfd == -1) {
perror("socket");
exit(-1);
}
//绑定本机ip地址和端口
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(9999);
saddr.sin_addr.s_addr = INADDR_ANY;
int ret = bind(lfd,(struct sockaddr*)&saddr,sizeof(saddr));
if(ret == -1) {
perror("bind");
exit(-1);
}
//监听连接
ret = listen(lfd,8);
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 caddr;
int len = sizeof(caddr);
int cfd = accept(lfd,(struct sockaddr*)&caddr,&len);
if(cfd == -1) {
if(errno == EINTR) continue;
perror("accept");
exit(-1);
}
struct sockInfo * pinfo;
//查找空闲的子进程
for(int i = 0;i<max;i++) {
if(sockinfos[i].fd == -1) {
pinfo = &sockinfos[i];
break;
}
if(i == max - 1) {//当没有空闲的 让客户端等待一秒。
sleep(1);
i--;
}
}
pinfo->fd = cfd;
memcpy(&pinfo->addr, &caddr, len);//注意这里拷贝结构体的方式!
//创建子线程
pthread_create(&pinfo->tid,NULL, working, pinfo);
//子线程自动回收,不用父进程回收
//不能用另一个pthread_join的原因是该函数阻塞
pthread_detach(pinfo->tid);
}
//关闭lfd (cfd在线程中已经关闭)
close(lfd);
return 0;
}
TCP状态转换
绿线:服务端
红线:客户端
黑线:产生错误会发生的状态转换
-
2MSL(Maximum Segment Lifetime)
-
主动断开连接的一方, 最后进入一个
TIME_WAIT
状态, 这个状态会持续:
2msl
- msl: 官方建议: 2分钟, 实际是30s
-
等待2msl的原因
:
-
主动断开连接的一方, 最后进入一个
端口复用
端口复用最常用的用途
:
- 防止服务器重启时之前绑定的端口还未释放
- 程序突然退出而系统没有释放端口
setsockopt 设置端口复用(还可以设置其他功能)
#include <sys/types.h>
#include <sys/socket.h>
// 设置套接字的属性(不仅仅能设置端口复用)
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
参数:
- sockfd : 要操作的文件描述符
-
level :选项所在的协议层(level指定控制套接字的层次,可以取三种值:1)
SOL_SOCKET
:通用套接字选项.2)
IPPROTO_IP
:IP选项.3)
IPPROTO_TCP
:TCP选项. )- SOL_SOCKET (“端口复用”这一功能所在的协议层)
-
optname : 选项的名称
- SO_REUSEADDR
- SO_REUSEPORT
-
optval : 端口复用的值(整形)
- 1 : 可以复用 –
- 0 : 不可以复用
- optlen : optval参数的大小
设置端口复用的位置
- 应在bind()之前就设置
//形式参数省略
setsockopt();
bind();
IO多路复用(select、poll、epoll)
-
概念
:I/O 多路复用使得程序能同时监听多个文件描述符,能够提高程序的性能,Linux 下实现 I/O 多路复用的
系统调用主要有
select、poll 和 epoll。
select
概念
select函数详解
操作fd_set函数
fd_set 是一个文件描述符表 ,有1024位 。前0 1 2三位默认占用。
select 服务端应用案例
重点看:IO多路复用部分
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/select.h>
int main() {
// 创建socket
int lfd = socket(PF_INET, SOCK_STREAM, 0);
struct sockaddr_in saddr;
saddr.sin_port = htons(9999);
saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr = INADDR_ANY;
// 绑定
bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));
// 监听
listen(lfd, 8);
//----------------------------------IO多路复用------------------------------//
// 创建一个fd_set的集合,存放的是需要检测的文件描述符
fd_set rdset, tmp;//tmp 的目的是防止本地的fd_set 文件被内核改变 /
FD_ZERO(&rdset);
FD_SET(lfd, &rdset);
int maxfd = lfd;
while(1) {
tmp = rdset;
// 调用select系统函数,让内核帮检测哪些文件描述符有数据
int ret = select(maxfd + 1, &tmp, NULL, NULL, NULL);
if(ret == -1) {
perror("select");
exit(-1);
} else if(ret == 0) {
continue;
} else if(ret > 0) {
// 说明检测到了有文件描述符的对应的缓冲区的数据发生了改变
//判断fd对应的标志位是0还是1, 返回值 : fd对应的标志位的值,0,返回0, 1,返回1
if(FD_ISSET(lfd, &tmp)) {
// 表示有新的客户端连接进来了
struct sockaddr_in cliaddr;
int len = sizeof(cliaddr);
int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);
// 将新的文件描述符加入到集合中
FD_SET(cfd, &rdset);
// 更新最大的文件描述符
maxfd = maxfd > cfd ? maxfd : cfd;
}
//遍历内核发回来的 fd_set 接收有数据的文件描述符
for(int i = lfd + 1; i <= maxfd; i++) {//注意等于号!!!
//判断fd对应的标志位是0还是1, 返回值 : fd对应的标志位的值,0,返回0, 1,返回1
if(FD_ISSET(i, &tmp)) {
// 说明这个文件描述符对应的客户端发来了数据
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 if(len > 0) {
printf("read buf = %s\n", buf);
write(i, buf, strlen(buf) + 1);
}
}
}
}
}
//----------------------------------IO多路复用------------------------------//
close(lfd);
return 0;
}
poll
poll函数详解
poll函数操作案例
- 注意:
-
if(fds[i].revents & POLLIN) {
//注意是用”&”,而不用”==”,因为 revents 可能 有 “|” 进行拼接多个事件,双等号不能判断。 - 关闭客户端文件描述符后要对myfd 中得对应位置进行复位
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <poll.h>
int main() {
// 创建socket
int lfd = socket(PF_INET, SOCK_STREAM, 0);
struct sockaddr_in saddr;
saddr.sin_port = htons(9999);
saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr = INADDR_ANY;
// 绑定
bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));
// 监听
listen(lfd, 8);
//----------------------IO多路复用--------------------------------------------//
// 初始化检测的文件描述符数组
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);//-1表示阻塞(当有客户端接入进来才不阻塞)
if(ret == -1) {
perror("poll");
exit(-1);
} else if(ret == 0) {
continue;
} else if(ret > 0) {
// 说明检测到了有文件描述符的对应的缓冲区的数据发生了改变
if(fds[0].revents & POLLIN) {//注意是用"&",而不用"==",因为 revents 可能 有 "|" 进行拼接多个事件,双等号不能判断。
// 表示有新的客户端连接进来了
struct sockaddr_in cliaddr;
int len = sizeof(cliaddr);
int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);
// 将新的文件描述符加入到集合中
for(int i = 1; i < 1024; i++) {
if(fds[i].fd == -1) {
fds[i].fd = cfd;
fds[i].events = POLLIN;
break;
}
}
// 更新最大的文件描述符的索引
nfds = nfds > cfd ? nfds : cfd;
}
for(int i = 1; i <= nfds; i++) {
if(fds[i].revents & POLLIN) {//注意是用"&",而不用"==",因为 revents 可能 有 "|" 进行拼接多个事件,双等号不能判断。
// 说明这个文件描述符对应的客户端发来了数据
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 if(len > 0) {
printf("read buf = %s\n", buf);
write(fds[i].fd, buf, strlen(buf) + 1);
}
}
}
}
}
//----------------------IO多路复用--------------------------------------//
close(lfd);
return 0;
}
☼epoll
epoll多路复用图解
struct rb_root rbr 是红黑树,查找效率高 告诉内核需要监听的文件描述符
struct list_head rdlist 是双向链表 用于记录 文件描述符变化
相较于 select 和 poll 节省了cpu算力,提高工作效率。
epoll相关函数详解
epoll_create()
epoll_crtl
-
结构体 epoll_event
-
上面结构体内的
epoll_data_t data
-
主要用于确定类型(这里是fd 文件描述符)
-
主要用于确定类型(这里是fd 文件描述符)
-
epoll_crtl 函数详解
epoll_wait
epoll 服务端操作案例
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/epoll.h>
int main() {
//创建socket
int lfd = socket(AF_INET,SOCK_STREAM,0);
if(lfd == -1) {
perror("socket");
exit(-1);
}
//绑定
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(9999);
saddr.sin_addr.s_addr = INADDR_ANY;
int len = sizeof(saddr);
int ret = bind(lfd,(struct sockaddr*)&saddr,len);
if(ret == -1) {
perror("bind");
exit(-1);
}
//监听
ret = listen(lfd,8);
if(ret == -1) {
perror("listen");
exit(-1);
}
//-----------------------------------------------------IO多路复用-----------------------------------------------------------------------//
// 调用epoll_create()创建一个epoll实例
int epfd = epoll_create(100); // 参数:size : 目前没有意义了。随便写一个数,必须大于0 - 返回值: -1 : 失败 > 0 : 文件描述符,操作epoll实例的
if(epfd == -1) {
perror("epollCreat");
exit(-1);
}
struct epoll_event epev;
epev.events = EPOLLIN;
epev.data.fd = lfd;
int ret_epc = epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &epev);
if(ret_epc== -1) {
perror("epoll_ctl");
exit(-1);
}
struct epoll_event epevs[1024];//保存了发送了变化的文件描述符的信息
while(1) {
int ret_wat = epoll_wait(epfd,epevs,1024,-1);
if(ret_wat== -1) {
perror("epoll_wait");
exit(-1);
}
printf("ret_wat = %d\n", ret_wat);//输出当前正在操作的客户端数
//遍历查找有变化的文件描述符
for(int i = 0 ;i < ret_wat;i++) {
int cur_fd = epevs[i].data.fd;
if(cur_fd == lfd) {
//检测到客户端连接进来;
struct sockaddr_in cliaddr;
int len = sizeof(cliaddr);
int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);
if(cfd== -1) {
perror("accept");
exit(-1);
}
//设置对应的客户端信息
epev.events = EPOLLIN;
epev.data.fd = cfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &epev);//将新的文件描述符添加到epev。
} else {
if(epevs[i].events & EPOLLOUT) {//不同事件不同的处理 此处略;EPOLLOUT是输出事件,服务端发送给客户端。
continue;
}
// 有数据到达,需要通信
char buf[1024] = {0};
int len = read(cur_fd, buf, sizeof(buf));
if(len == -1) {
perror("read");
exit(-1);
} else if(len == 0) {
printf("client closed...\n");
//需要在内核先删除当前文件描述符 再关闭,最后一个参数可以是NULL
epoll_ctl(epfd, EPOLL_CTL_DEL, cur_fd, NULL);
close(cur_fd);
} else if(len > 0) {
printf("read buf = %s\n", buf);
write(cur_fd, buf, strlen(buf) + 1);
}
}
}
}
//-----------------------------------------------------IO多路复用-----------------------------------------------------------------------//
close(epfd);
close(lfd);
return 0;
}
❁epoll 工作模式
-
LT
LT服务端示例代码
- 相较于之前的epoll服务端代码 ,读缓冲区大小改为 5
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/epoll.h>
int main() {
// 创建socket
int lfd = socket(PF_INET, SOCK_STREAM, 0);
struct sockaddr_in saddr;
saddr.sin_port = htons(9999);
saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr = INADDR_ANY;
// 绑定
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 cliaddr;
int len = sizeof(cliaddr);
int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);
epev.events = EPOLLIN;
epev.data.fd = cfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &epev);
} else {
if(epevs[i].events & EPOLLOUT) {
continue;
}
// 有数据到达,需要通信
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 if(len > 0) {
printf("read buf = %s\n", buf);
write(curfd, buf, strlen(buf) + 1);
}
}
}
}
close(lfd);
close(epfd);
return 0;
}
-
ET
ET服务端示例代码
- 如何设置ET?
- read函数如何设置非阻塞?使得服务端可以不因为缓冲区小而读不全
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.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_port = htons(9999);
saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr = INADDR_ANY;
// 绑定
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 cliaddr;
int len = sizeof(cliaddr);
int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &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(STDOUT_FILENO, buf, len);
write(curfd, buf, len);
}
if(len == 0) {
printf("client closed....");
}else if(len == -1) {
if(errno == EAGAIN) {
printf("data over.....");
}else {
perror("read");
exit(-1);
}
}
}
}
}
close(lfd);
close(epfd);
return 0;
}
-
设置 ET模式的关键字
UDP通信
读写函数详解
UDP通信案例
-
服务端
与TCP通信的区别
-
int fd = socket(PF_INET, SOCK_DGRAM, 0);
这里是
SOCK_DGRAM
数据报格式,与tcp通信不同! -
没有添加监听listen
- 通信api也不同
-
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
int main() {
// 1.创建一个通信的socket(这里是SOCK_DGRAM数据报格式!!!)
int fd = socket(PF_INET, SOCK_DGRAM, 0);
if(fd == -1) {
perror("socket");
exit(-1);
}
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(9999);
addr.sin_addr.s_addr = INADDR_ANY;
// 2.绑定
int ret = bind(fd, (struct sockaddr *)&addr, sizeof(addr));
if(ret == -1) {
perror("bind");
exit(-1);
}
// 3.通信
while(1) {
char recvbuf[128];
char ipbuf[16];
struct sockaddr_in cliaddr;
int len = sizeof(cliaddr);
// 接收数据
int num = recvfrom(fd, recvbuf, sizeof(recvbuf), 0, (struct sockaddr *)&cliaddr, &len);
printf("client IP : %s, Port : %d\n",
inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, ipbuf, sizeof(ipbuf)),
ntohs(cliaddr.sin_port));
printf("client say : %s\n", recvbuf);
// 发送数据
sendto(fd, recvbuf, strlen(recvbuf) + 1, 0, (struct sockaddr *)&cliaddr, sizeof(cliaddr));
}
close(fd);
return 0;
}
-
客户端
与TCP通信的区别
-
int fd = socket(PF_INET, SOCK_DGRAM, 0);
这里是
SOCK_DGRAM
数据报格式,与tcp通信不同! -
没有调用connect 函数连接服务器,只是在本地存有服务器ip和端口,通过sendto 直接发送
- 通信API 不同
-
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.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));
// 接收数据
int num = recvfrom(fd, sendBuf, sizeof(sendBuf), 0, NULL, NULL);
printf("server say : %s\n", sendBuf);
sleep(1);
}
close(fd);
return 0;
}
广播
setsockapt详解
广播通信案例
-
服务端
与TCP相比
-
int fd = socket(PF_INET, SOCK_DGRAM, 0);
这里是
SOCK_DGRAM
数据报格式。没有设置监听 、绑定 - 需要设置广播属性 setsockopt,并创建一个广播地址
- 通信API不同
-
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
int main() {
//创建socket 注意参数
int lfd = socket(PF_INET,SOCK_DGRAM,0);
if(lfd == -1) {
perror("socket");
exit(-1);
}
//设置广播属性
int op;
setsockopt(lfd,SOL_SOCKET,SO_BROADCAST,&op,sizeof(op));
//创建一个广播地址(不用绑定ip)
struct sockaddr_in addr;
//x.x.x.255为广播地址
inet_pton(AF_INET,"172.26.4.255",&addr.sin_addr.s_addr);
addr.sin_family = AF_INET;
addr.sin_port = htons(9999);
int num = 0;
while (1) {
//发送数据
char sendbuf[128];
sprintf(sendbuf,"Hello!Client..%d\n",num++);
sendto(lfd,sendbuf,strlen(sendbuf) + 1,0,(struct sockaddr*)&addr,sizeof(addr));
printf("广播数据:%s", sendbuf);
sleep(1);
}
close(lfd);
return 0;
}
-
客户端
与TCP相比
-
int fd = socket(PF_INET, SOCK_DGRAM, 0);
这里是
SOCK_DGRAM
数据报格式。 -
需要设置对应服务器信息并绑定
广播使用的端口(这里是9999)
,而TCP客户端不用调用bind,只需设置服务器ip和端口 - 通信API不同
-
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
int main() {
//创建socket 注意参数
int lfd = socket(PF_INET,SOCK_DGRAM,0);
if(lfd == -1) {
perror("socket");
exit(-1);
}
//绑定广播地址
struct sockaddr_in addr;
addr.sin_addr.s_addr = INADDR_ANY;
addr.sin_family = AF_INET;
addr.sin_port = htons(9999);
int ret = bind(lfd,(struct sockaddr*)&addr,sizeof(addr));
if(ret == -1) {
perror("bind");
exit(-1);
}
while (1) {
//接收广播数据
char revbuf[128];
recvfrom(lfd,revbuf,sizeof(revbuf),0,NULL,NULL);
printf("server send:%s",revbuf);
}
close(lfd);
return 0;
}
多播
setsockopt 设置组播
- 结构体imeq :保存多播地址ip 和本地ip
- level: IPPROTO_IP (协议族)
-
optname :IP_MULTICAST_IF(表示多播)
多播通信案例
-
服务端
- 设置多播属性,设置外出接口 ,没有绑定
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.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 in_addr imr_multiaddr;
// 初始化多播地址
inet_pton(AF_INET, "239.0.0.10", &imr_multiaddr.s_addr);
setsockopt(fd, IPPROTO_IP, IP_MULTICAST_IF, &imr_multiaddr, sizeof(imr_multiaddr));
// 3.初始化客户端的地址信息
struct sockaddr_in cliaddr;
cliaddr.sin_family = AF_INET;
cliaddr.sin_port = htons(9999);
inet_pton(AF_INET, "239.0.0.10", &cliaddr.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 *)&cliaddr, sizeof(cliaddr));
printf("组播的数据:%s\n", sendBuf);
sleep(1);
}
close(fd);
return 0;
}
-
客户端
- 设置多播地址、加入到多播组
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.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 in_addr in;
// 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);
}
//设置多播地址属性
struct ip_mreq op;
inet_pton(AF_INET, "239.0.0.10", &op.imr_multiaddr.s_addr);
op.imr_interface.s_addr = INADDR_ANY;
// 加入到多播组
setsockopt(fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &op, sizeof(op));
// 3.通信
while(1) {
char buf[128];
// 接收数据
int num = recvfrom(fd, buf, sizeof(buf), 0, NULL, NULL);
printf("server say : %s\n", buf);
}
close(fd);
return 0;
}
本地套接字通信
创建通信流程(服务端、客户端 )
-
注意创建socket的参数设置:
AF_LOCAL 、 SOCK_STREAM
-
绑定成功之后,指定的sun_path中的套接字文件(server.sock)会
自动生成。
-
要用strcpy 给套接字文件命名
strcpy(addr.sun_path, "server.sock")
。因为数组名是指针常量,是不能被修改的。
本地套接字通信案例
-
服务端
strcpy(addr.sun_path, “server.sock”);
#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);
}
// 2.绑定本地套接字文件
struct sockaddr_un addr;
addr.sun_family = AF_LOCAL;
strcpy(addr.sun_path, "server.sock");
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 cliaddr;
int len = sizeof(cliaddr);
int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);
if(cfd == -1) {
perror("accept");
exit(-1);
}
printf("client socket filename: %s\n", cliaddr.sun_path);
// 5.通信
while(1) {
char buf[128];
int len = recv(cfd, buf, sizeof(buf), 0);
if(len == -1) {
perror("recv");
exit(-1);
} else if(len == 0) {
printf("client closed....\n");
break;
} else if(len > 0) {
printf("client say : %s\n", buf);
send(cfd, buf, len, 0);
}
}
close(cfd);
close(lfd);
return 0;
}
-
客户端
strcpy(addr.sun_path, “client.sock”);
#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);
}
// 2.绑定本地套接字文件
struct sockaddr_un addr;
addr.sun_family = AF_LOCAL;
strcpy(addr.sun_path, "client.sock");
int ret = bind(cfd, (struct sockaddr *)&addr, sizeof(addr));
if(ret == -1) {
perror("bind");
exit(-1);
}
// 3.连接服务器
struct sockaddr_un seraddr;
seraddr.sun_family = AF_LOCAL;
strcpy(seraddr.sun_path, "server.sock");
ret = connect(cfd, (struct sockaddr *)&seraddr, sizeof(seraddr));
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);
printf("client say : %s\n", buf);
// 接收数据
int len = recv(cfd, buf, sizeof(buf), 0);
if(len == -1) {
perror("recv");
exit(-1);
} else if(len == 0) {
printf("server closed....\n");
break;
} else if(len > 0) {
printf("server say : %s\n", buf);
}
sleep(1);
}
close(cfd);
return 0;
}
阻塞与非阻塞、同步与异步(网络IO)
-
典型的一次IO的两个阶段是什么?数据就绪 和 数据读写
数据就绪
:根据系统IO操作的就绪状态,
分为阻塞、非阻塞
。如(read函数调用,非阻塞状态要通过返回值去判断)
数据读写
:根据应用程序和内核交互的方式,
分为同步、异步
。(异步api:aio_read(), aio_write()) -
如何区分同步和异步?
同步
:表示A向B请求调用一个网络IO接口时(或者调用某个业务逻辑API接口时),数据的读写都是
由请求方A自己来完成的(不管是阻塞还是非阻塞);
异步
:表示A向B请求调用一个网络IO接口时(或者调用某个业务逻辑API接口时),向B传入请求的事件以及事件发生时通知的方式,A就可以处理其它逻辑了,当B监听到事件处理完成后,会用事先约定好的通知方式,通知A处理结果。- 在处理 IO 的时候,阻塞和非阻塞都是同步 IO,只有使用了特殊的 API 才是异步 IO。
- 或者说自己应用程序处理是同步,交给内核去处理,自己应用程序往下执行,等待内核传递对应信号是异步
Unix、Linux上的五种IO模型
阻塞、非阻塞模型、IO复用、信号驱动、异步IO模型
阻塞
调用者调用了某个函数,等待这个函数返回,期间什么也不做,不停的去检查这个函数有没有返回,必
须等这个函数返回才能进行下一步动作。
非阻塞
非阻塞等待,每隔一段时间就去检测IO事件是否就绪。没有就绪就可以做其他事。非阻塞I/O执行系统调
用总是立即返回,不管事件是否已经发生,若事件没有发生,则返回-1,此时可以根据
errno 区分
这两种情况,对于accept,recv 和 send,事件未发生时,errno 通常被设置成
EAGAIN
。
IO复用
Linux 用 select/poll/epoll 函数实现 IO 复用模型,这些函数也会使进程阻塞,但是和阻塞IO所不同的是
这些函数可以同时阻塞多个IO操作。而且可以同时对多个读操作、写操作的IO函数进行检测。直到有数
据可读或可写时,才真正调用IO操作函数。
信号驱动
Linux 用套接口进行信号驱动 IO,安装一个信号处理函数,进程继续运行并不阻塞,当IO事件就绪,进
程收到SIGIO 信号,然后处理 IO 事件。
内核在第一个阶段是异步,在第二个阶段是同步;
与非阻塞IO的区别
在于它提供了消息通知机制,不需
要用户进程不断的轮询检查,减少了系统API的调用次数,提高了效率。
异步
Linux中,可以调用 aio_read 函数告诉内核描述字缓冲区指针和缓冲区的大小、文件偏移及通知的方
式,然后立即返回,当内核将数据拷贝到缓冲区后,再通知应用程序。