最近简单研究了一下JAVA的NIO,话说出来这么久了,感觉这一块一直是空白,用的也比较少。但是感觉socket io这部分在以后工作中的高并发项目可能会用到,所以最近抽空研究了解了一下。
1、阻塞IO
阻塞IO比较简单,就是用普通的socket去写,因为没有什么太复杂的处理。建立一个socket,然后,获取它的inputstream和outputstream,然后进行读写操作。
Server端主要代码,handleSocket就是从socket里面读取数据,然后向client写数据:
ServerSocket serverSocket = new ServerSocket(port);
while (true) {
socket = serverSocket.accept();
handleSocket(socket);
}
Client只是简单的发送一句消息:
socket = new Socket(host, port);
out = socket.getOutputStream();
out.write(data);
out.flush();
in = socket.getInputStream();
byte[] buffer = new byte[128];
int receivedBytes;
if((receivedBytes = in.read(buffer))!=-1){
System.out.println("Client: received msg from server: " + new String(buffer, 0, receivedBytes));
}
socket = new Socket(host, port);
out = socket.getOutputStream();
out.write(data);
out.flush();
in = socket.getInputStream();
byte[] buffer = new byte[128];
int receivedBytes;
if((receivedBytes = in.read(buffer))!=-1){
System.out.println("Client: received msg from server: " + new String(buffer, 0, receivedBytes));
}
同时client5000个,使用了大概5s。然后还可以使用多线程Server,就是在每一个client到的时候分配一个线程来处理这个IO,理论上可以增加效率,但是似乎是因为每次处理时间太短,效果不明显。主要代码:
boolean flag = false;
int count = 0;
ServerSocket serverSocket = new ServerSocket(port);
Date start = null;
while (true) {
socket = serverSocket.accept();
if (!flag) {
start = new Date();
flag = true;
}
pool.execute(new RequestHandler(socket));
if(++count== threadCount){
flag = false;
Date end = new Date();
System.out.println(threadCount+" client requests spends: " + (end.getTime() -start.getTime()));
}
}
其中的pool是ExecutorService,提供线程池,然后RequestHandler是一个Runnable类,用来处理一个socket连接。简单讲来,就是把前面server处理socket的代码放到了这个handler里面。
2.非阻塞方式的Socket编程
传统阻塞方式的Socket编程,在读取或者写入数据时,TCP程序会阻塞直到客户端和服务端成功连接,UDP程序会阻塞直到读取到数据或写入数据。阻塞方式会影响程序性能,JDK5之后的NIO引入了非阻塞方式的Socket编程,非阻塞方式的Socket编程主要是使用Socket通道和Selector通道选择器,将Socket通道注册到通道选择器上,通过通道选择器选择通道已经准备好的事件的进行相应操作。
NIO socket简单例子如下:
import java.net.*;
import java.nio.channels.*;
import java.util.*;
import java.io.*;
public class NIOSocket{
private static final int CLINET_PORT = 10200;
private static final int SEVER_PORT = 10201;
//面向流的连接套接字的可选择通道
private SocketChannel ch;
//通道选择器
private Selector sel;
public static void main(String[] args) throws IOException{
//打开套接字通道
ch = SocketChannel.open();
//打开一个选择器
sel = Selector.open();
try{
//获取与套接字通道关联的套接字,并将该套接字绑定到本机指定端口
ch.socket().bind(new InetSocketAddress(CLINET_PORT));
//调整此通道为非阻塞模式
ch.configureBlocking(false);
//为通道选择器注册通道,并指定操作的选择键集
ch.register(sel, SelectionKey.OP_READ | SelectionKey.OP_WRITE |
SelectionKey.OP_CONNECT);
//选择通道上注册的事件,其相应通道已为I/O操作准备就绪
sel.select();
//返回选择器的已选择键集
Iterator it = sel.selectedKeys().iterator();
while(it.hasNext()){
//获取通道的选择器的键
SelectionKey key = (SelectionKey)it.next();
it.remove();
//如果该通道已经准备好套接字连接
if(key.isConnectable()){
InetAddress addr = InetAddress.getLocalHost();
System.out.println(“Connect will not block”);
//调用此方法发起一个非阻塞的连接操作,如果立即建立连接,则此方法//返回true.否则返回false,且必须在以后使用finishConnect()完成连接操作
//此处建立和服务端的Socket连接
if(!ch.connect(new InetSocketAddress(addr, SEVER_PORT))){
//完成非立即连接操作
ch.finishConnect();
}
}
//此通道已准备好进行读取
if(key.isReadable()){
System.out.println(“Read will not block”);
}
//此通道已准备好进行写入
if(key.isWritable()){
System.out.println(“Write will not block”);
}
}
} finally{
ch.close();
sel.close();
}
}
}
NIO Socket编程中有一个主要的类Selector,这个类似一个观察者,只要我们把需要探知的套接字通道socketchannel注册到Selector,程序不用阻塞等待,可以并行做别的事情,当有事件发生时,Selector会通知程序,传回一组SelectionKey,程序读取这些Key,就会获得注册过的socketchannel,然后,从这个Channel中读取和处理数据。
Selector内部原理实际是在做一个对所注册的channel的轮询访问,不断的轮询(目前就这一个算法),一旦轮询到一个channel有所注册的事情发生,比如数据来了,他就会站起来报告,交出一把钥匙,让我们通过这把钥匙来读取这个channel的内容。
2.使用NIO非阻塞方式Socket实现服务端和客户端程序:
通过下面一个简单的客户端/服务端程序说明一下NIO socket的基本API和步骤。
(1).服务端程序:
import java.net.*;
import java.util.*;
import java.io.*;
import java.nio.*;
import java.nio.channels.*;
import java.nio.charset.*;
public class NIOSocketServer{
public static final int PORT = 8080;
public static void main(String[] args)throws IOException{
//NIO的通道channel中内容读取到字节缓冲区ByteBuffer时是字节方式存储的,
//对于以字符方式读取和处理的数据必须要进行字符集编码和解码
String encoding = System.getProperty(“file.encoding”);
//加载字节编码集
Charset cs = Charset.forName(encoding);
//分配两个字节大小的字节缓冲区
ByteBuffer buffer = ByteBuffer.allocate(16);
SocketChannel ch = null;
//打开服务端的套接字通道
ServerSocketChannel ssc = ServerSocketChannel.open();
//打开通道选择器
Selector sel = Selector.open();
try{
//将服务端套接字通道连接方式调整为非阻塞模式
ssc.configureBlocking(false);
//将服务端套接字通道绑定到本机服务端端口
ssc.socket().bind(new InetSocketAddress(PORT));
//将服务端套接字通道OP_ACCEP事件注册到通道选择器上
SelectionKey key = ssc.register(sel, SelectionKey.OP_ACCEPT);
System.out.println(“Server on port:” + PORT);
while(true){
//通道选择器开始轮询通道事件
sel.select();
Iterator it = sel.selectedKeys().iterator();
While(it.hasNext()){
//获取通道选择器事件键
SelectionKey skey = (SelectionKey)it.next();
it.remove();
//服务端套接字通道发送客户端连接事件,客户端套接字通道尚未连接
if(skey.isAcceptable()){
//获取服务端套接字通道上连接的客户端套接字通道
ch = ssc.accept();
System.out.println(“Accepted connection from:” + ch.socket());
//将客户端套接字通过连接模式调整为非阻塞模式
ch.configureBlocking(false);
//将客户端套接字通道OP_READ事件注册到通道选择器上
ch.register(sel, SelectionKey.OP_READ);
}
//客户端套接字通道已经连接
else{
//获取创建此通道选择器事件键的套接字通道
ch = (SocketChannel)skey.channel();
//将客户端套接字通道数据读取到字节缓冲区中
ch.read(buffer);
//使用字符集解码字节缓冲区数据
CharBuffer cb = cs.decode((ByteBuffer)buffer.flip());
String response = cb.toString();
System.out.println(“Echoing:” + response) ;
//重绕字节缓冲区,继续读取客户端套接字通道数据
ch.write((ByteBuffer)buffer.rewind());
if(response.indexOf(“END”) != -1) ch.close();
buffer.clear();
}
}
}
}finally{
if(ch != null) ch.close();
ssc.close();
sel.close();
}
}
}
(2).客户端程序:
import java.net.*;
import java.util.*;
import java.io.*;
import java.nio.*;
import java.nio.channels.*;
import java.nio.charset.*;
public class NIOSocketClient{
private static final int CLIENT_PORT = 10200;
public static void main(String[] args) throws IOException{
SocketChannel sc = SocketChannel.open();
Selector sel = Selector.open();
try{
sc.configureBlocking(false);
sc.socket.bind(new InetSocketAddress(CLIENT_PORT));
sc.register(sel, SelectionKey.OP_READ | SelectionKey.OP_WRITE
| SelectionKey.OP_CONNECT);
int i = 0;
boolean written = false;
boolean done = false;
String encoding = System.getProperty(“file.encoding”);
Charset cs = Charset.forName(encoding);
ByteBuffer buf = ByteBuffer.allocate(16);
while(!done){
sel.select();
Iterator it = sel.selectedKeys().iterator();
while(it.hasNext()){
SelectionKey key = (SelectionKey)it.next();
It.remove();
//获取创建通道选择器事件键的套接字通道
sc = (SocketChannel)key.channel();
//当前通道选择器产生连接已经准备就绪事件,并且客户端套接字
//通道尚未连接到服务端套接字通道
if(key.isConnectable() && !sc.isConnected()){
InetAddress addr = InetAddress.getByName(null);
//客户端套接字通道向服务端套接字通道发起非阻塞连接
boolean success = sc.connect(new InetSocketAddress(
addr, NIOSocketServer.PORT));
//如果客户端没有立即连接到服务端,则客户端完成非立即连接操作
if(!success) sc.finishConnect();
}
//如果通道选择器产生读取操作已准备好事件,且已经向通道写入数据
if(key.isReadable() && written){
if(sc.read((ByteBuffer)buf.clear()) > 0){
written = false;
//从套接字通道中读取数据
String response = cs.decode((ByteBuffer)buf.flip()).toString();
System.out.println(response);
if(response.indexOf(“END”) != -1) done = true;
}
}
//如果通道选择器产生写入操作已准备好事件,并且尚未想通道写入数据
if(key.isWritable() && !written){
//向套接字通道中写入数据
if(i < 10) sc.write(ByteBuffer.wrap(new String(“howdy ” + i +
‘\n’).getBytes()));
else if(i == 10)sc.write(ByteBuffer.wrap(newString(“END”).
getBytes()));
written = true;
i++;
}
}
}
}finally{
sc.close();
sel.close();
}
}
}