前几天我给大家分享了一些JAVA网络编程的知识,从基本的JAVA网络知识开始,我们学习了结合多线程管理的Socket编程,掌握了关于系统内核中的RecBuffer和SendBuffer的概念模型。然后我们开始了NIO编程模型的最佳搭档,Channel和Buffer,以及Channel的读写操作和Buffer的get、put的关系。因为我们有了前面知识的铺垫,所以今天我打算给大家分享一个不一样的网络编程模型,使用Selector实现多路复用网络编程。(实际上是JAVA NIO系列第五篇文章)
今天分享目录如下:
1. TCP上的Channel有哪些
2. TCP上Channel的读写操作
3. 使用Selector实现TCP多路复用网络编程
(还是老话,如果对Selector的op_read、op_write很熟悉,就不要往下看了,去看对你更有帮助的文章)
1. TCP上的Channel
1.1 Server Socket上的Channel:ServerSocketChannel
class ServerSocketChannel{
static ServerSocketChannel open();
ServerSocket socket();
SocketChannel accept();
}
从上面的API中我们可以看出,ServerSocketChannel是使用open这个静态方法中获得的。并且我们可以中该Channel中获得ServerSocket对象。
ServerSocketChannel channel = ServerSocketChannel.open();
ServerSocket serverSocket = channel.socket();
这里需要注意:
上面代码ServerSocket没有绑定地址,需要绑定之后才可以使用,例如使用serverSocket.bind(port)
另外我们要使用ServerSocketChannel的accept来获得SocketChannel,而不是使用ServerSocket的accept获得Socket。
如果serverSocketChannel是non-blocking的,那么accept可能返回null,因为没有连接进来。
使用accept获得的channel不是null,那么该channel默认是Blocking模式,需要手动指定成为Non-Blocking
SelectableChannel configureBlocking(boolean block);
1.2 客户端连接Socket的Channel:SocketChannel
class SocketChannel{
static SocketChannel open();
static SocketChannel open(SocketAddress address);
Socket socket();
}
从上面的API看到,大概和ServerSocketChannel类似,从Channel中获得的socket仍然没有连接到服务,需要使用带有参数的open方法才能打开连接。同样这里获得的channel默认是Blocking模式,需要手动指定成为Non-Blocking。
例如:我们指定ip和端口
SocketChannel channel = SocketChannel.open(new InetSocketAddress(ip,port));
2. Channel的操作
注意channel的Blocking模式,可以使用API设置
SelectableChannel configureBlocking(boolean block);
Channel的Read操作
在阻塞式编程中,TCP Channel的读操作会阻塞,一直等到有数据可用。而在非阻塞编程中,读操作不会被阻塞,如果recBuffer中有数据,则read返回数据,如果recBuffer空,就不会有数据返回。这里需要注意的地方是非阻塞编程读过程中,如果对方关闭了连接,或者本地关闭了read操作,会有异常抛出,否则read会立刻返回所读数据数,尽管数据可能是0。
代码示例稍后。
Channel的Write操作
在阻塞编程中,和读一样,写操作一直等待sendBuffer中有数据。在非阻塞编程中,写不会被阻塞。这里和读是一样的,如果没有异常抛出的话,这里的写操作会立即返回写的数据数,这里可以是0。什么时候抛出异常?连接被对端关闭了,或者write操作被关闭都会抛异常的。
代码示例稍后。
3. 多路复用网络编程模型
终于到了今天重点的地方,希望读者慢慢看,吸收里面的知识点。
除了上面的读、写操作之外,在多路复用编程中还有两个准备就绪类的操作,一个是准备好了Accept,另一个是准备连接已就绪。完整的操作是:
OP_ACCEPT:只有ServerSocketChannel才可以有这个操作,如果有连接进来,这里就会有返回。
OP_CONNECT:SocketChannel在连接过程中的操作,已经出于连接完成的状态,下一步是finishConnect。
OP_READ:只有连接完成的SocketChannel才有读操作。返回数据见上面,除非有异常,否则就会有数据个数返回,即使数据返回0.
OP_WRITE:只有连接完成的SocketChannel才有写操作。除非有异常,否则返回写出的数据数,或0.
稍后在深入解释这4个操作。我们先看代码,混个面熟先。
如果下面代码你会了,那么JAVA的网络编程你就掌握了。
代码亮点,关键点:
非阻塞配置。(nonblocking)
Selector.select方法的使用。(timeout)
使用key的attachement管理每个连接的ByteBuffer。(attach)
好了,我们回到上面的4个操作上,op_accept,connect,read,wirte。实际上,上面代码中select()操作,返回的只有两种事件,可读的事件和可写的事件两种。
可读事件:有数据在recBuffer中或者accept方法有可用连接进来了,换句话说,OP_READ和OP_ACCETP是一个事件。
可写事件:sendBuffer中有数据了,或者客户端连接完成了。换句话说,OP_CONNECT 和OP_WRITE是一回事。
我们定义accept和read事件在一起为可读事件没有什么问题,因accept在server端可以时,read还不可用,所有监听是分开的,不会冲突。但是connect事件和write事件为可写事件不能同时监听,因为在client socket中,这俩可以是同时存在的,会出现歧义。因为在一个准备就绪的Socket中,op_write事件几乎是一直可用的,所有除非有数据明确了,需要写出了,我们才可以在socket中把write事件添加到select流程中,否则NIO调度线程总是选择报告可写事件,CPU压力很大,因此我们才规定,除非有数据可以写,我们不要订阅可写事件。
总结:
今天只是网络编程基础知识的分享,所以并没有结合实际业务需求,明天我打算结合实际业务需求,实现一个断点续传的Socket编程模式。
感谢你的阅读,希望本文的分享能给你带来提升,有问题在下方评论中讨论,我会及时反馈。
閱讀更多 吳濤分享 的文章