1.同步与异步
同步与异步是应用程序和操作系统对IO事件的处理。
同步:程序在处理IO事件时必须阻塞在某个方法上等待IO完成(IO阻塞事件或轮询IO事件),通俗来说,也就是一次只能完成一个连接的读写事件,不能同时完成其它读写事件,而且一件事件的完成必须依赖另一件事件的完成,只有等待被依赖事件完成后,依赖事件才算完成,比如客户端向服务器发送数据,必须等待服务器端的读事件完成,客户端的写事件才算完成,这批操作要么都成功,要么都失败,两个事件的状态保持一致,这是一种可靠的任务序列。
异步:程序在处理IO事件时,不需要正真完成IO事件,给程序一个通知就行,依赖事件不需要等待被依赖事件的完成,只要完成自身的事件就算完成,依赖事件无法确定被依赖事件是否完成,这是一种不可靠的任务序列。
2.阻塞和非阻塞
阻塞和非阻塞是应用程序访问数据时,对数据是否准备就绪的处理方式。
阻塞:程序访问数据时,数据准备就绪,程序直接返回,数据未准备就绪,程序则处于等待状态,比如多个客户端请求服务器端,当某一个客户端向服务器发送数据,但数据仍然在准备,程序会处于读事件的等待中,直到这个读事件完成,其它客户端才能访问服务器。
非阻塞:不管数据是否准备就绪,程序直接返回,像刚才的例子中,即便数据仍在准备,但其它客户端可以继续访问服务器,执行各自的连接事件。
小结:
同步与异步主要是针对某个连接中,数据的读写是否同步。
阻塞与非阻塞主要指服务端和多个连接中是否存在阻塞。
3.BIO
3.1 BIO的概念
BIO是Blocking IO的缩写,它基于java.io包,是Java推出之初的IO操作模块,它是基于流式模式实现的,交互方式是同步阻塞的方式。
3.2 BIO的特点
程序的IO操作的完成的状态是一致的,在读写动作完成前,线程会一直处于阻塞的状态。它的优点是简单直观,缺点是效率和拓展性都很低。
3.3 BIO的重要组件
Socket和ServerSocket是BIO的最重要的组件,Socket套接字代表的是客户端对象,同时也是交互通道,而ServerSocke是服务器对象,同时绑定IP地址和端口号。通过ServerSocke的accept()完成三次握手,表示接收Socket通道,最终从Socket中获取字节IO流完成数据的交互。
3.4 BIO的特点展示和底层原理
刚才说到BIO的特点是:
a.程序的IO操作的完成的状态是一致的,
b.在读写动作完成前,线程会一直处于阻塞的状态。
下面用两个案例展示阻塞IO的特点。
a.连接事件的状态一致,读写动作要么都成功,要么都失败:
public class Client {
public static void main(String[] args) throws IOException {
Socket socket = new Socket("127.0.0.1", 10000);
PrintStream ps = new PrintStream(socket.getOutputStream());
ps.println("同时成功");
ps.flush();
ps.close();
}
}
public class Server {
public static void main(String[] args) throws Exception {
ServerSocket serverSocket = new ServerSocket(10000);
Socket socket = serverSocket.accept();
BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
System.out.println("Client:" + br.readLine());
br.close();
}
}
b.在读写动作完成前,线程会一直处于阻塞的状态:
下面是多个客户端请求服务器的案例:
public class Server {
public static void main(String[] args) throws Exception {
ServerSocket serverSocket = new ServerSocket(10000);
while (true) {
Socket socket = serverSocket.accept();
BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String line;
while ((line = br.readLine()) != null) {
System.out.println(socket.getRemoteSocketAddress() + ":" + line);
}
br.close();
}
}
}
public class Client1 {
public static void main(String[] args) throws Exception {
Socket socket = new Socket("127.0.0.1", 10000);
Scanner sc = new Scanner(System.in);
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
System.out.println("请说:");
bw.write(sc.nextLine());
bw.close();
}
}
public class Client2 {
public static void main(String[] args) throws Exception {
Socket socket = new Socket("127.0.0.1", 10000);
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
bw.write("Hello");
bw.close();
}
}
控制台结果:
针对此案例,其底层原理是这样的:
服务器接收客户端的数据,并不是一开始就来到服务器端的,而是首先来到终端的网卡上面,然后再来到操作系统的内核中,最后才会到达服务器。就拿接收事件举例,调用accept(),内核便会执行recvFrom(Block…)方法,进行数据准备,将数据(Socket)从网卡发送到内核空间中,最终将数据拷贝到用户空间中,这样就完成接收事件。其它的连接事件的原理也是一样的。
在此案例中,我们先启动Client1再启动Client2会发现,即便Client2的写事件完成,可是服务器端依然没有收到Client2的发送数据,原因是Client1程序的写事件数据没有准备好,使服务器一直阻塞在读事件read(读Client1的数据)中,程序一直处于等待状中,所以无办法接收Client2的数据。
只要在Client1的控制台输入数据,服务器就不会阻塞,自然也会收到Client2的发送的数据,结果如下图:
3.5 解决BIO的阻塞问题
解决BIO阻塞的常用方法有两种(下面通过两个案例展示):
1).通过多线程异步的原理,每创建一次连接就分配一个独立的线程来处理连接事件,即一个客户端对应一个线程。
2).通过线程池,同样是利用异步方式解决阻塞,也是一个客户对应一个线程。
案例1:
// 服务器端程序
public class ServerDemo3 {
public static void main(String[] args) throws Exception {
ServerSocket serverSocket = new ServerSocket(10004);
while (true) {
Socket socket = serverSocket.accept();
// 每创建一次连接,分配一个独立线程来处理连接事件
new Thread(new MyRunnable(socket)).start();
}
}
}
// 线程
class MyRunnable implements Runnable {
private Socket socket;
public MyRunnable(Socket socket) {
this.socket = socket;
}
public Socket getSocket() {
return socket;
}
public void setSocket(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String line;
while ((line = br.readLine()) != null) {
System.out.println("客户端" + socket.getRemoteSocketAddress() + "说:" + line);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
// 客户端A
class ClientDemo2 {
public static void main(String[] args) throws Exception {
// 1.创建客户端Socket对象,输入服务器端的IP地址和端口号,请求连接服务器端
Socket socket = new Socket("127.0.0.1", 10004);
// 2.通过Socket管道得到一个字节输出流并包装成高级的缓冲字符输出流
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
// 3.通过输出流发送数据到服务器端
while (true) {
Scanner sc = new Scanner(System.in);
bw.write(sc.nextLine());
bw.newLine();
bw.flush();
}
}
}
// 客户端B
class ClientDemo3 {
public static void main(String[] args) throws Exception {
// 1.创建客户端Socket对象,输入服务器端的IP地址和端口号,请求连接服务器端
Socket socket = new Socket("127.0.0.1", 10004);
// 2.通过Socket管道得到一个字节输出流
OutputStream os = socket.getOutputStream();
// 3.通过字节输出流输出数据到服务器端
PrintStream ps = new PrintStream(os);
while (true) {
Scanner sc = new Scanner(System.in);
ps.println(sc.nextLine());
ps.flush();
}
}
}
案例2:
// 服务端
// 通过线程池解决BIO的阻塞问题
// 每创建一次连接就分配一个线程处理连接事件
public class Server {
public static void main(String[] args) throws Exception {
ServerSocket serverSocket = new ServerSocket(10001);
ExecutorService pool = Executors.newFixedThreadPool(5);
while (true) {
Socket socket = serverSocket.accept();
pool.submit(new ServerReader(socket));
}
}
}
// 线程
class ServerReader implements Runnable {
private Socket socket;
public ServerReader(Socket socket) {
this.socket = socket;
}
public Socket getSocket() {
return socket;
}
public void setSocket(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String line;
while ((line = br.readLine()) != null) {
System.out.println("客户端" + socket.getRemoteSocketAddress() + "说:" + line);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
// 客户端A
class Client1 {
public static void main(String[] args) throws Exception {
Socket socket = new Socket("127.0.0.1", 10001);
// 2.通过Socket管道得到一个字节输出流
OutputStream os = socket.getOutputStream();
// 3.通过字节输出流输出数据到服务器端
PrintStream ps = new PrintStream(os);
while (true) {
Scanner sc = new Scanner(System.in);
ps.println(sc.nextLine());
ps.flush();
}
}
}
class Client2 {
public static void main(String[] args) throws Exception {
Socket socket = new Socket("127.0.0.1", 10001);
// 2.通过Socket管道得到一个字节输出流并包装成高级的缓冲字符输出流
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
// 3.通过输出流发送数据到服务器端
while (true) {
Scanner sc = new Scanner(System.in);
bw.write(sc.nextLine());
bw.newLine();
bw.flush();
}
}
}
小结:
1.多线程解决阻塞问题,具有程序效率高、但内存可能溢出的特点;
2.线程池解决阻塞问题,效率相对多线程而言比较低,但内存不会溢出。