分享

解密异步IO:使用C++进行高效的网络编程

 深度Linux 2023-10-09 发布于湖南

计算机网络计算机网络是指将地理位置不同的具有独立功能的多台计算机及其外部设备,通过通信线路连接起来,在网络操作系统,网络管理软件及网络通信协议的管理和协调下,实现资源共享和信息传递的计算机系统。网络编程的目的:传播交流选项,数据交换,通信。

一、软件开发架构

编写项目之前需要遵循的代码层面上的一些规范(例如,运行的步骤,流程等)

c/s架构

c:client 客户端
s:server 服务端
计算机或手机下载的各类app本质上就是客户端
优势是:下载对应的客户端,可以在客户端体验高度定制服务
劣势是:使用必须下载!!

b/s架构

b:broswer 浏览器
s:server 服务器
通过浏览器来充当各个服务器的客户端,用于想要体验服务又不用下载客户端
劣势是:网页的各种功能花里胡哨,观看效果不佳

架构发展趋势

1.统一接口原则
(可以通过一个接口去往其他程序)
2.cs和bs交叉使用,避免各自的劣势

二、网络编程

基于网络编写代码,程序可以实现远程数据交互。

2.1网络编程的起源

目的:为了解决计算机之间远程数据交互
网络编程的意义:学习完就可以编写cs架构
网络编程的起源:任何先进的技术一般都来源于军事
网络编程的要求:计算机之间想要实现远程数据交互,首要条件就是要有物理连接介质

2.2网络通信的要素

信双方的地址:
ip
端口号
192.168.XXX.XXX :端口
规则:网络通信协议

OSI七层协议

规定了计算机涉及到远程交互的时候必须要经过相同的流程
数据发送出去的时候 是从上往下走
数据接收回来的时候 是从下往上走

应用层

主要是程序员自己编写代码的地方,有什么协议和规范取决于程序员自己
常见协议有:HTTP,HTTPS等等

表示层

会话层

传输层

1、port协议

端口协议:规定了一台计算机上的每一个正在运行的app都必须要有一个端口号
端口号相当于计算机用于管理多个app的标记

端口号特征:
1.端口号范围:0-65535
2.端口号是动态分配的
3.同一时间同一台计算机的端口号是不能冲突的
4.
0-1024:一般是操作系统内部需要使用的
1024-8000:一般是常见的软件已经使用了
8000+:我们平时写代码可以使用8000之后的端口号

IP:用于标识全世界任意一台接入互联网的计算机
PORT:用于标识一台计算机上的某个app
IP+PORT:用于标识任意一台接入互联网的计算机上的某个app、

网址(URL):统一资源定位符
URL的本质就是IP+PORT

2.TCP协议与UDP协议
规定了数据传输所遵循的规则
ps:数据传输能够遵循的协议有很多,TCP协议与UDP协议是比较常见的两个

2、TCP协议

1.三次握手 (建连接)

1.三次握手 (建连接)建立双向通道 
在发数据前必须和对方建立可靠的连接,连接必须要经过三次,对话才能建立起来
eg图1:
c(表示客户端)s(表示服务端)
第一次握手:是c朝s发出请求,请求可以建立连接
第二次握手:是s收到c的请求,并发出可以建立连接的信号
(此时的数据通道是单向的,只有c可以向s发送数据)
第三次握手:是c朝s发送确认,并允许s和自己建立连接,双方建立连接,c和s可以传输数据了
(此时双方都可以基于彼此建立的连接互相发送数据了)

ps:基于tcp传输数据很安全的原因是TCP会二次确认,数据不容易丢失。每次发送出去的消息都必须要返回确认消息,否则的话再短时间内会反复发送

洪水攻击:同时有多个客户端向服务端发送TCP连接请求!

2.四次挥手 (断连接)

基于三次挥手之后,想要断开连接
eg下图:
第一次挥手:c朝s发起请求,想要断开连接

第二次挥手:s向c发起确认,允许断开连接
(此时的c已经不可以向s发送数据了 ,但是s还是可以给c发送数据)
从第二次到第三次挥手之间有一个时间间隔,s可能还需要有接受数据的时间

第三次挥手:s朝c发起请求,想要断开连接

第四次挥手:c向s发起确认,允许断开连接
(此时的双方都不可以再互相发送数据了)

3.UDP协议

基于UDP发送的数据没有任何通道也没有任何的限制,缺点是数据容易丢失
(可以基于发送过程做一些优化操作)
服务端代码
import socket

res = socket.socket(type=socket.SOCK_DGRAM)
res.bind(('127.0.0.1',8080))

msg,address = res.recvfrom(1024)
# 接收客户端发送过来的消息
print('msg>>>%s'%msg.decode('utf8'))
# 由于UDP没有双向通道 所以每次发送消息都会带着它的地址
print('address>>>>:',address)
res.sendto('服务端'.encode('utf8'),address)


客户端代码
import socket

c = socket.socket(type=socket.SOCK_DGRAM)
server_address = ('127.0.0.1',8080)
c.sendto('客户端'.encode('utf8'),server_address)
msg,address = c.recvfrom(1024)
print('msg>>>"%s'%msg.decode('utf8'))
print('address>>>>>:',address)

2.3网络层

ip协议
:规定了任何接入互联网的计算机都必须有IP地址
每个ip地址都自带定位
IP地址:
IPV4:点分十进制
最小:0.0.0.0
最大:255.255.255.255
IPV6:
无限大了 能够给地球上的每一粒沙子都分一个ip地址

IP

查询IP地址方法:InetAddress类 http://java.net包下的

IP地址能定位到唯一的一台计算机
127.0.0.1 本机地址
ip地址分类
IPV4/IPV6
IPV4 127.0.0.1 4个 字节组成 , 0-255 总共42亿 已用尽
IPV6 128位 8个无符号整数(16进制)
例如 0000:1111:2222:3456:aaaa:bbbb:cccc:defg
公网(互联网)-私网(局域网)


ABCD类
192.168.xxx.xxx

InetAddress类常用方法

package com.taodou;

import java.net.InetAddress;
import java.net.UnknownHostException;

public class TestInetAddress {
public static void main(String[] args) {

try {
//查询本机地址
InetAddress inetAddress1 = InetAddress.getByName("127.0.0.1");
InetAddress inetAddress2 = InetAddress.getByName("LocalHost");
InetAddress inetAddress3 = InetAddress.getLocalHost();
System.out.println(inetAddress1);
System.out.println(inetAddress2);
System.out.println(inetAddress3);

System.out.println("------------------------------------");

//查询网址IP地址
InetAddress inetAddress4 = InetAddress.getByName("www.baidu.com");
System.out.println(inetAddress4);

System.out.println(inetAddress4.getCanonicalHostName());//规范名
System.out.println(inetAddress4.getHostAddress());//IP
System.out.println(inetAddress4.getHostName());//域名或主机名

} catch (UnknownHostException e) {
e.printStackTrace();
}

}

}

端口

端口表示计算机上的一个程序的进程

不同的进程有不同的端口号
端口号 0~65536
端口号有两种TCP,UDP 65536*2 单协议下端口号不能冲突
端口分类
公有端口(系统程序)0~1023
HTTP:80
HTTPS:443
FTP:21
程序注册端口1024~49151
Tomcat : 8080
MySQL:3306
动态,私有 49152~65536
package com.taodou;

import java.net.InetSocketAddress;

public class TestInetSocketAddress {
public static void main(String[] args) {
InetSocketAddress inetSocketAddress1 = new InetSocketAddress("127.0.0.1",8080);
InetSocketAddress inetSocketAddress2 = new InetSocketAddress("LocalHost",8080);
System.out.println(inetSocketAddress1);
System.out.println(inetSocketAddress2);


System.out.println(inetSocketAddress1.getAddress());
System.out.println(inetSocketAddress1.getHostName());
System.out.println(inetSocketAddress1.getPort());
}
}

数据链路层

1.规定了电信号的分组方式
2.规定了每台计算机都必须有一块网卡,网卡上必须有一串记录(电脑的以太网址也称mac地址,身份证号)
3.mac地址:由12位16进制数组成
前6位:厂商编号
后6位:生产流水线号
可以根据该地址查找到计算机,基于mac地址实现数据交互

物理连接层

保证物理连接介质的条件,传递电信号(主要研究插网线情况)

网络相关设备

1.交换机
能够让接入交换机的多台计算机实现彼此互联
2.以太网通信(mac通信)
有了交换机以后,根据电脑的mac地址就可以实现数据交互
广播:就是在交换机中发出请求,所有接入交换机的设备都能收到
单播:只有被查找设备才会回复相应信息

缺陷:1.mac地址通信仅限于局域网
2.接入交换机的设备过多的话 可能会造成广播风暴

3.局域网
有某个固定的区域组成的网络

4.路由器
将多个局域网连接到一起的设备

三、socket套接字

主要作用于软件层和应用层中间 ,作为一个接口。如果没有socket,那么我们就需要手动操作各个层之间的代码了!!

3.1发展史

1.一开始,套接字被设计用在同一台主机上多个应用程序之间的通讯。这也被称进程间通讯,或IPC。套接字有两种(或者称为有两个种族),分别是基于文件型的和基于网络型的。
2.基于文件类型的套接字家族:套接字家族的名字:AF_UNIX
3.基于网络类型的套接字家族:套接字家族的名字:AF_INET,所有地址家族中,AF_INET是使用最广泛的一个

socket实例

先运行服务端再运行客户端
1.服务端
import socket

# 1.先创建一个socket对象
server = socket.socket()
# 绑定一个固定的地址IP和port
server.bind(('192.168.1.169',8080))
# 半连接池
server.listen(5)
# 开业等待接口
sock,address = server.accept()
print(sock,address) # sock是双向通道 address是客户端地址65459
# 数据交互
sock.send(b'hello') # 朝客户端发送数据
res=sock.recv(1024) # 接收客户端发送的数据
print(res)
# 断开连接
sock.close() # 断连接
server.close() # 关机

------------
2.客户端
import socket

# 1.产生一个socket对象
client = socket.socket()
# 2.连接服务端(拼接服务端的ip和port)
client.connect(('192.168.1.169', 8080))
# 3.数据交互
res=client.recv(1024) # 接收服务端发送的数据
print(res)
client.send(b'hei') # 朝服务端发送数据
# 关闭
client.close() # 直接把双向通道关闭

代码优化

1.send与recv
客户端与服务端不能同时执行同一个
有一个收 另外一个就是发
有一个发 另外一个就是收
不能同时收或者发!!!
2.消息自定义
input获取用户数据即可(主要编码解码)
3.循环通信
给数据交互环节添加循环即可
4.服务端能够持续提供服务
不会因为客户端断开连接而报错
异常捕获 一旦客户端断开连接 服务端结束通信循环 调到连接处等待
5.消息不能为空
判断是否为空 如果是则重新输入(主要针对客户端)
# 服务端
import socket

# 1.先创建一个socket对象
server = socket.socket()
# 绑定一个固定的地址IP和port
server.bind(('192.168.1.169',8080))
# 半连接池
server.listen(5)
# 开业等待接口
while True:
sock,address = server.accept()
print(sock,address) # sock是双向通道 address是客户端地址65459
# 数据交互
while True:
try:
msg = input('请输入你要发送的内容》》》:').strip()
if len(msg) == 0:
continue
sock.send(msg.encode('utf8')) # 朝客户端发送数据
res = sock.recv(1024) # 接收客户端发送的数据
print(res.decode('utf8'))
except ConnectionResetError:
break
# 客户端
import socket

# 1.产生一个socket对象
client = socket.socket()
# 2.连接服务端(拼接服务端的ip和port)
client.connect(('192.168.1.169', 8080))
# 3.数据交互
while True:
res = client.recv(1024) # 接收服务端发送的数据
print(res.decode('utf8'))
msg = input('请输入你要发送的内容》》》》:').strip()
if len(msg) == 0:
msg = '重来重来!!!'
client.send(msg.encode('utf8')) # 朝服务端发送数据

3.2半连接池

server.listen(5)
# 括号里的数字就是等待和服务端连接数据的客户端
这个功能主要是为了优化代码,避免无效等待
假设 现在有数不清的客户端想向服务端发数据 ,如果没有这个半连接池的话,是不是所有的用户都要等在外面 让服务端和上一个客户端交互完成再接着下一个,那么这个半连接池就是再告诉后面排队等待的用户 你的前面还有几个人再等着 你可以现在干别的事情,避免无效等待!!

3.3粘包现象

粘包问题的产生

1.TCP特性
流式协议:所有的数据类似于水流 连接在一起的
ps:数据量很小 并且时间间隔很多 那么就会自动组织到一起
2.recv
我们不知道即将要接收的数据量多大 如果知道的话不会产生黏包

代码实例

# 服务端
import socket

s = socket.socket()
s.bind(('192.168.1.169',8080))
s.listen(5)
sock,address =s.accept()
sock.send(b'hello')
sock.send(b'hai')
sock.send(b'hahaha')


------------


# 客户端
import socket

res = socket.socket()
res.connect(('192.168.1.169', 8080))
c = res.recv(1024)
print(c.decode('utf8'))
c = res.recv(1024)
print(c.decode('utf8'))
c = res.recv(1024)
print(c.decode('utf8'))


打印结果:
hello
haihahaha # 出现了粘包现象

3.4struct模块

简介

struct模块无论数据长度是多少 都可以帮你打包成固定长度
然后基于该固定长度 还可以反向解析出真实长度
ps : 对于数据量特别大的模块,会直接报错

粘包问题的解决方案

# 服务端
1.先将真实数据的长度制作成固定长度 4
2.先发送固定长度的报头
3.再发送真实数据

# 客户端
1.先接收固定长度的报头 4
2.再根据报头解压出真实长度
3.根据真实长度接收即可

代码实操

服务端
1.先构造一个详细的数据字典
2.对字典数据进行打包,获得一个打包之后的固定长度
3.发送打包以后的数据给客户端
4.将字典数据发送给客户端
4.发送真实数据给客户端
import os
import socket
import struct
import json

res = socket.socket()
res.bind(('192.168.1.169', 8080))
res.listen(5)

while True:
sock, address = res.accept()
while True:
# 1.先构建数据文件的字典
file_dict = {
'file_name': '视频合集',
'file_size': os.path.getsize(r'视频合集'),
'file_content': 'python课程',
'file_root': 'summer'

}
# 2.将文件字典打包成固定长度数据
dict_json = json.dumps(file_dict)
file_bytes_dict = len(dict_json.encode('utf8'))
dict_len = struct.pack('i',file_bytes_dict )
# 3.发送固定字典的长度
sock.send(dict_len)
# 4.发送真实字典数据 (先把字典转为json格式 再发送 )
res = json.dumps(file_dict)
sock.send(res.encode('utf8'))
# 5.发送真实数据
with open(r'视频合集','rb')as f:
for line in f:
sock.send(line)
break
客户端
1.先接收固定长度的数据
2.根据固定长度解析出即将要接收的字典的真实长度
3.接收字典数据
4.根据字典长度获取真实字典的数据长度
5.接收真实长度
import json
import socket
import struct


s = socket.socket()
s.connect(('192.168.1.169', 8080))


# 1.先接收固定长度的数据报头
res = s.recv(4)
# 2.根据报头解析出字典的长度
dict_len = struct.unpack('i',res)[0]
# 3.接收字典数据
dict_data = s.recv(dict_len)
# 4.解码并反序列化出字典
real_dict = json.loads(dict_data)
print(real_dict)
# 5.从数据字典中获取真实数据的各项信息
total_size = real_dict.get('file_size')
# 获取数据 由于获取到的是一个文件 必须先读出来再循环打印
# file_size = 0
# with open(r'%s'%real_dict.get('file_name'),'wb')as f:
# while file_size < total_size:
# data = s.recv(1024)
# f.write(data)
# file_size += len(data)
# print('文件接收完毕')
# break
# 这里也可以直接接收具体字节数 因为我们前面已经知道他这个文件到底有多大了嘛 就不需要写的这么复杂
dict1 = s.recv(75)
print(dict1.decode('utf8'))
dict1 = s.recv(75)
print(dict1.decode('utf8'))
dict1 = s.recv(75)
print(dict1.decode('utf8'))

代码运行结果:

四、为什么要有异步I/O

异步IO相比同步IO不会阻塞当前程序的执行,可以继续向下执行。即当应用程序发起一个IO操作后,调用者不会立刻得到结果,而是在内核完成IO操作后,通过信号或回调来通知调用者。

4.1信号驱动I/O

信号驱动IO是异步IO的一种实现,在异步IO中,当文件描述符上可以执行I/O操作时,进程可以请求内核为自己发送一个信号。之后进程就可以执行任何其他任务直到文件描述符可以执行I/O操作为止,此时内核会发送信号给进程。

使用信号驱动,程序需要按照如下步骤执行:

  • 通过指定O_NONBLOCK标志使能非阻塞I/O

  • 通过制定O_ASYNC标志使能异步I/O

  • 通过设置异步I/O时间的接收进程。当文件描述符上可执行I/O操作时会发送信号通知该进程。

  • 为内核发送的通知信号注册一个信号处理函数。异步信号I/O缺省是SIGIO,所以内核会给进程发送信号SIGIO。

以上步骤完成后,进程可以去执行其他的任务,当I/O就绪时,内核会向进程发送一个SIGIO信号,当进程接收到信号时,会执行预先注册号的信号处理函数,这样就可以在信号处理函数中进行I/O操作了

使能O_ASYNC

调用open时无法通过指定O_ASYNC标志来使能异步I/O,但是可以通过fcntl()函数添加O_ASYNC标志来使能I/O:

int flag;

flag = fcntl(fd,F_GETFL); // 先从打开的文件描述符中获取原来的flag
flag |= O_ASYNC; // 将O_ASYNC标志添加到flag
fcntl(fd,F_SETFL,flag); // 重新设置flag

设置异步I/O时间的接收过程

为文件描述符设置异步I/O时间的接收进程,也就是设置异步I/O的所有者:

fcntl(fd,F_SETOWN,getpid());   // 也可以传入其他进程的pid

注册SIGIO信号的处理函数

通过signal()或sigaction()函数为SIGIO信号注册一个信号处理函数,当进程接收到内核发送过来的SIGIO信号时,会执行该函数。

代码实例:

#define _GNU_SOURCE // F_SETSIG
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <signal.h>

#define MOUSE "/dev/input/mouse0"

static int fd;

static void sigio_handler(int sig)
{
static int loops = 5;
char buf[100] = {0};
int ret;

if(SIGIO != sig)
{
return ;
}

ret = read(fd,buf,sizeof(buf));
if(0 < ret)
printf("mouse : read %d bytes\n",ret);

loops--;
if(0>=loops)
{
close(fd);
exit(0);
}
}

int main(void)
{
int flag;

// 打开设备,使能非阻塞IO
fd = open(MOUSE,O_RDONLY|O_NONBLOCK);
if(-1 == fd)
{
perror("open mouse error");
exit(-1);
}

// 使能异步IO
flag = fcntl(fd,F_GETFL);
flag |= O_ASYNC;
fcntl(fd,F_SETFL,flag);

// 设置异步IO的所有者
fcntl(fd,F_SETOWN,getpid());

// 注册信号回调函数
signal(SIGIO,sigio_handler);

for(;;)
{
sleep(1);
}
}

运行结果:

但是使用默认信号SIGIO会存在一些问题,SIGIO是标准信号,不可靠信号,非实时信号,不支持信号排队机制,不知道文描述符发生了什么事件,未判断文件描述符是否处于可读的就绪态,所以需要进一步优化(实时信号替换)。

1.使用实时信号替换默认信号SIGIO

比如使用SIGRTMIN信号替换SIGIO,比如:

fcntl(fd,F_SETSIG,SIGRTMIN);

2.使用sigaction()函数注册信号处理函数

在应用程序中需要为实时信号注册信号处理函数,使用sigaction函数进行注册,sigaction原型:

#include <signal.h>

int sigaction(int signum,const struct sigaction *act,struct sigaction *oldact);

使用实例:

#define _GNU_SOURCE // F_SETSIG
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <signal.h>

#define MOUSE "/dev/input/mouse0"

static int fd;

static void io_handler(int sig,siginfo_t *info,void *context)
{
static int loops = 5;
char buf[100] = {0};
int ret;

if(SIGRTMIN != sig)
{
return ;
}

// 判断鼠标是否可读
if(POLL_IN == info->si_code)
{
ret = read(fd,buf,sizeof(buf));
if(0 < ret)
{
printf("mouse : read %d bytes\n",ret);
}

loops--;
if(0>=loops)
{
close(fd);
exit(0);
}
}
}

int main(void)
{
struct sigaction act;
int flag;

// 打开设备,使能非阻塞IO
fd = open(MOUSE,O_RDONLY|O_NONBLOCK);
if(-1 == fd)
{
perror("open mouse error");
exit(-1);
}

// 使能异步IO
flag = fcntl(fd,F_GETFL);
flag |= O_ASYNC;
fcntl(fd,F_SETFL,flag);

// 设置异步IO的所有者
fcntl(fd,F_SETOWN,getpid());

// 指定实时信号SIGRTMIN作为异步I/O通知信号
fcntl(fd,F_SETSIG,SIGRTMIN);

// 为实时信号SIGRTMIN注册信号处理函数
act.sa_sigaction = io_handler;
act.sa_flags = SA_SIGINFO;
sigemptyset(&act.sa_mask);
sigaction(SIGRTMIN,&act,NULL);

for(;;)
{
sleep(1);
}
}

运行结果:

4.2Linux异步I/O - Native AIO

Linux Native AIO是Linux支持的原生AIO,很多第三方的异步IO库,比如libeio和glibc AIO。很多三方库异步IO库不是真正的异步IO,而是通过多线程来模拟异步IO,比如libeio。

aio_*系列的调用是有glibc提供的,是glibc用线程+阻塞调用来模拟的,性能较差,为了能更多的控制io行为,可以使用更低级的libaio。

Ubuntu安装livaio:

sudo apt install libaio-dev

Linux AIO执行流程:

Linux原生AIO处理流程:

  • 当应用程序调用io_submit系统调用发起一个异步IO操作后,回想内核的IO任务队列添加一个IO任务,并且返回成功。

  • 内核会在后台处理IO任务队列中的IO任务,然后把处理结果存储在IO任务中

  • 应用程序可以调用io_getevents

从上面流程可以看出,Linux异步IO操作主要由两个步骤组成:

  • 1)调用io_submit函数发起一个异步IO操作

  • 2)调用io_getevents函数获取异步IO的结果

实例代码:

#define _GNU_SOURCE
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <libaio.h>

#define FILEPATH "./aio.txt"

int main()
{
io_context_t context; // 异步IO的上下文
struct iocb io[1],*p[1] = {&io[0]};
struct io_event e[1];
unsigned nr_events = 10;
struct timespec timeout;
char *wbuf;
int wbuflen = 1024;
int ret,num=0,i;

posix_memalign((void **)&wbuf,512,wbuflen);

memset(wbuf,'@',wbuflen);
memset(&context,0,sizeof(io_context_t));

timeout.tv_sec = 0;
timeout.tv_nsec = 10000000;

// 1.打开要进行异步IO的文件
int fd = open(FILEPATH,O_CREAT | O_RDWR | O_DIRECT,0644);
if (fd < 0) {
printf("open error: %d\n", errno);
return 0;
}

// 2.创建一个异步IO的上下文
if(0 != io_setup(nr_events,&context))
{
printf("io_setup error: %d\n", errno);
return 0;
}

// 3.创建一个异步IO任务
io_prep_pwrite(&io[0],fd,wbuf,wbuflen,0);

// 4.提交异步IO任务
if((ret = io_submit(context,1,p)) != 1)
{
printf("io_submit error: %d\n", ret);
io_destroy(context);
return -1;
}

// 5.获取异步IO的结果
while(1)
{
ret = io_getevents(context,1,1,e,&timeout);
if (ret < 0) {
printf("io_getevents error: %d\n", ret);
break;
}

if (ret > 0) {
printf("result, res2: %d, res: %d\n", e[0].res2, e[0].res);
break;
}
}
return 0;
}

编译命令:

cc aio_demo.c -laio

运行结果:

目录下会出现一个aio.txt的文件,内容为1024个@字符

程序说明:

  • 通过调用open系统调用打开要进行异步IO的文件,AIO操作必须设置O_DIRECT直接IO标志位

  • 调用io_setup系统调用创建一个异步IO上下文

  • 调用io_prep_pwrite或者io_prep_pread函数创建一个异步写或者异步读任务

  • 调用io_submit系统调用将异步IO提交到内核

  • 调用io_getevents系统调用获取异步IO的结果

以上示例使用while检测,还可以使用epoll结合eventfd,结合事件驱动的方式来获取异步IO操作的结果。

4.3同步、异步、阻塞、非阻塞

一个IO操作其实分成了两个步骤:发起IO请求和实际的IO操作。

同步IO和异步IO的区别就在于第二个步骤是否阻塞,如果实际的IO读写阻塞请求进程,那么就是同步IO。

阻塞IO和非阻塞IO的区别在于第一步,发起IO请求是否会被阻塞,如果阻塞直到完成那么就是传统的阻塞IO,如果不阻塞,那么就是非阻塞IO。

同步和异步是针对应用程序和内核的交互而言的,同步指的是用户进程触发IO操作并等待或者轮询的去查看IO操作是否就绪,而异步是指用户进程触发 IO操作以后便开始做自己的事情,而当IO操作已经完成的时候会得到IO完成的通知。而阻塞和非阻塞是针对于进程在访问数据的时候,根据IO操作的就绪状 态来采取的不同方式,说白了是一种读取或者写入操作函数的实现方式,阻塞方式下读取或者写入函数将一直等待,而非阻塞方式下,读取或者写入函数会立即返回 一个状态值。

所以,IO操作可以分为3类:同步阻塞(即早期的IO操作)、同步非阻塞(NIO)、异步(AIO)。

同步阻塞:在此种方式下,用户进程在发起一个IO操作以后,必须等待IO操作的完成,只有当真正完成了IO操作以后,用户进程才能运行。JAVA传统的IO模型属于此种方式。

同步非阻塞:在此种方式下,用户进程发起一个IO操作以后边可返回做其它事情,但是用户进程需要时不时的询问IO操作是否就绪,这就要求用户进程不停的去询问,从而引入不必要的CPU资源浪费。其中目前JAVA的NIO就属于同步非阻塞IO。

异步:此种方式下是指应用发起一个IO操作以后,不等待内核IO操作的完成,等内核完成IO操作以后会通知应用程序。

1)同步阻塞

2)同步非阻塞

3)异步阻塞

4)异步非阻塞

    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多