概述
初步了解了NIO核心组件的API,也大致知道了如何启动一个网络IO服务和客户端后。本篇在此基础上做一些补充,把一些必须要理解的
正文
ServerSocketChannel的accept方法和Selecor的select
在ServerSocketChannel的API中我们可以通过accept方法监听传入的连接,有传入连接的时候返回一个SocketChannel。也可以注册ACCEPT事件到Selector上,然后通过Selector的select方法监听传入的连接。
需要特别注意的是,accept方法返回的是一个连接,并不是连接集合,而通过Selector的select方法返回的是符合事件要求数量,然后通过selectedKeys获得SelectoinKey集合。
进行SelectionKey集合遍历处理的时候,需要删除元素
注意到通过selectedKeys获得SelectoinKey集合后,操作完会进行remove,或者遍历操作后一次性进行clear操作。因为Selector维护着selectedKeys,所以在操作完后如果不进行删除,那么下次轮询还会存在,而出现混乱,那么Selector为什么不自己删除呢,这就不对了,毕竟人家是告诉你现在的事件集合让你处理,你处理是否结束并不清楚,所以由真正处理方来决定删除最合理了。
selector.select()
一直返回大于0问题
在前面的例子的Server端代码中,当client发起连接然后发送消息后断开连接,服务端通过read方法获取数据并且执行selectionKeys.remove()
方法,但是在下一次while循环中selector.select()
结果还是大于0,debug代码可以发现此时selectionKey
任然是isReadable的,只是read方法返回并不大于0,所以不会进入第二个while循环中,虽然如此,这个时候是一直死循环在执行第一个while循环的,并不是我们预期的阻塞在selector.select()
上等待就绪事件的触发。
客户端关闭连接的时候,read会返回-1,所以我们可以在第二次循环的时候直接关闭Channel。
ByteBuffer的复用
可以看到在服务端read数据的时候使用ByteBuffer承接,获得的数据是不会超过ByteBuffer的长度,在使用完后就进行clear
操作切换到读模式进行复用,否则每次读事件都需要为ByteBuffer申请新的内存,非常浪费。当再次需要切换到写模式的时候使用flip
方法切换。
Selector.wakeup()
有意思的是Selector采用自己和自己建的TCP连接(Windows)或pipe(Linux)来实现wakeup,因为只要向这个连接或pipe里发点数据,就可以唤醒select了。
以下两个文章透析了这部分的知识:
SelectionKey.attach(Object ob)
SelectionKey关联一个对象,这个对象可以通过attachment方法取出,这点在前篇已经提过,这里再说明一下是要强调一遍通过这个方法,可以比较方便的围绕selectorKey来进行处理IO流程的拆分工作。比如,我们设计一个接口,所有的绑定对象都实现这个接口,接口中定义一个执行方法,在Accept的SelectorKey上绑定专门处理Accept时间的对象,在READ事件绑定专门处理的对象,如此代码可以是一致的都是从SelectorKey中拿到对象调用执行方法。
关于拆包粘包
前置说明一下这个概念,首先需要理解的是TCP是流式协议,传输内容像流水一样是没有明确段落的概念的,而实际使用时我们会把通信的消息进行定义,也就会把一段完整内容定义成有业务含义的消息,那么天然是需要解决这个矛盾的。这也是接触Netty时很快会了解解决拆包粘包问题的办法,这里只是提一下,了解是有这个问题的即可。
代码
在前面的代码基础上,进行了改造,实现一个C/S模型的文件传输功能,客户端将本地文件通过Channel传输给服务端,服务端将文件写到自己本地。
Client代码:
public static void start() throws IOException {
InetSocketAddress inetSocketAddress = new InetSocketAddress(20023);
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(inetSocketAddress);
ByteBuffer byteBuffer = ByteBuffer.allocate(204800);
FileChannel fileChannel = new FileInputStream("/Users/dongchao/test").getChannel();
if (socketChannel.finishConnect()) {
while (fileChannel.read(byteBuffer) > 0) {
byteBuffer.flip();
socketChannel.write(byteBuffer);
byteBuffer.clear();
}
}
fileChannel.close();
}
public static void main(String[] args) throws IOException {
start();
}
Server端代码:
public class Server {
public static void start() throws IOException {
// 开启Selector
Selector selector = Selector.open();
// 开启一个Server Socket Channel 用于监听连接Accept事件
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 设置成非阻塞模式
serverSocketChannel.configureBlocking(false);
// 监听端口
serverSocketChannel.bind(new InetSocketAddress(20023));
// 设置Selector 多路复用监听Accept事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
// 产生一个文件名
String localFile = getFileName();
// 获取一个File Channel
FileOutputStream fos = new FileOutputStream(localFile);
FileChannel outFileChannel = fos.getChannel();
// 记录文件字节量
long outLength = 0;
ByteBuffer byteBuffer = ByteBuffer.allocate(204800);
// 阻塞选择准备好进行I/O操作的键
while(selector.select() > 0) {
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
if (selectionKey.isAcceptable()) {
ServerSocketChannel serverSocketChannel1 = (ServerSocketChannel) selectionKey.channel();
SocketChannel socketChannel = serverSocketChannel1.accept();
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
} else if (selectionKey.isReadable()) {
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
int length;
// 对于Socket Channel来说是从Channel上读取数据,写入到ByteBuffer
while ((length = socketChannel.read(byteBuffer)) > 0) {
// 切换成读模式
byteBuffer.flip();
// 对于File Channel来说是读取ByteBuffer数据,写入到Channel
outFileChannel.write(byteBuffer);
outLength = outLength + length;
// 切换成写模式
byteBuffer.clear();
outFileChannel.force(true);
}
// read 返回-1 客户端关闭连接
if (length < 0) {
socketChannel.close();
}
}
iterator.remove();
}
System.out.print("file length : " + outLength);
}
fos.close();
outFileChannel.close();
}
private static String getFileName() {
long currentTimeMillis = System.currentTimeMillis();
return "/Users/dongchao/" + currentTimeMillis;
}