基本概念
Socket又称“套接字”,应用程序通过“套接字”向网络发出请求或者应答网络请求。Socket和SocketServer类库位于java.net包中,ServerSocket用于服务器端,Socket是建立网络链接使用的。在连接成功时,应用程序两端会产生一个Socket实例,操作这个实例,完成所需的会话。对于一个网络连接来说,套接字是平等的,不因为在服务器端或在客户端而产生不同的级别。不管是Socket还是ServerSocket它们的工作都是通过SocketImpl类及其子类完成的。套接字之间的连接过程可以分为四个步骤:
- (1)服务器监听:是服务器端套接字并不定位具体的客户端套接字,而是出于平等连接的状态,实时监控网络状态。
- (2)客户端请求:指由客户端的套接字提出连接请求,要连接的目标是服务器端的套接字。为此,客户端的套接字必须首先描述它要连接的服务器套接字,指出服务器套接字的地址和端口号,然后向服务器端套接字提出连接请求。
- (3)服务器连接确认:当服务器端套接字接收到客户端的套接字的连接请求,它就响应客户端套接字的请求,建立一个新的线程,把服务器端套接字的描述发给客户端。
- (4)客户端连接确认:一旦客户端确认了此描述,连接就建立好了,双方开始进行通信。而服务器端套接字出于监听状态,继续接受其他客户端套接字的连接请求。
IO(BIO)和NIO的区别,其本质就是阻塞和非阻塞的区别。
- 阻塞:应用程序在获取网络数据的时候,如果网络传输数据很慢,那么程序就一直等待,直到传输完毕为止。
- 非阻塞:应用程序直接可以获取已经准备就绪的数据,无须等待。
IO为同步阻塞形式,NIO为同步非阻塞形式。NIO并没有实现异步,在JDK1.7之后,升级了NIO库包,支持异步非阻塞通信模型即NIO2.0(AIO)。同步和异步一般是面向操作系统与应用程序对IO操作的层面来区别。
- 同步时:应用程序会直接参与IO读写操作,并且我们的应用程序会直接阻塞到某一个方法上,直到数据准备就绪;或者采用轮询的策略实时检查数据的就绪状态,如果就绪则获取数据。
- 异步时:则所有的IO读写操作交给操作系统处理,与我们的应用程序没有直接关系,我们程序不需要关系IO读写,当操作系统完成IO读写操作时,会给我们的应用程序发送通知,我们的应用程序直接拿走数据即可。
同步说的是server服务端的执行方式,阻塞说的是具体的技术,接收数据的方式、状态(io,nio)。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28public class Server {
final static int PORT = 8888;
public static void main(String[] args) {
ServerSocket server = null;
try{
server = new ServerSocket(PORT);
System.out.println("server start...");
//进行阻塞
Socket socket = server.accept();
//新建一个线程执行客户端的任务
new Thread(new ServerHandler(socket)).start();
} catch (Exception e){
e.printStackTrace();
} finally{
if(server != null){
try {
server.close();
} catch (IOException e) {
e.printStackTrace();
}
}
server = null;
}
}
}
1 | public class ServerHandler implements Runnable { |
1 | public class Client { |
传统的BIO(Blocking IO)编程
网络编程的基本模型是Client/Sever模型,也就是两个进程直接进行相互通信,其中服务端提供配置信息(绑定端口的ip地址和监听端口),客户端通过连接操作箱服务器端监听的地址发起连接请求,通过三次握手建立连接,如果连接成功,则双方即可以进行通信(网络套接字socket)。
伪异步IO
采用线程池和任务队列可以实现一种伪异步IO通信框架。在学习过连接池和队列的使用,我们将客户端的socket封装成一个task任务(实现Runnable接口的类)然后投递到线程池中去,配置相应的队列进行实现。
1 | /** |
1 | public class HandlerExecutorPool { |
NIO编程
NIO(Non-Block IO),即非阻塞IO。它包含以下几个概念:
- Buffer(缓冲区)
- Channel(管道、通道)
- Selector(选择器、多路复用器)
Buffer
Buffer是一个对象,它包含一些要写入或者读取的数据。在NIO类库中加入Buffer对夏宁,体现了新库与原IO的一个重要区别。在面向流的IO中,可以将数据直接写入或读取到Stream对象中。在NIO库中,所有的数据都是用缓冲区处理的。缓冲区实质上是一个数组,通常它是一个字节数组(ByteBuffer),也可以使用其他类型的数组。这个数组为缓冲区提供了数据的访问读写等操作属性,如位置、容量、上限等概念。我们常用的就是ByteBuffer,实际上每一种java基本类型都对应了一种缓冲区(除了Boolean类型)。
- ByteBuffer
- CharBuffer
- ShortBuffer
- IntBuffer
- LongBuffer
- FloatBuffer
- DoubleBuffer
1 | public class TestBuffer { |
Channel
通道(Channel),他就像自来水管道一样,网络数据通过Channel读取和写入,通道与流不同之处在于通道是双向的,而流只是一个方向上流动(一个流必须是InputStream或者OutputStream的子类),而通道可以用于读、写或者二者同时进行,最关键的是可以与多路复用器结合起来,有多种状态为,方便多路复用器去识别。事实上通道分为两大类,一类是网络读写的(SelectableChannel),一类是用于文件操作的(FileChannel),我们使用的SocketChannel和ServerSocketChannel都是SelectableChannel的子类。
Selector
多路复用器(Selector),它是NIO编程的基础,非常重要。多路复用器提供选择已经就绪的任务的能力。简单说,就是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
1 | /** |
1 | public class Client { |
AIO
AIO编程,在NIO基础之上引入了异步通信的概念,并提供了异步文件和异步套接字通道的实现,从而在真正意义上实现了异步非阻塞,之前我们学习的NIO只是非阻塞而非异步的。而AIO不需要通过多路复用器对注册的管道进行轮询操作即可实现异步读写,从而简化了NIO编程模型。也可以称之为NIO2.0,这种模式才真正的属于我们异步非阻塞模型。
- AsynchronousServerSocketChannel
- AsynchronousSocketChannel
1 | public class Server { |
1 | public class ServerCompletionHandler implements CompletionHandler<AsynchronousSocketChannel, Server> { |
1 | public class Client implements Runnable{ |