请注意,DatagramChannel 和SocketChannel 实现了定义读写功能的接口,而ServerSocketChannel 则没有。 ServerSocketChannel 负责侦听传入连接并创建新的SocketChannel 对象。它本身从不传输数据。
在我们详细讨论每个套接字通道之前,您应该了解套接字和套接字通道之间的关系。如前一章所述,通道是连接I/O 服务并提供与服务交互的方法的管道。就某个socket而言,它不会再次实现相应socket通道类中的socket协议API,并且java.net中现有的socket通道可以被大多数协议操作复用。
所有套接字通道类(DatagramChannel、SocketChannel 和ServerSocketChannel)在实例化时都会创建一个对等套接字对象。这些是来自java.net 的熟悉的类(Socket、ServerSocket 和DatagramSocket),它们已更新为可识别通道。可以通过调用socket() 方法从通道获取对等套接字。此外,这三个java.net 类现在具有getChannel() 方法。
套接字通道将与通信协议相关的操作委托给相应的套接字对象。 socket的方法看起来可能在通道类中重复,但实际上通道类上的方法会有一些新的或不同的行为。
要将套接字通道置于非阻塞模式,我们依赖所有套接字通道类的公共超类:SelectableChannel。就绪选择是一种可用于查询通道以确定其是否准备好执行目标操作(例如读取或写入)的机制。非阻塞I/O 和可选择性密切相关,这就是为什么管理阻塞模式的API 代码定义在SelectableChannel 超类中。
设置或重置通道的阻塞模式非常简单。只需调用configureBlocking()方法并传递参数值true来设置阻塞模式,传递参数值false来设置非阻塞模式。真的,就是这么简单!您可以通过调用isBlocking() 方法来确定套接字通道当前处于哪种模式。
AbstractSelectableChannel.java中实现的configureBlocking()方法如下:
publicfinalSelectableChannel configureBlocking(布尔块)
抛出IO异常
{
同步(regLock){
if(!isOpen())
抛出新的ClosedChannelException();
if(阻塞==阻塞)
返回这个;
if(块hasValidKeys())
抛出新的IllegalBlockingModeException();
implConfigureBlocking(块);
阻塞=阻塞;
}
返回这个;
}
服务器通常考虑使用非阻塞套接字,因为它们可以更轻松地同时管理许多套接字通道。然而,在客户端使用一个或多个非阻塞模式套接字通道也是有益的。例如,借助非阻塞套接字通道,GUI 程序可以专注于用户请求并同时维护与一台或多台服务器的会话。非阻塞模式在许多程序中都很有用。
有时,我们需要防止套接字通道的阻塞模式被更改。 API中有一个blockingLock()方法,它返回一个非透明的对象引用。当修改阻塞模式时,返回的对象由通道实现在内部使用。只有拥有该对象锁的线程才能更改通道的阻塞模式。
下面分别介绍这三个通道。
2.ServerSocketChannel
我们先从最简单的ServerSocketChannel开始讨论socket通道类。以下是ServerSocketChannel的完整API:
publicabstractclassServerSocketChannelextends AbstractSelectableChannel
{
publicstaticServerSocketChannel open() 抛出IOException;
publicabstract ServerSocket 套接字();
publicabstractServerSocketaccept()抛出IOException;
publicfinalint validOps();
}
ServerSocketChannel 是一个基于通道的套接字侦听器。它执行与熟悉的java.net.ServerSocket 相同的基本任务,但它添加了通道语义,因此可以在非阻塞模式下运行。
由于ServerSocketChannel没有bind()方法,因此需要取出对等方的套接字并使用它绑定到端口以开始侦听连接。我们还使用对等ServerSocket API 根据需要设置其他套接字选项。
与其对应的java.net.ServerSocket 一样,ServerSocketChannel 也有一个accept() 方法。创建ServerSocketChannel 并将其与对等套接字绑定后,您可以对其中一个套接字调用accept()。如果您选择在ServerSocket 上调用accept() 方法,它的行为与任何其他ServerSocket 类似:它始终阻塞并返回java.net.Socket 对象。如果选择调用ServerSocketChannel的accept()方法,将会返回一个SocketChannel类型的对象。返回的对象可以以非阻塞模式运行。
换句话说:
ServerSocketChannel的accept()方法返回一个SocketChannel类型的对象。 SocketChannel 可以以非阻塞模式运行。
其他Socket的accept()方法会阻塞并返回一个Socket对象。
如果以非阻塞模式调用ServerSocketChannel,当没有传入连接等待时,ServerSocketChannel.accept() 将立即返回null。正是这种在不阻塞的情况下检查连接的能力实现了可扩展性并降低了复杂性。从而实现了选择性。我们可以使用选择器实例注册一个ServerSocketChannel对象来实现新连接到达时的自动通知。以下代码演示了如何使用非阻塞accept()方法:
包com.dxz.springsession.nio.demo2;导入java.nio.ByteBuffer;导入java.nio.channels.ServerSocketChannel;导入java.nio.channels.SocketChannel;导入java.net.InetSocketAddress;publicclass ChannelAccept {
publicstaticfinalString GREETING="你好,我得走了。rn";
publicstaticvoidmain(String[] argv)抛出异常{
输入端口=1234; //defaultif(argv.length 0) {
端口=Integer.parseInt(argv[0]);
}
ByteBuffer 缓冲区=ByteBuffer.wrap(GREETING.getBytes());
ServerSocketChannel ssc=ServerSocketChannel.open();
ssc.socket().bind(new InetSocketAddress(端口));
ssc.configureBlocking(假);
而(真){
System.out.println("等待连接");
SocketChannel sc=ssc.accept();
如果(sc==null){
System.out.println("null");
线程睡眠(2000);
} 别的{
System.out.println("来自:的传入连接" + sc.socket().getRemoteSocketAddress());
buffer.rewind();
sc.write(缓冲区);
sc.close();
}
}
}
}
日志:
2.1.打开ServerSocketChannel
通过调用ServerSocketChannel.open() 方法打开ServerSocketChannel。例如:
ServerSocketChannel serverSocketChannel=ServerSocketChannel.open();
2.2.关闭ServerSocketChannel
通过调用ServerSocketChannel.close() 方法关闭ServerSocketChannel。例如:
serverSocketChannel.close();
2.3.监控新传入连接
通过ServerSocketChannel.accept() 方法监听新的传入连接。当accept()方法返回时,它返回一个包含新传入连接的SocketChannel。因此,accept() 方法将阻塞,直到新连接到达。
通常,accept() 方法不是只监听一个连接,而是在while 循环中调用。如下例所示:
而(真){
SocketChannel socketChannel=serverSocketChannel.accept();
//.}
2.4.阻塞模式
该进程将被阻塞在SocketChannel sc=ssc.accept(); 处。 2.5.非阻塞模式
ServerSocketChannel 可以设置为非阻塞模式。在非阻塞模式下,accept()方法将立即返回。如果没有新的传入连接,则返回值为null。因此,需要检查返回的SocketChannel是否为null。例如:
ServerSocketChannel ssc=ServerSocketChannel.open();
ssc.socket().bind(new InetSocketAddress(端口));
ssc.configureBlocking(假);
而(真){
System.out.println("等待连接");
SocketChannel sc=ssc.accept();
如果(sc!=空){
}
3.SocketChannel
我们开始学习SocketChannel,它是最常用的socket通道类:
Java NIO 中的SocketChannel 是连接到TCP 网络套接字的通道。 SocketChannel可以通过以下2种方式创建:
打开SocketChannel 并连接到Internet 上的服务器。
当一个新的连接到达ServerSocketChannel时,就会创建一个SocketChannel。
3.1.开放套接字通道
下面是如何打开SocketChannel:
SocketChannel socketChannel=SocketChannel.open();
socketChannel.connect(newInetSocketAddress("http://jenkov.com", 80));
3.2.关闭SocketChannel
使用完SocketChannel 后,调用SocketChannel.close() 关闭SocketChannel:
socketChannel.close();
3.3.从SocketChannel读取数据
要从SocketChannel 读取数据,请调用read() 方法之一。以下是示例:
ByteBuffer buf=ByteBuffer.allocate(48);intbytesRead=socketChannel.read(buf);
首先,分配一个Buffer。从SocketChannel读取到的数据会被放到这个Buffer中。然后,调用SocketChannel.read()。该方法从SocketChannel 读取数据到Buffer 中。 read() 方法返回的int 值表示读入Buffer 的字节数。如果返回-1,则表示已读取流末尾(连接已关闭)。
3.4.写入SocketChannel
向SocketChannel 写入数据使用SocketChannel.write() 方法,该方法采用Buffer 作为参数。示例如下:
String newData="要写入文件的新字符串." + System.currentTimeMillis();
ByteBuffer buf=ByteBuffer.allocate(48);
buf.clear();
buf.put(newData.getBytes());
buf.flip();
while(buf.hasRemaining()) {
通道.write(buf);
}
请注意,SocketChannel.write() 方法是在while 循环中调用的。 Write() 方法不能保证可以向SocketChannel 写入多少字节。因此,我们重复调用write() 直到Buffer 没有剩余字节可供写入。
3.5.非阻塞模式
您可以将SocketChannel 设置为非阻塞模式。设置完成后,就可以异步调用connect()、read()、write()。
3.5.1.连接()
如果SocketChannel处于非阻塞模式,此时调用connect(),该方法可能会在连接建立之前返回。要判断连接是否建立,可以调用finishConnect() 方法。像这样:
socketChannel.configureBlocking(假);
socketChannel.connect(newInetSocketAddress("http://jenkov.com", 80));
while(!socketChannel.finishConnect() ){
//等待,或者做其他事情.}
3.5.2、写()
在非阻塞模式下,write() 方法可能会在写入任何内容之前返回。所以需要在循环中调用write()。前面已经有例子了,这里不再赘述。
3.5.3、读取()
在非阻塞模式下,read() 方法可能会在读取任何数据之前返回。所以你需要注意它的int返回值,它会告诉你读取了多少字节。
3.6.非阻塞模式和选择器
非阻塞模式与选择器配合使用效果更好。通过向Selector注册一个或多个SocketChannel,可以询问选择器哪个Channel已经准备好进行读、写等操作。Selector和SocketChannel的结合将在后面详细讨论。
4. 数据报通道
最后一个套接字通道是DatagramChannel。正如SocketChannel对应Socket、ServerSocketChannel对应ServerSocket一样,每个DatagramChannel对象也有一个关联的DatagramSocket对象。然而,原来的命名模式在这里并不适用:“DatagramSocketChannel”似乎有点笨拙,因此采用了简洁的名称“DatagramChannel”。
正如SocketChannel 模拟面向连接的流协议(例如TCP/IP)一样,DatagramChannel 模拟面向数据包的无连接协议(例如UDP/IP)。
DatagramChannel 是无连接的。每个数据报都是一个独立的实体,具有自己的目标地址和不依赖于其他数据报的数据有效负载。与面向流的套接字不同,DatagramChannel 可以将单独的数据报发送到不同的目标地址。同样,DatagramChannel 对象也可以从任何地址接收数据包。每个到达的数据报都包含有关其来源(源地址)的信息。
4.1.打开数据报通道
DatagramChannel 的打开方式如下:
DatagramChannel 通道=DatagramChannel.open();
Channel.socket().bind(newInetSocketAddress(9999));
本例打开的DatagramChannel可以在UDP端口9999上接收数据包。
4.2.接收数据
通过receive()方法从DatagramChannel接收数据,如:
ByteBuffer buf=ByteBuffer.allocate(48);
buf.clear();
通道.接收(buf);
receive()方法会将接收到的数据包内容复制到指定的Buffer中。如果Buffer无法容纳接收到的数据,多余的数据将被丢弃。
4.3.发送数据
通过send()方法从DatagramChannel发送数据,如:
String newData="要写入文件的新字符串." + System.currentTimeMillis();
ByteBuffer buf=ByteBuffer.allocate(48);
buf.clear();
buf.put(newData.getBytes());
buf.flip();
intbytesSent=channel.send(buf,newInetSocketAddress("jenkov.com", 80));
此示例将字符串发送到“jenkov.com”服务器的UDP 端口80。因为服务器没有监听这个端口,所以什么也不会发生。它也不会通知您是否已收到传出数据包,因为UDP 在数据传输方面没有任何保证。
4.4.连接到特定地址
DatagramChannel 可以“连接”到网络中的特定地址。由于UDP 是无连接的,因此连接到特定地址不会像TCP 通道那样创建真正的连接。相反,DatagramChannel 被锁定,因此它只能从特定地址发送和接收数据。
这是一个例子:
Channel.connect(newInetSocketAddress("jenkov.com", 80));
连接后,您还可以使用read() 和write() 方法,就像使用传统通道一样。对于数据传输没有任何保证。以下是一些示例:
intbytesRead=通道.read(buf);
intbytesWritten=Channel.write(但是);
完整示例:
包com.dxz.springsession.nio.demo3;importjava.nio.channels.*;importjava.nio.charset.*;importjava.net.*;importjava.io.*;importjava.util.*;importjava.nio.* ;公共类DatagramChannelServerDemo {
//UDP协议服务器privateintport=9975;
DatagramChannel通道;
privateCharset charset=Charset.forName("UTF-8");
privateSelector选择器=null;
publicDatagramChannelServerDemo()抛出IOException {
尝试{
选择器=Selector.open();
通道=DatagramChannel.open();
} catch (异常e) {
选择器=空;
通道=空;
System.out.println("超时");
}
System.out.println("服务器启动");
}
/* 编码过程*/public ByteBufferencode(String str) {
返回charset.encode(str);
}
/* 解码过程*/public Stringdecode(ByteBuffer bb) {
返回charset.decode(bb).toString();
}
/* 服务器服务方法*/publicvoidservice() throws IOException {
if(通道==null||选择器==null)
返回;
通道.configureBlocking(假);
通道套接字
t().bind(new InetSocketAddress(port)); // channel.write(ByteBuffer.wrap(new String("aaaa").getBytes())); channel.register(selector, SelectionKey.OP_READ); /** 外循环,已经发生了SelectionKey数目 */while(selector.select() >0) { System.out.println("有新channel加入"); /* 得到已经被捕获了的SelectionKey的集合 */ Iterator iterator = selector.selectedKeys().iterator(); while (iterator.hasNext()) { SelectionKey key =null; try { key = (SelectionKey) iterator.next(); iterator.remove(); if (key.isReadable()) { reveice(key); } if (key.isWritable()) { // send(key); } } catch (IOException e) { e.printStackTrace(); try { if(key !=null) { key.cancel(); key.channel().close(); } } catch (ClosedChannelException cex) { e.printStackTrace(); } } } /* 内循环完 */ } /* 外循环完 */ } /* * 接收 用receive()读IO 作为服务端一般不需要调用connect(),如果未调用 * "font-family: Arial, Helvetica, sans-serif;">connect()时调 * style="font-family: Arial, Helvetica, sans-serif;" * >用read()write()读写,会报java.nio.channels.NotYetConnectedException * 只有调用connect()之后,才能使用read和write. */synchronizedpublicvoidreveice(SelectionKey key)throws IOException { if(key ==null) return; // ***用channel.receive()获取客户端消息***//// :接收时需要考虑字节长度DatagramChannel sc = (DatagramChannel) key.channel(); String content = ""; // create buffer with capacity of 48 bytesByteBuffer buf = ByteBuffer.allocate(1024);// java里一个(utf-8)中文3字节,gbk中文占2个字节 buf.clear(); SocketAddress address = sc.receive(buf);// read into buffer. 返回客户端的地址信息String clientAddress = address.toString().replace("/", "").split(":")[0]; String clientPost = address.toString().replace("/", "").split(":")[1]; buf.flip(); // make buffer ready for readwhile (buf.hasRemaining()) { buf.get(newbyte[buf.limit()]);// read 1 byte at a timecontent +=new String(buf.array()); } buf.clear(); // make buffer ready for writingSystem.out.println("接收:" + content.trim()); // 第一次发;udp采用数据报模式,发送多少次,接收多少次ByteBuffer buf2 = ByteBuffer.allocate(65507); buf2.clear(); buf2.put( "消息推送内容 abc..UDP是一个非连接的协议,传输数据之前源端和终端不建立连接,当它想传送时就简单地去抓取来自应用程序的数据,并尽可能快地把它扔到网络上。在发送端UDP是一个非连接的协议,传输数据之前源端和终端不建立连接,当它想传送时就简单地去抓取来自应用程序的数据,并尽可能快地把它扔到网络上。在发送端UDP是一个非连接的协议,传输数据之前源端和终端不建立连接,当它想传送时就简单地去抓取来自应用程序的数据,并尽可能快地把它扔到网络上。在发送端@Q" .getBytes()); buf2.flip(); channel.send(buf2, newInetSocketAddress(clientAddress, Integer.parseInt(clientPost)));// 将消息回送给客户端 // 第二次发ByteBuffer buf3 = ByteBuffer.allocate(65507); buf3.clear(); buf3.put("任务完成".getBytes()); buf3.flip(); channel.send(buf3, newInetSocketAddress(clientAddress, Integer.parseInt(clientPost)));// 将消息回送给客户端 } inty = 0; publicvoid send(SelectionKey key) { if(key ==null) return; // ByteBuffer buff = (ByteBuffer) key.attachment();DatagramChannel sc = (DatagramChannel) key.channel(); try { sc.write(ByteBuffer.wrap(newString("aaaa").getBytes())); } catch (IOException e1) { e1.printStackTrace(); } System.out.println("send2() " + (++y)); } /* 发送文件 */publicvoid sendFile(SelectionKey key) { if(key ==null) return; ByteBuffer buff = (ByteBuffer) key.attachment(); SocketChannel sc = (SocketChannel) key.channel(); String data = decode(buff); if(data.indexOf("get") == -1) return; String subStr = data.substring(data.indexOf(" "), data.length()); System.out.println("截取之后的字符串是 " + subStr); FileInputStream fileInput =null; try { fileInput =new FileInputStream(subStr); FileChannel fileChannel = fileInput.getChannel(); fileChannel.transferTo(0, fileChannel.size(), sc); fileChannel.close(); } catch (IOException e) { e.printStackTrace(); } finally { try { fileInput.close(); } catch (IOException ex) { ex.printStackTrace(); } } } publicstaticvoidmain(String[] args)throws IOException { new DatagramChannelServerDemo().service(); } }//客户端package com.dxz.springsession.nio.demo3;importjava.nio.channels.*;importjava.nio.charset.*;importjava.net.*;importjava.io.*;importjava.util.*;importjava.nio.*;publicclass DatagramChannelClientDemo { // UDP协议客户端privateString serverIp = "127.0.0.1"; privateintport = 9975; // private ServerSocketChannel serverSocketChannel; DatagramChannel channel; privateCharset charset = Charset.forName("UTF-8"); privateSelector selector =null; publicDatagramChannelClientDemo()throws IOException { try { selector = Selector.open(); channel = DatagramChannel.open(); } catch (Exception e) { selector =null; channel =null; System.out.println("超时"); } System.out.println("客户器启动"); } /* 编码过程 */public ByteBuffer encode(String str) { return charset.encode(str); } /* 解码过程 */public String decode(ByteBuffer bb) { return charset.decode(bb).toString(); } /* 服务器服务方法 */publicvoidservice()throws IOException { if(channel ==null|| selector ==null) return; channel.configureBlocking(false); channel.connect(newInetSocketAddress(serverIp, port));// 连接服务端channel.write(ByteBuffer.wrap(newString("客户端请求获取消息").getBytes())); channel.register(selector, SelectionKey.OP_READ); /** 外循环,已经发生了SelectionKey数目 */while(selector.select() >0) { /* 得到已经被捕获了的SelectionKey的集合 */ Iterator iterator = selector.selectedKeys().iterator(); while (iterator.hasNext()) { SelectionKey key =null; try { key = (SelectionKey) iterator.next(); iterator.remove(); if (key.isReadable()) { reveice(key); } if (key.isWritable()) { // send(key); } } catch (IOException e) { e.printStackTrace(); try { if(key !=null) { key.cancel(); key.channel().close(); } } catch (ClosedChannelException cex) { e.printStackTrace(); } } } /* 内循环完 */ } /* 外循环完 */ } // /* // * 接收 用read()读IO // * */ // synchronized public void reveice2(SelectionKey key) throws IOException { // if (key == null) // return; //// ***用channel.read()获取消息***////// :接收时需要考虑字节长度 // DatagramChannel sc = (DatagramChannel) key.channel(); // String content = ""; //// create buffer with capacity of 48 bytes // ByteBuffer buf = ByteBuffer.allocate(3);// java里一个(utf-8)中文3字节,gbk中文占2个字节 // int bytesRead = sc.read(buf); //read into buffer. //// while (bytesRead >0) { // buf.flip(); //make buffer ready for read // while(buf.hasRemaining()){ // buf.get(new byte[buf.limit()]); // read 1 byte at a time // content += new String(buf.array()); // } // buf.clear(); //make buffer ready for writing // bytesRead = sc.read(buf); // } // System.out.println("接收:" + content); // }/* 接收 */synchronizedpublicvoidreveice(SelectionKey key)throws IOException { String threadName = Thread.currentThread().getName(); if(key ==null) return; try { // ***用channel.receive()获取消息***//// :接收时需要考虑字节长度DatagramChannel sc = (DatagramChannel) key.channel(); String content = ""; // 第一次接;udp采用数据报模式,发送多少次,接收多少次ByteBuffer buf = ByteBuffer.allocate(65507);// java里一个(utf-8)中文3字节,gbk中文占2个字节 buf.clear(); SocketAddress address = sc.receive(buf);// read into buffer.String clientAddress = address.toString().replace("/", "").split(":")[0]; String clientPost = address.toString().replace("/", "").split(":")[1]; System.out.println(threadName + "t" + address.toString()); buf.flip(); // make buffer ready for readwhile (buf.hasRemaining()) { buf.get(newbyte[buf.limit()]);// read 1 byte at a timebyte[] tmp = buf.array(); content +=new String(tmp); } buf.clear(); // make buffer ready for writing次System.out.println(threadName + "接收:" + content.trim()); // 第二次接content = ""; ByteBuffer buf2 = ByteBuffer.allocate(65507);// java里一个(utf-8)中文3字节,gbk中文占2个字节 buf2.clear(); SocketAddress address2 = sc.receive(buf2);// read into buffer.buf2.flip();// make buffer ready for readwhile (buf2.hasRemaining()) { buf2.get(newbyte[buf2.limit()]);// read 1 byte at a timebyte[] tmp = buf2.array(); content +=new String(tmp); } buf2.clear(); // make buffer ready for writing次System.out.println(threadName + "接收2:" + content.trim()); } catch (PortUnreachableException ex) { System.out.println(threadName + "服务端端口未找到!"); } send(2); } booleanflag =false; publicvoidsend(int i) { if (flag) return; try { // channel.write(ByteBuffer.wrap(new // String("客户端请求获取消息(第"+i+"次)").getBytes())); // channel.register(selector, SelectionKey.OP_READ );ByteBuffer buf2 = ByteBuffer.allocate(48); buf2.clear(); buf2.put(("客户端请求获取消息(第" + i + "次)").getBytes()); buf2.flip(); channel.write(buf2); channel.register(selector, SelectionKey.OP_READ); // int bytesSent = channel.send(buf2, new // InetSocketAddress(serverIp,port)); // 将消息回送给服务端}catch (IOException e) { e.printStackTrace(); } flag =true; } inty = 0; publicvoid send(SelectionKey key) { if(key ==null) return; // ByteBuffer buff = (ByteBuffer) key.attachment();DatagramChannel sc = (DatagramChannel) key.channel(); try { sc.write(ByteBuffer.wrap(newString("aaaa").getBytes())); } catch (IOException e1) { e1.printStackTrace(); } System.out.println("send2() " + (++y)); } /* 发送文件 */publicvoid sendFile(SelectionKey key) { if(key ==null) return; ByteBuffer buff = (ByteBuffer) key.attachment(); SocketChannel sc = (SocketChannel) key.channel(); String data = decode(buff); if(data.indexOf("get") == -1) return; String subStr = data.substring(data.indexOf(" "), data.length()); System.out.println("截取之后的字符串是 " + subStr); FileInputStream fileInput =null; try { fileInput =new FileInputStream(subStr); FileChannel fileChannel = fileInput.getChannel(); fileChannel.transferTo(0, fileChannel.size(), sc); fileChannel.close(); } catch (IOException e) { e.printStackTrace(); } finally { try { fileInput.close(); } catch (IOException ex) { ex.printStackTrace(); } } } publicstaticvoidmain(String[] args)throws IOException { newThread(new Runnable() { publicvoid run() { try { new DatagramChannelClientDemo().service(); } catch (IOException e) { e.printStackTrace(); } } }).start(); // new Thread(new Runnable() { // public void run() { // try { // new DatagramChannelClientDemo().service(); // } catch (IOException e) { // e.printStackTrace(); // } // } // }).start(); } } Java NIO中的Buffer用于和NIO通道进行交互。如你所知,数据是从通道读入缓冲区,从缓冲区写入到通道中的。交互图如下: 缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存。这块内存被包装成NIO Buffer对象,并提供了一组方法,用来方便的访问该块内存。缓冲区实际上是一个容器对象,更直接的说,其实就是一个数组,在NIO库中,所有数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中的; 在写入数据时,它也是写入到缓冲区中的;任何时候访问 NIO 中的数据,都是将它放到缓冲区中。而在面向流I/O系统中,所有数据都是直接写入或者直接将数据读取到Stream对象中。 在NIO中,所有的缓冲区类型都继承于抽象类Buffer,最常用的就是ByteBuffer,对于Java中的基本类型,基本都有一个具体Buffer类型与之相对应,它们之间的继承关系如下图所示: Buffer的基本用法Buffer的基本用法 使用Buffer读写数据一般遵循以下四个步骤: 写入数据到Buffer 调用flip()方法 从Buffer中读取数据 调用clear()方法或者compact()方法 当向buffer写入数据时,buffer会记录下写了多少数据。一旦要读取数据,需要通过flip()方法将Buffer从写模式切换到读模式。在读模式下,可以读取之前写入到buffer的所有数据。 一旦读完了所有的数据,就需要清空缓冲区,让它可以再次被写入。有两种方式能清空缓冲区:调用clear()或compact()方法。clear()方法会清空整个缓冲区。compact()方法只会清除已经读过的数据。任何未读的数据都被移到缓冲区的起始处,新写入的数据将放到缓冲区未读数据的后面。 下面是一个使用Buffer的例子: 01RandomAccessFile aFile = newRandomAccessFile("data/nio-data.txt", "rw"); 02FileChannel inChannel = aFile.getChannel(); 03 04//create buffer with capacity of 48 bytes 05ByteBuffer buf = ByteBuffer.allocate(48); 06 07intbytesRead = inChannel.read(buf); //read into buffer. 08while(bytesRead != -1) { 09 10 buf.flip(); //make buffer ready for read 11 12 while(buf.hasRemaining()){ 13 System.out.print((char) buf.get()); // read 1 byte at a time 14 } 15 16 buf.clear(); //make buffer ready for writing 17 bytesRead = inChannel.read(buf); 18} 19aFile.close(); } 示例2: 下面是一个简单的使用IntBuffer的例子: package com.dxz.nio;import java.nio.IntBuffer;publicclass TestIntBuffer { publicstaticvoid main(String[] args) { // 分配新的int缓冲区,参数为缓冲区容量 // 新缓冲区的当前位置将为零,其界限(限制位置)将为其容量。它将具有一个底层实现数组,其数组偏移量将为零。 IntBuffer buffer = IntBuffer.allocate(8); for(inti = 0; i< buffer.capacity(); ++i) { intj = 2 * (i + 1); // 将给定整数写入此缓冲区的当前位置,当前位置递增 buffer.put(j); } // 重设此缓冲区,将限制设置为当前位置,然后将当前位置设置为0 buffer.flip(); // 查看在当前位置和限制位置之间是否有元素 while (buffer.hasRemaining()) { // 读取此缓冲区当前位置的整数,然后当前位置递增 intj = buffer.get(); System.out.print(j + " "); } } } 结果: 2 4 6 8 10 12 14 16 Buffer的capacity,position和limit 缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存。这块内存被包装成NIO Buffer对象,并提供了一组方法,用来方便的访问该块内存。 为了理解Buffer的工作原理,需要熟悉它的三个属性: capacity position limit position和limit的含义取决于Buffer处在读模式还是写模式。不管Buffer处在什么模式,capacity的含义总是一样的。 这里有一个关于capacity,position和limit在读写模式中的说明,详细的解释在插图后面。 capacity 作为一个内存块,Buffer有一个固定的大小值,也叫“capacity”.你只能往里写capacity个byte、long,char等类型。一旦Buffer满了,需要将其清空(通过读数据或者清除数据)才能继续写数据往里写数据。 position 当你写数据到Buffer中时,position表示当前的位置。初始的position值为0.当一个byte、long等数据写到Buffer后, position会向前移动到下一个可插入数据的Buffer单元。position最大可为capacity – 1. 当读取数据时,也是从某个特定位置读。当将Buffer从写模式切换到读模式,position会被重置为0. 当从Buffer的position处读取数据时,position向前移动到下一个可读的位置。 limit 在写模式下,Buffer的limit表示你最多能往Buffer里写多少数据。 写模式下,limit等于Buffer的capacity。 当切换Buffer到读模式时, limit表示你最多能读到多少数据。因此,当切换Buffer到读模式时,limit会被设置成写模式下的position值。换句话说,你能读到之前写入的所有数据(limit被设置成已写数据的数量,这个值在写模式下就是position) Buffer的类型 Java NIO 有以下Buffer类型 ByteBuffer MappedByteBuffer CharBuffer DoubleBuffer FloatBuffer IntBuffer LongBuffer ShortBuffer p<> 如你所见,这些Buffer类型代表了不同的数据类型。换句话说,就是可以通过char,short,int,long,float 或 double类型来操作缓冲区中的字节。 MappedByteBuffer 有些特别,在涉及它的专门章节中再讲。 Buffer的分配 要想获得一个Buffer对象首先要进行分配。 每一个Buffer类都有一个allocate方法。下面是一个分配48字节capacity的ByteBuffer的例子。 1ByteBuffer buf = ByteBuffer.allocate(48); 这是分配一个可存储1024个字符的CharBuffer: 1CharBuffer buf = CharBuffer.allocate(1024); 向Buffer中写数据 写数据到Buffer有两种方式: 从Channel写到Buffer。 通过Buffer的put()方法写到Buffer里。 从Channel写到Buffer的例子 1intbytesRead = inChannel.read(buf); //read into buffer. 通过put方法写Buffer的例子: 1buf.put(127); put方法有很多版本,允许你以不同的方式把数据写入到Buffer中。例如, 写到一个指定的位置,或者把一个字节数组写入到Buffer。 更多Buffer实现的细节参考JavaDoc。 flip()方法 flip英 [flɪp]美 [flɪp] 及物动词 轻弹,轻击; 按(开关); 快速翻转; 急挥 flip方法将Buffer从写模式切换到读模式。调用flip()方法会将position设回0,并将limit设置成之前position的值。 换句话说,position现在用于标记读的位置,limit表示之前写进了多少个byte、char等 —— 现在能读取多少个byte、char等。 从Buffer中读取数据 从Buffer中读取数据有两种方式: 从Buffer读取数据到Channel。 使用get()方法从Buffer中读取数据。 从Buffer读取数据到Channel的例子: //read from buffer into channel.intbytesWritten = inChannel.write(buf); 使用get()方法从Buffer中读取数据的例子 byteaByte = buf.get(); get方法有很多版本,允许你以不同的方式从Buffer中读取数据。例如,从指定position读取,或者从Buffer中读取数据到字节数组。更多Buffer实现的细节参考JavaDoc。 rewind()方法 Buffer.rewind()将position设回0,所以你可以重读Buffer中的所有数据。limit保持不变,仍然表示能从Buffer中读取多少个元素(byte、char等)。 clear()与compact()方法 一旦读完Buffer中的数据,需要让Buffer准备好再次被写入。可以通过clear()或compact()方法来完成。 如果调用的是clear()方法,position将被设回0,limit被设置成 capacity的值。换句话说,Buffer 被清空了。Buffer中的数据并未清除,只是这些标记告诉我们可以从哪里开始往Buffer里写数据。 如果Buffer中有一些未读的数据,调用clear()方法,数据将“被遗忘”,意味着不再有任何标记会告诉你哪些数据被读过,哪些还没有。 如果Buffer中仍有未读的数据,且后续还需要这些数据,但是此时想要先先写些数据,那么使用compact()方法。 compact()方法将所有未读的数据拷贝到Buffer起始处。然后将position设到最后一个未读元素正后面。limit属性依然像clear()方法一样,设置成capacity。现在Buffer准备好写数据了,但是不会覆盖未读的数据。 mark()与reset()方法 通过调用Buffer.mark()方法,可以标记Buffer中的一个特定position。之后可以通过调用Buffer.reset()方法恢复到这个position。例如: 1buffer.mark(); 2 3//call buffer.get() a couple of times, e.g. during parsing. 4 5buffer.reset(); //set position back to mark. equals()与compareTo()方法 可以使用equals()和compareTo()方法两个Buffer。 equals() 当满足下列条件时,表示两个Buffer相等: 有相同的类型(byte、char、int等)。 Buffer中剩余的byte、char等的个数相等。 Buffer中所有剩余的byte、char等都相同。 如你所见,equals只是比较Buffer的一部分,不是每一个在它里面的元素都比较。实际上,它只比较Buffer中的剩余元素。 compareTo()方法 compareTo()方法比较两个Buffer的剩余元素(byte、char等), 如果满足下列条件,则认为一个Buffer“小于”另一个Buffer: 第一个不相等的元素小于另一个Buffer中对应的元素 。 所有元素都相等,但第一个Buffer比另一个先耗尽(第一个Buffer的元素个数比另一个少)。 缓冲区分片 在NIO中,除了可以分配或者包装一个缓冲区对象外,还可以根据现有的缓冲区对象来创建一个子缓冲区,即在现有缓冲区上切出一片来作为一个新的缓冲区,但现有的缓冲区与创建的子缓冲区在底层数组层面上是数据共享的,也就是说,子缓冲区相当于是现有缓冲区的一个视图窗口。调用slice()方法可以创建一个子缓冲区,让我们通过例子来看一下: package com.dxz.nio;import java.nio.ByteBuffer;publicclass BufferDemo1 { staticpublicvoidmain(String args[])throws Exception { ByteBuffer buffer = ByteBuffer.allocate(10); // 缓冲区中的数据0-9for(inti = 0; i< buffer.capacity(); ++i) { buffer.put((byte) i); } // 创建子缓冲区buffer.position(3); buffer.limit(7); ByteBuffer slice = buffer.slice(); // 改变子缓冲区的内容for(inti = 0; i< slice.capacity(); ++i) { byteb = slice.get(i); b *= 10; slice.put(i, b); } buffer.position(0); buffer.limit(buffer.capacity()); while(buffer.remaining() >0) { System.out.println(buffer.get()); } } } 结果: 0 1 2 30 40 50 60 7 8 9 只读缓冲区 只读缓冲区非常简单,可以读取它们,但是不能向它们写入数据。可以通过调用缓冲区的asReadOnlyBuffer()方法,将任何常规缓冲区转 换为只读缓冲区,这个方法返回一个与原缓冲区完全相同的缓冲区,并与原缓冲区共享数据,只不过它是只读的。如果原缓冲区的内容发生了变化,只读缓冲区的内容也随之发生变化: package com.dxz.nio;import java.nio.ByteBuffer;publicclass BufferDemo2 { staticpublicvoidmain(String args[])throws Exception { ByteBuffer buffer = ByteBuffer.allocate(10); // 缓冲区中的数据0-9for(inti = 0; i< buffer.capacity(); ++i) { buffer.put((byte) i); } // 创建只读缓冲区ByteBuffer readonly = buffer.asReadOnlyBuffer(); // 改变原缓冲区的内容for(inti = 0; i< buffer.capacity(); ++i) { byteb = buffer.get(i); b *= 10; buffer.put(i, b); } readonly.position(0); readonly.limit(buffer.capacity()); // 只读缓冲区的内容也随之改变while(readonly.remaining() >0) { System.out.println(readonly.get()); } } } 结果: 0 10 20 30 40 50 60 70 80 90 如果尝试修改只读缓冲区的内容,则会报ReadOnlyBufferException异常。只读缓冲区对于保护数据很有用。在将缓冲区传递给某个 对象的方法时,无法知道这个方法是否会修改缓冲区中的数据。创建一个只读的缓冲区可以保证该缓冲区不会被修改。只可以把常规缓冲区转换为只读缓冲区,而不能将只读的缓冲区转换为可写的缓冲区。 直接缓冲区 直接缓冲区是为加快I/O速度,使用一种特殊方式为其分配内存的缓冲区,JDK文档中的描述为:给定一个直接字节缓冲区,Java虚拟机将尽最大努 力直接对它执行本机I/O操作。也就是说,它会在每一次调用底层操作系统的本机I/O操作之前(或之后),尝试避免将缓冲区的内容拷贝到一个中间缓冲区中 或者从一个中间缓冲区中拷贝数据。要分配直接缓冲区,需要调用allocateDirect()方法,而不是allocate()方法,使用方式与普通缓冲区并无区别,如下面的拷贝文件示例: package com.dxz.nio;import java.io.FileInputStream;import java.io.FileOutputStream;import java.nio.ByteBuffer;importjava.nio.channels.*;publicclass BufferDemo3 { staticpublicvoidmain(String args[])throws Exception { String infile = "e:\logs\test.txt"; FileInputStream fin =new FileInputStream(infile); FileChannel fcin = fin.getChannel(); String outfile = String.format("e:\logs\testcopy.txt"); FileOutputStream fout =new FileOutputStream(outfile); FileChannel fcout = fout.getChannel(); // 使用allocateDirect,而不是allocateByteBuffer buffer = ByteBuffer.allocateDirect(1024); while(true) { buffer.clear(); intr = fcin.read(buffer); if(r == -1) { break; } buffer.flip(); fcout.write(buffer); } } } 内存映射文件I/O 内存映射文件I/O是一种读和写文件数据的方法,它可以比常规的基于流或者基于通道的I/O快的多。内存映射文件I/O是通过使文件中的数据出现为 内存数组的内容来完成的,这其初听起来似乎不过就是将整个文件读到内存中,但是事实上并不是这样。一般来说,只有文件中实际读取或者写入的部分才会映射到内存中。如下面的示例代码: package com.dxz.nio;import java.io.RandomAccessFile;import java.nio.MappedByteBuffer;importjava.nio.channels.*;publicclass BufferDemo4 { staticprivatefinalintstart = 0; staticprivatefinalintsize = 1024; staticpublicvoidmain(String args[])throws Exception { RandomAccessFile raf =newRandomAccessFile("e:\logs\test.txt", "rw"); FileChannel fc = raf.getChannel(); MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE, start, size); mbb.put(0, (byte) 97); mbb.put(1023, (byte) 122); raf.close(); } }文章分享结束,深入Java NIO教程:Socket通道详解(第三篇)和的答案你都知道了吗?欢迎再次光临本站哦!
【深入Java NIO教程:Socket通道详解(第三篇)】相关文章:
2.米颠拜石
3.王羲之临池学书
8.郑板桥轶事十则
用户评论
终于等到这个了!想好好学习下NIO做socket通信。
有6位网友表示赞同!
这应该是我对Channel的最佳入门指南了吧。
有10位网友表示赞同!
一直觉得NIO很强大,希望这次教程能让我更好地理解它的应用。
有10位网友表示赞同!
Socket通道确实很有用,可以让我优化网络程序的性能。
有20位网友表示赞同!
学习Java NIO系列教程一直都在我的待办事项清单上!
有20位网友表示赞同!
期待这个教程能帮我掌握Channel的使用技巧。
有15位网友表示赞同!
以前只知道NIO的概念,现在终于有机会深入了解它了!
有10位网友表示赞同!
学习NIO可以让我写出更优雅、高效的网络程序代码。
有12位网友表示赞同!
Socket通道是一个很实用的技术,这个教程非常及时!
有16位网友表示赞同!
感谢作者分享这篇详细的Tutorial,期待继续学习后面的内容。
有20位网友表示赞同!
我已经对Channel的使用方法感兴趣了。
有10位网友表示赞同!
我的网络项目正需要用到NIO,这篇文章太适合我了!
有9位网友表示赞同!
希望教程能涵盖多方面Socket通道的知识和应用场景吧!
有9位网友表示赞同!
现在开始学习Java NIO也不算晚吧。期待这个教程可以让我入门。
有13位网友表示赞同!
以前没接触过NIO,打算从这个教程开始了解它。)
有8位网友表示赞同!
想看看Channel如何实现高效的网络数据传输。
有17位网友表示赞同!
我对nio和socket通道一直很好奇,希望能学到更多!
有20位网友表示赞同!
学习Java NIO可以让我拓展我的编程技能范围。
有11位网友表示赞同!
这个教程内容看起来很全面,值得深入学习!
有9位网友表示赞同!