1 BIO (BlockingIO)
1.1同步阻塞式I/O
服务器起一个监听端口,每次有新客户端接入,服务器便会起一个线程进行处理。由于传统IO是面向字节流/字符流进行操作,因此每次通信过程进行读写操作都会发生阻塞。优点是API容易理解上手,适合非高并发场景。缺点是缺乏弹性伸缩能力。由于服务器的线程数和客户端个数是1:1,在高并发环境下,线程开启过多,CPU在多个线程之间无意义轮询,系统性能下降。当开启线程过多时候,服务器就会发生宕机。
图 1 一请求一应答通信模型
public class MyServer {
public static void main(String[] args) throws IOException {
int port = 8090;
ServerSocket server = null;
Socket socket = null;
try {
server = new ServerSocket(port);
System.out.println("服务器启动了,监听端口" + port);
while(true){
socket = server.accept();
new Thread(new MyServerHandler(socket)).start();
}
} catch (Exception e) {
e.printStackTrace();
} finally{
if(server != null){
System.out.println("Myserver close");
try {
server.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
public class MyServerHandler implements Runnable {
private Socket socket;
public MyServerHandler(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
BufferedReader reader = null;
PrintWriter writer = null;
try {
// 读操作
reader = new BufferedReader(new InputStreamReader(this.socket.getInputStream()));
String rece = null;
rece = reader.readLine();
System.out.println("服务器收到客户端发送过来的信息: " + rece);
// 写操作
writer = new PrintWriter(this.socket.getOutputStream(), true);
System.out.println("请输入要发送给服务端的信息:");
Scanner scanner = new Scanner(System.in);
String resp = scanner.next();
writer.println(resp);
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (writer != null) {
writer.close();
}
if (reader != null) {
}
if (socket != null) {
socket.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
public class MyClient {
public static void main(String[] args) {
String host = "192.168.0.101";
int port = 8090;
Socket socket = null;
PrintWriter writer = null;
BufferedReader reader = null;
try {
socket = new Socket(host, port);
// 写操作
System.out.println("请输入要发送给服务端的内容:");
Scanner scanner = new Scanner(System.in);
String str = scanner.nextLine();
// true:表示自动刷新
writer = new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())), true);
writer.println(str);
// 读操作
reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String rece = null;
// 如果读取到最后行,返回null
rece = reader.readLine();
System.out.println("客户端收到服务器发送过来的信息: " + rece);
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (reader != null) {
reader.close();
}
if (writer != null) {
writer.close();
}
if (socket != null) {
socket.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
1.2伪异步I/O
同1.1最大区别就是在服务端增加了线程池技术。即客户端个数和IO线程的个数比是M:N(M >=N),当客户端个数超过线程池核心池大小时,多余线程会被放在阻塞队列中。优点:当客户端高并发访问时候,由于服务端起的线程数是可控的,因此不会导致服务器资源耗尽和宕机。缺点:本质上还是同步阻塞IO,处理效率低。当通信一方网络发生拥塞时,读取输入流的另一方也将被长时间阻塞。
public class MyServer {
public static void main(String[] args) throws IOException {
int port = 8090;
ServerSocket server = null;
Socket socket = null;
try {
server = new ServerSocket(port);
System.out.println("服务器启动了,监听端口" + port);
//线程池
//当客户端的个数超过核心线程池个数,多余的放入阻塞队列中进行等待
ThreadPoolExecutor executor = new ThreadPoolExecutor(50, 100, 120L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
while(true){
socket = server.accept();
executor.execute(new MyServerHandler(socket));
}
} catch (Exception e) {
e.printStackTrace();
} finally{
if(server != null){
System.out.println("Myserver close");
try {
server.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
public class MyServerHandler implements Runnable {
private Socket socket;
public MyServerHandler(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
BufferedReader reader = null;
PrintWriter writer = null;
try {
// 读操作
reader = new BufferedReader(new InputStreamReader(this.socket.getInputStream()));
String rece = null;
rece = reader.readLine();
System.out.println("服务器收到客户端发送过来的信息: " + rece);
// 写操作
writer = new PrintWriter(this.socket.getOutputStream(), true);
System.out.println("请输入要发送给服务端的信息:");
Scanner scanner = new Scanner(System.in);
String resp = scanner.next();
writer.println(resp);
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (writer != null) {
writer.close();
}
if (reader != null) {
}
if (socket != null) {
socket.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
public class MyClient {
public static void main(String[] args) {
String host = "192.168.0.101";
int port = 8090;
Socket socket = null;
PrintWriter writer = null;
BufferedReader reader = null;
try {
socket = new Socket(host, port);
// 写操作
System.out.println("请输入要发送给服务端的内容:");
Scanner scanner = new Scanner(System.in);
String str = scanner.nextLine();
// true:表示自动刷新
writer = new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())), true);
writer.println(str);
// 读操作
reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String rece = null;
// 如果读取到最后行,返回null
rece = reader.readLine();
System.out.println("客户端收到服务器发送过来的信息: " + rece);
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (reader != null) {
reader.close();
}
if (writer != null) {
writer.close();
}
if (socket != null) {
socket.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
2 NIO (NonBlockingIO)
同步非阻塞IO是从JDK1.4开始新增的IO操作API,是面向通道和缓冲区进行操作。在NIO库中,数据是从通道读取到缓冲区中,或者从缓冲区写入到通道中。通过设置通道为非阻塞机制,可以同时进行读写双向操作而不发生阻塞。
public class ServerSocketChannelDemo {
public static void main(String[] args) throws IOException {
ServerSocketChannel channel = ServerSocketChannel.open();
channel.socket().bind(new InetSocketAddress(8090));
channel.configureBlocking(true); //默认为true,是阻塞
SocketChannel sc = channel.accept();//发生阻塞
System.out.println("通道为阻塞");
}
}
图2 NIO通道为阻塞情况
public class ServerSocketChannelDemo {
public static void main(String[] args) throws IOException {
ServerSocketChannel channel = ServerSocketChannel.open();
channel.socket().bind(new InetSocketAddress(8090));
channel.configureBlocking(false);
SocketChannel sc = channel.accept();
System.out.println("通道为非阻塞");
}
}
NIO中另外一个重要概念是多路复用器(Selector)。Selector通过轮询检查每个注册在其上的NIO通道,如果某个通道上面发生读(SelectionKey.OP_READ)或者写(SelectionKey.OP_WRITE)操作,则这些通道会被Selector检测出来,并且存放在一个Set集合中,进行后续操作。因为JDK底层采用epoll()代替传统select实现,因此只需要一个线程负责Selector的轮询,可以管理多个channel,从而管理多个网络连接。 优点是在高并发场景下,可以有效减少服务端起的线程数量和CPU时间片的浪费,降低了内存的消耗。缺点是代码比较复杂。
public class Server implements Runnable{
//1 多路复用器(管理所有的通道)
private Selector seletor;
//2 接受数据缓冲区
private ByteBuffer readBuf = ByteBuffer.allocate(1024);
//3 发送数据缓冲区
private ByteBuffer writeBuf = ByteBuffer.allocate(1024);
//4 标识数字
private int flag = 0;
/**
* @param port
*/
public Server(int port){
try {
//1 打开路复用器
this.seletor = Selector.open();
//2 打开服务器通道
ServerSocketChannel ssc = ServerSocketChannel.open();
//3 设置服务器通道为非阻塞模式
ssc.configureBlocking(false);
//4 绑定地址
ssc.bind(new InetSocketAddress(port));
//5 把服务器通道注册到多路复用器上,并且监听阻塞事件
ssc.register(this.seletor, SelectionKey.OP_ACCEPT);
System.out.println("Server start, port :" + port);
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void run() {
while(true){
try {
//1 必须要让多路复用器开始监听
this.seletor.select();
//2 返回多路复用器已经选择的结果集
Iterator<SelectionKey> keys = this.seletor.selectedKeys().iterator();
//3 进行遍历
while(keys.hasNext()){
//4 获取一个选择的元素
SelectionKey key = keys.next();
//5 直接从容器中移除就可以了
keys.remove();
//6 如果是有效的
if(key.isValid()){
//7 如果为阻塞状态
if(key.isAcceptable()){
this.accept(key);
}
//8 如果为可读状态
if(key.isReadable()){
this.read(key);
}
//9 如果为可写状态
if(key.isWritable()){
this.write(key);
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* @descriptions 写方法
* @param key
*/
private void write(SelectionKey key){
try {
// 返回为之创建此键的通道。
SocketChannel sc = (SocketChannel) key.channel();
//清空缓冲区数据
writeBuf.clear();
//向缓冲区中输入数据
String sendText="你好,客户端!" + flag++;
//把数据放到缓冲区中
writeBuf.put(sendText.getBytes());
//对缓冲区进行复位
writeBuf.flip();
//写出数据
sc.write(writeBuf);
System.out.println("服务器端向客户端发送数据: "+sendText);
sc.register(this.seletor, SelectionKey.OP_READ);
// Thread.sleep(1000);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* @descriptions 读方法
* @param key
*/
private void read(SelectionKey key) {
try {
//1 清空缓冲区旧的数据
this.readBuf.clear();
//2 获取之前注册的socket通道对象
SocketChannel sc = (SocketChannel) key.channel();
//3 读取数据
int count = sc.read(this.readBuf);
//4 如果该通道已到达流的末尾,则返回 -1
if(count == -1){
key.channel().close();
key.cancel();
return;
}
//5 有数据则进行读取 读取之前需要进行复位方法(把position 和limit进行复位)
this.readBuf.flip();
//6 根据缓冲区的数据长度创建相应大小的byte数组,接收缓冲区的数据
byte[] bytes = new byte[this.readBuf.remaining()];
//7 接收缓冲区数据
this.readBuf.get(bytes);
//8 打印结果
String body = new String(bytes).trim();
System.out.println("服务器收到客户端发过来的消息: " + body);
//9 注册到多路复用器上,并设置读写标识
sc.register(this.seletor, SelectionKey.OP_WRITE);
Thread.sleep(1000);
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
}
/**@descriptions 接受方法
* @param key
*/
private void accept(SelectionKey key) {
try {
System.out.println("有客户端连接进来客户端");
//1 获取服务通道
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
//2 执行阻塞方法
SocketChannel sc = ssc.accept();
//3 设置阻塞模式
sc.configureBlocking(false);
//4 注册到多路复用器上,并设置读取标识
sc.register(this.seletor, SelectionKey.OP_READ );
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
new Thread(new Server(8090)).start();;
}
}
public class NIOClient {
// 缓冲区大小
private static int block = 1024;
// 接受数据缓冲区
private static ByteBuffer sendbuffer = ByteBuffer.allocate(block);
//发送数据缓冲区*
private static ByteBuffer receivebuffer = ByteBuffer.allocate(block);
//服务器端地址
private final static InetSocketAddress SERVER_ADDRESS = new InetSocketAddress(
"192.168.0.101", 8090);
public static void main(String[] args) throws IOException {
// 打开socket通道
SocketChannel socketChannel = SocketChannel.open();
// 设置为非阻塞方式
socketChannel.configureBlocking(false);
// 打开选择器
Selector selector = Selector.open();
// 注册连接服务端socket动作
socketChannel.register(selector, SelectionKey.OP_CONNECT);
// 连接
socketChannel.connect(SERVER_ADDRESS);
// 分配缓冲区大小内存
Set<SelectionKey> selectionKeys;
Iterator<SelectionKey> iterator;
SelectionKey selectionKey;
SocketChannel client;
String receiveText;
String sendText;
int count=0;
while (true) {
//选择一组键,其相应的通道已为 I/O 操作准备就绪。
//此方法执行处于阻塞模式的选择操作。
selector.select();
//返回此选择器的已选择键集。
selectionKeys = selector.selectedKeys();
iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
selectionKey = iterator.next();
if (selectionKey.isConnectable()) {
System.out.println("客户端尝试连接服务端");
client = (SocketChannel) selectionKey.channel();
// 判断此通道上是否正在进行连接操作。
// 完成套接字通道的连接过程。
if (client.isConnectionPending()) {
client.finishConnect();
System.out.println("客户端和服务端连接成功");
sendbuffer.clear();
sendbuffer.put("Hello,Server".getBytes());
sendbuffer.flip();
client.write(sendbuffer);
}
client.register(selector, SelectionKey.OP_READ| SelectionKey.OP_WRITE);
} else if (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);
client.register(selector, SelectionKey.OP_WRITE);
}
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
} else if (selectionKey.isWritable()) {
sendbuffer.clear();
client = (SocketChannel) selectionKey.channel();
System.out.println("请输入要发送给服务器的信息:");
//定义一个字节数组,然后使用系统录入功能:
Scanner scanner = new Scanner(System.in);
sendText = scanner.nextLine();
sendbuffer.put(sendText.getBytes());
//将缓冲区各标志复位,因为向里面put了数据标志被改变要想从中读取数据发向服务器,就要复位
sendbuffer.flip();
client.write(sendbuffer);
client.register(selector, SelectionKey.OP_READ);
}
}
selectionKeys.clear();
}
}
}
3 AIO (AsynchronousIO)
异步非阻塞是从JDK7新增的IO操作API,使用回调的方式,真正实现了高效异步IO。当进行读写操作时,只须直接调用API的read或write方法即可,不需要再使用Selector。由于这两种方法均为异步的,所以均由底层操作系统进行处理,完成后主动通知应用程序。缺点是代码复杂,比较难理解。
public class Server {
// 线程池
private ExecutorService executorService;
// 线程组
private AsynchronousChannelGroup threadGroup;
// 服务器通道
public AsynchronousServerSocketChannel assc;
public Server(int port){
try {
//创建一个缓存池
executorService = Executors.newCachedThreadPool();
//创建一个线程组
threadGroup = AsynchronousChannelGroup.withCachedThreadPool(executorService, 1);
//创建服务通道
assc = AsynchronousServerSocketChannel.open(threadGroup);
//进行绑定
assc.bind(new InetSocketAddress(port));
System.out.println("Server start, port : " + port);
//进行阻塞
ServerCompletionHandler completionHandler = new ServerCompletionHandler();
assc.accept(this, completionHandler);
//一直阻塞不让服务器停止
Thread.sleep(Integer.MAX_VALUE);
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Server server = new Server(8090);
}
}
public class ServerCompletionHandler implements CompletionHandler<AsynchronousSocketChannel,Server>{
@Override
public void completed(AsynchronousSocketChannel asc, Server attachment) {
//当有下一个客户端接入的时候直接调用Server的accept方法,这样反复执行下去,保证多个客户端 都可以阻塞
attachment.assc.accept(attachment, this);
read(asc);
}
@Override
public void failed(Throwable exc, Server attachment) {
exc.printStackTrace();
}
private void read(final AsynchronousSocketChannel asc){
//读取数据
ByteBuffer buf = ByteBuffer.allocate(1024);
asc.read(buf, buf, new CompletionHandler<Integer, ByteBuffer>(){
@Override
public void completed(Integer resultSize, ByteBuffer attachment) {
//进行读取后,重置标识符
attachment.flip();
//获取读写的字符数
System.out.println("Server ->" + "收到客户段的数据长度为:" + resultSize);
String resultData = new String(attachment.array()).trim();
System.out.println("Server ->" + "收到客户段的信息为:" + resultData);
String respond = "服务器响应,收到了客户发过来的数据" + resultData;
write(asc, respond);
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
exc.printStackTrace();
}
});
}
private void write(AsynchronousSocketChannel asc, String respond){
try {
ByteBuffer buf = ByteBuffer.allocate(1024);
buf.put(respond.getBytes());
buf.flip();
asc.write(buf).get();
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
}
public class Client {
private AsynchronousSocketChannel asc;
public Client() {
try {
asc = AsynchronousSocketChannel.open();
} catch (IOException e) {
e.printStackTrace();
}
}
// @Override
// public void run() {
// while(true){
//
// }
// }
public void connect(){
asc.connect(new InetSocketAddress("127.0.0.1", 8090));
}
public void write(String request){
try {
asc.write(ByteBuffer.wrap(request.getBytes())).get();
read();
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
public void read(){
ByteBuffer buf = ByteBuffer.allocate(1024);
try {
asc.read(buf).get();
buf.flip();
byte[] respByte = new byte[buf.remaining()];
buf.get(respByte);
System.out.println(new String(respByte, "utf-8").trim());
} catch (InterruptedException e1) {
e1.printStackTrace();
} catch (ExecutionException e1) {
e1.printStackTrace();
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
for(int i = 0; i < 5; i++){
executorService.execute(new Runnable() {
@Override
public void run() {
Client client = new Client();
client.connect();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
client.write(System.currentTimeMillis() + "");
}
});
}
}
}
4 参考文献
[1] 李林锋, Netty权威指南 第2版. 2014, 电子工业出版社.