1. 在了解bio和nio之前,我们需要知道同步异步和阻塞非阻塞的区别
异步/同步:
- 异步: 你去书店,问最新一期的漫画《柯南》到货没,老板说,没到货,到货了给你打电话。你就走了,而且在没接到老板电话之前, 你可以不用再跑到书店问。(比如$ajax中的success回调方法)
- 同步:你去书店,问漫画《柯南》到货没,老板说,没到货,你走了,过了一段时间,你又到书店,问漫画《柯南》到货没,老板说没有,。。。。就这样,你不断的来书店自己询问。直到你鞋都跑破了。。。注意:你离开书店(不管是异步还是同步),是可以干自己的事情的,比如去做个大保健。。
阻塞/非阻塞:
- 阻塞:你去书店,问漫画《柯南》到货没有,老板说,没到货,于是你就在书店苦等,等书到货,除了干坐着,啥也干不了,直到书来 (比如控制台输入命令, 控制台一直检查有没有输入的数据可以读取)。
- 非阻塞:你去书店,问漫画《柯南》到货没,老板说,没到货,你就走了。(至于还来不来书店,取决于你的心情吧。但是在计算机中,没处理完的事情,是要处理的。)异步/同步 是你得到消息的方式。阻塞/非阻塞是你怎样处理事情。两组概念不是一个层面上的。
也就是同步和异步是在通信方面的概念: 一个请求一个响应算一个闭合, 同步就是一个闭合走完再走一个闭合, 异步就是一个请求,还没响应我也可以再发一个请求, 等那边响应好了通知我callback
就行,
阻塞和非阻塞, 更多的是handle的方式, 阻塞是比如读取控制台输入的值, 如果控制台没有输入值, 读取的线程也一直挂着, 不能继续往下执行代码, 非阻塞就是如果没有东西可以读取, 可以先去干别的活,但得时不时的询问有东西读了没有。
bio就是一种同步阻塞的通信
bio是一个连接开一个线程
在我们传统的bio里面服务端是怎么处理客户端的请求的呢,首先我们的服务端需要实例化一个socket给我们的该实例socket 绑定ip(本机)和端口,然后进行监听,然后就会一直堵塞等待和客户端stoket建立连接,此时客户端也需要实例化一个socket来和我们服务端建立请求,具体操作和服务端一样,绑定ip和端口(服务端监控的ip和port),然后通过三次握手确认和服务端建立连接通讯.成功建立连接之后服务端需要新建一个线程去处理该客户端的通讯请求(io操作),然后主线程会一直堵塞等到下一个客户端socket发起连接请求,在处理下一个,这就是最原始的堵塞式的bio
这种方式的最明显的缺点就是,服务端与每个客户端socket建立连接都需要实时的去创建一个线程去处理该客户端操作,加入同时又1000个客户端socket同时求情建立连接,那我们的服务端就需要同时创建1000个线程去处理,完全没有一点点的缓冲也不能拒绝,显然我们的服务器会吃不消.一个连接开启一个线程处理
2. bio 代码
首先我们需要一个处理请求的类,就是通过输入输出流去读取和反馈客户端的这样的一个类,这个类实现Runnable接口
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
public class ServerHandler implements Runnable{
private Socket socket ;
public ServerHandler(Socket socket){
this.socket = socket;
}
@Override
public void run() {
BufferedReader in = null;
PrintWriter out = null;
try {
in = new BufferedReader(new InputStreamReader(this.socket.getInputStream()));
out = new PrintWriter(this.socket.getOutputStream(), true);
String body = null;
while(true){
body = in.readLine();
if(body == null) break;
System.out.println("Server :" + body);
out.println("服务器端回送响的应数据.");
}
} catch (Exception 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;
}
}
}
然后就是用于接受客户端请求的入口
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
public class Server {
final static int PROT = 8765;
public static void main(String[] args) {
ServerSocket server = null;
try {
server = new ServerSocket(PROT);
System.out.println(" server start .. ");
//进行阻塞
Socket socket = server.accept();
//新建一个线程执行客户端的任务
new Thread(new ServerHandler(socket)).start();
} catch (Exception e) {
e.printStackTrace();
} finally {
if(server != null){
try {
server.close();
} catch (IOException e) {
e.printStackTrace();
}
}
server = null;
}
}
}
接下来就是我那的客户端了
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
public class Client {
final static String ADDRESS = "127.0.0.1";
final static int PORT = 8765;
public static void main(String[] args) {
Socket socket = null;
BufferedReader in = null;
PrintWriter out = null;
try {
socket = new Socket(ADDRESS, PORT);
in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
out = new PrintWriter(socket.getOutputStream(), true);
//向服务器端发送数据
out.println("接收到客户端的请求数据...");
out.println("接收到客户端的请求数据1111...");
String response = in.readLine();
System.out.println("Client: " + response);
} catch (Exception 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;
}
}
}
nio和buffer通道
我们前面介绍了bio是客户端和服务端各自创建一个socket实例建立连接相互通信,并且通信是通过单向io客户端发送数据的时候客户端就必须同步去接收收据,同样的服务端向客户端相应数据的时候也必须同保持连接,如果说客户端和服务端的网速非常慢,这样就会导致客户端和服务端的连接长时间不能关闭,从而浪费了很多资源。
1. buffer是什么?
因为是直连和单向io所以每请次求和响应客户端和服务端都需要创建一个输出流和输入流,受网速的影响流长时间不能关闭
那我们nio的改进方式是建立一个缓冲区buffer
,这个是nio里面特有的,有了这个缓冲区我们客户端和服务端的输出流和输入流就不用直连了,之前是输出流和输入流都是单向的流(单向io),但是nio里面的buffer可以当做双向流的, 既可以写数据又可以读数据,
比如快递员从A市骑着摩托车送快递到B市 , 要花3天时间 , 现在在中间位置指定一个临时仓库, 送快递的送到中间仓库就ok, 收件人从B市开着汽车去仓库拿,整个流程只要2天.
2.channel是什么?
同时nio和引入了管道的概念channel
,和选择器也叫多路复用器Selector
.相比传统的bio的建立连接的方式,nio的练级方式是在原有socket的基础上进行了封装和加强,通过管道注册的方式去建立通讯,首先我们的服务端的socketChanenl(我们称之为通讯管道)注册到我们的多路复用器上,然后客户端的通讯管道也注册到我们的多路复用器上,区别是服务端的管道状态是堵塞状态,而客户端的管道是可读状态
在这里补充一下管道的状态有四种,分别是
连接状态(Connect)
,堵塞状态(Accecp)
,可读状态(Read)
,可写状态(Write)
,连接状态就是管道刚刚连接,堵塞就是一直堵塞(多半用于服务端管道),可读状态就是该管道可以读取数据,可写状态是该管道可以写入数据.
channel和流的区别:
1、流是单向的,通道是双向的,可读可写。
2、流读写是阻塞的,通道可以异步读写。
3、流中的数据可以选择性的先读到缓存中,通道的数据总是要先读到一个缓存中,或从缓存中写入
所以这里的cannel就相当于上面提到的快递员走的是双向车道, 而传统的流是单向车道
3.那什么是selector呢?
还是用上面的快递员的例子, 通道channel就相当于双向车道的高速公路, selector
就相当于一个横跨很多条高速的收费站, 首先要将channel
通道注册到selector
中, 并且告诉selector
需要监控的事件是什么, 是寄货? 还是取货? 然后当这条高速channel
有人寄货的时候, 就让他去寄到中间临时仓库中去, 如果有人取货, 就让他去中间临时仓库去取.
// 注册
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
register 方法的第二个 int 型参数(使用二进制的标记位)用于表明需要监听哪些感兴趣的事件,共以下四种事件:
- SelectionKey.OP_READ
对应 00000001,通道中有数据可以进行读取 - SelectionKey.OP_WRITE
对应 00000100,可以往通道中写入数据 - SelectionKey.OP_CONNECT
对应 00001000,成功建立 TCP 连接 - SelectionKey.OP_ACCEPT
对应 00010000,接受 TCP 连接
selector可以用一个线程监控多个channel:
回到服务器和客户端
那刚刚说到服务端管道一直堵塞在这里,然后服务端会起一个线程去轮询selector上已经注册并且处于连接状态的客户端socketChannel,并和他建立管道通讯(注意这里不是socket直连)而是通过管道和缓冲区做通讯交互.在根据通道的状态变化去执行相应的操作,顺带说明一下管道注册其实并不是吧管道本身注册在多路复用器上,而是通过selectedKey去注册的,可以理解为selectedKeys是唯一识别指定管道的标识列,同样Selector轮询获取的也不是管道本身,而是获取的一组管道的key,然后建立通讯的时候通过key获取该管道,再在次深挖一下每个socketChinnel底层必然对应一个socket实例,获取该管道本身以后然后就开始根据管道的状态执行对应的操作,这样就达到了通讯。
bio和nio的优势
讲完了nio这里就简单的对应一下之前的bio有哪些优势和好处,最明显的就是传统的bio, 是一个服务端socket和一个客户端socket建立直连,并且服务端需要为每个客户端新起一个线程去处理客户端的通讯交互,这样必然会无故的开销服务端的很多资源.而我们的nio只需要一个选择器和一个轮询线程就能接入成千上万甚至更多的客户端连接,这点是nio相比bio最大的进步和改变.其次就是建立连接的方式,传统的bio是通过客户端服务端三次握手的方式建立tcp连接,而nio是客户端直接把通道注册到服务端的多路复用器上,然后服务端去轮询,这就减少了三次握手请求响应的开销。再次之就是缓冲区代码直连流,传统的bio请求和响应数据读是通过一端创建输出流直接向另一端输出,而另一点穿件输入流写入数据,这样就很依赖网络,如果网络不好就会导致流长时间不能关闭,从而导致资源无故浪费,增加开销.而nio引入了缓冲区都数据写数据都是直接向缓冲区读写,这样就不依赖网络,一端吧数据写完到缓冲区就可以关闭写入流,这时候只需要通知另一端去读。另一端开启读取流快速的读取缓冲区的数据,然后就可以快速的关闭.如果网络不好情况向就不会开销另一端的资源。
3. nio代码
首先我们的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;
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 设置服务器通道为非阻塞模式
ssc.configureBlocking(false);
//4 绑定地址
ssc.bind(new InetSocketAddress(port));
//5 把服务器通道注册到多路复用器上,并且监听阻塞事件
ssc.register(this.seletor, SelectionKey.OP_ACCEPT);
System.out.println("Server start, port :" + port);
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void run() {
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 如果为阻塞状态
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 {
//需要一个Selector
public static void main(String[] args) {
//创建连接的地址
InetSocketAddress address = new InetSocketAddress("127.0.0.1", 8765);
//声明连接通道
SocketChannel sc = null;
//建立缓冲区
ByteBuffer buf = ByteBuffer.allocate(1024);
try {
//打开通道
sc = SocketChannel.open();
//进行连接
sc.connect(address);
while(true){
//定义一个字节数组,然后使用系统录入功能:
byte[] bytes = new byte[1024];
System.in.read(bytes);
//把数据放到缓冲区中
buf.put(bytes);
//对缓冲区进行复位
buf.flip();
//写出数据
sc.write(buf);
//清空缓冲区数据
buf.clear();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if(sc != null){
try {
sc.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}