概述
套接字的某些操作可能会无限期地阻塞,如accept()方法的调用可能会因为等待一个客户端连接而阻塞,read()方法的调用可能会因为没有数据可读而阻塞。
NIO的channel抽象的一个重要特征就是可以通过配置它的阻塞行为,以实现非阻塞式的信道。
在非阻塞式的信道上调用一个方法总是会立即返回。这种调用的返回值指示了所请求的操作完成的程度。例如,在一个非阻塞式的ServerSocketChannel上调用accept()方法,如果有连接请求来了,则返回客户端SocketChannel,否则返回null。
这里举一个Socket通信的例子,客户端使用NIO实现,服务端先使用BIO实现。
客户端实现
public class NIOSocketClient {
public static void main(String[] args) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
SocketChannel socketChannel = null;
try {
socketChannel = SocketChannel.open();
//配置为非阻塞
socketChannel.configureBlocking(false);
socketChannel.connect(new InetSocketAddress("127.0.0.1",8080));
if(socketChannel.finishConnect()){
int i = 0;
while(true){
TimeUnit.SECONDS.sleep(1);
String msg = ++i + "times msg from client";
//重置缓冲区
buffer.clear();
//对缓冲区设值
buffer.put(msg.getBytes());
//准备读取缓冲区
buffer.flip();
//循环写入缓冲区数据到通道中 可能一次写不完 所以在循环中处理
while (buffer.hasRemaining()){
System.out.println(buffer);
//从缓冲区写入数据到通道中
socketChannel.write(buffer);
}
}
}
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
finally {
try {
if (socketChannel != null) {
socketChannel.close();
}
}catch (IOException e){
e.printStackTrace();
}
}
}
}
服务端的BIO实现,该实现同时只能处理一个客户端连接
public class BIOSocketServer {
public static void main(String[] args) {
ServerSocket serverSocket = null;
InputStream in = null;
try {
serverSocket = new ServerSocket(8080);
int recvMsgSize = 0;
byte[] recvBuf = new byte[1024];
while(true){
Socket clntSocket = serverSocket.accept(); //该方法阻塞 直到有连接进来为止
SocketAddress clientAddr = clntSocket.getRemoteSocketAddress();
System.out.println("a client is linked," + clientAddr);
in = clntSocket.getInputStream();
//read方法将阻塞 直到有数据为止
while ((recvMsgSize = in.read(recvBuf)) != -1){
byte[] temp = new byte[recvMsgSize];
System.arraycopy(recvBuf,0,temp,0,recvMsgSize);
System.out.println(new String(temp));
}
}
} catch (IOException e){
}
finally {
}
}
}
服务端的NIO实现。该实现中使用了Selector选择器,该选择器监控多个事件,当有一个事件就绪时,会通知程序,并指出是哪个信道哪个事件。在linux下的底层实现是I/O多路复用的epoll模型。
public class NIOSocketServer {
private static final int BUF_SIZE = 1024; //buffer长度
private static final int PORT = 8080; //监听端口
private static final int TIMEOUT = 3000; //select()方法超时时长
public static void main(String[] args) {
selector();
}
public static void selector(){
Selector selector = null;
ServerSocketChannel ssc = null;
try{
//创建Selector
selector = Selector.open();
//打开ServerSocketChannel
ssc = ServerSocketChannel.open();
ssc.socket().bind(new InetSocketAddress(PORT));
//Channel和Selector组合使用 必须把Channel设置为非阻塞模式
ssc.configureBlocking(false);
//注册Channel到Selector上
ssc.register(selector, SelectionKey.OP_ACCEPT);
while (true){
//select里面加入一个超时时间
//防止程序卡死的假象
if(selector.select(TIMEOUT) == 0){
System.out.println("===");
continue;
}
//有事件发生了 遍历事件
Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
while (iter.hasNext()){
SelectionKey key = iter.next();
if(key.isAcceptable()){
//处理客户端连接
handleAccpet(key);
}
if(key.isReadable()){
//处理读操作
handleRead(key);
}
if(key.isWritable() && key.isValid()){
//处理写操作
handleWrite(key);
}
if(key.isConnectable()){
System.out.println("连接成功...");
}
//处理完后 将当前事件移除 避免重复处理
iter.remove();
}
}
}catch (Exception e){
e.printStackTrace();
} finally {
try{
if(selector != null){
selector.close();
}
if(ssc != null){
//关闭ServerSocketChannel
ssc.close();
}
}catch (IOException e){
e.printStackTrace();
}
}
}
public static void handleAccpet(SelectionKey key) throws IOException {
ServerSocketChannel ssChannel = (ServerSocketChannel) key.channel();
//这里会马上返回 因为目前已经有一个连接事件已经准备就绪
SocketChannel sc = ssChannel.accept();
sc.configureBlocking(false);
sc.register(key.selector(),SelectionKey.OP_READ, ByteBuffer.allocateDirect(BUF_SIZE));
}
public static void handleRead(SelectionKey key) throws IOException {
SocketChannel sc = (SocketChannel)key.channel();
ByteBuffer buffer = (ByteBuffer)key.attachment();
long bytesRead = sc.read(buffer);
while(bytesRead > 0){
//准备读缓冲区
buffer.flip();
while (buffer.hasRemaining()){
//读取一个字节
System.out.print((char)buffer.get());
}
System.out.println();
//重置缓冲区
buffer.clear();
bytesRead = sc.read(buffer);
}
if(bytesRead == -1){
sc.close();
}
}
public static void handleWrite(SelectionKey key) throws IOException {
ByteBuffer buffer = (ByteBuffer)key.attachment();
buffer.flip();
SocketChannel sc = (SocketChannel)key.channel();
while (buffer.hasRemaining()){
sc.write(buffer);
}
buffer.compact();
}
}
程序说明
在selector方法中,我们只对accept事件感兴趣
当accept事件就绪时,表示有一个连接进来了,在handleAccpet方法中注册read事件的监控
当read事件就绪时,表示客户端有数据过来了,在handleRead方法中处理读取数据操作
上面的程序中并没有注册write事件,所以Selector中不会有write事件就绪
Selector说明
创建Selector
selector = Selector.open();
为了将Channel和Selector配合使用,必须将Channel注册到Selector上,通过SelectableChannel类中的register()方法来实现。
//打开ServerSocketChannel
ssc = ServerSocketChannel.open();
ssc.socket().bind(new InetSocketAddress(PORT));
//Channel和Selector组合使用 必须把Channel设置为非阻塞模式
ssc.configureBlocking(false);
//注册Channel到Selector上
ssc.register(selector, SelectionKey.OP_ACCEPT);
与Selector一起使用时,Channel必须处于非阻塞模式下。
这意味着不能将FileChannel与Selector一起使用,因为FileChannel不能切换到非阻塞模式,而套接字通道都可以。
register()方法的第二个参数是一个interest集合,有以下四种选择:
- SelectionKey.OP_ACCEPT
- SelectionKey.OP_CONNECT
- SelectionKey.OP_WRITE
- SelectionKey.OP_READ
对应下面4个事件
- Accept
- Connect
- Write
- Read
SelectionKey
当向Selector注册Channel时,register()方法会返回一个SelectionKey对象。这个对象包含下面一些属性:
-
interest集合
在register()方法中,interest集合是你所选择的感兴趣的事件集合。可以通过SelectionKey读写interest集合
-
ready集合
ready 集合是通道已经准备就绪的操作的集合。在一次选择(Selection)之后,你会首先访问这个ready set。可以这样访问ready集合
int readySet = selectionKey.readyOps();
-
Channel
访问Channel
Channel channel = selectionKey.channel();
-
Selector
访问Selector
Selector selector = selectionKey.selector();
-
附加的对象(可选)
可以将一个对象或者更多信息附加在SelectionKey上,这样就能方便的识别某个给定的通道。
selectionKey.attach(theObject); Object attachedObj = selectionKey.attachment();
或者在register方法中附加对象
SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);
通过Selector选择通道
一旦向Selector注册了一个或多个通道,就可以调用几个重载的select()方法。
这些方法返回你所感兴趣的事件,如连接,接受,读,写 这些已经准备就绪的通道。
select()方法有以下几个重载:
- int select()
- int select(long timeout)
- int selectNow()
select()阻塞到至少有一个通道在你注册的事件上就绪为止
select(long timeout)阻塞到一个注册事件就绪或者超时为止
selectNow()不会阻塞,如果没有事件就绪,直接返回0
返回int值表示有多少通道已经就绪
如果返回值大于0,则表示至少有一个事件准备就绪,可以通过下列语句查看就绪的状态
Set selectedKeys = selector.selectedKeys();
当处理完成一个事件时,必须从就绪集合中移除该事件。
最后
以上就是痴情短靴为你收集整理的JavaNIO(4)、SocketChannel的全部内容,希望文章能够帮你解决JavaNIO(4)、SocketChannel所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复