BIO 全称Block-IO 是一种同步阻塞的通信模式。我们常说的Stock IO 一般指的是BIO。是一个比较传统的通信方式,模式简单,使用方便。但并发处理能力低,通信耗时,依赖网速。
BIO 设计原理:
服务器通过一个Acceptor线程负责监听客户端请求和为每个客户端创建一个新的线程进行链路处理。典型的一请求一应答模式。若客户端数量增多,频繁地创建和销毁线程会给服务器打开很大的压力。后改良为用线程池的方式代替新增线程,被称为伪异步IO。
服务器提供IP地址和监听的端口,客户端通过TCP的三次握手与服务器连接,连接成功后,双放才能通过套接字(Stock)通信。
小结:BIO模型中通过Socket和ServerSocket完成套接字通道的实现。阻塞,同步,建立连接耗时。
BIO服务器代码,负责启动服务,阻塞服务,监听客户端请求,新建线程处理任务。
/**
* ServerSocket 服务器端 代码
* Created by lyyz on 2018/5/24.
*/
public class BioSocketServer {
public static void main(String[] args) {
ServerSocket serverSocket = null;
Socket socket = null;
try {
//socket地址
SocketAddress socketAddress = new InetSocketAddress("127.0.0.1",8765);
// 创建ServerScoket 服务端
serverSocket = new ServerSocket();
// 绑定地址
serverSocket.bind(socketAddress);
// 循环阻塞 等待客户端连接
while(true){
socket = serverSocket.accept();
//新建一个线程执行 客户端的请求 并回复响应
Thread thread = new Thread(new BioSocketHandler(socket));
thread.start();
}
} catch (IOException e) {
e.printStackTrace();
}finally {
if(serverSocket != null){
try {
serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
serverSocket = null;
}
}
}
/**
* 响应的具体处理者
* Created by lyyz on 2018/5/24.
*/
public class BioSocketHandler implements Runnable {
private Socket socket;
public BioSocketHandler(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
BufferedReader in = null;
PrintWriter out = null;
try {
in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
out = new PrintWriter(new OutputStreamWriter(socket.getOutputStream()),true);
String request;
request = in.readLine();
//收取客户端请求并回复响应
System.out.println(request);
out.println("Bio 服务器响应数据 response");
} catch (IOException e) {
e.printStackTrace();
}finally {
if(in != null){
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(out != null){
try {
out.close();
} catch (Exception e) {
e.printStackTrace();
}
}
if(socket != null){
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
socket = null;
}
}
}
/**
* 客户端代码
* Created by lyyz on 2018/5/24.
*/
public class BioSocketClient {
public static void main(String[] args) {
Socket socket =null;
PrintWriter out = null;
BufferedReader in = null;
try {
// 创建Socket
socket = new Socket();
SocketAddress socketAddress = new InetSocketAddress("127.0.0.1",8765);
//连接服务器
socket.connect(socketAddress);
out = new PrintWriter(new OutputStreamWriter(socket.getOutputStream()),true);
in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
//向服务器发送请求
out.println("Bio 客户端发送请求信息 request");
String response;
//收到服务器响应数据
response = in.readLine();
System.out.println(new String(response));
} catch (IOException e) {
e.printStackTrace();
}finally {
if(in != null){
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(out != null){
try {
out.close();
} catch (Exception e) {
e.printStackTrace();
}
}
if(socket != null){
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
socket = null;
}
}
}
为了改进这种一连接一线程的模型,我们可以使用线程池来管理这些线程(需要了解更多请参考前面提供的文章),实现1个或多个线程处理N个客户端的模型(但是底层还是使用的同步阻塞I/O),通常被称为“伪异步I/O模型“。
实现很简单,我们只需要将新建线程的地方,交给线程池管理即可,只需要改动刚刚的Server代码即可:
/**
* 伪异步IO模式
* ServerSocket 服务器端 代码
* Created by lyyz on 2018/5/24.
*/
public class Bio2SocketServer {
public static void main(String[] args) {
// 创建线程池 避免每次客户端连接都创建一个线程
ExecutorService executorService = new ThreadPoolExecutor(Runtime.getRuntime().availableProcessors(),100,120, TimeUnit.SECONDS,new ArrayBlockingQueue(400));
ServerSocket serverSocket = null;
Socket socket = null;
try {
//socket地址
SocketAddress socketAddress = new InetSocketAddress("127.0.0.1",8765);
// 创建ServerScoket 服务端
serverSocket = new ServerSocket();
// 绑定地址
serverSocket.bind(socketAddress);
// 循环阻塞 等待客户端连接
while(true){
socket = serverSocket.accept();
//新建一个线程执行 客户端的请求 并回复响应
executorService.execute(new BioSocketHandler(socket));
}
} catch (IOException e) {
e.printStackTrace();
}finally {
if(serverSocket != null){
try {
serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
serverSocket = null;
}
}
}
NIO 全称New IO,也叫Non-Block IO 是一种非阻塞同步的通信模式。
NIO 相对于BIO来说一大进步。客户端和服务器之间通过Channel通信。NIO可以在Channel进行读写操作。这些Channel都会被注册在Selector多路复用器上。Selector通过一个线程不停的轮询这些Channel。找出已经准备就绪的Channel执行IO操作。
NIO 通过一个线程轮询,实现千万个客户端的请求,这就是非阻塞NIO的特点。
NIO提供了与传统BIO模型中的Socket和ServerSocket相对应的SocketChannel和ServerSocketChannel两种不同的套接字通道实现。
新增的着两种通道都支持阻塞和非阻塞两种模式。
阻塞模式使用就像传统中的支持一样,比较简单,但是性能和可靠性都不好;非阻塞模式正好与之相反。
对于低负载、低并发的应用程序,可以使用同步阻塞I/O来提升开发速率和更好的维护性;对于高负载、高并发的(网络)应用,应使用NIO的非阻塞模式来开发。
缓冲区Buffer:它是NIO与BIO的一个重要区别。BIO是将数据直接写入或读取到Stream对象中。而NIO的数据操作都是在缓冲区中进行的。缓冲区实际上是一个数组。Buffer最常见的类型是ByteBuffer,另外还有CharBuffer,ShortBuffer,IntBuffer,LongBuffer,FloatBuffer,DoubleBuffer。
通道Channel:和流不同,通道是双向的。NIO可以通过Channel进行数据的读,写和同时读写操作。通道分为两大类:一类是网络读写(SelectableChannel),一类是用于文件操作(FileChannel),我们使用的SocketChannel和ServerSocketChannel都是SelectableChannel的子类。
多路复用器Selector:NIO编程的基础。多路复用器提供选择已经就绪的任务的能力。就是Selector会不断地轮询注册在其上的通道(Channel),如果某个通道处于就绪状态,会被Selector轮询出来,然后通过SelectionKey可以取得就绪的Channel集合,从而进行后续的IO操作。服务器端只要提供一个线程负责Selector的轮询,就可以接入成千上万个客户端,这就是JDK NIO库的巨大进步。
说明:这里的代码只实现了客户端发送请求,服务端接收数据的功能。其目的是简化代码,方便理解。github源码中有完整代码。
小结:NIO模型中通过SocketChannel和ServerSocketChannel完成套接字通道的实现。非阻塞/阻塞,同步,避免TCP建立连接使用三次握手带来的开销。
NIO服务器代码,负责开启多路复用器,打开通道,注册通道,轮询通道,处理通道。
/**
* Created by lyyz on 2018/5/24.
*/
public class NioSocketServer implements Runnable{
//1 多路复用器(管理所有的通道)
private Selector selector;
// 读缓冲区
private ByteBuffer readBuffer = ByteBuffer.allocate(1024);
public NioSocketServer() {
try {
// 1.开启多路复用器
selector = Selector.open();
//2.打开服务器通道(网络读写通道)
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 3.设置服务器通道为非阻塞模式,true为阻塞,false为非阻塞
serverSocketChannel.configureBlocking(false);
// 4.绑定端口
serverSocketChannel.bind(new InetSocketAddress("127.0.0.1",8765));
// 5 将服务器通道注册到多线路复用器上
// SelectionKey.OP_READ : 表示关注读数据就绪事件
// SelectionKey.OP_WRITE : 表示关注写数据就绪事件
// SelectionKey.OP_CONNECT: 表示关注socket channel的连接完成事件
// SelectionKey.OP_ACCEPT : 表示关注server-socket channel的accept事件
serverSocketChannel.register(selector,SelectionKey.OP_ACCEPT);
System.out.println("服务器初始化完毕-------------");
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void run() {
while(true){
try {
/**
* a.select() 阻塞到至少有一个通道在你注册的事件上就绪
* b.select(long timeOut) 阻塞到至少有一个通道在你注册的事件上就绪或者超时timeOut
* c.selectNow() 立即返回。如果没有就绪的通道则返回0
* select方法的返回值表示就绪通道的个数。
*/
// 1.多路复用器监听阻塞
selector.select();
// 2.多路复用器已经选择的结果集
Iterator selectionKeys = selector.selectedKeys().iterator();
// 3.不停的轮询
while (selectionKeys.hasNext()) {
// 4.获取一个选中的key
SelectionKey key = selectionKeys.next();
// 5.获取后便将其从容器中移除
selectionKeys.remove();
// 6.只获取有效的key
if (!key.isValid()) {
continue;
}
// 阻塞状态处理
if (key.isAcceptable()) {
accept(key);
}
// 可读状态处理
if (key.isReadable()) {
read(key);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
// 设置阻塞,等待Client请求。在传统IO编程中,用的是ServerSocket和Socket。在NIO中采用的ServerSocketChannel和SocketChannel
private void accept(SelectionKey selectionKey) {
try {
// 1.获取通道服务
ServerSocketChannel serverSocketChannel = (ServerSocketChannel) selectionKey.channel();
// 2.执行阻塞方法
SocketChannel socketChannel = serverSocketChannel.accept();
// 3.设置服务器通道为非阻塞模式,true为阻塞,false为非阻塞
socketChannel.configureBlocking(false);
// 4.把通道注册到多路复用器上,并设置读取标识
socketChannel.register(selector, SelectionKey.OP_READ);
} catch (IOException e) {
e.printStackTrace();
}
}
private void read(SelectionKey selectionKey) {
try {
// 1.清空缓冲区数据
readBuffer.clear();
// 2.获取在多路复用器上注册的通道
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
// 3.读取数据,返回
int count = socketChannel.read(readBuffer);
// 4.返回内容为-1 表示没有数据
if (-1 == count) {
selectionKey.channel().close();
selectionKey.cancel();
return ;
}
// 5.有数据则在读取数据前进行复位操作
readBuffer.flip();
// 6.根据缓冲区大小创建一个相应大小的bytes数组,用来获取值
byte[] bytes = new byte[readBuffer.remaining()];
// 7.接收缓冲区数据
readBuffer.get(bytes);
// 8.打印获取到的数据
System.out.println("NIO Server : " + new String(bytes)); // 不能用bytes.toString()
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
new Thread(new NioSocketServer()).start();
}
}
/**
* Created by lyyz on 2018/5/24.
*/
public class NioSocketClient {
public static void main(String[] args) {
// 1.创建连接地址
InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 8765);
// 2.声明一个连接通道
SocketChannel socketChannel = null;
// 3.创建一个缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
try {
// 4.打开通道
socketChannel = SocketChannel.open();
// 5.连接服务器
socketChannel.connect(inetSocketAddress);
while(true){
// 6.定义一个字节数组,然后使用系统录入功能:
byte[] bytes = new byte[1024];
// 7.键盘输入数据
System.in.read(bytes);
// 8.把数据放到缓冲区中
byteBuffer.put(bytes);
// 9.对缓冲区进行复位
byteBuffer.flip();
// 10.写出数据
socketChannel.write(byteBuffer);
// 11.清空缓冲区数据
byteBuffer.clear();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (null != socketChannel) {
try {
socketChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
AIO 也叫NIO2.0 是一种非阻塞异步的通信模式。在NIO的基础上引入了新的异步通道的概念,并提供了异步文件通道和异步套接字通道的实现。
AIO 并没有采用NIO的多路复用器,而是使用异步通道的概念。其read,write方法的返回类型都是Future对象。而Future模型是异步的,其核心思想是:去主函数等待时间。
小结:AIO模型中通过AsynchronousSocketChannel和AsynchronousServerSocketChannel完成套接字通道的实现。非阻塞,异步。
/**
* AIO, 也叫 NIO2.0 是一种异步非阻塞的通信方式
* AIO 引入了异步通道的概念 AsynchronousServerSocketChannel和AsynchronousSocketChannel 其read和write方法返回值类型是Future对象。
*/
public class AioSocketServer {
private ExecutorService executorService; // 线程池
private AsynchronousChannelGroup threadGroup; // 通道组
public AsynchronousServerSocketChannel asynServerSocketChannel; // 服务器通道
public void start(Integer port){
try {
// 1.创建一个缓存池
executorService = Executors.newCachedThreadPool();
// 2.创建通道组
threadGroup = AsynchronousChannelGroup.withCachedThreadPool(executorService, 1);
// 3.创建服务器通道
asynServerSocketChannel = AsynchronousServerSocketChannel.open(threadGroup);
// 4.进行绑定
asynServerSocketChannel.bind(new InetSocketAddress(port));
System.out.println("server start , port : " + port);
// 5.等待客户端请求
asynServerSocketChannel.accept(this, new AioServerHandler());
// 一直阻塞 不让服务器停止,真实环境是在tomcat下运行,所以不需要这行代码
Thread.sleep(Integer.MAX_VALUE);
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
AioSocketServer server = new AioSocketServer();
server.start(8888);
}
}
public class AioSocketClient implements Runnable{
private static Integer PORT = 8888;
private static String IP_ADDRESS = "127.0.0.1";
private AsynchronousSocketChannel asynSocketChannel ;
public AioSocketClient() throws Exception {
asynSocketChannel = AsynchronousSocketChannel.open(); // 打开通道
}
public void connect(){
asynSocketChannel.connect(new InetSocketAddress(IP_ADDRESS, PORT)); // 创建连接 和NIO一样
}
public void write(String request){
try {
asynSocketChannel.write(ByteBuffer.wrap(request.getBytes())).get();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
asynSocketChannel.read(byteBuffer).get();
byteBuffer.flip();
byte[] respByte = new byte[byteBuffer.remaining()];
byteBuffer.get(respByte); // 将缓冲区的数据放入到 byte数组中
System.out.println(new String(respByte,"utf-8").trim());
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void run() {
while(true){
}
}
public static void main(String[] args) throws Exception {
for (int i = 0; i < 10; i++) {
AioSocketClient myClient = new AioSocketClient();
myClient.connect();
new Thread(myClient, "myClient").start();
myClient.write("aaaaaaaaaaaaaaaaaaaaaa");
}
}
}
public class AioServerHandler implements CompletionHandler {
private final Integer BUFFER_SIZE = 1024;
@Override
public void completed(AsynchronousSocketChannel asynSocketChannel, AioSocketServer attachment) {
// 保证多个客户端都可以阻塞
attachment.asynServerSocketChannel.accept(attachment, this);
read(asynSocketChannel);
}
//读取数据
private void read(final AsynchronousSocketChannel asynSocketChannel) {
ByteBuffer byteBuffer = ByteBuffer.allocate(BUFFER_SIZE);
asynSocketChannel.read(byteBuffer, byteBuffer, new CompletionHandler() {
@Override
public void completed(Integer resultSize, ByteBuffer attachment) {
//进行读取之后,重置标识位
attachment.flip();
//获得读取的字节数
System.out.println("Server -> " + "收到客户端的数据长度为:" + resultSize);
//获取读取的数据
String resultData = new String(attachment.array()).trim();
System.out.println("Server -> " + "收到客户端的数据信息为:" + resultData);
String response = "服务器响应, 收到了客户端发来的数据: " + resultData;
write(asynSocketChannel, response);
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
exc.printStackTrace();
}
});
}
// 写入数据
private void write(AsynchronousSocketChannel asynSocketChannel, String response) {
try {
// 把数据写入到缓冲区中
ByteBuffer buf = ByteBuffer.allocate(BUFFER_SIZE);
buf.put(response.getBytes());
buf.flip();
// 在从缓冲区写入到通道中
asynSocketChannel.write(buf).get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
@Override
public void failed(Throwable exc, AioSocketServer attachment) {
exc.printStackTrace();
}
}
本博客参考
https://blog.csdn.net/anxpp/article/details/51512200
https://segmentfault.com/a/1190000012976683