二、Socket网络通信编程
1、IO(BIO阻塞IO)
1.1基本概念
Socket又称”套接字”,应用程序通常通过“套接字”向网络发出请求或者应答网络请求。
Socket和ServerSocket类库位于java.net包中。ServerSocket用于服务器端,Socket是简历网络连接时使用的。在连接成功是,应用程序两端都会产生一个Socket实例,操作这个实例完成所需的会话。对于一个网络连接来说,套接字是平等的,不因为在服务器端或在客户端而产生不同级别。不管是Socket还是ServerSocket他们的工作都是通过SocketImpl类及其子类完成的。
套接字之间的连接过程可以分为四个步骤(根据我们的例子来表述的,传统TCP连接遵循3次握手,4次挥手即可):服务器监听,客户端请求服务器,服务器确认,客户端确认,进行通信。
- (1)服务器监听:服务器端套接字并不定位具体的客户端套接字,而是出去等待连接的状态,实时监控网络状态。
- (2)客户端请求:是指由客户端的套接字提出的连接请求,要连接的目标是服务器端的套接字。因此,客户端的套接字必须首先描述它要连接的服务器的套接字,指出服务器端套接字的地址和端口,然后就想服务器端套接字提出连接请求
- (3)服务器端连接确认:是指当服务器端套接字监听到或者说接收到客户端套接字的连接请求,他就响应客户端套接字的请求,建立一个新的线程,把服务器端套接字的描述发给客户端。
- (4)客户端连接确认:一旦客户端确认了此描述,连接就建立好了。双方开始进行通信。而服务器端套接字继续处于监听状态,继续接受其他客户端套接字的连接请求。
三次握手:首先Client端发送连接请求报文,Server段接受连接后回复ACK报文,并为这次连接分配资源。Client端接收到ACK报文后也向Server段发生ACK报文,并分配资源,这样TCP连接就建立了。
四次挥手:假设Client端发起中断连接请求,也就是发送FIN报文。Server端接到FIN报文后,意思是说”我Client端没有数据要发给你了”,但是如果你还有数据没有发送完成,则不必急着关闭Socket,可以继续发送数据。所以你先发送ACK,”告诉Client端,你的请求我收到了,但是我还没准备好,请继续你等我的消息”。这个时候Client端就进入FIN_WAIT状态,继续等待Server端的FIN报文。当Server端确定数据已发送完成,则向Client端发送FIN报文,”告诉Client端,好了,我这边数据发完了,准备好关闭连接了”。Client端收到FIN报文后,”就知道可以关闭连接了,但是他还是不相信网络,怕Server端不知道要关闭,所以发送ACK后进入TIME_WAIT状态,如果Server端没有收到ACK则可以重传。“,Server端收到ACK后,”就知道可以断开连接了”。Client端等待了2MSL后依然没有收到回复,则证明Server端已正常关闭,那好,我Client端也可以关闭连接了。Ok,TCP连接就这样关闭了!
2、NIO(非阻塞IO)
2.1 基本概念
IO(BIO)与NIO的区别:其本质就是阻塞和非阻塞的区别。
- 阻塞:应用程序在获取网络数据的时候,如果网络传输数据很慢,那么程序就一直等着,知道传输完毕为止。
- 非阻塞:程序直接可以获取已经准备就绪的数据,无需等待。
BIO为同步阻塞形式,NIO为同步非阻塞形式。NIO并没有实现异步,在JDK1.7之后,升级了NIO包,支持异步非阻塞通信模型,即NIO2.0(AIO)
同步与异步:同步和异步一定是面向操作系统与应用程序对IO操作的层面上来区别的。
- 同步:应用程序会直接参与IO读写操作,并且我们的应用程序会直接阻塞到某个方法上,直到数据准备就绪;或者采用轮询的策略实时检查数据的就绪状态,如果就绪则获取数据。
- 异步:异步时所有的IO读写操作都交个操作系统处理,与我们的应用程序没有直接关系,我们程序不需要关心IO读写,当操作系统完成了IO读写操作时,会给我们应用程序发送通知,我们的应用程序直接拿走数据即可。
同步说的是你的Server服务器端的执行方式
阻塞说的是具体的技术,接收数据的方式、状态(IO,NIO)
2.2 NIO编程介绍
几个概念:
- Buffer(缓冲区)
- Channel(管道,通道)
- Selectir(选择器、多路复用器)
NIO的本质就是避免原始的TCP建立连接使用3次握手的操作,减少连接的开销
2.2.1 Buffer(缓冲区)
Buffer是一个对象,它包含一些要写入或者要读取的数据。在NIO类库中加入了Buffer对象,体现了新库与原IO的一个重要的区别。在面向流的IO中,可以将数据直接写入或者读取到Stream对象中。在NIO库中,所有数据都是用缓冲区处理的(读写)。缓冲区实质上是一个数组,通常它是一个字节数组(ByteBuffer),也可以使用其他类型的数据。这个数组为缓冲区提供了数据的访问读写等操作属性,如位置、容量、上限等概念。
Buffer类型:我们最常用的就是ByteBuffer,实际上每一种java基本类型都对应了一个种缓存区(除了Boolean类型以外)
- ByteBuffer
- CharBuffer
- ShortBuffer
- IntBuffer
- LongBuffer
- FloatBuffer
- DoubleBuffer
2.2.2 NIO应用
- SelectionKey.OP_ACCEPT 服务端接收客户端连接事件
- SelectionKey.OP_CONNECT 客户端连接服务端事件
- SelectionKey.OP_READ 读事件
- SelectionKey.OP_WRITE 写事件
服务端和客户端各自维护一个管理通道的对象,我们称之为selector,该对象能检测一个或多个通道 (channel) 上的事件。我们以服务端为例,如果服务端的selector上注册了读事件,某时刻客户端给服务端发送了一些数据,阻塞I/O这时会调用read()方法阻塞地读取数据,而NIO的服务端会在selector中添加一个读事件。服务端的处理线程会轮询地访问selector,如果访问selector时发现有感兴趣的事件到达,则处理这些事件,如果没有感兴趣的事件到达,则处理线程会一直阻塞直到感兴趣的事件到达为止。下面是我理解的java NIO的通信模型示意图:
Server.java
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 seletor; //2 建立缓冲区 private ByteBuffer readBuf = ByteBuffer.allocate(1024); //3 private ByteBuffer writeBuf = ByteBuffer.allocate(1024); public Server(int port){ try { //1 打开路复用器 this.seletor = Selector.open(); //2 打开服务器通道 ServerSocketChannel ssc = ServerSocketChannel.open(); //3 设置服务器通道为非阻塞模式 ssc.configureBlocking(false); //4 绑定地址 ssc.bind(new InetSocketAddress(port)); //5 把服务器通道注册到多路复用器上,并且监听阻塞事件 ssc.register(this.seletor, SelectionKey.OP_ACCEPT); System.out.println("Server start, port :" + port); } catch (IOException e) { e.printStackTrace(); } } @Override public void run() { while(true){ try { //1 必须要让多路复用器开始监听 this.seletor.select(); //2 返回多路复用器已经选择的结果集 Iterator<SelectionKey> keys = this.seletor.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); } //9 写数据 if(key.isWritable()){ this.write(key); //ssc } } } } catch (IOException e) { e.printStackTrace(); } } } private void write(SelectionKey key){ System.out.println("Server Write..."); try { SocketChannel sc = (SocketChannel)key.channel(); String nameString = "[" + sc.socket().getInetAddress().toString().substring(1) + "]"; String respStr = nameString + " 你好,客户端,Server已经准备就绪,可以传输数据." + System.currentTimeMillis(); System.out.println(respStr); byte[] response = respStr.getBytes(); this.writeBuf.put(response); this.writeBuf.flip(); sc.write(this.writeBuf); this.writeBuf.clear(); sc.register(this.seletor, SelectionKey.OP_READ); } catch (IOException e) { e.printStackTrace(); } } private void result(SelectionKey key, String info){ SocketChannel sc = (SocketChannel)key.channel(); try { String nameString = "[" + sc.socket().getInetAddress().toString().substring(1) + "]"; this.writeBuf.clear(); this.writeBuf.put((nameString + ":" + info).getBytes()); this.writeBuf.flip(); sc.write(this.writeBuf); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } private void read(SelectionKey key) { System.out.println("Server Read..."); try { //1 清空缓冲区旧的数据 this.readBuf.clear(); //2 获取之前注册的socket通道对象 SocketChannel sc = (SocketChannel) key.channel(); //3 读取数据 int count = sc.read(this.readBuf); //4 如果没有数据 if(count == -1){ key.channel().close(); key.cancel(); return; } //5 有数据则进行读取 读取之前需要进行复位方法(把position 和limit进行复位) this.readBuf.flip(); //6 根据缓冲区的数据长度创建相应大小的byte数组,接收缓冲区的数据 byte[] bytes = new byte[this.readBuf.remaining()]; //7 接收缓冲区数据 this.readBuf.get(bytes); //8 打印结果 String body = new String(bytes).trim(); System.out.println("我是Server : " + body); sc.register(this.seletor, SelectionKey.OP_WRITE); } catch (IOException e) { e.printStackTrace(); } } private void accept(SelectionKey key) { System.out.println("Server Accept..."); try { //1 获取服务通道 ServerSocketChannel ssc = (ServerSocketChannel) key.channel(); //2 执行阻塞方法 SocketChannel sc = ssc.accept(); //3 设置阻塞模式 sc.configureBlocking(false); //4 注册到多路复用器上,并设置读取标识 sc.register(this.seletor, SelectionKey.OP_WRITE); } catch (IOException e) { e.printStackTrace(); } } public static void main(String[] args) { new Thread(new Server(8765)).start();; } }
Client.java
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.SocketChannel; import java.util.Iterator; public class Client implements Runnable{ private ByteBuffer writeBuf = ByteBuffer.allocate(1024); private ByteBuffer readBuf = ByteBuffer.allocate(1024); private Selector selector; private String IP = "127.0.0.1"; private int PORT = 8765; private InetSocketAddress address; public Client() { // TODO Auto-generated constructor stub try { this.address = new InetSocketAddress(IP,PORT); this.selector = Selector.open(); SocketChannel clientChannel = SocketChannel.open(); clientChannel.configureBlocking(false); //clientChannel.connect(address); //clientChannel.register(selector, SelectionKey.OP_CONNECT); //测试发现,如果先connect再register OP_CONNECT会导致客户端选择不到connect事件 clientChannel.register(selector, SelectionKey.OP_CONNECT); clientChannel.connect(address); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } @Override public void run() { // TODO Auto-generated method stub while(true){ try { this.selector.select(); Iterator<SelectionKey> skIterator = this.selector.selectedKeys().iterator(); while(skIterator.hasNext()){ SelectionKey nextKey = skIterator.next(); skIterator.remove(); if( nextKey.isValid() ){ if( nextKey.isAcceptable() ){ this.accept(nextKey); }else if(nextKey.isReadable()){ this.read(nextKey); }else if(nextKey.isWritable()){ this.write(nextKey); }else if(nextKey.isConnectable()){ this.connect(nextKey); } } } } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } private void connect(SelectionKey key){ System.out.println("Client Connect..."); SocketChannel sc = (SocketChannel)key.channel(); try { if( sc.isConnectionPending() ){ sc.finishConnect(); } sc.configureBlocking(false); //在和服务端连接成功之后,为了可以接收到服务端的信息,需要给通道设置读的权限 sc.register(this.selector, SelectionKey.OP_READ); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } private void accept(SelectionKey key){ System.out.println("Client Accept..."); } private void write(SelectionKey key){ System.out.println("Client Write..."); SocketChannel sc = (SocketChannel)key.channel(); try { //定义一个字节数组,然后使用系统录入功能: byte[] bytes = new byte[1024]; System.in.read(bytes); //清空缓冲区数据 writeBuf.clear(); //把数据放到缓冲区中 writeBuf.put(bytes); //对缓冲区进行复位 writeBuf.flip(); //写出数据 sc.write(writeBuf); sc.register(this.selector, SelectionKey.OP_READ); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } private void read(SelectionKey key){ System.out.println("Client Read..."); SocketChannel sc = (SocketChannel)key.channel(); try { this.readBuf.clear(); sc.read(this.readBuf); this.readBuf.flip(); byte[] bytes = new byte[this.readBuf.remaining()]; this.readBuf.get(bytes); System.out.println("Client打印:" + new String(bytes).trim()); sc.register(this.selector, SelectionKey.OP_WRITE); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } //需要一个Selector public static void main(String[] args) { new Thread(new Client()).start(); } }
2.3 AIO编程介绍
AIO编程,在NIO基础之上移入了异步通道的概念。并提供了异步文件和异步套接字通道的实现,从而在真正意义上实现了异步非阻塞,在之前我们学习的NIO只能是非阻塞而并非异步。而AIO他不需要通过多路复用器对注册的通道进行轮询操作即可实现异步读写,从而简化了NIO编程模型。也可以称之为NIO2.0,这种模式才真正的属于我们异步非阻塞的模型。
AsynchronousServerSocketChannel
AsynchronousSocketChannel
Server.java
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 executorService; //线程组 private AsynchronousChannelGroup threadGroup; //服务器通道 public AsynchronousServerSocketChannel assc; public Server(int port){ try { //创建一个缓存池 executorService = Executors.newCachedThreadPool(); //创建线程组 threadGroup = AsynchronousChannelGroup.withCachedThreadPool(executorService, 1); //创建服务器通道 assc = AsynchronousServerSocketChannel.open(threadGroup); //进行绑定 assc.bind(new InetSocketAddress(port)); System.out.println("server start , port : " + port); //进行阻塞 assc.accept(this, new ServerCompletionHandler()); //一直阻塞 不让服务器停止 Thread.sleep(Integer.MAX_VALUE); } catch (Exception e) { e.printStackTrace(); } } public static void main(String[] args) { Server server = new Server(8765); } }
Client.java
import java.io.UnsupportedEncodingException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.AsynchronousSocketChannel; import java.util.concurrent.ExecutionException; public class Client implements Runnable{ private AsynchronousSocketChannel asc ; public Client() throws Exception { asc = AsynchronousSocketChannel.open(); } public void connect(){ asc.connect(new InetSocketAddress("127.0.0.1", 8765)); } public void write(String request){ try { asc.write(ByteBuffer.wrap(request.getBytes())).get(); read(); } catch (Exception e) { e.printStackTrace(); } } private void read() { ByteBuffer buf = ByteBuffer.allocate(1024); try { asc.read(buf).get(); buf.flip(); byte[] respByte = new byte[buf.remaining()]; buf.get(respByte); System.out.println(new String(respByte,"utf-8").trim()); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } } @Override public void run() { while(true){ } } public static void main(String[] args) throws Exception { Client c1 = new Client(); c1.connect(); Client c2 = new Client(); c2.connect(); Client c3 = new Client(); c3.connect(); new Thread(c1, "c1").start(); new Thread(c2, "c2").start(); new Thread(c3, "c3").start(); Thread.sleep(1000); c1.write("c1 aaa"); c2.write("c2 bbbb"); c3.write("c3 ccccc"); } }
ServerCompletionHandler.java
import java.nio.ByteBuffer; import java.nio.channels.AsynchronousSocketChannel; import java.nio.channels.CompletionHandler; import java.util.concurrent.ExecutionException; /** * @param AsynchronousSocketChannel异步的Socket对象 * @param Server就是Server.java中accept方法中传入的this,这里为了能够递归调用而传入 * @author jliu10 * */ public class ServerCompletionHandler implements CompletionHandler<AsynchronousSocketChannel, Server> { @Override public void completed(AsynchronousSocketChannel asc, Server attachment) { //当有下一个客户端接入的时候 直接调用Server的accept方法,这样反复执行下去,保证多个客户端都可以阻塞 //递归调用,以保证能够接收新的连接请求 attachment.assc.accept(attachment, this);//只有调用了accept方法之后,server才能接收到新的client请求,并且调用一次accept只能接收一个请求,所以再消耗了前一个accept之后需要递归调用accept方法来保证新的连接能够接入 read(asc); } private void read(final AsynchronousSocketChannel asc) { //读取数据 ByteBuffer buf = ByteBuffer.allocate(1024); asc.read(buf, buf, new CompletionHandler<Integer, ByteBuffer>() { @Override public void completed(Integer resultSize, ByteBuffer attachment) { //进行读取之后,重置标识位 attachment.flip(); //获得读取的字节数 System.out.println("Server -> " + "收到客户端的数据长度为:" + resultSize); //获取读取的数据 String resultData = new String(attachment.array()).trim(); System.out.println("Server -> " + "收到客户端的数据信息为:" + resultData); String response = "服务器响应, 收到了客户端发来的数据: " + resultData; write(asc, response); } @Override public void failed(Throwable exc, ByteBuffer attachment) { exc.printStackTrace(); } }); } private void write(AsynchronousSocketChannel asc, String response) { try { ByteBuffer buf = ByteBuffer.allocate(1024); buf.put(response.getBytes()); buf.flip(); asc.write(buf).get(); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } } @Override public void failed(Throwable exc, Server attachment) { exc.printStackTrace(); } }