Netty 系列(二)NIO 入门
Socket(套接字),应用程序通过"套接字"向网络发出请求或者应答网络请求。最早是 Unix 上的一套网络程序通讯的标准,已被广泛移植到其它平台。
Socket 通信原理
在 Internet 上的主机一般运行了多个服务软件,同时提供了几种服务,每种服务都打开一个 Socket 并绑定到一个端口上,不同的端口对应不同的服务进程。
Socket 实质上提供了进程通信的端点,网络上的两个程序通过一个双向的通讯链路实现数据的交换,这个双向链路的一端称为一个 Socket。
Socket 封装了应用层和传输层的功能,不需要我们自己去实现
Socket 类型
Socket 有以下三种类型:流式套接字(SOCK_STREAM)、数据套接字(SOCK_DGRAM)、原始式套接字(SOCK_RAW)。
流式套接字:提供了一个面向连接,可靠的数据传输服务,数据无差错、无重复的发送,且按发送顺序接收。对应使用的是 TCP 协议。
数据套接字:提供了无连接服务。数据包以独立包形式被发送,不提供无差错保证,数据可能丢失或重复,并且接收顺序无序。对应使用的是 UDP 协议。
原始式套接字:该接口允许对较低层次协议,如IP直接访问。可以接收本机网卡上的数据帧或数据包,对监听网络流量和分析很有用。
TCP通信
Socket 和 ServerSocket 类库位于 java.net 包中。ServerSocket 用于服务器端,Socket 是建立网络连接时使用的。在连接成功时,应用程序两端都会产生一个 Socket 实例,操作这个实现完成所需的会话。对于一具网络连接来说,套接字是平等的,不因为在服务器端或客户端而产生不同级别。不管是 Socket 还是 ServerSocket 它们的工作都是通过 SocketImpl 类及其子类完成的。
我们从一个简单的例子 com.github.binarylei.network.socket.bio.demo1 开始学习 Socket,首先编写一个服务器:
//1. 绑定端口
ServerSocket server = new ServerSocket(6275);
System.out.println("server listening on " + 6275);
while (true) {
//2. 获取客户端请求的Socket,没有请求就阻塞
Socket socket = server.accept();
//3. 开启一个线程执行客户端的任务
new Thread(new ServerHandler(socket)).start();
}
开启线程 ServerHandler 处理客户端的请求:
@Override
public void run() {
BufferedReader in = new BufferedReader(new InputStreamReader(this.socket.getInputStream()));
PrintWriter out = new PrintWriter(this.socket.getOutputStream(), true);
String body = null;
while(true){
//1. 接收客户端数据
body = in.readLine();
if(body == null) break;
System.out.println("Server: " + body);
//2. 发送响应数据
out.println("服务器端回送响应数据.");
}
}
启动客户端:
//1. 连接服务器
Socket socket = new Socket("127.0.0.1", 6275);
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
//2. 发送请求数据
out.println("客户端发送请求数据...");
//3. 接收服务端数据
String response = in.readLine();
System.out.println("Client: " + response);
OK! 一个最简单的 Socket 通信就建好了,执行结果如下:
// Server 端:
server listening on 8765
Server: 客户端发送请求数据...
// Client 端:
Client: 服务器端回送响应数据.
上述方案虽然实现了 Socket 通信,但是每来一个请求就开启一个线程,1000 个请求就开启 1000 个线程,而服务器能承受的线程数是有限的,这明显不科学。怎么办呢?就是下面要讲的,利用线程池实现伪异步I/O (JDK1.7 之前)
伪异步I/O
采用线程池和任务队列可以实现一种伪异步的IO通信框架。就是将客户端的 socket 封装成一个 task 任务(实现 Runable 接口的类),然后投递到线程池中,配置相应的队列进行实现。
看下面这个例子:com.github.binarylei.network.socket.bio.demo2
//1. 绑定端口
ServerSocket server = new ServerSocket(6275);
System.out.println("server listening on " + 6275);
//2. 获取客户端请求的Socket,没有请求就阻塞
Socket socket = null;
ThreadPoolExecutor executorPool = = new ThreadPoolExecutor(
Runtime.getRuntime().availableProcessors(),
50,
120L,
TimeUnit.SECONDS,
new ArrayBlockingQueue<Runnable>(1000));
while(true){
socket = server.accept();
executorPool.execute(new ServerHandler(socket));
}
//3. 开启一个线程执行客户端的任务
new Thread(new ServerHandler(socket)).start();
BIO和NIO的区别
BIO(Block IO)和NIO(NoN-Block IO)本质就是阻塞和非阻塞的区别。
阻塞概念:应用程序在获取网络数据的时候,如果网络传输数据很慢,那么程序就一直等待,直到传输完毕为止。
非阻塞概念:应用程序直接可以获取已经准备就绪的数据,无需等待。
注意:BIO 为同步阻塞形式,NIO 为同步非阻塞形式,NIO 并没有实现异步。在 JDK 1.7 后,升级了 NIO 库,支持异步非阻塞通信模型,即 NIO2.0(AIO)。
同步和异步的区别:
同步和异步一般是面向操作系统与应用程序对 IO 操作的层面上来区别的。
同步时,应用程序会直接参与 IO 读写操作,并且我们的应用程序会直接阻塞到某一个方法上,直到数据准备就绪;或者采用轮询的策略实时检查数据的就绪状态,如果就绪则获取数据。
异步时,所有的 IO 读写操作交给操作系统处理,与我们的应用程序没有直接关系,我们程序不需要关心 IO 读写,当操作系统完成 IO 读写操作时,会给我们应用程序发送,应用程序直接拿走数据即可。
总结:同步是 server 服务器端的执行方式。阻塞是具体的技术,接收数据的方式、状态(BIO/NIO)
每天用心记录一点点。内容也许不重要,但习惯很重要!