BIO
传统的TCP和UDP通讯:Blocking I/O
I/O设备一边在写数据,由于写的速度过慢,另一边读的话必须要等待,出现阻塞,导致性能较差(两边速度不匹配,相差较大就会出现阻塞)。
Non-Blocking I/O
- 提供非阻塞通讯等方式
- 避免同步I/O通讯效率过低
- 一个线程管理多个连接
- 减少线程多的压力
- Non-Blocking I/O,非阻塞I/O,(又名 New I/O)
- JDK1.4引入,1.7升级NIO 2.0(包括AIO)
- 主要类
- Buffer 缓存区
- Channel 通道
- Selector 多路选择器
Buffer 缓冲区
- Buffer 缓冲区, 一个可以读写的内存区域
- ByteBuffer,CharBuffer,DoubleBuffer,IntBuffer,LongBuffer,ShortBuffer(StringBuffer 不是 Buffer缓冲区,而是在原地可以进行字符上修改的一种数据类型)
- 四个主要属性
- capacity 容量,position 当前读写位置
- limit 界限(值容量里空间还剩下多少),mark 标记,用于重复一个读写操作
Channel通道
- 全双工的,支持读/写(而Stream流是单向的)
- 支持异步读写
- 和Buffer配合,提高效率
- 在NIO里面主要的子类
- ServerSocketChannel 服务器TCP Socket接入通道,接收客户端
- SocketChannel TCP Socket通道,可支持阻塞/非阻塞通讯(用来支持服务端和客户端的通讯管道)
- DatagramChannel UDP通道
- FileChannel 文件通道
Selector 多路选择器
- 是一个轮询开关
- 每隔一段时间,不断轮询注册在其上的Channel
- 如果有一个Channel有接入、读、写操作,就会被轮询出来
- 根据SelectionKey可以获取相应的Channel,进行后续IO操作
- 避免过多的线程
- SelectionKey四种类型
- OP_CONNECT(通道上面,有人连接过来)
- OP_ACCEPT(连接成功)
- OP_READ
- OP_WRITE
示例:先启动服务器端程序
package nio;
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.util.Iterator;
import java.util.Set;
public class NioServer {
public static void main(String[] args) throws IOException {
int port = 8001;
Selector selector = null;
ServerSocketChannel servChannel = null;
try {
//完成服务器Channel的初始化
selector = Selector.open(); //多路选择器
servChannel = ServerSocketChannel.open(); //建立服务器通道,等待客户端连接
servChannel.configureBlocking(false); //配置为非阻塞模式
servChannel.socket().bind(new InetSocketAddress(port), 1024); //服务器channel驻守在本机的8001端口
servChannel.register(selector, SelectionKey.OP_ACCEPT); //将多路选择器与channel进行绑定
//选择器就可以控制ServerChannel所接入的所有的子Channel
System.out.println("服务器在8001端口守候");
} catch (IOException e) {
e.printStackTrace();
System.exit(1);
}
while(true)
{
try {
//轮询所有的Channel,看哪一个有动静
selector.select(1000);
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> it = selectedKeys.iterator();
SelectionKey key = null;
while (it.hasNext()) {
key = it.next();
it.remove();
try {
handleInput(selector,key);
} catch (Exception e) {
if (key != null) {
key.cancel();
if (key.channel() != null)
key.channel().close();
}
}
}
}
catch(Exception ex)
{
ex.printStackTrace();
}
try
{
Thread.sleep(500);
}
catch(Exception ex)
{
ex.printStackTrace();
}
}
}
public static void handleInput(Selector selector, SelectionKey key) throws IOException {
if (key.isValid()) {
// 处理新接入的请求消息
if (key.isAcceptable()) {
// Accept the new connection
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
SocketChannel sc = ssc.accept();
sc.configureBlocking(false);
// Add the new connection to the selector
sc.register(selector, SelectionKey.OP_READ);
}
if (key.isReadable()) {
// Read the data
SocketChannel sc = (SocketChannel) key.channel();
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
int readBytes = sc.read(readBuffer);
if (readBytes > 0) {
readBuffer.flip();
byte[] bytes = new byte[readBuffer.remaining()];
readBuffer.get(bytes);
String request = new String(bytes, "UTF-8"); //接收到的输入
System.out.println("client said: " + request);
String response = request + " 666";
doWrite(sc, response);
} else if (readBytes < 0) {
// 对端链路关闭
key.cancel();
sc.close();
} else
; // 读到0字节,忽略
}
}
}
public static void doWrite(SocketChannel channel, String response) throws IOException {
if (response != null && response.trim().length() > 0) {
byte[] bytes = response.getBytes();
ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
writeBuffer.put(bytes);
writeBuffer.flip();
channel.write(writeBuffer);
}
}
}
再启动客户端程序
package nio;
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;
import java.util.UUID;
public class NioClient {
public static void main(String[] args) {
String host = "127.0.0.1";
int port = 8001;
Selector selector = null;
SocketChannel socketChannel = null;
try
{
selector = Selector.open();
socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false); // 非阻塞
// 如果直接连接成功,则注册到多路复用器上,发送请求消息,读应答
if (socketChannel.connect(new InetSocketAddress(host, port)))
{
socketChannel.register(selector, SelectionKey.OP_READ);
doWrite(socketChannel);
}
else
{
socketChannel.register(selector, SelectionKey.OP_CONNECT);
}
} catch (IOException e) {
e.printStackTrace();
System.exit(1);
}
while (true)
{
try
{
selector.select(1000);
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> it = selectedKeys.iterator();
SelectionKey key = null;
while (it.hasNext())
{
key = it.next();
it.remove();
try
{
//处理每一个channel
handleInput(selector, key);
}
catch (Exception e) {
if (key != null) {
key.cancel();
if (key.channel() != null)
key.channel().close();
}
}
}
}
catch (Exception e)
{
e.printStackTrace();
}
}
// 多路复用器关闭后,所有注册在上面的Channel资源都会被自动去注册并关闭
// if (selector != null)
// try {
// selector.close();
// } catch (IOException e) {
// e.printStackTrace();
// }
//
// }
}
//把产生的随机字符串放到缓冲区里面
public static void doWrite(SocketChannel sc) throws IOException {
byte[] str = UUID.randomUUID().toString().getBytes(); //一个产生随机字符串
ByteBuffer writeBuffer = ByteBuffer.allocate(str.length); //申请一个缓冲区
writeBuffer.put(str);
writeBuffer.flip();
sc.write(writeBuffer);
}
public static void handleInput(Selector selector, SelectionKey key) throws Exception {
if (key.isValid()) {
// 判断是否连接成功
SocketChannel sc = (SocketChannel) key.channel();
if (key.isConnectable()) {
if (sc.finishConnect()) {
sc.register(selector, SelectionKey.OP_READ);
}
}
if (key.isReadable()) {
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
int readBytes = sc.read(readBuffer);
if (readBytes > 0) {
readBuffer.flip();
byte[] bytes = new byte[readBuffer.remaining()];
readBuffer.get(bytes);
String body = new String(bytes, "UTF-8");
System.out.println("Server said : " + body);
} else if (readBytes < 0) {
// 对端链路关闭
key.cancel();
sc.close();
} else
; // 读到0字节,忽略
}
Thread.sleep(3000);
doWrite(sc);
}
}
}
客户端每次发送一个随机字符串给服务端,服务端就会返回客户发送的字符串在+666
也可以再启动一个客户端。两个Client同时向一个Server 发起请求,Server用一个线程,通过两个Channel和两个客户端进行通讯,从而节省了线程数量。
总结:不管客户端或服务器端的 Selector、Channel、Buffer三个组件之间的配合关系。
1.通过Selector的轮询,获取到所有 有事件操作通道的集合(这里都放在SelectionKey),
然后对每个SelectKey拿出每个具体的内容交给handleInput来处理
2.在handleInput中通过SocketChannel读出所有的所有的数据
3.把读出来的数据放到Buffer里面去处理
SocketChannel sc = (SocketChannel) key.channel();
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
NIO服务端–客户端通讯示意图: