BIO学习
- 模拟客户端与服务器端通信
QQServer代码
public class QQServer {
static byte[] bytes = new byte[1024];
public static void main(String[] args) {
try {
//Listener(socket1)
ServerSocket serverSocket = new ServerSocket();
serverSocket.bind(new InetSocketAddress(8080));
while(true)
{
System.out.println("wait conn");
// 阻塞 (socket2)
Socket socket = serverSocket.accept();
System.out.println("conn success");
System.out.println("wait data");
// 阻塞 read 表示读了多少字节
int read = socket.getInputStream().read(bytes);
System.out.println("read data success");
String content = new String(bytes);
System.out.println(content);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
模拟客户端代码
package com;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
public class Client {
public static void main(String[] args) {
try {
// socket3
Socket socket = new Socket("127.0.0.1",8080);
//socket.connect();
Scanner sc = new Scanner(System.in);
String data = sc.next();
socket.getOutputStream().write(data.getBytes());
} catch (IOException e) {
e.printStackTrace();
}
}
}
BIO就是来了一个用户连接,服务器端accept等待到这个连接,解阻塞往下走,到读数据阻塞,如果客户端传来了数据,则解阻塞,不然就read时一直阻塞,服务器端会阻塞两次,分别是等待连接阻塞、等待读数据阻塞;客户端会阻塞一(或者0)次,即等待数据输入阻塞。因此,一般需要结合多线程来使用BIO,也就是将服务器端读取数据的工作交给其他线程去做,自身可以继续监听阻塞等待新的客户端连接。
即使使用了多线程,新问题是,太多线程容易导致内存崩溃(没用控制并发),因此引入线程池控制并发。用线程池时,新问题是,很多线程可能被阻塞(比如等待客户端写数据给服务器端读),但是他们只是在等待读数据,表现“很忙”,但是没用做事情,因此浪费了很多cpu资源,
NIO学习
- NIO 模拟代码
java模拟单线程轮询
public class QQServerNIO {
static ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
static List<SocketChannel> list = new ArrayList<SocketChannel>();
// 模拟NIO原理
public static void main(String[] args) {
try {
//Listener(socket1)
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(8080));
serverSocketChannel.configureBlocking(false); //设置为非阻塞
while (true) {
// 阻塞 (socket2)
SocketChannel socketChannel = serverSocketChannel.accept();
if(socketChannel == null)
{
try {
sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("没人连接");
}else
{
System.out.println("one get connect ==================");
socketChannel.configureBlocking(false); // //设置为非阻塞
list.add(socketChannel);
}
for(SocketChannel s1: list)
{
// 阻塞 read 表示读了多少字节
int read = s1.read(byteBuffer);
if(read != 0)
{// logic
byteBuffer.flip();
System.out.println(byteBuffer.toString());
System.out.println(Charset.forName("utf-8").decode(byteBuffer));
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
NIO做的事情就是把Server上的两个阻塞变成不阻塞,但是ServerSocket和Socket是无法设置的,所以引入了ServerSocketChannel 和SocketChannel来变成不阻塞。
轮询操作交给操作系统实现
轮询由一个(操作系统)大管家[Selector]来做,这样的好处就是由操作系统内核实现轮询,每次返回有事件(比如,Accept,Read)的keys (对应插槽),Selector取代之前BIO的一个客户端一个线程的模式。
假设serversocket是一个面板,上面很多的插槽,客户端连上,则使用一个插槽,建立一个通道。
每次将ServerSocketChannel 定义的面板所有插槽给Selector管理(初始是监听Accept事件),来了连接,则将建立连接的通道也交给Selector管理(更新需要监听的事件),服务器会进行轮询,如果有新事件,则会返回那些有新事件的插槽的keys,如果服务器上来了数据,则在handle中处理。
package com.mashibing.io.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 Server {
public static void main(String[] args) throws IOException {
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.socket().bind(new InetSocketAddress("127.0.0.1", 8888));
ssc.configureBlocking(false);
System.out.println("server started, listening on :" + ssc.getLocalAddress());
Selector selector = Selector.open();
ssc.register(selector, SelectionKey.OP_ACCEPT);
while(true) {
selector.select();
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> it = keys.iterator();
while(it.hasNext()) {
SelectionKey key = it.next();
it.remove();
handle(key);
}
}
}
private static void handle(SelectionKey key) {
if(key.isAcceptable()) {
try {
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
SocketChannel sc = ssc.accept();
sc.configureBlocking(false);
//new Client
//
//String hostIP = ((InetSocketAddress)sc.getRemoteAddress()).getHostString();
/*
log.info("client " + hostIP + " trying to connect");
for(int i=0; i<clients.size(); i++) {
String clientHostIP = clients.get(i).clientAddress.getHostString();
if(hostIP.equals(clientHostIP)) {
log.info("this client has already connected! is he alvie " + clients.get(i).live);
sc.close();
return;
}
}*/
sc.register(key.selector(), SelectionKey.OP_READ );
} catch (IOException e) {
e.printStackTrace();
} finally {
}
} else if (key.isReadable()) { //flip
SocketChannel sc = null;
try {
sc = (SocketChannel)key.channel();
ByteBuffer buffer = ByteBuffer.allocate(512);
buffer.clear();
int len = sc.read(buffer);
if(len != -1) {
System.out.println(new String(buffer.array(), 0, len));
}
ByteBuffer bufferToWrite = ByteBuffer.wrap("HelloClient".getBytes());
sc.write(bufferToWrite);
} catch (IOException e) {
e.printStackTrace();
} finally {
if(sc != null) {
try {
sc.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
}
以上java NIO单线程的问题是,只有一个线程做所有的事情,如果在读写数据的过程中发生了中断,则无法处理建立客户端连接通道。
响应式 (Reactor)NIO模型
单线程NIO的不足,推动了多线程NIO的出现,响应式NIO通过将读写操作交给其他线程去做从而避免读写时发生无法建立客户端连接的问题。可以比喻成,Selector就是一个老板,带着一群工人干活,所有的工人是一个线程池,如果需要读写,Selector就把读写的工作给工人完成,自己只负责客户端连上插槽。
其实JAVA的NIO都是响应式的(基于IO多路复用),因为这是调用了系统的select函数实现连接资源的轮询。响应式的好处就是能够实现假的异步,当有连接要进行处理,再通知用户线程(用户程序)。IO多路复用就是基于select函数,select函数的实现比如select、poll、epoll。只用一个线程,实现处理多个连接请求状态的处理。可见:https://www.zhihu.com/question/28594409。
存在问题:仍然需要操作系统不断的轮询。
AIO异步非阻塞
引入通知的概念,也就是不再进行轮询,仅当这个有新连接或者新的读写数据需求,就由操作系统去发送一个通知给应用程序,在应用程序中提前设置好钩子函数(回调),这个通知来了就进行相应的处理,同样的,处理工作也类似NIO,读写交给线程池(工人们),老板也可以是多个。(当然,所有工作由一个线程干也是可以的,类似单线程NIO)
多线程AIO
public class ServerWithThreadGroup {
public static void main(String[] args) throws Exception {
ExecutorService executorService = Executors.newCachedThreadPool();
AsynchronousChannelGroup threadGroup = AsynchronousChannelGroup.withCachedThreadPool(executorService, 1);
//中文测试
final AsynchronousServerSocketChannel serverChannel = AsynchronousServerSocketChannel.open(threadGroup)
.bind(new InetSocketAddress(8888));
serverChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Object>() {
@Override
public void completed(AsynchronousSocketChannel client, Object attachment) {
serverChannel.accept(null, this);
try {
System.out.println(client.getRemoteAddress());
ByteBuffer buffer = ByteBuffer.allocate(1024);
client.read(buffer, buffer, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer result, ByteBuffer attachment) {
attachment.flip();
System.out.println(new String(attachment.array(), 0, result));
client.write(ByteBuffer.wrap("HelloClient".getBytes()));
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
exc.printStackTrace();
}
});
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void failed(Throwable exc, Object attachment) {
exc.printStackTrace();
}
});
while (true) {
Thread.sleep(1000);
}
}
}
单线程AIO
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousServerSocketChannel;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;
public class Server {
public static void main(String[] args) throws Exception {
final AsynchronousServerSocketChannel serverChannel = AsynchronousServerSocketChannel.open()
.bind(new InetSocketAddress(8888));
serverChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Object>() {
@Override
public void completed(AsynchronousSocketChannel client, Object attachment) {
serverChannel.accept(null, this);
try {
System.out.println(client.getRemoteAddress());
ByteBuffer buffer = ByteBuffer.allocate(1024);
client.read(buffer, buffer, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer result, ByteBuffer attachment) {
attachment.flip();
System.out.println(new String(attachment.array(), 0, result));
client.write(ByteBuffer.wrap("HelloClient".getBytes()));
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
exc.printStackTrace();
}
});
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void failed(Throwable exc, Object attachment) {
exc.printStackTrace();
}
});
while (true) {
Thread.sleep(1000);
}
}
}
Netty
大多数服务器都是linux,在linux里面,AIO、NIO使用的都是epoll底层轮询实现,AIO经过一层封装,某种程度来说,性能还不如NIO,Netty就是将NIO封装的像AIO,优化BufferSize,方便于编程使用的NIO。