发展历程
Java1.0开始提供的IO都同步阻塞IO,即BIO。
Java1.4开始提供了同步非阻塞IO,即NIO。
Java1.7开始出现的NIO2.0版本,真正提供了异步非阻塞IO,即AIO。
引申:什么是“同步/异步”?什么是“阻塞/非阻塞”?
一个IO操作其实分成了两个步骤:发起IO请求和实际的IO操作。
同步IO和异步IO的区别就在于第二个步骤是否阻塞,如果实际的IO读写阻塞请求进程,那么就是同步IO。
阻塞IO和非阻塞IO的区别在于第一步,发起IO请求是否会被阻塞,如果阻塞直到完成那么就是传统的阻塞IO,如果不阻塞,那么就是非阻塞IO。很明显,通常来说,非阻塞IO比阻塞IO效率高,异步IO比同步IO效率高
BIO
Blocking IO - (同步)阻塞IO
根据Linux IO模型可知,BIO的IO都是阻塞的。
在此种方式下,用户进程在发起一个IO操作以后,必须等待IO操作的完成,只有当真正完成了IO操作以后,用户进程才能运行。JAVA传统的IO模型属于此种方式。
BIO的读写操作都是阻塞的,即调用到read()
方法时,如果没有数据,会一直阻塞等待。BIO对应的形象比喻:打电话时,如果对方不说话,本方会一直等待。很显然,这个效率是很低的。
BIO网络编程示例
网络编程大部分都是基于C/S模式的。
TCP
TCP Server端使用ServerSocket
类,负责绑定IP,启动监听端口。
TCP Client端使用Socket
类,负责连接Server
UDP
Java通过DatagramPacket
类和DatagramSocket
类来使用UDP套接字,客户端和服务器端都通过DatagramSocket
的send()
方法和receive()
方法来发送和接收数据,用DatagramPacket来包装需要发送或者接收到的数据。
发送信息时,Java创建一个包含待发送信息的DatagramPacket
实例,并将其作为参数传递给DatagramSocket
实例的send()
方法;接收信息时,Java程序首先创建一个DatagramPacket
实例,该实例预先分配了一些空间,并将接收到的信息存放在该空间中,然后把该实例作为参数传递给DatagramSocket
实例的receive()
方法
注意:如果该实例用来包装待接收的数据,则不指定数据来源的远程主机和端口,只需指定一个缓存数据的byte数组即可(在调用receive()
方法接收到数据后,源地址和端口等信息会自动包含在DatagramPacket
实例中),而如果该实例用来包装待发送的数据,则要指定要发送到的目的主机和端口。
public class ServerSocket implements java.io.Closeable {
public Socket accept() throws IOException;
public void bind(SocketAddress endpoint) throws IOException;
public void close() throws IOException;
}
Client端依赖Socket
类,负责连接Server
public class Socket implements java.io.Closeable{
public void bind(SocketAddress bindpoint) throws IOException;
public void connect(SocketAddress endpoint) throws IOException;
public synchronized void close() throws IOException;
}
public class DatagramSocket implements java.io.Closeable {
public synchronized void bind(SocketAddress addr) throws SocketException;
public void connect(InetAddress address, int port);
public void send(DatagramPacket p) throws IOException;
public synchronized void receive(DatagramPacket p) throws IOException;
}
TCP
Server端
单线程模型
//服务端的端口号
int port = 8080;
//创建服务端Socket
ServerSocket server = new ServerSocket(port);
//与客户端建立连接的Socket
Socket socket = null;
while(true){
//等待客户端接入
socket = server.accept();
//创建InputStream,读入数据
BufferedReader in = new BufferedReader(new InputStreamReader(this.socket.getInputStream()));
//创建OutputStream,写出数据
PrintWriter out = new PrintWriter(this.socket.getOutputStream());
//从网络中读取数据
String body = in.readLine();
//将数据写入到网络
out.println(body);
}
多线程模型
//服务端的端口号
int port = 8080;
//创建服务端Socket
ServerSocket server = new ServerSocket(port);
//与客户端建立连接的Socket
Socket socket = null;
while(true){
//等待客户端接入
socket = server.accept();
//某个客服端接入后,启动新的线程,在新线程中与客户端进行读写交互
new Thread(new ServerHandler(socket)).start();
}
public class ServerHandler implements Runnable{
private Socket socket;
public ServerHandler(Socket socket){
this.socket = socket;
}
@Override
public void run(){
BufferedReader in = new BufferedReader(new InputStreamReader(this.socket.getInputStream()));
PrintWriter out = new PrintWriter(this.socket.getOutputStream());
while(true){
//从网络读取数据
String body = in.readLine();
//将数据写入到网络
out.println(body)
}
}
}
Client端
String ip = "127.0.0.1";
int port = 8080;
//连接远程Server
Socket socket = new Socket(ip, port);
//输入流
PrintWriter out = new PrintWriter(socket.getOutputStream());
//输出流
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
//向Server发送信息
out.println("Hello World");
//从Server收到信息
String res = in.readLine();
UDP
UDP的通信建立的步骤
一个典型的UDP客户端要经过下面三步操作:
- 创建一个DatagramSocket实例,可以有选择地对本地地址和端口号进行设置,如果设置了端口号,则客户端会在该端口号上监听从服务器端发送来的数据;
- 使用DatagramSocket实例的send()和receive()方法来发送和接收DatagramPacket实例,进行通信;
- 通信完成后,调用DatagramSocket实例的close()方法来关闭该套接字。
由于UDP是无连接的,因此UDP服务端不需要等待客户端的请求以建立连接。另外,UDP服务器为所有通信使用同一套接字,这点与TCP服务器不同,TCP服务器则为每个成功返回的accept()方法创建一个新的套接字。
一个典型的UDP服务端要经过下面三步操作:
- 创建一个DatagramSocket实例,指定本地端口号,并可以有选择地指定本地地址,此时,服务器已经准备好从任何客户端接收数据报文;
- 使用DatagramSocket实例的receive()方法接收一个DatagramPacket实例,当receive()方法返回时,数据报文就包含了客户端的地址,这样就知道了回复信息应该发送到什么地方;
- 使用DatagramSocket实例的send()方法向服务器端返回DatagramPacket实例。
Server端
//端口
int port = 8080;
//创建DatagramSocket
DatagramSocket server = new DatagramSocket(port);
byte[] recvBuf = new byte[100];
DatagramPacket recvPacket = new DatagramPacket(recvBuf , recvBuf.length);
//接收数据
server.receive(recvPacket);
String sendStr = "Hello ! I'm Server";
byte[] sendBuf;
sendBuf = sendStr.getBytes();
DatagramPacket sendPacket = new DatagramPacket(sendBuf , sendBuf.length , addr , port );
//发送数据
server.send(sendPacket);
Client端
//创建客户端
DatagramSocket client = new DatagramSocket();
String sendStr = "Hello! I'm Client";
byte[] sendBuf;
sendBuf = sendStr.getBytes();
InetAddress addr = InetAddress.getByName("127.0.0.1");
int port = 8080;
//构建发送数据包
DatagramPacket sendPacket = new DatagramPacket(sendBuf ,sendBuf.length , addr , port);
//发送数据
client.send(sendPacket);
byte[] recvBuf = new byte[100];
//构建结束数据包
DatagramPacket recvPacket = new DatagramPacket(recvBuf , recvBuf.length);
//接收数据
client.receive(recvPacket);
String recvStr = new String(recvPacket.getData() , 0 ,recvPacket.getLength());
System.out.println("收到:" + recvStr);
client.close();
NIO
Non-Blocking IO - 同步非阻塞IO
在此种方式下,用户进程发起一个IO操作以后便可返回做其它事情,但是用户进程需要时不时的询问IO操作是否就绪,这就要求用户进程不停的去询问,从而引入不必要的CPU资源浪费。其中目前JAVA的NIO就属于同步非阻塞IO。
NIO对应的形象比喻:打电话时,如果对方没有话说,先挂掉电话,干点其他事情。过段时间再打过去,看看对方有没有话要说。如果对方有话要说,则拿着电话听对方说话。如此循环往复。
Java NIO编程是一个很重要的部分,会做专门的介绍:
Java NIO...
AIO
异步非阻塞IO
适用场景
BIO、NIO、AIO适用场景分析:
- BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序直观简单易理解。
- NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4开始支持。
- AIO方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持。
参考
- Java中BIO,NIO,AIO的理解