1、相关概念
1、Socket
Socket又称“套接字”。网络上的两个程序通过一个双向的通讯连接实现数据的交换,这个双向链路的一端称为一个Socket。Socket通常用来实现客户方和服务方的连接。Socket是TCP/IP协议的一个十分流行的编程界面,一个Socket由一个IP地址和一个端口号唯一确定。但是,Socket所支持的协议种类也不光TCP/IP一种,因此两者之间是没有必然联系的。在Java环境下,Socket编程主要是指基于TCP/IP协议的网络编程。
套接字的连接过程可以分为四个步骤:服务器监听、客户端请求服务器、服务器端连接确认、客户端连接确认并进行通信。
2、同步和异步
同步:在发出一个功能调用时,在没有得到结果之前,该调用就不返回。
异步:异步与同步相对,当一个异步过程调用发出后,调用者在没有得到结果之前,就可以继续执行后续操作。当这个调用完成后,一般通过状态、通知和回调来通知调用者。对于异步调用,调用的返回并不受调用者控制。
3、阻塞和非阻塞
阻塞:阻塞调用是指服务器端被调用者调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。
非阻塞:指在不能立即得到结果之前,该调用不会阻塞当前线程。
2、多种io的简介
1、BIO(同步阻塞IO)
使用ServerSocket绑定IP地址和监听端口,客户端发起连接,通过三次握手建立连接,用socket来进行通信,通过输入输出流的方式来进行同步阻塞的通信。每次客户端发起连接请求,都会启动一个线程。
2、socket通讯代码实现
服务器端:
package com.xyq.bio; import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; public class Server { final static int PORT = 8765; public static void main(String[] args) { /* 1. 建立用于监听和接受客户端连接请求的套接字 */ ServerSocket server = null; try { server = new ServerSocket(PORT); System.out.println("server start .."); /* 2.进行阻塞 等待客户端连接请求,在没有客户端连接请求到来之前程序会一直阻塞在这个函数里。*/ Socket socket = server.accept(); /*3.新建一个线程执行客户端的任务 */ new Thread(new ServerHandler(socket)).start(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); }finally { if(server != null){ try { server.close(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } server = null; } } }
服务器端响应工具类:
package com.xyq.bio; 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 (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); }finally { if(in != null){ try { in.close(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } if(out != null){ try { out.close(); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } } if(socket != null){ try { socket.close(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } socket = null; } } }
客户端:
package com.xyq.bio; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintWriter; import java.net.Socket; public class Client { //1、定义端口和地址 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("接收到客户端请求数据"); String response = in.readLine(); System.out.println("Client " + response); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); }finally { if(in != null){ try { in.close(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } if(out != null){ try { out.close(); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } } if(socket != null){ try { socket.close(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } socket = null; } } }
3、传统的socket通讯优化(采用伪异步)
传统的Socket通讯是每次有客户端请求服务器端都会创建一个线程,当线程过多时,服务器端可能会宕机。解决这个问题,可以使用JDK提供的线程池(伪异步)。
代码实现(客户端和服务端响应类保持不变)
server端
package com.xyq.bio2; import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; public class Server { final static int PORT = 8765; public static void main(String[] args) { //建立用于监听和接受客户端连接请求的套接字 ServerSocket server = null; try { server = new ServerSocket(PORT); System.out.println("server start .."); Socket socket = null; HandlerExecutorPool handlerExecutorPool = new HandlerExecutorPool(50, 1000); while(true){ socket = server.accept(); handlerExecutorPool.excute(new ServerHandler(socket)); } } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); }finally { if(server != null){ try { server.close(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } server = null; } } }
自定义线程池
package bhz.bio2; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.ExecutorService; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; public class HandlerExecutorPool { private ExecutorService executor; public HandlerExecutorPool(int maxPoolSize, int queueSize){ this.executor = new ThreadPoolExecutor( Runtime.getRuntime().availableProcessors(), maxPoolSize, 120L, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(queueSize)); } public void execute(Runnable task){ this.executor.execute(task); } }
2、NIO(同步非阻塞IO)
用NIO方式处理IO使用多路复用器Selector来轮询每个通道Channel,当通道中有事件时就通知处理,不会阻塞。但是使用相当复杂。
3、AIO(真正的异步非阻塞IO)
NIO2.0引入了新的异步通道的概念,不需要使用多路复用器(Selector)对注册通道进行轮询即可实现异步读写,从而简化了NIO编程模型。
3、BIO和NIO的区别
1、BIO(同步阻塞IO)
BIO是面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。
BIO的各种流是阻塞的。这意味着,当一个线程调用read()或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了
2、NIO(同步非阻塞IO)
NIO的缓冲导向方法。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含所有您需要处理的数据。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据。
NIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取。而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。
4、NIO详解
1、NIO的构成
NIO主要有三大核心部分:Buffer(缓冲区), Channel(通道), Selector。
Buffer:
Buffer是一个对象,它包含一些需要写入或者读取的数据。在NIO类库中加入Buffer对象,体现了新类库与原IO的一个重要区别。在面向流的IO中,可以直接将数据写入或读取到Stream对象中。在NIO类库中,所有的数据都是用缓冲区处理的(读写)。缓冲区实质上是一个数组,通常它是一个字节数组(ByteBuffer),也可以使用其他类型的数组。这个数组为缓冲区提供了访问数据的读写等操作属性,如位置、容量、上限等概念。
注 Buffer类型:最常使用的是ByteBuffer,实际上每一种java基本类型都对应了一种缓存区(除了Boolean类型)。ByteBuffer,CharBuffer, DoubleBuffer, FloatBuffer, IntBuffer, LongBuffer, ShortBuffer
Buffer常用API(以IntBuffer为例)
package com.xyq.nio; import java.nio.IntBuffer; public class BufferTest { public static void main(String[] args) { // 1 基本操作 /*//创建指定长度的缓冲区 IntBuffer buffer = IntBuffer.allocate(10); //装载数据 buffer.put(10);// position位置:0 - > 1 buffer.put(20);// position位置:1 - > 2 buffer.put(30);// position位置:2 - > 3 System.out.println("未调用flip方法前的buffer " + buffer);//position位置为3 buffer.flip(); //把位置复位为0,也就是position位置由3->0 System.out.println("调用flip方法之后的buffer " + buffer); System.out.println("buffer的容量 " + buffer.capacity());//容量一旦初始化后不允许改变(warp方法包裹数组除外) System.out.println("buffer的限制 " + buffer.limit());//由于只装载了三个元素,所以可读取或者操作的元素为3 则limit=3 System.out.println("获取下标为1的元素 " + buffer.get(1)); //调用get(index)方法,不会改变position的值 System.out.println("调用get(index)之后的buffer " + buffer); //修改下标为index的元素的值 buffer.put(1, 50); System.out.println("调用put(int index, int i)之后的buffer " + buffer);//调用put(int index, int i)不会改变position的值 System.out.println("获取下标为1的元素 " + buffer.get(1)); //get()方法 for(int i=0; i<buffer.limit(); i++){ System.out.println(buffer.get() + "\t");//调用get方法会使其缓冲区位置(position)向后递增一位 } System.out.println("调用get()方法之后的buffer" + buffer);*/ /*// 2 wrap方法使用 // wrap方法会包裹一个数组: 一般这种用法不会先初始化缓存对象的长度,因为没有意义,最后还会被wrap所包裹的数组覆盖掉。 // 并且wrap方法修改缓冲区对象的时候,数组本身也会跟着发生变化。 int []arr = new int[]{1, 2, 3}; IntBuffer buffer = IntBuffer.wrap(arr); System.out.println("调用wrap(int[] array)之后的buffer" + buffer); IntBuffer buffer2 = IntBuffer.wrap(arr, 0, 2);//这样使用表示容量为数组arr的长度,但是可操作的元素只有实际进入缓存区的元素长度 System.out.println("调用了wrap(int[] array, int offset, int length)之后的buffer" + buffer2);*/ // 3 其他方法 IntBuffer buffer = IntBuffer.allocate(10); int []arr = new int[]{1, 2, 3}; buffer.put(arr); System.out.println("调用了put(int[] src)之后的buffer" + buffer); //复制 // IntBuffer buffer2 = buffer.duplicate(); //buffer2的pos、lim、cap与buffer的一样 // System.out.println("buffer2" + buffer2); // //buffer的位置属性 // buffer.position(2);//手动设置pos的值 // System.out.println("调用了position(int newPosition)方法之后的buffer " + buffer); // // System.out.println("buffer的可读数据 " + buffer.remaining()); int[] arr2 = new int[buffer.remaining()]; buffer.get(arr2);//将缓冲区数据放入arr2数组中去 for(int i : arr2){ System.out.println(Integer.toString(i) + ","); } } }
Channel:
通道,被建立的一个应用程序和操作系统交互事件、传递内容的渠道(注意是连接到操作系统)。一个通道会有一个专属的文件状态描述符。那么既然是和操作系统进行内容的传递,那么说明应用程序可以通过通道读取数据,也可以通过通道向操作系统写数据。
Channel就像自来水管道一样,网络数据通过Channel读取和写入,通道与流的不同之处在于通道是双向的,而流只能在一个方向上移动(一个流必须是InputStream或者OutputStream的子类),而通道可以用于读、写或者二者同时进行,最关键的是可以和多路复用器集合起来,有多种的状态位,方便多路复用器去识别。通道分为两大类:一类是用于网络读写的SelectableChannel,另一类是用于文件操作的FileChannel,我们使用的SocketChannel和ServerSocketChannel都是SelectableChannel的子类。
在NIO中channel的实现主要有:1、FileChannel (文件IO)2、DatagramChannel (UDP)3、SocketChannel (TCP)4、ServerSocketChannel (TCP)
Selector:
多路复用器提供选择已经就绪的任务的能力。简单说,就是Selector会不断的轮询注册在其上的通道(Channel),如果某个通道发生了读写操作,这个通道就处于就绪状态,会被Selector轮询出来,然后通过SelectionKey可以取得就绪的Channel集合,从而进行后续的IO操作。一个多路复用器(Selector)可以负责成千上万的通道(Channel),没有上限。这也是JDK使用了epoll代替传统的select实现,获得连接句柄(客户端)没有限制。那也就意味着我们只要一个线程负责Selector的轮询,就可以接入成千上万个客户端,这是JDK NIO库的巨大进步。
Selector线程类似一个管理者(Master),管理了成千上万个管道,然后轮询哪个管道的数据已经准备好了,通知CPU执行IO的读取或写入操作。
Selector模式:当IO事件(管道)注册到选择器以后,Selector会分配给每个管道一个key值,相当于标签。Selector选择器是以轮询的方式进行查找注册的所有IO事件(管道),当IO事件(管道)准备就绪后,Selector就会识别,会通过key值来找到相应的管道,进行相关的数据处理操作(从管道中读取或写入数据,写到缓冲区中)。每个管道都会对选择器进行注册不同的事件状态,以便选择器查找。
事件状态 SelectionKey.OP_CONNECT(连接状态) SelectionKey.OP_ACCEPT(阻塞状态) SelectionKey.OP_READ(可读状态) SelectionKey.OP_WRITE(可写状态)
2、NIO的结构
3、NIO通信模型图解:
4、NIO通信代码实例
服务器端
package com.xyq.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 selector; //2 建立缓冲区 private ByteBuffer buffer = ByteBuffer.allocate(1024); public static void main(String[] args) { new Thread(new Server(8765)).start(); } public Server(int port) { try { //1 打开路复用器 this.selector = Selector.open(); //2 打开服务器通道 ServerSocketChannel ssc = ServerSocketChannel.open(); //3 设置服务器通道为非阻塞模式 ssc.configureBlocking(false); //4 绑定地址 ssc.bind(new InetSocketAddress(port)); //5 把服务器通道注册到多路复用器上,并且监听阻塞事件 ssc.register(this.selector, SelectionKey.OP_ACCEPT); System.out.println("server start, port " + port); } catch (IOException e) { e.printStackTrace(); } } @Override public void run() { while(true){ try { //1 必须要让多路复用器开始监听 this.selector.select(); //2 返回多路复用器已经选择的结果集 Iterator<SelectionKey> keys = this.selector.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); } } } } catch (IOException e) { e.printStackTrace(); } } } /** * 阻塞状态是调用的方法 * @param key */ 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.selector, SelectionKey.OP_READ); } catch (IOException e) { e.printStackTrace(); } } /** * 可读状态时调用的方法 * @param key */ private void read(SelectionKey key){ try { //1 清空缓冲区旧的数据 this.buffer.clear(); //2 获取之前注册的socket通道对象 SocketChannel sc = (SocketChannel) key.channel(); //3 读取数据 int count = sc.read(this.buffer); //4 如果没有数据 if(count == -1){ key.channel().close(); key.channel(); return; } //5 有数据则进行读取 读取之前需要进行复位方法(把position 和limit进行复位) this.buffer.flip(); //6 根据缓冲区的数据长度创建相应大小的byte数组,接收缓冲区的数据 byte[] bs = new byte[this.buffer.remaining()]; //7 接收缓冲区数据 this.buffer.get(bs); //8 打印结果 String body = new String(bs).trim(); System.out.println("server : " + body); } catch (IOException e) { e.printStackTrace(); } } }
客户端
package com.xyq.nio; import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.SocketChannel; public class Client { public static void main(String[] args) { //创建连接的地址 InetSocketAddress address = new InetSocketAddress("127.0.0.1", 8765); //声明连接通道 SocketChannel sc = null; //建立缓冲区 ByteBuffer buffer = ByteBuffer.allocate(1204); try { //打开通道 sc = SocketChannel.open(); //进行连接 sc.connect(address); while(true){ //定义一个字节数组,然后使用系统录入功能: byte[] bs = new byte[1024]; //提示用户输入 System.out.println("请输入你要输入的信息"); //系统录入 System.in.read(bs); //把数据放到缓冲区中 buffer.put(bs); //对缓冲区进行复位 buffer.flip(); //写出数据 sc.write(buffer); //清空缓冲区数据 buffer.clear(); } } catch (IOException e) { e.printStackTrace(); }finally { //关闭资源 if(sc != null){ try { sc.close(); } catch (IOException e) { e.printStackTrace(); } } } } }
5、AIO
异步IO则是采用“订阅-通知”模式:即应用程序向操作系统注册IO监听,然后继续做自己的事情。当操作系统发生IO事件,并且准备好数据后,在主动通知应用程序,触发相应的函数.
AIO是在NIO的基础上引入了异步通道的概念,并提供了异步文件和异步套接字通道的实现,从而在真正意义上实现了异步非阻塞,之前的NIO只是非阻塞而并非异步。AIO不需要通过对多路复用器对注册的通道进行轮询操作即可实现异步读写,从而简化NIO编程模型。
代码示例
服务器端
package com.xyq.aio; import java.net.InetSocketAddress; import java.nio.channels.AsynchronousChannelGroup; import java.nio.channels.AsynchronousServerSocketChannel; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class Server { //线程池 private ExecutorService es; //线程组 private AsynchronousChannelGroup acg; //服务器通道 public AsynchronousServerSocketChannel assc; public static void main(String[] args) { Server server = new Server(8765); } public Server(int port) { try { //创建一个缓存池 es = Executors.newCachedThreadPool(); //创建线程组 acg = AsynchronousChannelGroup.withCachedThreadPool(es, 1); //创建服务器通道 assc = AsynchronousServerSocketChannel.open(acg); //进行绑定 assc.bind(new InetSocketAddress(port)); System.out.println("server start, port " + port); //进行阻塞 assc.accept(this, new ServerHandler()); //一直阻塞 不让服务器停止 Thread.sleep(Integer.MAX_VALUE); } catch (Exception e) { e.printStackTrace(); } } }
服务器响应工具类
package com.xyq.aio; import java.nio.ByteBuffer; import java.nio.channels.AsynchronousSocketChannel; import java.nio.channels.CompletionHandler; public class ServerHandler implements CompletionHandler<AsynchronousSocketChannel, Server>{ @Override public void completed(AsynchronousSocketChannel asc, Server server) { //当有下一个客户端接入的时候 直接调用Server的accept方法,这样反复执行下去,保证多个客户端都可以阻塞 server.assc.accept(server, this); //执行读方法 read(asc); } private void read(final AsynchronousSocketChannel asc){ //读取数据 ByteBuffer buffer = ByteBuffer.allocate(1024); asc.read(buffer, buffer, new CompletionHandler<Integer, ByteBuffer>() { @Override public void completed(Integer result, ByteBuffer buffer) { //进行读取之后,重置标识位 buffer.flip(); //获得读取的字节数 System.out.println("server 收到客户端的数据长度为" + result); //获取读取的数据 String resultDate = new String(buffer.array()).trim(); System.out.println("server 收到客户端的数据为" + resultDate); //给客户端响应信息 String response = "服务器响应 收到了客户端的信息" + resultDate; //调用写方法 write(asc, response); } @Override public void failed(Throwable exc, ByteBuffer attachment) { exc.printStackTrace(); } }); } private void write(AsynchronousSocketChannel asc, String response){ try { //创建缓冲区 ByteBuffer buffer = ByteBuffer.allocate(1024); //加载数据 buffer.put(response.getBytes()); //复位 buffer.flip(); //写出数据 asc.write(buffer).get(); } catch (Exception e) { e.printStackTrace(); } } @Override public void failed(Throwable exc, Server attachment) { exc.printStackTrace(); } }
客户端
package com.xyq.aio; import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.AsynchronousSocketChannel; public class Client implements Runnable{ //客户端通道 AsynchronousSocketChannel asc; public static void main(String[] args) throws Exception { Client client = new Client(); client.connect(); Client client2 = new Client(); client2.connect(); Client client3 = new Client(); client3.connect(); new Thread(client, "client").start(); new Thread(client2, "client2").start(); new Thread(client3, "client3").start(); Thread.sleep(1000); client.write("c1 aaa"); client2.write("c2 bbb"); client3.write("c3 ccc"); } public Client() throws IOException { //打开通道 asc = AsynchronousSocketChannel.open(); } //建立连接 public void connect(){ asc.connect(new InetSocketAddress("127.0.0.1", 8765)); } //写出数据 public void write(String response){ try { asc.write(ByteBuffer.wrap(response.getBytes())).get(); read(); } catch (Exception e) { e.printStackTrace(); } } //读入数据 public void read(){ //建立缓冲区 ByteBuffer buffer = ByteBuffer.allocate(1024); try { //读数据 asc.read(buffer).get(); //复位 buffer.flip(); byte[] respon = new byte[buffer.remaining()]; buffer.get(respon); System.out.println(new String(respon, "utf-8").trim()); } catch (Exception e) { e.printStackTrace(); } } @Override public void run() { while(true){ } } }