Github示例:https://github.com/Nuclear-Core-Learning/TCPIP-Socket/tree/master/src/Chapter5
目录
JavaNIO
同步与异步
阻塞与非阻塞
如何理解同步与阻塞,异步与非阻塞呢?
NIO 优势
NIO核心
Channel
Buffer
Selector
BIO 和NIO编程
BIO示例
NIO编程
Selector多路复用
NIO 服务端和客户端
java.nio全称java non-blocking IO,是指jdk1.4 及以上版本里提供的新api(New I/O) ,为所有的原始类型(boolean类型除外)提供缓存支持的数据容器,使用它可以提供非阻塞式的高伸缩性网络。
JavaNIO书籍示例:http://www.javanio.org/
同步与异步是相对应,是针对两个线程说的,两个线程要么是同步的,要么是异步的。
举个栗子:假设 A B是两个线程, A 调用 B。
如果是 同步的,那么 A必须等待 B 返回之后再去做其它的事情, 假设 B 是个 IO 操作,那么 CPU 此时就是闲置的;
如果是 异步 的,A 调用 B 后,不用等待 B 完成, A 就可以直接返回,此时 A 可以继续利用 CPU资源。 如果 A关注 B 的结果,等 B 完成后可以通过状态或事件通知 A。
阻塞与非阻塞是对用一个线程来说的,这个线程要么处于阻塞状态,要么处于非阻塞状态。应用程序的一次 IO操作一般氛围两部分,1. 将硬盘(或网络)的数据拷贝到内核空间(kernel space)。 2. 将内核空间的数据拷贝到应用程序空间。 阻塞与非阻塞就是指在这两个数据准备的过程中 ,线程是否是阻塞的。
同步异步、阻塞非阻塞这两组形容词描述的东西不同。同步与异步说的是实现机制, 阻塞与非阻塞说的是线程状态。 阻塞就是一种同步机制的结果,非阻塞则是异步机制的结果。
**JAVA NIO 是非阻塞的 IO, 但是通常大家认为它是同步的非阻塞 IO。**为什么呢?非阻塞本身不就是一种异步实现机制么?为啥还会有同步非阻塞这种奇怪的形容词?同步非阻塞IO 岂不是说这个 IO 既是同步的又是异步的?
NIO主要有三大核心部分:Channel(通道),Buffer(缓冲区), Selector。传统IO基于字节流和字符流进行操作,而NIO基于Channel和Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择区)用于监听多个通道的事件(比如:连接打开,数据到达)。因此,单个线程可以监听多个数据通道。
NIO和传统IO(一下简称IO)之间第一个最大的区别是,IO是面向流的,NIO是面向缓冲区的。 Java IO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。NIO的缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含所有您需要处理的数据。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据。
IO的各种流是阻塞的。这意味着,当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。 NIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取。而不是保持线程阻塞,所以直至数据变得可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)。
首先说一下Channel,国内大多翻译成“通道”。Channel和IO中的Stream(流)是差不多一个等级的。只不过Stream是单向的,譬如:InputStream, OutputStream.而Channel是双向的,既可以用来进行读操作,又可以用来进行写操作。
channel(通道)表示到实体如硬件设备、文件、网络套接字或可以执行一个或多个不同I/O操作的程序组件的开放的连接。
Channel和传统IO中的Stream很相似。主要区别为:通道是双向的,通过一个Channel既可以进行读,也可以进行写;而Stream只能进行单向操作,通过一个Stream只能进行读或者写,比如InputStream只能进行读取操作,OutputStream只能进行写操作;通道是一个对象,通过它可以读取和写入数据,当然了所有数据都通过Buffer对象来处理。我们永远不会将字节直接写入通道中,相反是将数据写入包含一个或者多个字节的缓冲区。同样不会直接从通道中读取字节,而是将数据从通道读入缓冲区,再从缓冲区获取这个字节。
NIO中的Channel的主要实现有:
FileChannel
DatagramChannel
SocketChannel
ServerSocketChannel
这里看名字就可以猜出个所以然来:分别可以对应文件IO、UDP和TCP(Server和Client)。下面演示的案例基本上就是围绕这4个类型的Channel进行陈述的。
缓冲区有直接缓冲区和非直接缓冲区之分(关于两者的区别可以看这里),它实际上也是一段内存空间。在NIO库中,所有数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中的; 在写入数据时,它也是写入到缓冲区中的。流程如下图:
NIO中的关键Buffer实现有:ByteBuffer, CharBuffer, DoubleBuffer, FloatBuffer, IntBuffer, LongBuffer, ShortBuffer,分别对应基本数据类型: byte, char, double, float, int, long, short。当然NIO中还有MappedByteBuffer, HeapByteBuffer, DirectByteBuffer等这里先不进行陈述。
Selector类是NIO的核心类,Selector(选择器)选择器提供了选择已经就绪的任务的能力。Selector会不断的轮询注册在上面的所有channel,如果某个channel为读写等事件做好准备,那么就处于就绪状态,通过Selector可以不断轮询发现出就绪的channel,进行后续的IO操作。一个Selector能够同时轮询多个channel。这样,一个单独的线程就可以管理多个channel,从而管理多个网络连接。这样就不用为每一个连接都创建一个线程,同时也避免了多线程之间上下文切换导致的开销。
与Selector有关的一个关键类是SelectionKey,一个SelectionKey表示一个到达的事件,这2个类构成了服务端处理业务的关键逻辑。关于SelectionKey的详细介绍可以参考这篇博文
Selector运行单线程处理多个Channel,如果你的应用打开了多个通道,但每个连接的流量都很低,使用Selector就会很方便。例如在一个聊天服务器中。要使用Selector, 得向Selector注册Channel,然后调用它的select()方法。这个方法会一直阻塞到某个注册的通道有事件就绪。一旦这个方法返回,线程就可以处理这些事件,事件的例子有如新的连接进来、数据接收等。
BIO(Blocking I/O)即同步阻塞I/O,在NIO出现之前主要使用BIO及新建线程的方式来解决并发请求,但这样很容易因线程瓶颈而造成限制。下面是BIO的经典编程模型(主要代码):
{
ExecutorService executor = Excutors.newFixedThreadPollExecutor(100);//线程池
ServerSocket serverSocket = new ServerSocket();
serverSocket.bind(8088);
while(!Thread.currentThread.isInturrupted()){//当前线程未中断
Socket socket = serverSocket.accept();
executor.submit(new ConnectIOnHandler(socket));//为新的连接创建新的线程
}
class ConnectIOnHandler extends Thread{
private Socket socket;
public ConnectIOnHandler(Socket socket){
this.socket = socket;
}
public void run(){
while(!Thread.currentThread.isInturrupted()&&!socket.isClosed()){死循环处理读写事件
String someThing = socket.read()....//读取数据
if(someThing!=null){
......//处理数据
socket.write()....//写数据
}
}
}
}
之所已使用多线程,因为accept()、read()、write()三个函数都是同步阻塞的,当一个连接存在的时候,系统是阻塞的,所以利用多线程让cpu处理更多的申请。多线程的本质:
从JDK1.4开始,Java提供了一系列改进的输入/输出处理的新特性,被统称为NIO(即New I/O)。新增了许多用于处理输入输出的类,这些类都被放在java.nio包及子包下,并且对原java.io包中的很多类进行改写,新增了满足NIO的功能。NIO采用内存映射文件的方式来处理输入输出,NIO将文件或文件的一段区域映射到内存中,这样就可以像访问内存一样访问文件了。NIO与原来的IO有同样的作用和目的,但是使用的方式完全不同, NIO支持面向缓冲区(Buffer)的、基于通道(Channel)的IO操作。NIO将以更加高效的方式进行文件的读写操作。
一个简单的NIO程序
我们用到了SocketChannel,并设置阻塞模式的值为false
package Chapter5;
import java.net.InetSocketAddress;
import java.net.SocketException;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
/**
*
* @TODO (功能说明:TCP-NIO非阻塞客户端)
* @author PJL
* @package Chapter5
* @motto 学习需要毅力,那就秀毅力
* @file TCPEchoClientNonblocking.java
* @time 2019年10月29日 下午11:13:45
*/
public class TCPEchoClientNonblocking {
public static void main(String args[]) throws Exception {
if(args.length==0) {
args=new String[3];
args[0]="127.0.0.1";
args[1]="I am the man who is looking at you standing near the window.";
args[2]="5433";
}
if ((args.length < 2) || (args.length > 3)) // Test for correct # of args
throw new IllegalArgumentException("Parameter(s): []");
String server = args[0]; // Server name or IP address
// Convert input String to bytes using the default charset
byte[] argument = args[1].getBytes();
int servPort = (args.length == 3) ? Integer.parseInt(args[2]) : 7;
// Create channel and set to nonblocking
SocketChannel clntChan = SocketChannel.open();
clntChan.configureBlocking(false);
// 一直等待直到连接成功===忙等
// Initiate connection to server and repeatedly poll until complete
if (!clntChan.connect(new InetSocketAddress(server, servPort))) {
while (!clntChan.finishConnect()) {
System.out.print("."); // Do something else
}
}
// 装饰一个ByteBuffer缓冲区
ByteBuffer writeBuf = ByteBuffer.wrap(argument);
// 根据需要申请一块ByteBuffer缓冲区
ByteBuffer readBuf = ByteBuffer.allocate(argument.length);
int totalBytesRcvd = 0; // Total bytes received so far
int bytesRcvd; // Bytes received in last read
// 知道读取写完数据
while (totalBytesRcvd < argument.length) {
if (writeBuf.hasRemaining()) {
clntChan.write(writeBuf);
}
if ((bytesRcvd = clntChan.read(readBuf)) == -1) {
throw new SocketException("Connection closed prematurely");
}
totalBytesRcvd += bytesRcvd;
System.out.print("."); // Do something else
}
System.out.println("Received: " + // convert to String per default charset
new String(readBuf.array(), 0, totalBytesRcvd));
clntChan.close();
}
}
NIO通信少不了建立连接accept()、写入数据write()、读取数据read(),我们定义一个协议接口用于对SelectionKey进行操作。
package Chapter5;
import java.nio.channels.SelectionKey;
import java.io.IOException;
/**
*
* @TODO (功能说明:NIO模式读写和建立连接方法)
* @author PJL
* @package Chapter5
* @motto 学习需要毅力,那就秀毅力
* @file TCPProtocol.java
* @time 2019年10月29日 下午11:33:43
*/
public interface TCPProtocol {
void handleAccept(SelectionKey key) throws IOException;
void handleRead(SelectionKey key) throws IOException;
void handleWrite(SelectionKey key) throws IOException;
}
实现接口:
package Chapter5;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;
import java.nio.channels.ServerSocketChannel;
import java.nio.ByteBuffer;
import java.io.IOException;
/**
*
* @TODO (功能说明:NIO 连接、读、写方法实现)
* @author PJL
* @package Chapter5
* @motto 学习需要毅力,那就秀毅力
* @file EchoSelectorProtocol.java
* @time 2019年10月29日 下午11:34:34
*/
public class EchoSelectorProtocol implements TCPProtocol {
private int bufSize; // Size of I/O buffer
public EchoSelectorProtocol(int bufSize) {
this.bufSize = bufSize;
}
public void handleAccept(SelectionKey key) throws IOException {
SocketChannel clntChan = ((ServerSocketChannel) key.channel()).accept();
clntChan.configureBlocking(false); // Must be nonblocking to register
// Register the selector with new channel for read and attach byte buffer
clntChan.register(key.selector(), SelectionKey.OP_READ, ByteBuffer.allocate(bufSize));
}
public void handleRead(SelectionKey key) throws IOException {
// Client socket channel has pending data
SocketChannel clntChan = (SocketChannel) key.channel();
ByteBuffer buf = (ByteBuffer) key.attachment();
long bytesRead = clntChan.read(buf);
if (bytesRead == -1) { // Did the other end close?
clntChan.close();
} else if (bytesRead > 0) {
// Indicate via key that reading/writing are both of interest now.
key.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE);
}
}
public void handleWrite(SelectionKey key) throws IOException {
/*
* Channel is available for writing, and key is valid (i.e., client channel not
* closed).
*/
// Retrieve data read earlier
ByteBuffer buf = (ByteBuffer) key.attachment();
buf.flip(); // Prepare buffer for writing
SocketChannel clntChan = (SocketChannel) key.channel();
clntChan.write(buf);
if (!buf.hasRemaining()) { // Buffer completely written?
// Nothing left, so no longer interested in writes
key.interestOps(SelectionKey.OP_READ);
}
buf.compact(); // Make room for more data to be read in
}
}
Selector多路先择器服务端:
package Chapter5;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.util.Iterator;
/**
* @TODO (功能说明:Selector NIO多路选择器)
* @author PJL
* @package Chapter5
* @motto 学习需要毅力,那就秀毅力
* @file TCPServerSelector.java
* @time 2019年10月29日 下午11:29:07
*/
public class TCPServerSelector {
private static final int BUFSIZE = 256; // Buffer size (bytes)
private static final int TIMEOUT = 3000; // Wait timeout (milliseconds)
public static void main(String[] args) throws IOException {
if(args.length==0) {
args=new String[1];
args[0]="5433";
}
if (args.length < 1) { // Test for correct # of args
throw new IllegalArgumentException("Parameter(s): ...");
}
// Create a selector to multiplex listening sockets and connections
Selector selector = Selector.open();
// Create listening socket channel for each port and register selector
for (String arg : args) {
ServerSocketChannel listnChannel = ServerSocketChannel.open();
listnChannel.socket().bind(new InetSocketAddress(Integer.parseInt(arg)));
listnChannel.configureBlocking(false); // must be nonblocking to register
// Register selector with channel. The returned key is ignored
listnChannel.register(selector, SelectionKey.OP_ACCEPT);
}
// Create a handler that will implement the protocol
TCPProtocol protocol = new EchoSelectorProtocol(BUFSIZE);
while (true) { // Run forever, processing available I/O operations
// Wait for some channel to be ready (or timeout)
if (selector.select(TIMEOUT) == 0) { // returns # of ready chans
System.out.print(".");
continue;
}
// Get iterator on set of keys with I/O to process
Iterator keyIter = selector.selectedKeys().iterator();
while (keyIter.hasNext()) {
SelectionKey key = keyIter.next(); // Key is bit mask
// Server socket channel has pending connection requests?
if (key.isAcceptable()) {
protocol.handleAccept(key);
}
// Client socket channel has pending data?
if (key.isReadable()) {
protocol.handleRead(key);
}
// Client socket channel is available for writing and
// key is valid (i.e., channel not closed)?
if (key.isValid() && key.isWritable()) {
protocol.handleWrite(key);
}
keyIter.remove(); // remove from set of selected keys
}
}
}
}
服务端:
/**
* nio是面向缓冲区的
* bio是面向流的
* @author zmrwego
* @descreption
* @create 2018-10-15
**/
public class Server {
private Selector selector;
private ByteBuffer readBuffer = ByteBuffer.allocate(1024);//调整缓冲区大小为1024字节
private ByteBuffer sendBuffer = ByteBuffer.allocate(1024);
String str;
public void start() throws IOException{
//打开服务器套接字通道
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false); //服务器配置为非阻塞 即异步IO
ssc.bind(new InetSocketAddress(3400)); //绑定本地端口
//创建选择器
selector = Selector.open();
ssc.register(selector, SelectionKey.OP_ACCEPT);//ssc注册到selector准备连接
//无限判断当前线程状态,如果没有中断,就一直执行while内容。
while(! Thread.currentThread().isInterrupted()){
selector.select(); //select()方法返回的值表示有多少个 Channel 可操作
Set keys = selector.selectedKeys();
Iterator keyIterator = keys.iterator();
while (keyIterator.hasNext()){//处理客户端连接
SelectionKey key = keyIterator.next();
if (!key.isValid()){
continue;
}
if (key.isAcceptable()){
accept(key);
}
if(key.isReadable()){
read(key);
}
if (key.isWritable()){
write(key);
}
keyIterator.remove(); //移除当前的key
}
}
}
private void read(SelectionKey key) throws IOException{
SocketChannel socketChannel = (SocketChannel) key.channel();
this.readBuffer.clear();//清除缓冲区,准备接受新数据
int numRead;
try{
numRead = socketChannel.read(this.readBuffer);
}catch (IOException e){
key.cancel();
socketChannel.close();
return;
}
str = new String(readBuffer.array(),0,numRead);
System.out.println(str);
socketChannel.register(selector,SelectionKey.OP_WRITE);
}
private void write(SelectionKey key) throws IOException, ClosedChannelException{
SocketChannel channel = (SocketChannel) key.channel();
System.out.println("write:"+str);
sendBuffer.clear();
sendBuffer.put(str.getBytes());
sendBuffer.flip();//反转,由写变为读
channel.write(sendBuffer);
//注册读操作 下一次进行读
channel.register(selector,SelectionKey.OP_READ);
}
private void accept(SelectionKey key) throws IOException {
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
SocketChannel clientChannel = ssc.accept();
clientChannel.configureBlocking(false);
clientChannel.register(selector, SelectionKey.OP_READ);
System.out.println("a new client connected "+clientChannel.getRemoteAddress());
}
public static void main(String[] args) throws Exception {
System.out.println("sever start...");
new Server().start();
}
}
客户端:
public class Client {
ByteBuffer writeBuffer = ByteBuffer.allocate(1024);
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
public void start() throws IOException{
//打开socket通道
SocketChannel sc = SocketChannel.open();
sc.configureBlocking(false);
sc.connect(new InetSocketAddress("localhost",3400));
//创建选择器
Selector selector = Selector.open();
//将channel注册到selector中
sc.register(selector, SelectionKey.OP_CONNECT);
Scanner scanner = new Scanner(System.in);
while (true){
selector.select();
Set keys = selector.selectedKeys();
System.out.println("keys:"+keys.size());
Iterator iterator = keys.iterator();
while (iterator.hasNext()){
SelectionKey key = iterator.next();
iterator.remove();
//判断此通道上是否在进行连接操作
if (key.isConnectable()){
sc.finishConnect();
//注册写操作
sc.register(selector,SelectionKey.OP_WRITE);
System.out.println("server connected...");
break;
}else if (key.isWritable()){
System.out.println("please input message:");
String message = scanner.nextLine();
writeBuffer.clear();
writeBuffer.put(message.getBytes());
//将缓冲区各标志复位,因为向里面put了数据标志被改变要想从中读取数据发向服务器,就要复位
writeBuffer.flip();
sc.write(writeBuffer);
//注册写操作,每个chanel只能注册一个操作,最后注册的一个生效
//如果你对不止一种事件感兴趣,那么可以用“位或”操作符将常量连接起来
//int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
//使用interest集合
sc.register(selector,SelectionKey.OP_WRITE | SelectionKey.OP_READ);
}else if(key.isReadable()){
System.out.print("receive message:");
SocketChannel client = (SocketChannel) key.channel();
//将缓冲区清空以备下次读取
readBuffer.clear();
int num = client.read(readBuffer);
System.out.println(new String(readBuffer.array(),0, num));
//注册写操作,下一次写
sc.register(selector, SelectionKey.OP_WRITE);
}
}
}
}
public static void main(String[] args) throws Exception {
new Client().start();
}
}
参考文章:
https://blog.csdn.net/u011381576/article/details/79876754
https://blog.csdn.net/y418662591/article/details/89192750
https://www.jianshu.com/p/d47835316016
https://ifeve.com/overview/