最近学习了一下Java NIO的开发,刚开始接触selector,觉得有点绕,弄的有点晕,所以在这里写几个简单的例子,记录一下,也与大家分享一下,对刚开始学习NIO的同学,希望有一些帮忙。大神就请多指点了。
开发稳定NIO对工程师的要求很高,NIO本身也存在很多的BUG,本文的例子只简单的帮助简单NIO的一些概念,对于一些例如TCP粘包/拆包等问题,不予以考虑。
对于NIO的一些概念什么的,就不在这里阐述了,给大家推荐几个文章,可以参考一下,里面有详细的解释。
Java NIO基本概念:
(1) http://ifeve.com/java-nio-all/
(2)http://blog.csdn.net/jeffleo/article/details/54695959?locationNum=6
Java NIO开发的注意事项:
(1)http://blog.csdn.net/martin_liang/article/details/41224503
import org.apache.log4j.Logger;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.text.SimpleDateFormat;
import java.util.Date;
public class SocketTimePrint {
private static final Logger logger = Logger.getLogger(SocketTimePrint.class);
public static void main(String[] args) throws Exception{
Thread server = new Thread(new ServerMsgNoselector());
Thread client = new Thread(new ClientMsg());
logger.info("Beginning to start the server for message");
server.start();
Thread.sleep(100);
logger.info("Beginning to start the client for message");
client.start();
}
}
class ClientMsg implements Runnable {
private static final Logger logger = Logger.getLogger(ClientMsg.class);
@Override
public void run() {
try {
SocketChannel socket = SocketChannel.open();
socket.connect(new InetSocketAddress("127.0.0.1", 9999));
socket.configureBlocking(false);
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
while (true) {
String datestr = format.format(new Date());
ByteBuffer buff = ByteBuffer.wrap(datestr.getBytes());
socket.write(buff);
Thread.sleep(2000);
}
} catch (IOException | InterruptedException ie) {
ie.printStackTrace();
}
}
}
class ServerMsgNoselector implements Runnable {
private static final Logger logger = Logger.getLogger(ServerMsgNoselector.class);
@Override
public void run() {
try {
ServerSocketChannel serverChannel = ServerSocketChannel.open();
ServerSocket socket = serverChannel.socket();
socket.bind(new InetSocketAddress(9999));
serverChannel.configureBlocking(true);
SocketChannel client = serverChannel.accept();
client.configureBlocking(true);
ByteBuffer buffer = ByteBuffer.allocate(1024);
while(true) {
int len = client.read(buffer);
if (len > 0 && buffer.hasArray()) {
String msg = new String(buffer.array(), 0, len);
logger.info("The recvicing message is " + msg);
} else {
logger.info("Waiting for message ...");
}
buffer.clear();
}
}catch(Exception ie) {
ie.printStackTrace();
}
}
}
例子很简单,分别在两个进程中启动一个client与一个server,通过9999端口进行通信。服务端的channel都设置为了阻塞模式。该例子中,只有一个客户端与一个server,而当在实际应用时,client会有很多连接,我可能也不只监听一个端口,这时就会出现各种问题(可能看一下《Netty权威指南》里面对IO的基本概念及Java IO的演进有一个大致的介绍),而selector就是为了解决这些问题,以达到更高的性能。
import org.apache.log4j.Logger;
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.text.SimpleDateFormat;
import java.util.Date;
import java.util.Iterator;
public class SocketSCMode {
private static final Logger logger = Logger.getLogger(SocketSCMode.class);
public static void main(String[] args) throws Exception{
SocketSCMode leo = new SocketSCMode();
leo.exe();
}
public void exe() throws InterruptedException{
Thread server = new Thread(new ServerMsg());
Thread client = new Thread(new ClientMsg());
logger.info("Beginning to start the server for message");
server.start();
Thread.sleep(100);
logger.info("Beginning to start the client for message");
client.start();
}
class ClientMsg implements Runnable {
@Override
public void run() {
try {
SocketChannel socket = SocketChannel.open();
socket.connect(new InetSocketAddress("127.0.0.1", 9999));
socket.configureBlocking(false);
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
while (true) {
String datestr = format.format(new Date());
ByteBuffer buff = ByteBuffer.wrap(datestr.getBytes());
//logger.info("Sending the message " + datestr);
socket.write(buff);
Thread.sleep(2000);
}
} catch (IOException | InterruptedException ie) {
ie.printStackTrace();
}
}
}
class ServerMsg implements Runnable {
@Override
public void run() {
logger.info("Enter the server thread");
try {
ServerSocketChannel serverChannel = ServerSocketChannel.open();
ServerSocket socket = serverChannel.socket();
socket.bind(new InetSocketAddress(9999));
serverChannel.configureBlocking(false);
Selector selector = Selector.open();
SelectionKey serverKey = serverChannel.register(selector, SelectionKey.OP_ACCEPT);
long flag = 1L;
while (true) {
int count = selector.select();
if (count > 0) {
Iterator iter = selector.selectedKeys().iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next();
logger.info("Flag-->" + flag + " The interest is " + key.interestOps());
if (key.isAcceptable()) {
logger.info("Flag-->" + flag + " The server accept one requesting");
iter.remove();
ServerSocketChannel server = (ServerSocketChannel) key.channel();
SocketChannel client = server.accept();
client.configureBlocking(false);
client.register(selector, SelectionKey.OP_READ);
} else if (key.isReadable()) {
iter.remove();
logger.info("Flag-->" + flag + " Reading the mesage");
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer buff = ByteBuffer.allocate(1024);
int len = client.read(buff);
if (len > 0 && buff.hasArray()) {
byte[] arr = buff.array();
String msg = new String(arr, 0, len);
logger.info("Flag-->" + flag + " The recevied message is " + msg);
}
}
}
}
flag++;
}
} catch (IOException ie) {
ie.printStackTrace();
}
}
}
}
逻辑上与第一个例子完全一样,要注意的是iter.remove()这个操作。
在该例子中,把if (key.isAcceptable())下的remove操作去掉,会报一个空指针异常。过程如下:
1、把ServerSocketChannel注册到selector上,启动client
2、第一次select,发现ServerSocketChannel是acceptable的,即有已选择的键(不然进程会阻塞在select方法上),因些,返回遍历SelectionKey的iter(已选择键的集合)
3、进入if (key.isAcceptable())下的代码块,通过accept返回一个SocketChannel与client进行通信,并把这个SocketChannel注册到selector上。遍历结束
4、此时server端接收了client的连接,client开发发送数据。server再执行select方法,发现SocketChannel是Readable,再次返回已选择键的集合
5、由于第一次遍历,ServerSocketChannel没有从已选择键的集合去掉,并且,selector不会更新已选择键的集合中键的read集合,因此,遍历iter时,会再次进入if (key.isAcceptable()),而此时,没有client进行连接请求,因些,accept方法返回的是一下null,也就出现了空指针异常。
如果我们去掉程序中的第二个remove会发生什么?进程会一直阻塞在select方法中,因为select方法是增量的,已在已选择键的集合中的channel,不会去再处理。也就是说,select方法,只有当已选择键的集合有新元素添加时才会返回(这么理解有点不准确。。。)
这个例子是参照别人的代码写的,当然,没人家写的好,修修补补的,总算能按大概的意思运行了,提供给大家做反面教材吧(-_-!!!)
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
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.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.text.SimpleDateFormat;
import java.util.*;
public class MultiplayerChat {
static {
Logger.getLogger(ServerMsgRoom.class).setLevel(Level.ERROR);
Logger.getLogger(ClientMsgRoom.class).setLevel(Level.INFO);
}
public static void main(String[] args) throws Exception{
Thread client1 = new Thread(new ClientMsgRoom(true), "leo");
Thread client2 = new Thread(new ClientMsgRoom(true), "Makukin");
Thread client3 = new Thread(new ClientMsgRoom(true), "Forrestleo");
Thread server = new Thread(new ServerMsgRoom(), "ServerRoom");
System.out.println("Start server ...");
server.start();
Thread.sleep(100);
System.out.println("Start client");
client1.start();
client2.start();
client3.start();
server.join();
}
public static boolean canReadable(SocketChannel channel) {
return (channel.validOps() & SelectionKey.OP_READ) == SelectionKey.OP_READ;
}
public static boolean canWriteable(SocketChannel channel) {
return (channel.validOps() & SelectionKey.OP_WRITE) == SelectionKey.OP_WRITE;
}
}
class ClientMsgRoom implements Runnable{
private static final Logger logger = Logger.getLogger(ClientMsgRoom.class);
private static final SimpleDateFormat format = new SimpleDateFormat("HH:mm:ss");
private String name;
private String HOST;
private int PORT;
private ByteBuffer buff;
private boolean pringFlag;
public ClientMsgRoom(boolean flag) {
HOST = "127.0.0.1";
PORT = 9999;
buff = ByteBuffer.allocate(1024);
pringFlag = flag;
}
@Override
public void run() {
name = Thread.currentThread().getName();
try {
SocketChannel client = SocketChannel.open();
client.connect(new InetSocketAddress(HOST, PORT));
client.configureBlocking(false);
String headMsg = "Register : " + name + "\n";
logger.info(headMsg);
buff.put(headMsg.getBytes());
buff.flip();
while(buff.hasRemaining()) {
client.write(buff);
}
logger.info("Register the client ... ");
while(true) {
boolean flag = false;
if(MultiplayerChat.canReadable(client)){
int len = 0;
buff.clear();
while((len = client.read(buff)) > 0) {
String msg = new String(buff.array(), 0, len);
if(pringFlag)
System.out.println(name + "-->\n" + msg);
flag = true;
}
}
if(flag && MultiplayerChat.canWriteable(client)) {
String reMsg = name + "'s time is " + format.format(new Date()) + "\n";
//logger.info("Sending message " + reMsg);
buff.clear();
buff.put(reMsg.getBytes());
buff.flip();
while(buff.hasRemaining()) {
client.write(buff);
}
}
Thread.sleep(5000);
}
}catch(IOException | InterruptedException ie) {
ie.printStackTrace();
}
}
}
class ServerMsgRoom implements Runnable {
private static final Logger logger = Logger.getLogger(ServerMsgRoom.class);
private int PORT;
private ByteBuffer buff;
private Vector chaters;
public ServerMsgRoom() {
PORT = 9999;
buff = ByteBuffer.allocate(1024);
chaters = new Vector<>();
}
@Override
public void run() {
try {
ServerSocketChannel server = ServerSocketChannel.open();
server.configureBlocking(false);
server.socket().bind(new InetSocketAddress(PORT));
Selector selector = Selector.open();
server.register(selector, SelectionKey.OP_ACCEPT);
while(true) {
int count = selector.select();
if(count <= 0)
continue;
logger.info("Begin to ack the keys and iter's size is " + count);
Iterator iter = selector.selectedKeys().iterator();
while(iter.hasNext()) {
SelectionKey key = iter.next();
if(key.isAcceptable()) {
ServerSocketChannel serverChannel = (ServerSocketChannel)key.channel();
SocketChannel socket = serverChannel.accept();
logger.info("Add one register");
socket.configureBlocking(false);
SelectionKey rkey = socket.register(selector, SelectionKey.OP_READ);
chaters.add(rkey);
}else if(key.isValid() && key.isReadable()) {
SocketChannel clientChannel = (SocketChannel)key.channel();
int len = 0;
StringBuilder builder = new StringBuilder();
buff.clear();
while((len = clientChannel.read(buff)) > 0) {
String msg = new String(buff.array(), 0 , len);
builder.append(msg);
}
logger.info("PrintMessage " + builder.toString());
Iterator iterWriter = chaters.iterator();
while(iterWriter.hasNext()) {
SelectionKey keyWriter = iterWriter.next();
List list = (List) keyWriter.attachment();
if (list == null) {
list = new ArrayList<>();
keyWriter.attach(list);
}
list.add(builder.toString());
}
key.interestOps(key.interestOps() | SelectionKey.OP_WRITE);
}else if(key.isValid() && key.isWritable()) {
List list = (List) key.attachment();
SocketChannel writer = (SocketChannel) key.channel();
if(list != null) {
logger.info("The attaching list is " + list.toString());
Iterator iterReader = list.iterator();
while(iterReader.hasNext()) {
String message = iterReader.next();
buff.clear();
buff.put(message.getBytes());
buff.flip();
while(buff.hasRemaining()) {
writer.write(buff);
}
logger.info("Writing message is " + message);
iterReader.remove();
}
}else{
logger.warn("The list is null");
}
key.interestOps(SelectionKey.OP_READ);
}
iter.remove();
}
}
}catch(IOException ie) {
ie.printStackTrace();
}
}
}