5. IO 通信模型
网络通信的本质是网络间的数据 IO。只要有 IO,就会有阻塞或非阻塞的问题,无论这个 IO 是网络的,还是硬盘的。原因在于程序是运行在系统之上的,
任何形式的 IO 操作发起都需要系统的支持
使用套接字建立TCP连接后,通过使用 I/O 流在进行通信,每一个socket都有一个输出流和一个输入流,客户端的输出流连接到服务器端的输入流,而客
户端的输入流连接到服务器端的输出流
5.1. BIO(阻塞模式)
BIO 即 blocking IO,是一种阻塞式的 IO;jdk1.4 版本之前 Socket 即 BIO 模式;BIO 的问题在于 accept()、read()的操作点都是被阻塞的
服务器线程发起一个 accept 动作(侦听并接受到此套接字的连接),询问操作系统是否有新的 socket 信息从端口 X 发送过来。注意,是询问操作系
统;如果操作系统没有发现有 socket从指定的端口 X 来,那么操作系统就会等待。这样serverSocket.accept()方法就会一直等待。这就是为什么 accept()方
法为什么会阻塞
如果想让 BIO 同时处理多个客户端请求,就必须使用多线程,即每次 accept阻塞等待来自客户端请求,一旦收到连接请求就建立通信,同时开启一个新
的线程来处理这个套接字的数据读写请求,然后立刻又继续 accept 等待其他客户端连接请求,即为每一个客户端连接请求都创建一个线程来单独处理
传统socket代码:客户端
import java.io.DataOutputStream; import java.io.FileInputStream; import java.net.Socket; public class TraditionalClient { public static void main(String[] args) throws Exception { long start = System.currentTimeMillis(); // 创建socket链接 Socket socket = new Socket("localhost", 2000); System.out.println("Connected with server: " + socket.getInetAddress() + ":" + socket.getPort()); // 读取文件 FileInputStream inputStream = new FileInputStream("C:/sss.txt"); // 输出文件 DataOutputStream output = new DataOutputStream(socket.getOutputStream()); // 缓冲区4096K byte[] b = new byte[4096]; // 传输长度 long read = 0, total = 0; // 读取文件,写到socketio中 while ((read = inputStream.read(b)) >= 0) { total = total + read; output.write(b); } // 关闭 output.close(); socket.close(); inputStream.close(); // 打印时间 System.out.println("bytes send--" + total + " and totaltime--" + (System.currentTimeMillis() - start)); } }
传统socket代码:服务端
package cn.itcast_02_nio; import java.io.DataInputStream; import java.net.ServerSocket; import java.net.Socket; public class TraditionalServer { @SuppressWarnings("resource") public static void main(String args[]) throws Exception { // 监听端口 ServerSocket server_socket = new ServerSocket(2000); System.out.println("等待,端口为:" + server_socket.getLocalPort()); while (true) { // 阻塞接受消息 Socket socket = server_socket.accept(); // 打印链接信息 System.out.println("新连接: " + socket.getInetAddress() + ":" + socket.getPort()); // 从socket中获取流 DataInputStream input = new DataInputStream(socket.getInputStream()); // 接收数据 byte[] byteArray = new byte[4096]; while (true) { int nread = input.read(byteArray, 0, 4096); System.out.println(new String(byteArray, "UTF-8")); if (-1 == nread) { break; } } socket.close(); System.out.println("Connection closed by client"); } } }
5.2.NIO(非阻塞模式)
NIO 即 non-blocking IO,是一种非阻塞式的 IO。jdk1.4 之后提供
NIO 三大核心部分:Channel(通道),Buffer(缓冲区), Selector(选择器)
Buffer:容器对象,包含一些要写入或者读出的数据。在 NIO 库,所有数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中的;
在写入数据时,也是写入到缓冲区中。任何时候访问 NIO 中的数据,都是通过缓冲区进行操作
Channel:通道对象,对数据的读取和写入要通过 Channel,它就像水管一样。通道不同于流的地方就是通道是双向的,可以用于读、写和同
时读写操作。Channel 不会直接处理字节数据,而是通过 Buffer 对象来处理数据
Selector:多路复用器,选择器。提供选择已经就绪的任务的能力。Selector会不断轮询注册在其上的 Channel,如果某个 Channel 上面发生
读或者写事件,这个 Channel 就处于就绪状态,会被 Selector 轮询出来,进行后续的 I/O 操作。这样服务器只需要一两个线程就可以进行多客户端
通信
Socket Nio代码:客户端
import java.io.FileInputStream; import java.io.IOException; import java.net.InetSocketAddress; import java.net.SocketAddress; import java.nio.channels.FileChannel; import java.nio.channels.SocketChannel; public class TransferToClient { @SuppressWarnings("resource") public static void main(String[] args) throws IOException { long start = System.currentTimeMillis(); // 打开socket的nio管道 SocketChannel sc = SocketChannel.open(); sc.connect(new InetSocketAddress("localhost", 9026));// 绑定相应的ip和端口 sc.configureBlocking(true);// 设置阻塞 // 将文件放到channel中 FileChannel fc = new FileInputStream("C:/sss.txt").getChannel();// 打开文件管道 //做好标记量 long size = fc.size(); int pos = 0; int offset = 4096; long curnset = 0; long counts = 0; //循环写 while (pos<size) { curnset = fc.transferTo(pos, 4096, sc);// 把文件直接读取到socket chanel中,返回文件大小 pos+=offset; counts+=curnset; } //关闭 fc.close(); sc.close(); //打印传输字节数 System.out.println(counts); // 打印时间 System.out.println("bytes send--" + counts + " and totaltime--" + (System.currentTimeMillis() - start)); } }
Socket Nio代码:服务端
import java.io.IOException; import java.net.InetSocketAddress; import java.net.ServerSocket; import java.nio.ByteBuffer; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; public class TransferToServer { public static void main(String[] args) throws IOException { // 创建socket channel ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); ServerSocket ss = serverSocketChannel.socket(); ss.setReuseAddress(true);// 地址重用 ss.bind(new InetSocketAddress("localhost", 9026));// 绑定地址 System.out.println("监听端口 : " + new InetSocketAddress("localhost", 9026).toString()); // 分配一个新的字节缓冲区 ByteBuffer dst = ByteBuffer.allocate(4096); // 读取数据 while (true) { SocketChannel channle = serverSocketChannel.accept();// 接收数据 System.out.println("Accepted : " + channle); channle.configureBlocking(true);// 设置阻塞,接不到就停 int nread = 0; while (nread != -1) { try { nread = channle.read(dst);// 往缓冲区里读 byte[] array = dst.array();//将数据转换为array //打印 String string = new String(array, 0, dst.position()); System.out.print(string); dst.clear(); } catch (IOException e) { e.printStackTrace(); nread = -1; } } } } }
5.3. 阻塞/非阻塞、同步/非同步
阻塞 IO 和非阻塞 IO 这两个概念是程序级别的。主要描述的是程序请求操作系统 IO 操作后,如果 IO 资源没有准备好,那么程序该如何处理的问题:
前者等待;后者继续执行(并且使用线程一直轮询,直到有 IO 资源准备好了)
同步 IO 和非同步 IO,这两个概念是操作系统级别的。主要描述的是操作系统在收到程序请求 IO 操作后,如果 IO 资源没有准备好,该如何响应程序
的问题:前者不响应,直到 IO 资源准备好以后;后者返回一个标记(好让程序和自己知道以后的数据往哪里通知),当 IO 资源准备好以后,再用事件机
制返回给程序