本文主要通过一个非常简单的Server实例总结一下基于Java非阻塞IO的网络编程。
希望对大家有所帮助,欢迎拍砖!
一、Server端代码:
该示例主要实现一个简单功能,server端直接打印客户端发送的数据。
由于例子非常简单,一个线程循环处理所有任务,算是一个最简单的NIO Server吧(实际应用开发中是不会采用这种方式的)。
MyNIOServer.java:
public class MyNIOServer {
private static final int TIMEOUT = 30000;
private static final int BUFSIZE = 10;
private Selector selector;
public MyNIOServer(int port) throws Exception{
System.out.println("server start on port:"+port);
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
ServerSocket serverSocket = serverSocketChannel.socket();
serverSocket.bind(new InetSocketAddress(port));
selector = Selector.open();
serverSocketChannel.register(selector,SelectionKey.OP_ACCEPT);
}
public void listen() throws Exception{
System.out.println("server listen!");
MyNIOServerHandler handler = new MyNIOServerHandler(BUFSIZE);
while(true){
System.out.println("listen while!");
if (selector.select(TIMEOUT) == 0) {
System.out.print(".");
continue;
}
Iterator<SelectionKey> keyIter = selector.selectedKeys().iterator();
while(keyIter.hasNext()){
SelectionKey key = keyIter.next();
if(key.isAcceptable()){
System.out.println("isAcceptable!");
handler.handleAccept(key);
}else if(key.isReadable()){
System.out.println("isReadable!");
handler.handleRead(key);
}else if(key.isValid() && key.isWritable()){
System.out.println("isWritable!");
handler.handleWrite(key);
}
keyIter.remove();
}
}
}
public static void main(String[] args){
try{
MyNIOServer server=new MyNIOServer(9009);
server.listen();
}catch(Exception e){
e.printStackTrace();
}
}
}
处理器MyNIOServerHandler.java:
public class MyNIOServerHandler {
private int BUFSIZE;
public MyNIOServerHandler(int bufferSize){
this.BUFSIZE = bufferSize;
}
public void handleAccept(SelectionKey key) throws IOException{
System.out.println("handleAccept...");
ServerSocketChannel serverChannel = (ServerSocketChannel)key.channel();
SocketChannel clientChannel = serverChannel.accept();
clientChannel.configureBlocking(false);
Selector selector = key.selector();
clientChannel.register(selector, SelectionKey.OP_READ,ByteBuffer.allocate(BUFSIZE));
}
public void handleRead(SelectionKey key) throws IOException{
System.out.println("handleRead...");
SocketChannel clientChannel = (SocketChannel)key.channel();
ByteBuffer buf = (ByteBuffer) key.attachment();
long bytesRead = clientChannel.read(buf);
System.out.println("bytesRead:"+bytesRead);
if(bytesRead==-1){
System.out.println("clientChannel close!");
clientChannel.close();
}else if (bytesRead > 0) {
String receiveText = new String(buf.array(),0,new Long(bytesRead).intValue());
System.out.println("服务器端接受客户端数据--:"+receiveText);
key.interestOps( SelectionKey.OP_WRITE);
}
}
public void handleWrite(SelectionKey key) throws IOException{
System.out.println("handleWrite...");
ByteBuffer buf = (ByteBuffer) key.attachment();
buf.flip();
SocketChannel clntChan = (SocketChannel) key.channel();
clntChan.write(buf);
if (!buf.hasRemaining()) {
key.interestOps(SelectionKey.OP_READ);
}
buf.compact();
}
}
代码注解:
服务器端创建一个选择器,将其与每个侦听客户端连接的套接字所对应的ServerSocketChannel注册在一起,然后反复循环,调用select()方法,并调用相应的操作器对各种类型的IO操作进行处理。
1、创建一个Selector选择器。
2、创建ServerSocketChannel实例,获得底层的ServerSocket,并以端口号作为参数绑定bind()。
3、设置信道为非阻塞模式,只有非阻塞信道才可以注册选择器。
4、为信道注册选择器,指出该信道可以进行accept操作。
5、反复轮询,等待IO,select()方法将阻塞等待,直到有准备好IO操作的信道,或者直到超时。
6、调用selectedKeys()方法,返回一个Set实例,并从中获取一个Iterator。该集合中包含了每个准备好某一IO操作的信道的SelectionKey(注册时创建)。
7、对于每个键,检查是否准备好accept()操作,是否可读或可写。
8、select()操作只是向selector所关联的键集合中添加元素。因此,不移除处理过的键,下次调用select时仍保留在集合中。
9、channel方法返回注册时用来创建键的channel,即ServerSocketChannel,这是我们注册的唯一一种支持accept操作的信道。accept为传入的连接返回一个SocketChannel实例。
10、可以通过SelectionKey方法获取相应的Selector,当SocketChannel信道准备好读数据的IO操作时,可以通过选出的键集对其进行访问。
12、handleRead,根据其支持数据读取操作可知,这是一个SocketChannel。
13、如果read方法返回-1,则表示底层连接已经关闭,此时需要关闭信道。关闭信道时,将从选择器的各种集合中移除与该信道关联的键。读取完数据将信道标记为可写。
14、handleWrite,如果缓冲区之前接收的数据已经没有剩余,则修改键关联的操作集,指示其只能进行读操作。
二、Client端代码:
MyNIOClient.java
String server = "localhost";
byte[] datas = "1234567890abcdef".getBytes();
int servPort = 9009;
SocketChannel clntChan = SocketChannel.open();
clntChan.configureBlocking(false);
if (!clntChan.connect(new InetSocketAddress(server, servPort))){
while (!clntChan.finishConnect()){
System.out.print(".");
}
}
ByteBuffer writeBuf = ByteBuffer.wrap(datas);
ByteBuffer readBuf = ByteBuffer.allocate(datas.length);
int totalBytesRcvd = 0;
int bytesRcvd;
while(totalBytesRcvd < datas.length){
if(writeBuf.hasRemaining()){
clntChan.write(writeBuf);
}
if ((bytesRcvd = clntChan.read(readBuf)) == -1){
throw new SocketException("Connection closed prematurely");
}
totalBytesRcvd += bytesRcvd;
System.out.print(".");
}
System.out.println("Received:"+new String(readBuf.array(),0,totalBytesRcvd));
clntChan.close();
代码注解:
1、该套接字是非阻塞式的,因此对connect方法的调用可能会在连接建立之前返回。如果在返回前已经成功建立了连接,返回true,否则返回false。返回false时,任何发送或接受数据都将抛出异常。因此通过调用finishConnect方法轮询连接状态。不过这种忙等非常浪费系统资源,此处只是举例。
2、分别采用包装byte数组和allocate方法创建要用来读写数据的bytebuffer实例。
3、反复循环直到发送和接收完所有字节,只要输出缓冲区还留有数据,就调动write方法,对read方法的调用不会阻塞等待,但当没有数据可读时返回0.
4、打印接收到的数据,然后在信道完成其任务后也需要关闭。