共计 5269 个字符,预计需要花费 14 分钟才能阅读完成。
准备
首先了解一下 Socket
网络上的两个程序通过一个双向的通信连接实现数据的交换,这个连接的一端称为一个 socket。
建立网络通信连接至少要一对端口号(socket)。socket 本质是编程接口(API),对 TCP/IP 的封装,TCP/IP 也要提供可供程序员做网络开发所用的接口,这就是 Socket 编程接口;HTTP 是轿车,提供了封装或者显示数据的具体形式;Socket 是发动机,提供了网络通信的能力。
Socket 的英文原义是“孔”或“插座”。作为 BSD UNIX 的进程通信机制,取后一种意思。通常也称作套接字,用于描述 IP 地址和端口,是一个通信链的句柄,可以用来实现不同虚拟机或不同计算机之间的通信。在 Internet 上的主机一般运行了多个服务软件,同时提供几种服务。每种服务都打开一个 Socket,并绑定到一个端口上,不同的端口对应于不同的服务
然后回顾一下 HTTP 服务流程
- 服务端创建 ServerSocket,监听一个端口
- 客户端请求服务端
- 服务端获取一个请求的 Socket 对象
- 开启新线程
- 读取 socket 字节流
- 解码协议(HTTP 协议),得到 http 请求对象
- 处理业务,将结果封装成一个 HttpResponse 对象
- 编码协议(HTTP 协议),将结果序列化字节流写进 socket
- 将字节流返回给客户端
- 结束
示例代码
//Server 端首先创建了一个 serverSocket 来监听 8000 端口,然后创建一个线程,线程里面不断调用阻塞方法 serversocket.accept(); 获取新的连接,见(1),当获取到新的连接之后,给每条连接创建一个新的线程,这个线程负责从该连接中读取数据,见(2),然后读取数据是以字节流的方式,见(3)。public class IOServer {public static void main(String[] args) throws Exception {ServerSocket serverSocket = new ServerSocket(8000);
// (1) 接收新连接线程
new Thread(() -> {while (true) {
try {// (1) 阻塞方法获取新的连接
Socket socket = serverSocket.accept();
// (2) 每一个新的连接都创建一个线程,负责读取数据
new Thread(() -> {
try {
int len;
byte[] data = new byte[1024];
InputStream inputStream = socket.getInputStream();
// (3) 按字节流方式读取数据
while ((len = inputStream.read(data)) != -1) {System.out.println(new String(data, 0, len));
}
} catch (IOException e) {}}).start();} catch (IOException e) {}}
}).start();}
}
// 连接上服务端 8000 端口之后,每隔 2 秒,我们向服务端写一个带有时间戳的 "hello world"
public class IOClient {public static void main(String[] args) {new Thread(() -> {
try {Socket socket = new Socket("127.0.0.1", 8000);
while (true) {
try {socket.getOutputStream().write((new Date() + ": hello world").getBytes());
Thread.sleep(2000);
} catch (Exception e) {}}
} catch (IOException e) {}}).start();}
}
IO NIO AIO BIO
同步 & 异步,阻塞 & 非阻塞
- 同步与异步(synchronous/asynchronous):同步是一种可靠的有序运行机制,当我们进行同步操作时,后续的任务是等待当前调用返回,才会进行下一步;而异步则相反,其他任务不需要等待当前调用返回,通常依靠事件、回调等机制来实现任务间次序关系
- 阻塞与非阻塞:在进行阻塞操作时,当前线程会处于阻塞状态,无法从事其他任务,只有当条件就绪才能继续,比如 ServerSocket 新连接建立完毕,或者数据读取、写入操作完成;而非阻塞则是不管 IO 操作是否结束,直接返回,相应操作在后台继续处理
java.io 包的好处是代码比较简单、直观,缺点则是 IO 效率和扩展性存在局限性,容易成为应用性能的瓶颈。
很多时候,人们也把 java.net 下面提供的部分网络 API,比如 Socket、ServerSocket、HttpURLConnection 也归类到同步阻塞 IO 类库,因为网络通信同样是 IO 行为。
在 Java 1.4 中引入了 NIO 框架(java.nio 包),提供了 Channel、Selector、Buffer 等新的抽象,可以构建多路复用的、同步非阻塞 IO 程序,同时提供了更接近操作系统底层的高性能数据操作方式。
在 Java 7 中,NIO 有了进一步的改进,也就是 NIO 2,引入了异步非阻塞 IO 方式,也有很多人叫它 AIO(Asynchronous IO)。异步 IO 操作基于事件和回调机制,可以简单理解为,应用操作直接返回,而不会阻塞在那里,当后台处理完成,操作系统会通知相应线程进行后续工作
IO(同步、阻塞)
IO 流即 input 和 output 流,是同步 阻塞
NIO(同步、非阻塞)
NIO 之所以是同步,是因为它的 accept/read/write 方法的内核 I / O 操作都会阻塞当前线程
NIO 三个组成部分,Channel(通道)、Buffer(缓冲区)、Selector(选择器)
-
Channel:Channel 是一个对象,可以通过它读取和写入数据。可以把它看做是 IO 中的流, 不同的是:
Channel 是双向的,既可以读又可以写,而流是单向的 Channel 可以进行异步的读写 对 Channel 的读写必须通过 buffer 对象
所有数据都通过 Buffer 对象处理,所以永远不会将字节直接写入到 Channel 中
在 Java NIO 中的 Channel 主要有如下几种类型:FileChannel:从文件读取数据的 DatagramChannel:读写 UDP 网络协议数据 SocketChannel:读写 TCP 网络协议数据 ServerSocketChannel:可以监听 TCP 连接
-
Buffer:Buffer 是一个对象,它包含一些要写入或者读到 Stream 对象的。应用程序不能直接对 Channel 进行读写操作,而必须通过 Buffer 来进行,即 Channel 是通过 Buffer 来读写数据的
使用 Buffer 读写数据一般遵循以下四个步骤:1. 写入数据到 Buffer;2. 调用 flip() 方法;3. 从 Buffer 中读取数据;4. 调用 clear() 方法或者 compact() 方法。
//CopyFile 执行三个基本的操作:创建一个 Buffer,然后从源文件读取数据到缓冲区,然后再将缓冲区写入目标文件
public static void copyFileUseNIO(String src,String dst) throws IOException{
// 声明源文件和目标文件
FileInputStream fi=new FileInputStream(new File(src));
FileOutputStream fo=new FileOutputStream(new File(dst));
// 获得传输通道 channel
FileChannel inChannel=fi.getChannel();
FileChannel outChannel=fo.getChannel();
// 获得容器 buffer
ByteBuffer buffer=ByteBuffer.allocate(1024);
while(true){
// 判断是否读完文件
int eof =inChannel.read(buffer);
if(eof==-1){break;}
// 重设一下 buffer 的 position=0,limit=position
buffer.flip();
// 开始写
outChannel.write(buffer);
// 写完要重置 buffer,重设 position=0,limit=capacity
buffer.clear();}
inChannel.close();
outChannel.close();
fi.close();
fo.close();}
- Selector:Selector 是一个对象,它可以注册到很多个 Channel 上,监听各个 Channel 上发生的事件,并且能够根据事件情况决定 Channel 读写。这样,通过一个线程管理多个 Channel,就可以处理大量网络连接了。
一条连接来了之后,现在不创建一个 while 死循环去监听是否有数据可读了,而是直接把这条连接注册到 selector 上,然后,通过检查这个 selector,就可以批量监测出有数据可读的连接,进而读取数据
线程之间的切换对操作系统来说代价是很高的,并且每个线程也会占用一定的系统资源。所以,对系统来说使用的线程越少越好。
NIO 多路复用
主要步骤和元素:
- 首先,通过 Selector.open() 创建一个 Selector,作为类似调度员的角色。
- 然后,创建一个 ServerSocketChannel,并且向 Selector 注册,通过指定 SelectionKey.OP_ACCEPT,告诉调度员,它关注的是新的连接请求。
- 注意,为什么我们要明确配置非阻塞模式呢?这是因为阻塞模式下,注册操作是不允许的,会抛出 IllegalBlockingModeException 异常。
- Selector 阻塞在 select 操作,当有 Channel 发生接入请求,就会被唤醒。
- 在 具体的 方法中,通过 SocketChannel 和 Buffer 进行数据操作
IO 都是同步阻塞模式,所以需要多线程以实现多任务处理。而 NIO 则是利用了单线程轮询事件的机制,通过高效地定位就绪的 Channel,来决定做什么,仅仅 select 阶段是阻塞的,可以有效避免大量客户端连接时,频繁线程切换带来的问题,应用的扩展能力有了非常大的提高
NIO2(异步、非阻塞)
AIO 是异步 IO 的缩写
对于 NIO 来说,我们的业务线程是在 IO 操作准备好时,得到通知,接着就由这个线程自行进行 IO 操作,IO 操作本身是同步的
但是对 AIO 来说,则更加进了一步,它不是在 IO 准备好时再通知线程,而是在 IO 操作已经完成后,再给线程发出通知。因此 AIO 是不会阻塞的,此时我们的业务逻辑将变成一个回调函数,等待 IO 操作完成后,由系统自动触发
与 NIO 不同,当进行读写操作时,只须直接调用 API 的 read 或 write 方法即可。这两种方法均为异步的,对于读操作而言,当有流可读取时,操作系统会将可读的流传入 read 方法的缓冲区,并通知应用程序;对于写操作而言,当操作系统将 write 方法传递的流写入完毕时,操作系统主动通知应用程序。即可以理解为,read/write 方法都是异步的,完成后会主动调用回调函数。在 JDK1.7 中,这部分内容被称作 NIO.2,主要在 Java.nio.channels 包下增加了下面四个异步通道:
- AsynchronousSocketChannel
- AsynchronousServerSocketChannel
- AsynchronousFileChannel
- AsynchronousDatagramChannel
在 AIO socket 编程中,服务端通道是 AsynchronousServerSocketChannel,这个类提供了一个 open()静态工厂,一个 bind()方法用于绑定服务端 IP 地址(还有端口号),另外还提供了 accept()用于接收用户连接请求。在客户端使用的通道是 AsynchronousSocketChannel, 这个通道处理提供 open 静态工厂方法外,还提供了 read 和 write 方法。
在 AIO 编程中,发出一个事件(accept read write 等)之后要指定事件处理类(回调函数),AIO 中的事件处理类是 CompletionHandler<V,A>,这个接口定义了如下两个方法,分别在异步操作成功和失败时被回调。
void completed(V result, A attachment);
void failed(Throwable exc, A attachment);
单工 半双工 双工通信
单工通信(simplex)
只支持信号在一个方向上传输
适用:数据收集系统,如气象数据收集 话费收集的集中计算等
半双工通信(half-duplex)
允许信号在两个方向上传输,但某一时刻只允许在一个信道上单向传输
适用:问讯 检索 科学计算等数据通信系统 如对讲机
全双工通信(dull-duplex)
允许数据同时在两个方向上传输,既有两个信道
适用:如计算机 手机 电话通信
未完待续。。。