Netty学习
1、 BIO/NIO/AIO
BIO:blocking io,阻塞io。阻塞节点有以下两点:
模式一、单线程bio
1、serverSocket.accept()时,一直等待客户端的连接请求;
2、socket.getInputStream().read(bytes)一直等待读取客户端的消息。
可使用telnet localhost:8099 模拟客户端的连接请求,使阻塞节点一继续往下;使用telnet.send命令模拟客户端发起命令。
该模式是最早期时,用户量很少,互联网不是很普及的时候使用的处理方式,不用考虑并发情况。
public static void main(String[] args) {
try {
ServerSocket serverSocket = new ServerSocket(8099);
while (true){
logger.info("等待客户端连接。。。");
Socket socket = serverSocket.accept();
logger.info("客户端已连接。。。。");
handle(socket);
}
} catch (IOException e) {
logger.error("服务端异常。。。");
}
}
private static void handle(Socket socket){
byte[] bytes = new byte[1024];
try {
logger.info("等待客户端发送消息。。。");
int read = socket.getInputStream().read(bytes);
if (read != -1){
logger.info(String.format("客户端发送消息:[%s]", new String(bytes, 0, read)));
}
socket.getOutputStream().write("hello client".getBytes());
socket.getOutputStream().flush();
} catch (IOException e) {
logger.error("服务端处理消息异常。。。");
}
}
模式二、多线程bio
1、serverSocket.accept()时,一直等待客户端的连接请求;
2、当多个客户端连接时,会使用子线程单独处理各个客户端的数据处理。
但同时存在以下问题:
1、线程上下文切换比较耗时且耗资源,影响性能;
2、当客户端只连接,没有发送消息,则会一直占用该线程,造成线程浪费。
比如C10k问题:服务器如何支持10k个并发连接,也就是concurrent 10000 connection
public static void main(String[] args) {
try {
ServerSocket serverSocket = new ServerSocket(8099);
while (true){
logger.info("等待客户端连接。。。");
Socket socket = serverSocket.accept();
logger.info("客户端已连接。。。。");
new Thread(new Runnable() {
@Override
public void run() {
logger.info(String.format("current thread:[%s]", Thread.currentThread().getName()));
handle(socket);
}
});
}
} catch (IOException e) {
logger.error("服务端异常。。。");
}
}
模式三、NIO(non blocking io)
采用Selector多路复用器,服务器采用一个线程可以处理多个请求,客户端发送的请求都会注册到多路复用器selector上,多路复用器轮询到连接有IO请求就会处理。
I/O多路复用底层使用到linux api(select,poll,epoll)来实现,区别如下:
NIO从1.4开始支持,流程基本如下:
NIO主要有三大核心:channel(通道),Buffer(缓存区),Selector(选择器)
1、channel类似于流,每个channel对应一个buffer缓冲区,buffer底层就是一个byte数组;
2、channel会注册到selector上,由seletor根据channel的读写事件的发生将其交由一个空闲线程处理;
3、selector可以对应一个或多个线程;4、NIO的buffer都是既可以读又可以写。
public static void main(String[] args) {
try {
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false);
ssc.socket().bind(new InetSocketAddress(8099));
logger.info("服务端启动。。。");
Selector selector = Selector.open();
ssc.register(selector, SelectionKey.OP_ACCEPT);
while (true){
logger.info("等待事件发生。。。");
int select = selector.select();
logger.info("有事件发生了。。。");
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()){
SelectionKey sk = iterator.next();
iterator.remove();
handle(sk);
}
}
} catch (IOException e) {
logger.info("有事件发生了。。。");
}
}
private static void handle(SelectionKey selectionKey){
if (selectionKey.isAcceptable()){
try {
ServerSocketChannel ssc = (ServerSocketChannel)selectionKey.channel();
SocketChannel sc = ssc.accept();
sc.configureBlocking(false);
sc.register(selectionKey.selector(), SelectionKey.OP_READ);
} catch (IOException e) {
logger.info("客户端连接异常。。。");
}
}
if (selectionKey.isReadable()){
SocketChannel sc = (SocketChannel)selectionKey.channel();
ByteBuffer bb = ByteBuffer.allocate(1024);
try {
int read = sc.read(bb);
logger.info(String.format("读取客户端数据:[%s]", new String(bb.array(), 0, read)));
ByteBuffer bbWrite = ByteBuffer.wrap("hello client".getBytes());
sc.write(bbWrite);
selectionKey.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE);
} catch (IOException e) {
e.printStackTrace();
}
}
if (selectionKey.isWritable()){
SocketChannel sc = (SocketChannel)selectionKey.channel();
logger.info("write事件");
selectionKey.interestOps(SelectionKey.OP_READ);
}
}
1、创建一个ServerSocketChannel和Selector,将ServerSocketChannel注册到Selector上;
2、Selector通过select()方法监听channel事件,当客户端连接时,Selector监听到连接事件,获取到ServerSocketChannel注册时绑定的selectionKey;
3、selectionKey通过channel()方法可以获取绑定的ServerSocketChannel;
4、ServerSocketChannel通过accept()方法得到SocketChannel;
5、将SocketChannel注册到Selector上,关心read事件;
6、注册后返回一个selectionKey,会和该SocketChannel关联;
7、Selector继续通过select()方法监听事件,当客户端发送数据给服务端,Selector监听到read事件,获取到SocketChannel注册时绑定的selectionKey;
8、selectionKey通过channel()方法客户获取绑定的SocketChannel;
9、将SocketChannel里的数据读取出来;
10、用SocketChannel将服务端的数据写回客户端。
总结:NIO模型的selector 就像一个大总管,负责监听各种IO事件,然后转交给后端线程去处理
使用man 函数名,查看linux中该方法的作用。
当s收到c的请求到达系统网卡时,操作系统内核会硬中断。
elector.open JNI调用epoll.create方法创建一个epoll实例,是一个空的就绪事件列表rdlist,当client有发起连接、读写等事件响应时,操作系统内核能感知到,中断程序,将socketchannel事件放到rdlist中,调用epoll.wait时,只是查看rdlist中有没有数据,有数据就跳出阻塞,没数据就阻塞上。
selector有两个集合,一个时所有socketchannel集合,一个是wait做真正的绑定,有响应就放到rdlist中。
最早1.4版本时。selector遍历,没有用到内核中断回调函数,所以最大连接有1024的限制,1.5使用epoll,使用内核的中断、回调函数,放到epoll描述文件中,epoll_ctl添加绑定事件到epfd中。redis线程模型实现主要在。_epoll.c _selector.c文件中。
Version:0.9 StartHTML:0000000105 EndHTML:0000000965 StartFragment:0000000141 EndFragment:0000000925
主要用到了AsynchronousServerSocketChannel,目前因操作系统无法友好的支持,所以未普及。