软件系统的模块类型有哪些:如何正确理解软件应用系统中关于系统通信的那些事
软件系统的模块类型有哪些:如何正确理解软件应用系统中关于系统通信的那些事Java 中的 NIO 于 JDK 1.4 中引入,对应 java.nio 包,提供了 Channel Selector,Buffer 等抽象。NIO 中的 N 可以理解为 Non-blocking,不单纯是 New。它支持面向缓冲的,基于通道的 I/O 操作方法。 对于高负载、高并发的(网络)情况下,应使用 NIO 。NIO模型同步阻塞 IO 模型中,服务器应用程序发起 read 系统调用后,会一直阻塞,直到内核把数据拷贝到用户空间。完整的架构应该是 客户端-内核-服务器,客户端发起IO请求,服务器发起系统调用,内核把IO数据从内核空间拷贝到用户空间,服务器应用程序才能使用到客户端发送的数据。一般来说,客户端、服务端其实都属于用户空间,借助内核交流数据。当用户进程发起了read系统调用,kernel就开始了IO的第一个阶段:准备数据。对于网络IO来说,很多时候数据在一开始还没有到达内
在Java语言中,应用程序发起 I/O 调用后,会经历两个阶段:
- 内核等待 I/O 设备准备好数据;
- 内核将数据从内核空间拷贝到用户空间。
其中,阻塞和非阻塞:
- 阻塞调用会一直等待远程数据就绪再返回,即上面的阶段1会阻塞调用者,直到读取结束;
- 而非阻塞无论在什么情况下都会立即返回,虽然非阻塞大部分时间不会被block,但是它仍要求进程不断地去主动询问kernel是否准备好数据,也需要进程主动地再次调用recvfrom来将数据拷贝到用户内存。
而我们常说的同步和异步主要如下:
- 同步方法会一直阻塞进程,直到I/O操作结束,注意这里相当于上面的阶段1,阶段2都会阻塞调用者。其中BIO,NIO,IO多路复用,信号驱动IO,这四种IO都可以归类为同步IO;
- 而异步方法不会阻塞调用者进程,即使是从内核空间的缓冲区将数据拷贝到进程中这一操作也不会阻塞进程,拷贝完毕后内核会通知进程数据拷贝结束。
BIO模型
同步阻塞 IO 模型中,服务器应用程序发起 read 系统调用后,会一直阻塞,直到内核把数据拷贝到用户空间。完整的架构应该是 客户端-内核-服务器,客户端发起IO请求,服务器发起系统调用,内核把IO数据从内核空间拷贝到用户空间,服务器应用程序才能使用到客户端发送的数据。一般来说,客户端、服务端其实都属于用户空间,借助内核交流数据。
当用户进程发起了read系统调用,kernel就开始了IO的第一个阶段:准备数据。对于网络IO来说,很多时候数据在一开始还没有到达内核(比如说客户端目前只是建立了连接,还没有发送数据 或者是 网卡等待接收数据),所以kernel就需要要等待足够的数据到来。而在服务器进程这边,整个进程会被阻塞。当kernel一直等到数据准备好了,它就会将数据从kernel中拷贝到用户内存,然后kernel返回结果,用户进程才解除阻塞状态,重新运行起来。
Java中的JDBC也使用到了BIO技术。BIO在客户端连接数量不高的情况下是没问题的,但是当面对十万甚至百万级连接的时候,无法处理这种高并发情况,因此我们需要一种更高效的 I/O 处理模型来应对。
NIO模型
Java 中的 NIO 于 JDK 1.4 中引入,对应 java.nio 包,提供了 Channel Selector,Buffer 等抽象。NIO 中的 N 可以理解为 Non-blocking,不单纯是 New。它支持面向缓冲的,基于通道的 I/O 操作方法。 对于高负载、高并发的(网络)情况下,应使用 NIO 。
当服务器进程发出read操作时,如果kernel中数据还没准备好,那么并不会阻塞服务器进程,而是立即返回error,用户进程判断结果是error,就知道数据还没准备好,此时用户进程可以去干其他的事情。一段时间后用户进程再次发read,一直轮询直到kernel中数据准备好,此时用户发起read操作,产生system call,kernel 马上将数据拷贝到用户内存,然后返回,进程就能使用到用户空间中的数据了。
BIO一个线程只能处理一个IO流事件,想处理下一个必须等到当前IO流事件处理完毕。而NIO其实也只能串行化的处理IO事件,只不过它可以在内核等待数据准备数据时做其他的工作,不像BIO要一直阻塞住。NIO它会一直轮询操作系统,不断询问内核是否准备完毕。但是,NIO这样又引入了新的问题,如果当某个时间段里没有任何客户端IO事件产生时,服务器进程还在不断轮询,占用着CPU资源。所以要解决该问题,避免不必要的轮询,而且当无IO事件时,最好阻塞住(线程阻塞住就会释放CPU资源了)。所以NIO引入了多路复用机制,可以构建多路复用的、同步非阻塞的IO程序。
AIO模型
AIO 也就是 NIO 2。Java 7 中引入了 NIO 的改进版 NIO 2,它是异步 IO 模型。异步 IO 是基于事件和回调机制实现的,也就是进程操作之后会直接返回,不会阻塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。用户进程发起read操作之后,立刻就可以开始去做其它的事。
内核收到一个asynchronous read之后,首先它会立刻返回,所以不会对用户进程产生任何阻塞。kernel会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel会给用户进程发送一个signal,告诉它read操作完成了。
IO多路复用模型
Java 中的 NIO ,提供了 Selector(选择器)这个封装了操作系统IO多路复用能力的工具,通过Selector.select(),我们可以阻塞等待多个Channel(通道),知道任意一个Channel变得可读、可写,如此就能实现单线程管理多个Channels(客户端)。当所有Socket都空闲时,会把当前线程(选择器所处线程)阻塞掉,当有一个或多个Socket有I/O事件发生时,线程就从阻塞态醒来,并返回给服务端工作线程所有就绪的socket(文件描述符)。各个操作系统实现方案:
- linux:select、poll、epoll
- MacOS/FreeBSD:kqueue
- Windows/Solaris:IOCP
IO多路复用题同非阻塞IO本质一样,只不过利用了新的select系统调用,由内核来负责本来是服务器进程该做的轮询操作。看似比非阻塞IO还多了一个系统调用的开销,不过因为可以支持多路复用IO,即一个进程监听多个socket,才算提高了效率。进程先是阻塞在select/poll上(进程是因为select/poll/epoll函数调用而阻塞,不是直接被IO阻塞的),再是阻塞在读写操作的第二阶段上(等待数据从内核空间拷贝到用户空间)。
IO多路复用的实现原理:利用select、poll、epoll可以同时监听多个socket的I/O事件的能力,而当有I/O事件产生时会被注册到Selector中。在所有socket空闲时,会把当前选择器进程阻塞掉,当有一个或多个流有I/O事件(或者说 一个或多个流有数据到达)时,选择器进程就从阻塞态中唤醒。通过select或poll轮询所负责的所有socket(epoll是只轮询那些真正产生了事件的socket),返回fd文件描述符集合给主线程串行执行事件。
⚠️[特别注意]:
select和poll每次调用时都需要将fd_set(文件描述符集合)从用户空间拷贝到内核空间中,函数返回时又要拷贝回来(epoll使用mmap,避免了每次wait都要将数组进行拷贝)。
在实际开发过程中,基于消息进行系统间通信,我们一般会有四种方法实现:
基于TCP/IP BIO实现:
在Java中可基于Socket、ServerSocket来实现TCP/IP BIO的系统通信。
- Socket主要用于实现建立连接即网络IO的操作
- ServerSocket主要用于实现服务器端口的监听即Socket对象的获取
为了满足服务端可以同时接受多个请求,最简单的方法是生成多个Socket。但这样会产生两个问题:
- 生成太对Socket会消耗过多资源
- 频繁创建Socket会导致系统性能的不足
为了解决上面的问题,通常采用连接池的方式来维护Socket。一方面能限制Socket的个数;另一方面避免重复创建Socket带来的性能下降问题。这里有一个问题就是设置合适的相应超时时间。因为连接池中Socket个数是有限的,肯定会造成激烈的竞争和等待。
Server服务端:
//创建对本地端口的监听
PrintWriter out = new PrintWriter(socket.getOutputStream() true);
//向服务器发送字符串信息
out.println("hello");
//阻塞读取服务端的返回信息
in.readLine();
Client客户端:
//创建连接
Socket socket = new Socket(目标IP或域名 目标端口);
//BufferedReader用于读取服务端返回的数据
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
//PrintWriter向服务器写入流
PrintWriter out = new PrintWriter(socket.getOutputStream() true);
//像服务端发送流
out.println("hello");
//阻塞读取服务端的返回信息
in.readLine();
基于TCP/IP NIO实现:
Java可以基于Clannel和Selector的相关类来实现TCP/IP NIO方式的系统间通信。Channel有SocketClannel和ServerSocketChannel两种:
- SocketClannel: 用于建立连接、监听事件及操作读写。
- ServerSocketClannel: 用于监听端口即监听连接事件。
- Selecter: 获取是否有要处理的事件。
Server服务端:
SocketChannel channel = SocketChannel.open();
//设置为非阻塞模式
channel.configureBlocking(false);
//对于非阻塞模式,立即返回false,表示连接正在建立中
channel.connect(SocketAdress);
Selector selector = Selector.open();
//向channel注册selector以及感兴趣的连接事件
channel.regester(selector SelectionKey.OP_CONNECT);
//阻塞至有感兴趣的IO事件发生,或到达超时时间
int nKeys = selector.select(超时时间【毫秒计】);
//如果希望一直等待知道有感兴趣的事件发生
//int nKeys = selector.select();
//如果希望不阻塞直接返回当前是否有感兴趣的事件发生
//int nKeys = selector.selectNow();
//如果有感兴趣的事件
SelectionKey sKey = null;
if(nKeys>0){
Set keys = selector.selectedKeys();
for(SelectionKey key:keys){
//对于发生连接的事件
if(key.isConnectable()){
SocketChannel sc = (SocketChannel)key.channel();
sc.configureBlocking(false);
//注册感兴趣的IO读事件
sKey = sc.register(selector SelectionKey.OP_READ);
//完成连接的建立
sc.finishConnect();
}
//有流可读取
else if(key.isReadable()){
ByteBuffer buffer = ByteBuffer.allocate(1024);
SocketChannel sc = (SocketChannel) key.channel();
int readBytes = 0;
try{
int ret = 0;
try{
//读取目前可读取的值,此步为阻塞操作
while((ret=sc.read(buffer))>0){
readBytes = ret;
}
}
fanally{
buffer.flip();
}
}
finally{
if(buffer!=null){
buffer.clear();
}
}
}
//可写入流
else if(key.isWritable()){
//取消对OP_WRITE事件的注册
key.interestOps(key.interestOps() & (!SelectionKey.OP_WRITE));
SocketChannel sc = (SocketChannel) key.channel();
//此步为阻塞操作
int writtenedSize = sc.write(ByteBuffer);
//如未写入,则继续注册感兴趣的OP_WRITE事件
if(writtenedSize==0){
key.interestOps(key.interestOps()|SelectionKey.OP_WRITE);
}
}
}
Selector.selectedKeys().clear();
}
//对于要写入的流,可直接调用channel.write来完成。只有在未写入成功时才要注册OP_WRITE事件
int wSize = channel.write(ByteBuffer);
if(wSize == 0){
key.interestOps(key.interestOps() | SelectionKey.OP_WRITE);
}
Server端实体:
ServerSocketChannel ssc = ServerSocketChannel.open();
ServerSocket serverSocket = ssc.socket();
//绑定要监听的接口
serverSocket.bind(new InetSocketAdress(port));
ssc.configureBlocking(false);
//注册感兴趣的连接建立事件
ssc.register(selector SelectionKey.OP_ACCEPT);
基于UDP/IP BIO实现:
Java对UDP/IP方式的网络数据传输同样采用Socket机制,只是UDP/IP下的Socket没有建立连接,因此无法双向通信。如果需要双向通信,必须两端都生成UDP Server。 Java中通过DatagramSocket和DatagramPacket来实现UDP/IP BIO方式和系统间通信:
DatagramSocket:负责监听端口和读写数据
- DatagramPacket:作为数据流对象进行传输 由于UDP双端不建立连接,所以也就不存在竞争问题,只是最终读写流的动作是同步的。
//如果希望双向通信,必须启动一个监听端口承担服务器的职责
//如果不能绑定到指定端口,则抛出SocketException
DatagramSocket serverSocket = new DatagramSocket(监听的端口);
byte[] buffer = new byte[65507];
DatagramPacket receivePacket = new DatagramPacket(buffer buffer.length);
DatagramSocket socket = new DatagramSocket();
DatagramPacket packet = new DatagramPacket(datas datas.length server.length);
//阻塞方式发送packet到指定的服务器和端口
socket.send(packet);
//阻塞并同步读取流消息,如果读取的流消息比packet长,则删除更长的消息
//当连接不上目标地址和端口时,抛出PortUnreachableException
DatagramSocket.setSoTimeout(超时时间--毫秒级);
serverSocket.receive(receivePacket);
基于UDP/IP NIO实现:
Java中可以通过DatagramClannel和ByteBuffer来实现UDP/IP方式的系统间通信:
- DatagramClannel:负责监听端口及进行读写
- ByteBuffer:用于数据传输
//读取流信息
DatagramChannel receiveChannel = DatagramChannel.open();
receiveChannel.configureBlocking(false);
DatagramSocket socket = receiveChannel.socket();
socket.bind(new InetSocketAddress(rport));
Selector selector = Selector.open();
receiveChannel.register(selector SelectionKey.OP_REEAD);
//之后即可像TCP/IP NIO中对selector遍历一样的方式进行流信息的读取
//...
//写入流信息
DatagramChannel sendChannel = DatagramChannel.open();
sendChannel.configureBlocking(false);
SocketAdress target = new InetSocketAdress("127.0.0.1" sport);
sendChannel.connect(target);
//阻塞写入流
sendChannel.write(ByteBuffer);
发展历程