从事网络编程的应该都知道传输层的主要协议是TCP/UDP,关于两者的区别网络上有好多资料这里就不多说介绍,然而数据的传输过程大都有个IO操作,因此就衍生出了BIO,NIO,AIO三大模型,关于这三者的区别本系列博客有介绍,欢迎大家参考并指正,本篇主要写基于Java实现的NIO编程模型的一些使用细节,欢迎正在使用NIO编程的朋友们出来讨论,希望起到一个抛砖引玉的效果。
最近一直在看mina与netty的源代码,从中学习到了好多编程技巧与编程方式,于是花了点时间研究了NIO的Java层面的调用,本篇主要以代码为主:
首先我需要实现的是:
1:服务端启动监听socket机制
2:客户端发起与服务端建立连接,并注册写事件与读事件发送心跳包机制
3:服务端建立连接后注册读事件读取心跳包并注册写事件回应心跳包
下面先贴出服务端的代码:import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
public class NIOServer {
/*标识数字*/
private int flag = 0;
/*缓冲区大小*/
private int BLOCK = 4096;
/*接受数据缓冲区*/
private ByteBuffer sendbuffer = ByteBuffer.allocate(BLOCK);
/*发送数据缓冲区*/
private ByteBuffer receivebuffer = ByteBuffer.allocate(BLOCK);
private Selector selector;
private boolean connecFa=true;
public NIOServer() throws IOException{
this(8080);
}
public NIOServer(int port) throws IOException {
// 打开服务器套接字通道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 服务器配置为非阻塞
serverSocketChannel.configureBlocking(false);
// 检索与此通道关联的服务器套接字
ServerSocket serverSocket = serverSocketChannel.socket();
// 进行服务的绑定
serverSocket.bind(new InetSocketAddress(port));
// 通过open()方法找到Selector
selector = Selector.open();
// 注册到selector,等待连接
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("Server Start----:"+port);
}
// 监听
private void listen() throws IOException {
while (connecFa) {
// 选择一组键,并且相应的通道已经打开
selector.select();
// 返回此选择器的已选择键集。
Set selectionKeys = selector.selectedKeys();
Iterator iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
System.out.println("selectionKey.interestOps()\t"+selectionKey.interestOps());
//iterator.remove();
handleKey(selectionKey);
selectionKeys.clear();
}
}
}
// 处理请求
private void handleKey( SelectionKey selectionKey) throws IOException {
// 接受请求
ServerSocketChannel server = null;
SocketChannel client = null;
String receiveText;
String sendText;
int count=0;
// 测试此键的通道是否已准备好接受新的套接字连接。
if (selectionKey.isAcceptable()) {
// 返回为之创建此键的通道。
server = (ServerSocketChannel) selectionKey.channel();
// 接受到此通道套接字的连接。
// 此方法返回的套接字通道(如果有)将处于阻塞模式。
client = server.accept();
client.configureBlocking(false);
// 注册到selector,等待连接
client.register(selector, SelectionKey.OP_READ);
} else if (selectionKey.isReadable()) {
System.err.println("服务端开始读操作==="+selectionKey.isReadable());
System.err.println("selectionKey.p_w_upload()==="+selectionKey.p_w_upload());
// 返回为之创建此键的通道。
client = (SocketChannel) selectionKey.channel();
//将缓冲区清空以备下次读取
receivebuffer.clear();
//读取服务器发送来的数据到缓冲区中
count = client.read(receivebuffer);
if (count > 0) {
receiveText = new String( receivebuffer.array(),0,count);
System.out.println("服务器端接受客户端数据--:"+receiveText);
/*if(selectionKey.interestOps()==1){
selectionKey.interestOps(selectionKey.interestOps() | (SelectionKey.OP_WRITE));
}*/
client.register(selector, SelectionKey.OP_WRITE);
}
} else if (selectionKey.isWritable()&&selectionKey.isValid()) {
System.err.println("客户端开始写操作==="+selectionKey.isWritable());
System.err.println("selectionKey.p_w_upload()==="+selectionKey.p_w_upload());
//将缓冲区清空以备下次写入
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);
/* if(!"heart".equals(selectionKey.p_w_upload())){
selectionKey.interestOps(selectionKey.interestOps() & (~SelectionKey.OP_WRITE));
}*/
}
}
/**
* @throws IOException
* @param args
* @throws
*/
public static void main(String[] args) throws IOException {
// TODO Auto-generated method stub
int port = 8080;
NIOServer server = new NIOServer(port);
server.listen();
}
}
以下是客户端的代码:import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
public class NIOClient {
/*标识数字*/
private static int flag = 0;
/*缓冲区大小*/
private static int BLOCK = 4096;
/*接受数据缓冲区*/
private static ByteBuffer sendbuffer = ByteBuffer.allocate(BLOCK);
/*发送数据缓冲区*/
private static ByteBuffer receivebuffer = ByteBuffer.allocate(BLOCK);
/*服务器端地址*/
private final static InetSocketAddress SERVER_ADDRESS = new InetSocketAddress(
"localhost", 1111);
public static void main(String[] args) throws IOException {
// TODO Auto-generated method stub
int read=0;
int write=0;
// 打开socket通道
SocketChannel socketChannel = SocketChannel.open();
// 设置为非阻塞方式
socketChannel.configureBlocking(false);
// 打开选择器
Selector selector = Selector.open();
// 注册连接服务端socket动作
socketChannel.register(selector, SelectionKey.OP_CONNECT);
// 连接
socketChannel.connect(SERVER_ADDRESS);
// 分配缓冲区大小内存
Set selectionKeys;
Iterator iterator;
SelectionKey selectionKey;
SocketChannel client;
String receiveText;
String sendText;
int count=0;
while (true) {
//选择一组键,其相应的通道已为 I/O 操作准备就绪。
//此方法执行处于阻塞模式的选择操作。
selector.select();
//返回此选择器的已选择键集。
selectionKeys = selector.selectedKeys();
//System.out.println(selectionKeys.size());
iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
selectionKey = iterator.next();
System.out.println("selectionKey.interestOps()\t"+selectionKey.interestOps());
if (selectionKey.isConnectable()) {
System.err.println("selectionKey.isAcceptable()==="+selectionKey.isAcceptable());
System.err.println("selectionKey.isConnectable()==="+selectionKey.isConnectable());
client = (SocketChannel) selectionKey.channel();
client.configureBlocking(false);
// 判断此通道上是否正在进行连接操作。
// 完成套接字通道的连接过程。
if (client.isConnectionPending()) {
client.finishConnect();
System.out.println("完成连接!");
sendbuffer.clear();
sendbuffer.put("Hello,Server".getBytes());
sendbuffer.flip();
client.write(sendbuffer);
}
//heart可以理解为事件类型在读写的时候可以通过selectionKey.p_w_upload()获得注册的值
client.register(selector, SelectionKey.OP_READ|SelectionKey.OP_WRITE,"heart");
} else if (selectionKey.isReadable()) {
System.err.println("客户端开始读操作==="+selectionKey.isReadable());
client = (SocketChannel) selectionKey.channel();
//将缓冲区清空以备下次读取
receivebuffer.clear();
//读取服务器发送来的数据到缓冲区中
count=client.read(receivebuffer);
if(count>0){
receiveText = new String( receivebuffer.array(),0,count);
System.out.println("客户端接受服务器端数据--:"+receiveText);
}
} else if (selectionKey.isWritable()) {
System.err.println("客户端开始写操作==="+selectionKey.isWritable());
sendbuffer.clear();
client = (SocketChannel) selectionKey.channel();
sendText = "发送心跳包" + (flag++);
if("heart".equals(selectionKey.p_w_upload())){
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
sendText = "发送心跳包" + (flag++);
}
sendbuffer.put(sendText.getBytes());
//将缓冲区各标志复位,因为向里面put了数据标志被改变要想从中读取数据发向服务器,就要复位
sendbuffer.flip();
client.write(sendbuffer);
System.out.println("客户端向服务器端发送数据--:"+sendText);
}
iterator.remove();
}
}
}
}
1:运行服务端
2:运行客户端
发现服务端写事件死循环,在这里问题就来了,我这边只想每次收到心跳包才回应一个写事件而不是注册了写事件后就陷入死循环状态,分析问题应该是在写事件上,那是什么时候才能触发写事件呢?
总结如下:
写操作的就绪条件为底层缓冲区有空闲空间,而写缓冲区绝大部分时间都是有空闲空间的,所以当注册写事件后,写操作一直是就绪的,选择处理线程会占用整个CPU资源。所以,只有当确实有数据要写时再注册写操作,并在写完以后马上取消注册。
解决方法:思路是每次写完之后取消写事件的注册,在写玩之后增加如下代码:selectionKey.interestOps(selectionKey.interestOps() & (~SelectionKey.OP_WRITE));
当然读完之后要重新注册写事件,代码如下:if(selectionKey.interestOps()==1){
selectionKey.interestOps(selectionKey.interestOps() | (SelectionKey.OP_WRITE));
}
以上代码均在服务端实现的注释部分存在,通过这个实践彻底的了解了Java NIO基于事件驱动模型的编程思想与处理细节,不过这里有点想不通的是为什么对写事件在操作层面上没有一个好的解决方案,我想应该跟操作系统的底层实现有关系。