相关文章
java网络编程—NIO与Netty(四)
java网络编程—NIO与Netty(三)
java网络编程—NIO与Netty(二)
java网络编程—NIO与Netty(一)
java网络编程—基石:五种IO模型及原理(多路复用\epoll)
文章目录
继续接着总结NIO
Java NIO:transferFrom与transferTo
两个channel间的通信,不需要通过buffer直接进行数据交换。
示例:
/**
* @author zhangsh
*/
public class NIOTransferData {
public static void main(String[] args) throws IOException {
transferData();
}
public static void transferData() throws IOException {
RandomAccessFile fromFile = new RandomAccessFile("fromFile.txt", "rw");
FileChannel fromChannel = fromFile.getChannel();
RandomAccessFile toFile = new RandomAccessFile("toFile.txt", "rw");
FileChannel toChannel = toFile.getChannel();
long position = 0;
long count = fromChannel.size();
toChannel.transferFrom(fromChannel, position, count);
RandomAccessFile toFileEnd = new RandomAccessFile("toFileEnd.txt", "rw");
FileChannel toChannelEnd = toFileEnd.getChannel();
toChannel.transferTo(position, toChannel.size(), toChannelEnd);
}
}
Java NIO:Selector
java NIO通过使用Selector可以单线程的管理多个(channel)通道的读写。多个通道可以注册在一个Selector上,一个持有这个Selector的线程可以通过Selector去管理多个通道,每个通道都是某种类型的连接
Selector的创建
Selector selector = Selector.open();
为了将Channel和Selector配合使用,必须将channel注册到selector上。通过SelectableChannel.register()方法来实现,如下:
channel.configureBlocking(false);
SelectionKey key = channel.register(selector,Selectionkey.OP_READ);
与Selector一起使用时,Channel必须处于非阻塞模式下。这意味着不能将FileChannel与Selector一起使用,因为FileChannel不能切换到非阻塞模式
注意register()方法的第二个参数。这是一个“interest集合——感兴趣的事件集合,也就是这个selector准备监听的集合。注意区分 感兴趣的时间 与 准备好的事件的区别。
SelectionKey
关键属性:
- interest集合
selectionKey.interestOps()
interestOps()是你注册的感兴趣的事件,通过如下方法获取,并判断(详见代码见最后实例)
如上边所说,register第二个参数是“interest set”,指定了channel监听的事件类型:
Connect: SelectionKey.OP_CONNECT
一个成功连接了Server的channel,注册为Connect
Accept :SelectionKey.OP_ACCEPT
一个接受连接请求的 serverSocketChannel,被注册为Accept状态(注意该事件只用于服务端)
Read :SelectionKey.OP_READ
一个有数据并可被读取的channel,注册为Read状态
Write SelectionKey.OP_WRITE
一个可写入数据的channel,注册为Write状态
//如要你要注册多种事件,使用"|"操作
int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
interestOps: channel上的关注的事件,通过&运算可以得到相应的判断
int interestSet = selectionKey.interestOps();
boolean isInterestedInAccept = (interestSet &
SelectionKey.OP_ACCEPT)!=0;
boolean isInterestedInConnect = (interestSet &
SelectionKey.OP_CONNECT)!=0;
boolean isInterestedInRead = (interestSet & SelectionKey.OP_READ)!=0;
boolean isInterestedInWrite = (interestSet &
SelectionKey.OP_WRITE)!=0;
为什么这么判断?
比如,public static final int OP_WRITE = 1 << 2; 也就是说OP_WRITE =0010(2进制);
当你的interestSet 也是SelectionKey.OP_WRITE(1<<2)时,按位与操作后的结果还是本身 10,
否则其他的事件类型会得到0的结果。
- ready集合
与interest集合类似,但是这个ready集合中的事件表示客户端与服务端已经成功建立了连接。
同样是4种类型:
Connect: SelectionKey.OP_CONNECT
一个成功连接了Server的channel,注册为Connect
Accept :SelectionKey.OP_ACCEPT
一个接受连接请求的 serverSocketChannel,被注册为Accept状态(注意该事件只用于服务端)
Read :SelectionKey.OP_READ
一个有数据并可被读取的channel,注册为Read状态
Write SelectionKey.OP_WRITE
一个可写入数据的channel,注册为Write状态
//源码也是使用位操作判断的
selectionKey.isAcceptable()//这个用在服务端
selectionKey.isConnectable()
selectionKey.isReadable()
selectionKey.isWritable()
代码实践,请仔细阅读注释:
Java NIO 中所讲述的 Selector 的使用流程:
1 通过 Selector.open() 打开一个 Selector.
2 将 Channel 注册到 Selector 中, 并设置需要监听的事件(interest set)
3 不断重复:
调用 select() 方法
调用 selector.selectedKeys() 获取 selected keys
迭代每个 selected key:
1) 从 selected key 中获取 对应的 Channel 和附加信息(如果有的话)
2) 判断是哪些 IO 事件已经就绪了, 然后处理它们. 如果是 OP_ACCEPT 事件, 则调用
"SocketChannel clientChannel = ((ServerSocketChannel) key.channel()).accept()"
获取 SocketChannel, 并将它设置为 非阻塞的, 然后将这个 Channel 注册到 Selector 中.
3) 根据需要更改 selected key 的监听事件.
4) 将已经处理过的 key 从 selected keys 集合中删除.
服务端:
public class NIOServer {
/* 标识数字 */
private int flag = 0;
/* 缓冲区大小 */
private int BLOCK = 4096;
/* 接受数据缓冲区 */
private ByteBuffer sendbuffer = ByteBuffer.allocate(BLOCK);// 4KB
/* 发送数据缓冲区 */
private ByteBuffer receivebuffer = ByteBuffer.allocate(BLOCK);// 4KB
private Selector selector;
ServerSocketChannel serverSocketChannelTemp;
public NIOServer(int port) throws IOException {
// ServerSocketChannel用来在服务端监听Socket连接
// 在这个ServerSocketChannel建立之后(open静态方法建立),创建ServerSocket相应TCP请求
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
// 在这个ServerSocketChannel
// 创建ServerSocket相应TCP请求。一个ServerSocketChannel上的ServerSocket是单例对象
System.out.println("serverSocketChannel" + serverSocketChannel);
ServerSocket serverSocket = serverSocketChannel.socket();
// 然后在这个ServerSocket上绑定ip+port
serverSocket.bind(new InetSocketAddress(port));
serverSocketChannelTemp = serverSocketChannel;
// 最后,把那个ServerSocketChannel对象注册到selector上
// -----------------------------------------------------------Selector--------------------------------------------------------------
// 可以从selector中获取多个注册了的channel,一个selector可以管理多个channel,
// 因此消除了创建多个线程去处理多个请求的做法。
// 另外,selector中对channel的管理都是非阻塞的,所以FileChannel这种阻塞的channel不能使用selector
selector = Selector.open();
// register第二个参数是“interest set”,指定了channel监听的事件类型
/**
* <pre>
* -Connect SelectionKey.OP_CONNECT 一个成功连接了Server的channel,注册为Connect
*
* -Accept SelectionKey.OP_ACCEPT 一个接受连接请求的 serverSocketChannel,被注册为Accept状态
*
* -Read SelectionKey.OP_READ 一个有数据并可被读取的channel,注册为Read状态
*
* -Write SelectionKey.OP_WRITE 一个可写入数据的channel,注册为Write状态
*
* 如果你对不止一种事件感兴趣,那么可以用“位或”操作符将常量连接起来
* int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
* </pre>
*/
// 每次在一个selector上注册一个channel,就会产生一个SelectionKey对象,
// 需要说一下,SelectionKey对象的如下属性
//
/**
* 1)interestOps: channel上的关注的事件,通过&运算可以得到相应的判断
*
* <pre>
* int interestSet = selectionKey.interestOps();
*
* boolean isInterestedInAccept = (interestSet & SelectionKey.OP_ACCEPT)==SelectionKey.OP_ACCEPT;
*
* boolean isInterestedInConnect = (interestSet & SelectionKey.OP_CONNECT)==SelectionKey.OP_CONNECT;
*
* boolean isInterestedInRead = (interestSet & SelectionKey.OP_READ)==SelectionKey.OP_READ;
*
* boolean isInterestedInWrite = (interestSet & SelectionKey.OP_WRITE)==SelectionKey.OP_WRITE;
*
* 2)readyOps 是channel准备好了的事件类型;注意与interestOps并不一样!
* Selector.select()就是检查是否有注册的兴趣事件中已经准备好了的事件!
* 可以通过如下方式判断:
* selectionKey.isAcceptable(); 一个server socket channel准备好接收新进入的连接
* selectionKey.isConnectable(); 某个channel成功连接到另一个服务器
* selectionKey.isReadable(); 一个有数据可读的通道
* selectionKey.isWritable();等待写数据的通道
* </pre>
*/
SelectionKey selectionKey = serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT) " + selectionKey);
System.out.println("Server Start----:" + port + " selector " + selector);
}
// 监听
private void listen() throws IOException {
while (true) {
/**
* <pre>
* select 返回在这个selector上注册过的兴趣事件 (interestsSet)对应的channel.
* 比如你在这个selector上注册过,Accept事件,
* 那么select的含义就是去选择已经准备好的,accept事件对应的channel。
*
* int select():阻塞方法,直到至少返回一个你注册过的兴趣事件对应的channel.
* int select(long timeout):与select()类似,不同之处在于设定了阻塞超时时间
* int selectNow():与select()类似,只是不会产生阻塞,立即返回
*
* </pre>
*/
// It returns only after at least one channel is selected, this
// selector's wakeup method is invoked, or the current thread is
// interrupted, whichever comes first.
if (selector.select() <= 0) {// Selector.select()就是检查是否有注册的兴趣事件中已经准备好了的事件.
//注意.这里返回的是处于ready状态的事件对应的channel数量
continue;
}
// 执行完selector.select(),会暗示你是否有准备好的channel,
// 接着执行 Set<SelectionKey> selectionKeys = selector.selectedKeys();
// 遍历获取准备好的channel
// 每次在一个selector上注册一个channel,就会产生一个SelectionKey对象
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
int i = 0;
while (iterator.hasNext()) {
++i;
SelectionKey selectionKey = iterator.next();
iterator.remove();// 必须手动remove这个使用过的key
handleKey(selectionKey);
}
System.out.println("iterator size" + i);
}
}
/**
* 通过selectKey可以获取到对应的channel和selector (selectionKey.selector())
* @param selectionKey
* @throws IOException
*/
private void handleKey(SelectionKey selectionKey) throws IOException {
// 接受请求
ServerSocketChannel server = null;
SocketChannel client = null;
String receiveText;
String sendText;
int count = 0;
// 测试此键的通道是否已准备好接新的Socket connection。
if (selectionKey.isAcceptable()) {
server = (ServerSocketChannel) selectionKey.channel();
// 可阻塞模式:若为阻塞方法,只用于ServerSocketChannel,去监听建立的连接。
// 如果有连接过来,就返回这个新连接的channel
// 如非阻塞模式:若没有连接建立,会返回null
System.out.println("serverSocketChannel:" + server);
System.err.println(serverSocketChannelTemp == server);// 可以看到还是server端之前自己注册的那个serverSocketChannel
// 通过 ServerSocketChannel.accept() 方法监听新进来的连接。当
// accept()方法返回的时候,它返回一个包含新进来的连接的 SocketChannel。
// 通常不会仅仅只监听一个连接,在while循环中调用 accept()方法
client = server.accept();
client.configureBlocking(false);
// 配置为非阻塞
System.out.println("clientSocketChannel:" + client);
// 注册到selector,等待连接
client.register(selector, SelectionKey.OP_READ);
} else if (selectionKey.isConnectable()) {
// a connection was established with a remote server.
System.out.println(" this is connectable !");
} else if (selectionKey.isReadable()) {
// 返回为之创建此键的通道。
client = (SocketChannel) selectionKey.channel();
System.err.println(client.toString());// 可以看到还是server端之前自己注册的那个serverSocketChannel
// 将缓冲区清空以备下次读取
receivebuffer.clear();
// 读取服务器发送来的数据到缓冲区中
count = client.read(receivebuffer);
if (count > 0) {
receiveText = new String(receivebuffer.array(), 0, count);
System.out.println("服务器端接受客户端数据--:" + receiveText);
client.register(selector, SelectionKey.OP_WRITE);// 客户端消息获取后,读取掉。接着注册一个可写事件,用来向客户端发送消息
}
} else if (selectionKey.isWritable()) {
// 将缓冲区清空以备下次写入
sendbuffer.clear();
// 返回为之创建此键的通道。
client = (SocketChannel) selectionKey.channel();
sendText = "message from server--" + flag++;
// 向缓冲区中输入数据
sendbuffer.put(sendText.getBytes());
// 将缓冲区各标志复位,因为向里面put了数据标志被改变要想从中读取数据发向服务器,就要复位
sendbuffer.flip();
// 输出到通道
client.write(sendbuffer);
System.out.println("服务器端向客户端发送数据--:" + sendText);
client.register(selector, SelectionKey.OP_READ);// 向客户端发送消息后,注册一个可读事件,当客户端再次发送消息时,这个事件将ready
}
}
public static void main(String[] args) throws IOException {
int port = 8989;
NIOServer server = new NIOServer(port);
server.listen();
}
}
客户端:
public class NIOClient {
/* 标识数字 */
private static int flag = 0;
/* 缓冲区大小 */
private static int BLOCK = 4096;
/* 接受数据缓冲区 */
private static ByteBuffer sendbuffer = ByteBuffer.allocate(BLOCK);// 4KB;
/* 发送数据缓冲区 */
private static ByteBuffer receivebuffer = ByteBuffer.allocate(BLOCK);
/* 服务器端地址 */
private final static InetSocketAddress SERVER_ADDRESS = new InetSocketAddress("localhost", 8989);
public static void main(String[] args) throws IOException {
/**
* SocketChannel创建的两种方式:
*
* 1.客户端 : SocketChannel.open(); socketChannel.connect(SERVER_ADDRESS);
*
*
* 2.服务端: SocketChannel client =serverSocketChannel.accept();
* 当有连接建立,accept方法返回建立连接的SocketChannel
*/
SocketChannel socketChannel = SocketChannel.open();
// You can set a SocketChannel into non-blocking mode. When you do so,
// you can call connect(), read() and write() in asynchronous mode.
socketChannel.configureBlocking(false);
// 打开选择器
Selector selector = Selector.open();
// 注册连接服务端socket动作
socketChannel.register(selector, SelectionKey.OP_CONNECT);
// 异步连接操作,如 connect() read() write()
// 即使没有建立连接也会立刻返回,使用socketChannel.finishConnect()检查连接建立是否成功,未成功会抛出异常
socketChannel.connect(SERVER_ADDRESS);
// 分配缓冲区大小内存
Set<SelectionKey> selectionKeys;
Iterator<SelectionKey> iterator;
SelectionKey selectionKey;
SocketChannel client;
String receiveText;
String sendText;
int count = 0;
while (true) {
// 选择一组键,其相应的通道已为 I/O 操作准备就绪。
// 此方法执行处于阻塞模式的选择操作。
// This method performs a blocking selection operation. It returns
// only after at least one channel is selected, this selector's
// wakeup method is invoked, or the current thread is interrupted,
// whichever comes first.
System.out.println(selector.select());
// 返回此选择器的已选择键集。
selectionKeys = selector.selectedKeys();
// System.out.println(selectionKeys.size());
iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
selectionKey = iterator.next();
if (selectionKey.isConnectable()) {
System.out.println(" this is connectable !");
//通过selectKey可以获取到对应的channel和selector
//selectionKey.selector()
client = (SocketChannel) selectionKey.channel();
System.err.println(client == socketChannel);
// 异步连接操作,如 connect() read() write()
// 即使没有建立连接也会立刻返回,使用socketChannel.finishConnect()检查连接建立是否成功,未成功会抛出异常
if (client.isConnectionPending() && client.finishConnect()) {
System.out.println("完成连接!");
sendbuffer.clear();
sendbuffer.put("Hello,Server".getBytes());
sendbuffer.flip();
client.write(sendbuffer);
}
client.register(selector, SelectionKey.OP_READ);
} else if (selectionKey.isReadable()) {
client = (SocketChannel) selectionKey.channel();
// 将缓冲区清空以备下次读取
receivebuffer.clear();
// 异步连接操作 read(),即使socketChannel中没有可读内容,也会立刻返回
count = client.read(receivebuffer);
if (count > 0) {
receiveText = new String(receivebuffer.array(), 0, count);
System.out.println("客户端接受服务器端数据--:" + receiveText);
client.register(selector, SelectionKey.OP_WRITE);// 读取后接着注册一个可写事件,为了向服务端发消息
}
} else if (selectionKey.isWritable()) {
sendbuffer.clear();
client = (SocketChannel) selectionKey.channel();
sendText = "message from client--" + (flag++);
sendbuffer.put(sendText.getBytes());
// 将缓冲区各标志复位,因为向里面put了数据标志被改变要想从中读取数据发向服务器,就要复位
sendbuffer.flip();
while (sendbuffer.hasRemaining()) {// 无法保证一次全部写完,所以使用循环方式
client.write(sendbuffer); // 异步连接操作
// write(),什么都没写入也会返回,所以循环使用
}
System.out.println("客户端向服务器端发送数据--:" + sendText);
client.register(selector, SelectionKey.OP_READ);// 向服务端发送消息后,注册一个可读事件,当服务端再次返回消息时,这个事件将ready
}
}
selectionKeys.clear();
}
}
}
SocketChannel
Java NIO中的SocketChannel是一个连接到TCP网络套接字的通道。可以通过以下2种方式创建SocketChannel:
-
打开一个SocketChannel并连接到互联网上的某台服务器。上边示例中,Client使用的方式。
-
一个新连接到达ServerSocketChannel时,会创建一个SocketChannel。上边示例中,Server使用的方式,ServerSocketChannel.accept()
非阻塞模式
阻塞模式中,当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。
Java NIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取。而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)。
就是说channel可以使用异步的方式调用connect()、write()、read()等方法
connect()
socketChannel.configureBlocking(false);
socketChannel.connect(new InetSocketAddress("http://jenkov.com", 80));
while(! socketChannel.finishConnect() ){
//wait, or do something else...
}
write()
非阻塞模式下,write()方法在尚未写出(指数据未到达tcp写缓冲区)任何内容时可能就返回了。所以需要在循环中调用write()。前面已经有例子了,这里就不赘述了。
read()
非阻塞模式下,read()方法在尚未读取到任何数据时可能就返回了。所以需要关注它的int返回值,它会告诉你读取了多少字节。
serverSocketChannel.accept()
非阻塞的监听连接到当前服务器上的Socket连接。如果没有则返回null.
为什么说JAVA NIO提供了基于Selector的异步网络I/O?
第一节介绍了NIO是同步非阻塞的。那么我们为什么经常说的”NIO异步网络模型“
NIO异步网络模型指的编程模型上的异步,通过reactor模型将具体IO操作放入线程池异步化。
下一节开始介绍Netty。