Java - 网络IO

发展历程

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套接字,客户端和服务器端都通过DatagramSocketsend()方法和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客户端要经过下面三步操作:

  1. 创建一个DatagramSocket实例,可以有选择地对本地地址和端口号进行设置,如果设置了端口号,则客户端会在该端口号上监听从服务器端发送来的数据;
  2. 使用DatagramSocket实例的send()和receive()方法来发送和接收DatagramPacket实例,进行通信;
  3. 通信完成后,调用DatagramSocket实例的close()方法来关闭该套接字。

由于UDP是无连接的,因此UDP服务端不需要等待客户端的请求以建立连接。另外,UDP服务器为所有通信使用同一套接字,这点与TCP服务器不同,TCP服务器则为每个成功返回的accept()方法创建一个新的套接字。
一个典型的UDP服务端要经过下面三步操作:

  1. 创建一个DatagramSocket实例,指定本地端口号,并可以有选择地指定本地地址,此时,服务器已经准备好从任何客户端接收数据报文;
  2. 使用DatagramSocket实例的receive()方法接收一个DatagramPacket实例,当receive()方法返回时,数据报文就包含了客户端的地址,这样就知道了回复信息应该发送到什么地方;
  3. 使用DatagramSocket实例的send()方法向服务器端返回DatagramPacket实例。
Java - 网络IO_第1张图片
image.png
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的理解

你可能感兴趣的:(Java - 网络IO)