网络通信IO的演变过程(二)(一个门外汉的理解)
2.NIO
当与别人谈论NIO时,一定要弄清楚别人说的NIO是指哪个含义?
NIO有2种含义:
1、NonBlocking IO,基于操作系统谈
2、Java New IO,基于Java谈
我们这里主要说的是NonBlocking IO
NonBlocking IO
基于上一篇文章https://www.cnblogs.com/1626ace/p/13193435.html,我了解到BIO的特点就是需要一个线程去处理一个客户端的连接。
因为系统调用accept和recvfrom是阻塞的,这是最大的问题。
随着技术的发展,Linux内核在2.6.27之后,支持非阻塞IO了。
Show me the code:
public class TestNio {
public static void main(String[] args) throws Exception {
LinkedList<SocketChannel> clients = new LinkedList<>();
ServerSocketChannel ss = ServerSocketChannel.open();
ss.bind(new InetSocketAddress(9090));
ss.configureBlocking(false); //设置这个socket系统调用为非阻塞的。
while(true) {
//接受客户端区域
//不停的接收客户端的连接
Thread.sleep(1000);
SocketChannel client = ss.accept(); //本方法因为设置了非阻塞,所以这里不会阻塞,如果JVM返回null,代表没收到连接;底层的返回是-1
if(client == null) {
System.out.println("null...");
} else {
client.configureBlocking(false);
int port = client.socket().getPort();
System.out.println("client---port:" + port);
clients.add(client);
}
ByteBuffer buffer = ByteBuffer.allocate(4096);
//数据读取区域
//循环已经连接进来的client能否读取数据
for(SocketChannel c : clients) {
int num = c.read(buffer);
if (num > 0) {
buffer.flip();
byte[] aaa = new byte[buffer.limit()];
buffer.get(aaa);
String b = new String(aaa);
System.out.println(c.socket().getPort() + ":" + b);
buffer.clear();
}
}
}
}
}
主要特点:
NonBlockingIO主要是在accept()和read()俩方法不阻塞了。
这就是最大的区别。当客户端不连接,或者不输入,那么方法立即返回,得到-1或null.
一个线程在NIO模型里面就可以处理很多很多的客户端线程,不会存在浪费线程内存的情况。
底层系统调用伪代码:
socket()=3 返回文件描述符3
bind(3, 9090) 把socket和9090端口绑定起来
listen(3) 监听FD 3
while(true) {
accept(3) 有两种返回:5 或者 -1. 返回5表示有客户端连接上来了,客户端的socket文件描述符为5.返回-1表示没有客户端连入。
5.nonBlocking = true; 把客户端文件描述符(file descriptor)5的socket设置为非阻塞
for(Client client : allClient) {
read(client); 读取客户端,因为设置了客户端的socket也为非阻塞,所以这里依然不会阻塞住
}
}
这个nio的strace这里就不赘述了,可以参照BIO来做测试。
NIO的优势:规避了多线程问题。因为它可以一个线程处理很多的客户端连接与输入。
NIO的劣势:假设有一万个客户端连入,只有一个客户端有输入,那么服务端需要循环一万次,并且每次都需要调用recvFrom()系统调用,才知道这一个客户端在输入,然后获取它的输入。因为系统调用的开销会存在用户态到内核态的转换,CPU利用率不会高。所以需要解决。
这里问题的关键是这个循环【for(Client client : allClient) 】的复杂度是O(n),每次循环只能问一个客户端是否发来数据。
所以需要解决这个循环的复杂度。
这个解决方案就是“多路复用”。
简单理解多路复用:
基于上述问题的描述,手里拿着10000个客户端,我把这10000个客户端全部传给内核,内核返回告诉我这里面有3个客户端准备好了,可以读取数据了,那么我单独循环这3个客户端去读取数据。
时间复杂度瞬间变为:询问O(1) + 读取O(M),M为已准备发送数据的客户端。
周老师讲课笔记图:
3.多路复用器
为啥叫多路复用呢?
我乱写的:基于NIO的弊端,每次问内核10000次【一次问一条路】,实在太慢,那么一次就问内核10000个客户端【一次问多条路】,一条路复用了以前的一万条路。所以叫做多路复用。
我们的多路复用器讨论分为2类,第一类是poll/select,第二类是epoll
3.1 poll/select
随着Linux内核的发展,内核支持多路复用,下面简单写一个伪代码表示底层系统调用过程:
底层系统调用伪代码:
socket()=3 返回文件描述符3
bind(3, 9090) 把socket和9090端口绑定起来
listen(3) 监听FD 3
while(true) {
select(fds);或poll(fds); //把所有的fd全部全给内核,内核帮我们遍历,返回给我们那些状态变化了的FDs(file descriptor),时间复杂度为O(1)
accept(fd); //接受那些准备连接的客户端
recvFrom(fd); //读取那些准备写入的客户端
}
周老师讲课笔记图:
select和poll的劣势老师已经说得很清楚:
1、每次select这个系统调用都需要传递所有的fds,浪费,损失效率
2、每次select这个系统调用执行时,都需要遍历所有的fd
基于此,epoll出现来解决它~
3.2 epoll
Linux系统就是用epoll,这个多路复用器是用得最多的。
在使用多路复用器的时候,程序关注的是IO状态。
epoll基本理论
解决select和poll的两个问题:
1.在内核开辟空间,使每次系统调用的时候,不传递全量的fds
2.通过epoll_ctl,让内核在cpu工作时,把IO状态变化的fd放入到ready区,用户程序一旦调用epoll_wait,则直接获取到那些fd。最优的时间复杂度变为O(1)
Linux操作系统中的epoll系统调用API:
~# man 2 epoll_create //在内核空间创建一个空间epfd,以便存储需要监听的fd
~# man 2 epoll_ctl //往上面创建的这个epfd中放入需要监听的fd,并设置关注IO变化的事件
~# man 2 epoll_wait //用户调用内核,直接拿走IO状态改变了的fd
epoll_create:
EPOLL_CREATE(2) Linux Programmer’s Manual EPOLL_CREATE(2)
NAME
epoll_create, epoll_create1 - open an epoll file descriptor
SYNOPSIS
#include <sys/epoll.h>
int epoll_create(int size);
int epoll_create1(int flags);
DESCRIPTION
epoll_create() creates an epoll "instance", requesting the kernel to allocate an event backing store
dimensioned for size descriptors. The size is not the maximum size of the backing store but just a
hint to the kernel about how to dimension internal structures. (Nowadays, size is ignored; see NOTES
below.)
epoll_ctl:
EPOLL_CTL(2) Linux Programmer’s Manual EPOLL_CTL(2)
NAME
epoll_ctl - control interface for an epoll descriptor
SYNOPSIS
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
DESCRIPTION
This system call performs control operations on the epoll instance referred to by the file descriptor
epfd. It requests that the operation op be performed for the target file descriptor, fd.
Valid values for the op argument are :
EPOLL_CTL_ADD
Register the target file descriptor fd on the epoll instance referred to by the file descrip-
tor epfd and associate the event event with the internal file linked to fd.
EPOLL_CTL_MOD
Change the event event associated with the target file descriptor fd.
EPOLL_CTL_DEL
Remove (deregister) the target file descriptor fd from the epoll instance referred to by epfd.
The event is ignored and can be NULL (but see BUGS below).
epoll_wait
EPOLL_WAIT(2) Linux Programmer’s Manual EPOLL_WAIT(2)
NAME
epoll_wait, epoll_pwait - wait for an I/O event on an epoll file descriptor
SYNOPSIS
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *events,
int maxevents, int timeout);
int epoll_pwait(int epfd, struct epoll_event *events,
int maxevents, int timeout,
const sigset_t *sigmask);
DESCRIPTION
The epoll_wait() system call waits for events on the epoll instance referred to by the file descrip-
tor epfd. The memory area pointed to by events will contain the events that will be available for
the caller. Up to maxevents are returned by epoll_wait(). The maxevents argument must be greater
than zero.
epoll底层调用伪代码
socket()=3 返回socket文件描述符3
bind(3, 9090) 把socket和9090端口绑定起来
listen(3) 监听FD 3
epoll_create() = 7 通过epoll创建内核空间的fd存储空间:假设为7
epoll_ctl(7, ADD, 3, accept); 往空间7里添加对socket3,关注事件为accept。即客户端一旦连接到3这个socket,这个fd就被选中。
epoll_wait(); 用户程序调用内核(APP调用),获取哪些IO状态发生了变化。本方法是阻塞方法,但是可以传参代表超时时间,如阻塞500毫秒,超时返回-1.
epoll周老师笔记
epoll对应于java
public class SocketMultiplexingSingleThread {
private ServerSocketChannel server = null;
private Selector selector = null;
int port = 9090;
public void initServer() {
try {
server = ServerSocketChannel.open();
server.configureBlocking(false);
server.bind(new InetSocketAddress(port));
//实例server 约等于 listen状态的fd3
//得到的这个selector是JVM抽象的多路复用器,具体是select,poll,还是epoll 根据不同系统决定
//open等价于底层的epoll_create,在内核空间开辟了空间,准备存放所有的文件描述符fd
//假设这里epoll_create 返回的是 fd7, fd7代表的就是内核里面存放fd的空间,他自己也是一个fd,即fd7
selector = Selector.open();
//对select、poll来说,JVM开辟了空间,fd传进去
//对epoll,epoll_ctl(fd7, EPOLL_CTL_ADD, fd3, EPOLLIN) EPOLLIN既可能是客户端连接,也可能是数据到达
server.register(selector, SelectionKey.OP_ACCEPT);
} catch (Exception e ) {
}
}
public void start() {
initServer();
try {
while(true) {
Set<SelectionKey> keys = selector.keys();
/**
* 调用多路复用器
* selector.select(500)
* 对select、poll来说,==内核的系统调用select(fd3) 、poll(fd3)
* 对epoll来说,==内核的系统调用epoll_wait(500)
*/
while(selector.select(500) > 0) {
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
//
while(iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
if(key.isAcceptable()) {
} else if(key.isReadable()) {
}
}
}
}
} catch (Exception e ) {
}
}
}
如果有兴趣,可以自己把代码贴进虚拟机,使用strace追踪底层调用。是非常清晰的。这里不再截屏。
4.Netty入门
基于3.2里epoll(JAVA)示例代码,其中的对象Selector selector
是需要同时处理客户端的连接和客户端数据的读取的。
假设有1000个客户端,
我们完全可以创建3个selector,
selector1 只处理接受客户端的连接。然后把连接依次,往下面2个selector里扔。
selector2 接受客户端的输入。只关注epoll_wait.
selector3 接受客户端的输入。只关注epoll_wait.
然后把selector1、2、3分别扔到3个线程里面去处理。
这就是netty的入门原理:(下面是netty官网的hello world)
https://netty.io/wiki/user-guide-for-4.x.html
public void run() throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup(5);
EventLoopGroup workerGroup = new NioEventLoopGroup(5);
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new DiscardServerHandler());
}
})
.option(ChannelOption.SO_BACKLOG, 128)
.childOption(ChannelOption.SO_KEEPALIVE, true);
// Bind and start to accept incoming connections.
ChannelFuture f = b.bind(port).sync();
// Wait until the server socket is closed.
// In this example, this does not happen, but you can do that to gracefully
// shut down your server.
f.channel().closeFuture().sync();
} finally {
workerGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
}
}
- EventLoopGroup bossGroup = new NioEventLoopGroup(5); 代表的是启动5个线程来处理;
- EventLoopGroup workerGroup = new NioEventLoopGroup(5); 代表的是workerGroup来处理客户端的输入。
- b.group(bossGroup, workerGroup) 代表的是bossGroup来处理接受客户端的连接,workerGroup处理客户端输入。如果两个组分工处理,bossGroup的线程数取决于需要监听一个端口,new NioEventLoopGroup(5)不代表写了5就启动5个线程。
- 也可以这样:b.group(bossGroup, bossGroup)这样的话不是分工处理,启动的boss线程既处理客户端连接又处理客户端输入。
Netty这一小块说得很可能有问题,自己还没学过。所以请勿信任本文。我只是给自己此时的记忆做个记录。以后自会改正。
总结
至此,网络IO的演变过程超级粗略的学习完毕。虽然不是特深入,但是对自己理解BIO到epoll非常有帮助。
接下来希望趁热把Netty入个门。
网络通信IO的演变过程(二)(一个门外汉的理解)的更多相关文章
- 网络通信IO的演变过程(一)(一个门外汉的理解)
以前从来不懂IO的底层,只知道一个大概,就是输入输出的管道怼到一起,然后就可以传输数据了. 最近看了周志垒老师的公开课后,醍醐灌顶. 所以做一个简单的记录. 0 计算机组成原理相关 0.1. 计算机的 ...
- 一个门外汉的理解 ~ Faster R-CNN
首先放R-CNN的原理图 显然R-CNN的整过过程大致上划分为四步: 1.输入图片 2.生成候选窗口 3.对局部窗口进行特征提取(CNN) 4.分类(Classify regions) 而R-CNN的 ...
- 从春节送祝福谈谈 IO 模型(二)
上期结合程序员小猿用温奶器给孩子热奶的故事,把面试中常聊的“同步.异步与阻塞.非阻塞有啥区别”简单进行普及. 不过,恰逢春节即将到来,应个景,不妨就通过实现新春送祝福的需求,深入了解一下 Java I ...
- Java IO学习笔记二
Java IO学习笔记二 流的概念 在程序中所有的数据都是以流的方式进行传输或保存的,程序需要数据的时候要使用输入流读取数据,而当程序需要将一些数据保存起来的时候,就要使用输出流完成. 程序中的输入输 ...
- java基础之IO流(二)之字符流
java基础之IO流(二)之字符流 字符流,顾名思义,它是以字符为数据处理单元的流对象,那么字符流和字节流之间的关系又是如何呢? 字符流可以理解为是字节流+字符编码集额一种封装与抽象,专门设计用来读写 ...
- [转]文件IO详解(二)---文件描述符(fd)和inode号的关系
原文:https://www.cnblogs.com/frank-yxs/p/5925563.html 文件IO详解(二)---文件描述符(fd)和inode号的关系 ---------------- ...
- 用socket.io实现websocket的一个简单例子
socket.io 是基于 webSocket 构建的跨浏览器的实时应用. 逛博客发现几个比较好的 一.用socket.io实现websocket的一个简单例子 http://biyeah.iteye ...
- Java IO学习笔记二:DirectByteBuffer与HeapByteBuffer
作者:Grey 原文地址:Java IO学习笔记二:DirectByteBuffer与HeapByteBuffer ByteBuffer.allocate()与ByteBuffer.allocateD ...
- 配置Docker中国区官方镜像http://get.daocloud.io/ 很好的一个源http://get.daocloud.io/#install-docker
https://www.daocloud.io/mirror#accelerator-doc 配置Docker中国区官方镜像http://get.daocloud.io/ 很好的一个源http://g ...
随机推荐
- Java程序中使用Spire Jar包报java.lang.NoSuchMethodError类型错误的解决方法
Jar包功能概述 使用Spire系列的Jar包可以操作Word.Excel.PPT.PDF.Barcode等格式的文件,分别对应使用的jar包是Spire.Doc for Java.Spire.XLS ...
- 【进阶之路】持续集成、持续交付与持续部署(CI/CD)
由来 记得7月份刚刚换工作的时候,中午和老大一起去吃饭,回来的路上老大问我:"南橘,CI/CD有没有研究过?" 我隐隐约约在哪里听过这个名词,但是又想不起来,秉着实事求是的态度,我 ...
- Redis的安装、基本使用以及与SpringBoot的整合
1.概述 Redis 是现在很流行的一个 NoSql 数据库,每秒读取可以达到10万次,能够将数据持久化,支持多种数据结构,容灾性强,易扩展,常用于项目的缓存中间件. 今天我们就来聊聊关于Redis的 ...
- 这些解决 Bug 的套路,你都会了不?
最近整理了我原创的 140 篇编程经验和技术文章,欢迎大家阅读,一起成长!指路:https://t.1yb.co/ARnD 大家好,我是鱼皮. 学编程的过程中,我们会遇到各式各样的 Bug,也常常因为 ...
- 解决umount: /home: device is busy
取消挂载/home时出现umount: /home: device is busy, 原因是因为有程序在使用/home目录,我们可以使用fuser查看那些程序的进程, 然后 ...
- 第七章:网络优化与正则化(Part2)
文章相关 1 第七章:网络优化与正则化(Part1) 2 第七章:网络优化与正则化(Part2) 7.3 参数初始化 神经网络的参数学习是一个非凸优化问题.当使用梯度下降法来进行优化网络参数时,参数初 ...
- 对象赋值在PHP中到底是不是引用?
之前的文章中,我们说过变量赋值的问题,其中有一个问题是对象在进行变量赋值的时候,直接就是引用赋值.那么到底真实情况是怎样呢? 之前变量赋值的文章 PHP的变量赋值 对象引用测试 在继续深入的学习PHP ...
- SQLSTATE[HY000]: General error: 1366 Incorrect string value: '\xF0\x9F\x90\xA3\xF0\x9F...' for column
在做微信公众号保存用户数据时出现这种错误,一直不知道是哪里的原因,后来发现那个用户昵称带着一只兔子表情,由于数据库编码限制不能保存数据,所有需要先编码, 用PHP的函数就是base64_encode, ...
- 大白话透彻讲解 Promise 的使用,读完你就懂了
一.为什么使用Promise? 我们知道 js 执行的时候,一次只能执行一个任务,它会阻塞其他任务.由于这个缺陷导致 js 的所有网络操作,浏览器事件,都必须是异步执行.异步执行可以使用回调函数执行. ...
- 关于selenium添加使用代理ip
最近在爬某个网站,发现这个网站的反爬太厉害了,正常时候的访问有时候都会给你弹出来验证,验证你是不是蜘蛛,而且requests发的请求携带了请求头信息,cookie信息,代理ip,也能识别是爬虫,他应该 ...