网络通讯概念

本文介绍了Java的BIO、NIO和AIO三种IO模型。BIO是同步阻塞,每个请求都需要一个线程处理;NIO引入了选择器,通过单线程轮询通道,减少了线程消耗,但仍有同步等待问题;AIO是异步非阻塞,操作系统完成IO操作后通知应用,避免了同步等待,提高了效率。文章详细讲解了NIO的缓冲区、通道和选择器,并指出Linux上的AIO仍然基于epoll实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

阻塞和同步概念:
按照《Unix网络编程》的划分,IO模型可以分为:阻塞IO、非阻塞IO、IO复用、信号驱动IO和异步IO,
按照POSIX标准来划分只分为两类:同步IO和异步IO。

如何区分呢?
首先一个 IO操作其实分成了两个步骤:发起IO请求和实际的IO操作

同步IO和异步IO的区别就在于第二个步骤(IO操作)是否阻塞
如果实际的IO读写阻塞请求进程,那么就是同步IO,因此阻塞IO、非阻塞IO、IO复用、信号驱动IO都是同步IO,
如果不阻塞,而是操作系统帮你做完IO操作再将结果返回给你,那么就是异步IO。

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

如果不阻塞,那么就是非阻塞IO。


BIO,NIO,AIO理解:
BIO,同步阻塞式IO,个人理解:每个io请求都会开一个线程去处理,而tomcat默认配置的并发连接数是150,
并发过高,请求可能被丢弃。线程数过高,性能会大大下降!

NIO,同步非阻塞IO,个人理解:nio引入selector机制(多路复用器),每个io请求会作为一个socketChannel(管道)
注册到selector上,不会被阻塞在外面(非阻塞),selector会有一个循环的线程轮询检查每个socketChannel是否有实际的io操作,
如果并发很高,要处理的io操作很多,还是会有BIO一样的问题,实际的io操作可能会排队处理(同步)。

AIO,异步非阻塞IO,个人理解:在nio的基础上,引进操作系统的I/O操作,nio中,实际的io操作(读写)是要
线程来单独实现的,而aio是操作系统(见下面说明)来完成读写操作,我们程序不需要关心IO读写,
当操作系统完成了IO读写操作,会给应用程序发送通知,应用程序直接拿数据即可,这样就做到了异步。


BIO:
在JDK1.4出来之前,我们建立网络连接的时候采用BIO模式,需要先在服务端启动一个ServerSocket,
然后在客户端启动Socket来对服务端进行通信,默认情况下服务端需要对每个请求建立一堆线程等待请求,
而客户端发送请求后,先咨询服务端是否有线程相应,如果没有则会一直等待或者遭到拒绝请求,
如果有的话,客户端线程会等待请求结束后才继续执行。

NIO:
BIO与NIO一个比较重要的不同,是我们使用BIO的时候往往会引入多线程,每个连接一个单独的线程;
而NIO则是使用单线程或者只使用少量的多线程,多个连接共用一个线程

NIO的最重要的地方是当一个连接创建后,不需要对应一个线程,这个连接会被注册到多路复用器上面,
所以所有的连接只需要一个线程就可以搞定,当这个线程中的多路复用器进行轮询的时候,
发现连接上有操作的话,才开启一个线程进行处理,也就是一个请求一个线程模式。


在NIO的处理方式中,当一个请求来的话,开启线程进行处理,可能会等待后端应用的资源(JDBC连接等),
其实这个线程就被阻塞了,当并发上来的话,还是会有BIO一样的问题。

API由三个主要的部分组成:

缓冲区(Buffers)
BIO是面向流(Stream对象)操作的,NIO则引入Buffer对象,所有数据都是通过缓冲区处理(读写)
缓冲区实际上是一个数组,通常是字节数组ByteBuffer,然而是每个java基本类型都对应一个Buffer对象(Boolean除外)
通道(Channels)
像自来水管,网络数据通过channel读取和写入,当对于流,通道是双向的,而流是一个方向上的移动
流必须是InputStream,OutputStream的子类,而通道可以用于读、写或者二者同时进行。最关键是可以和selector结合起来,
有多种状态位,方便selector识别, 事实上Channel分成两类,一类是网络读写(SelectableChannel),
一类是用于文件操作(FileChannel),SocketChannel,ServerSocketChannel都是SelectableChannel的子类。
非阻塞I/O的核心类(Selector...):
通过SelectKey(注册时Selector会分配每个管道一个key值,相当于标签)轮询所有注册的通道,
如果通道发生了读写操作,这个通道就处于就绪状态,通过轮询key可以得到就绪的Channel,
从而通知CPU执行IO操作(从管道里读或写数据到Buffer中),
一个Selector可以负责成千上万的Channel通道,没有上线,
这意味我们只要一个线程负责Selector的轮询,就可以接入成千上万的客户端,是JDK NIO库的巨大进步。

每个通道都会对Selector进行注册不同的事件状态,以便Selector查找。
SelectionKey.OP_CONNECT
SelectionKey.OP_ACCEPT
SelectionKey.OP_READ
SelectionKey.OP_WRITE

AIO:
关于操作系统的IO模型,就不得不提select/poll/epoll/iocp
在Linux 2.6以后,java NIO的实现,是通过epoll来实现的,这点可以通过jdk的源代码发现。而AIO,
在windows上是通过IOCP实现的,在linux上还是通过epoll来实现的。

这里强调一点:AIO,这是I/O处理模式,而epoll等都是实现AIO的一种编程模型;
换句话说,AIO是一种接口标准,各家操作系统可以实现也可以不实现。
在不同操作系统上在高并发情况下最好都采用操作系统推荐的方式。Linux上还没有真正实现网络方式的AIO。



NIO通讯服务端步骤:
1、创建ServerSocketChannel,为它配置非阻塞模式
2、绑定监听,配置TCP参数,录入backlog大小等
3、创建一个独立的IO线程,用于轮询多路复用器Selector
4、创建Selector,将之前的ServerSocketChannel注册到Selector上,并设置监听标识位SelectionKey.ACCEPT
5、启动IO线程,在循环体中执行Selector.select()方法,轮询就绪的通道
6、当轮询到处于就绪的通道时,需要进行判断操作位,如果是ACCEPT状态,说明是新的客户端介入,则调用accept方法接受新的客户端。
7、设置新接入客户端的一些参数,并将其通道继续注册到Selector之中。设置监听标识等
8、如果轮询的通道操作位是READ,则进行读取,构造Buffer对象等
9、更细节的还有数据没发送完成继续发送的问题

nio实现代码(单向:服务器接收,客户端发送):

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;

/**
 * nio server实现
 * 主要概念:多路复用器selector,服务端管道ServerSocketChannel, 客户端管道SocketChannel,缓存区Buffer
 * 思路:每个io请求都可以进来(非阻塞),然后注册到selector, selector无限循环判断注册进来的管道是否有io操作,
 * 	如果有io操作,查看状态,并做处理, 读取都在通道中进行
 * @author pc
 *
 */
public class Server implements Runnable{
	//1 多路复用器(管理所有的通道)
	private Selector seletor;
	//2 建立缓冲区,缓冲读
	private ByteBuffer readBuf = ByteBuffer.allocate(1024);
	//3、建立缓冲区,缓冲写,暂时没实现
	//private ByteBuffer writeBuf = ByteBuffer.allocate(1024);
	
	public Server(int port){
		try {
			//1 打开路复用器
			this.seletor = Selector.open();
			//2 打开服务器通道
			ServerSocketChannel ssc = ServerSocketChannel.open();
			//3 设置服务器通道为非阻塞模式,如果为true就和传统的io效果一样了
			ssc.configureBlocking(false);
			//4 绑定地址,监听端口
			ssc.bind(new InetSocketAddress(port));
			//5 把服务器通道注册到多路复用器上,并且监听阻塞事件
			//服务器管道和客户端管道都注册到这个selector
			ssc.register(this.seletor, SelectionKey.OP_ACCEPT);
			System.out.println("服务端开始监听,监听端口为:" + port);
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	@Override
	public void run() {
		//多路复用器selector无限循环查看注册的管道是否有io操作
		while(true){
			try {
				//1 必须要让多路复用器开始监听
				this.seletor.select();
				//2 返回多路复用器已经选择的结果集
				Iterator<SelectionKey> keys = this.seletor.selectedKeys().iterator();
				//3 进行遍历
				while(keys.hasNext()){
					//4 获取一个选择的元素
					SelectionKey key = keys.next();
					//5开始处理通道了,把它从容器中移除
					keys.remove();
					//6 如果是有效的
					if(key.isValid()){
						//7这里体现了io操作的两种状态:发起io请求,实际io操作
						//Tests whether this key's channel is ready to accept a new socket connection. 
						//如果这个管道已经准备好接受socket连接了,那么要把这个管道标识为OP_READ,再注册到selector
						if(key.isAcceptable()){
							this.accept(key);
						}
						//8 如果为可读状态,那么直接读取
						if(key.isReadable()){
							this.read(key);
						}
						//9 写数据
						if(key.isWritable()){
							//this.write(key); //ssc
						}
					}
				}
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}
	
//	private void write(SelectionKey key){
//		//ServerSocketChannel ssc =  (ServerSocketChannel) key.channel();
//		//ssc.register(this.seletor, SelectionKey.OP_WRITE);
//	}

	private void read(SelectionKey key) {
		try {
			//1 清空缓冲区旧的数据
			this.readBuf.clear();
			//2 获取之前注册的socket通道对象
			SocketChannel sc = (SocketChannel) key.channel();
			//3 读取数据
			int count = sc.read(this.readBuf);
			//4 如果没有数据
			if(count == -1){
				key.channel().close();
				key.cancel();
				return;
			}
			//5 有数据则进行读取 读取之前需要进行复位方法(把position 和limit进行复位)
			this.readBuf.flip();
			//6 根据缓冲区的数据长度创建相应大小的byte数组,接收缓冲区的数据
			byte[] bytes = new byte[this.readBuf.remaining()];
			//7 接收缓冲区数据
			this.readBuf.get(bytes);
			//8 打印结果
			String body = new String(bytes).trim();
			System.out.println("Server : " + body);
			// 9..可以写回给客户端数据 
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	private void accept(SelectionKey key) {
		try {
			//1 获取服务通道
			ServerSocketChannel ssc =  (ServerSocketChannel) key.channel();
			//2 执行阻塞方法
			SocketChannel sc = ssc.accept();
			//3 设置阻塞模式
			sc.configureBlocking(false);
			//4 注册到多路复用器上,并设置读取标识
			sc.register(this.seletor, SelectionKey.OP_READ);
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	public static void main(String[] args) {
		new Thread(new Server(8765)).start();;
	}
}

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;

public class Client {

	public static void main(String[] args) {
		//创建连接的地址
		InetSocketAddress address = new InetSocketAddress("127.0.0.1", 8765);
		//声明连接通道
		SocketChannel sc = null;
		//建立缓冲区
		ByteBuffer writeBuf = ByteBuffer.allocate(1024);
		try {
			//打开通道
			sc = SocketChannel.open();
			//进行连接
			sc.connect(address);
			while(true){
				//定义一个字节数组,然后使用系统录入功能:
				byte[] bytes = new byte[1024];
				System.in.read(bytes);
				
				//把数据放到缓冲区中
				writeBuf.put(bytes);
				//对缓冲区进行复位
				writeBuf.flip();
				//写出数据
				sc.write(writeBuf);
				//清空缓冲区数据
				writeBuf.clear();
			}
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			if(sc != null){
				try {
					sc.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}
		
	}
	
}


Tomcat结合NIO理解:
Tomcat7以及以下默认的HTTP实现是采用阻塞式的Socket通信,每个请求都需要创建一个线程处理,
当一个进程有500个线程在跑的话,那性能已经是很低很低了。
Tomcat 默认配置的最大请求数是150,也就是说同时支持150个并发。
具体能承载多少并发,需要看硬件的配置,CPU 越多性能越高,分配给JVM的内存越多性能也就越高,但也会加重GC的负担。
某个应用拥有 250 个以上并发的时候,应考虑应用服务器的集群

操作系统对于进程中的线程数有一定的限制:
Windows 每个进程中的线程数不允许超过 2000
Linux 每个进程中的线程数不允许超过 1000
另外,在 Java 中每开启一个线程需要耗用 1MB 的 JVM 内存空间用于作为线程栈之用。

tomcat配置io的三种模式的不同之处如下:

BIO:
一个线程处理一个请求。缺点:并发量高时,线程数较多,浪费资源。
Tomcat7或以下,在Linux系统中默认使用这种方式。

NIO:
利用Java的异步IO处理,可以通过少量的线程处理大量的请求。
Tomcat8在Linux系统中默认使用这种方式。
Tomcat7必须修改Connector配置来启动:

<Connector port="8080" protocol="org.apache.coyote.http11.Http11NioProtocol" 
         connectionTimeout="20000" redirectPort="8443"/> 

APR:
即Apache Portable Runtime,从操作系统层面解决io阻塞问题。
Tomcat7或Tomcat8在Win7或以上的系统中启动默认使用这种方式。
Linux如果安装了apr和native,Tomcat直接启动就支持apr。
安装方法:http://my.oschina.net/lsw90/blog/181161



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值