简单整理了一下IO多路复用技术,主要是为了给Netty铺垫。
可能的疑问
-
为什么不用多线程?
- 在很多rpc模型中可能会用到多线程,但是多线程上下文切换要处理操作句柄,线程多了的时候,代价很高。
-
单线程操作会不会因为堵塞导致数据丢失?
- 不会,因为
DMA
控制器会操作。
- 不会,因为
模型
C10K
是一个优化网络套接字以同时处理大量客户端连接的问题,表示处理 10000 个并发连接。
Socket模型
关于Socket,我看过最简洁的描述就是
Socket可以看成是一个四元组(Source:port, Desc:port)
他就是一个插头,一头插在客户端,一头插在服务端。 他如何运行的呢? 我们可以观察一下系统调用介绍 也就是说在调用的时候,需要传入三个参数,篇幅过长我直接总结,感兴趣的可以通过
man 2 socket
自行查阅。
- domain : 指定地址类型,比如ipv4 ipv6
- type: 通信的类型,比如tcp, udp
- protocol: socket传输协议编号,一般不设置。
返回值就是
On success, a file descriptor for the new socket is returned. On error, -1 is returned, and errno is set to in‐dicate the error.
Socket模型生命周期
简单粗暴的IO模型?
BIO
Blocking
显而易见这是堵塞的IO模型
public class SocketBio {
public static void main(String[] args) throws IOException {
ServerSocket server = new ServerSocket(9999);
System.out.println("listening on 9999");
while (true){
Socket client = server.accept();
System.out.printf("client %s #online\n", client.getPort());
new Thread(new Runnable() {
@Override
public void run() {
InputStream in = null;
try{
in = client.getInputStream();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(in));
while(true){
String dateLine = bufferedReader.readLine();
if(dateLine != null){
System.out.printf("client %s #received: \n",client.getPort(), dateLine);
}else{
System.out.printf("client: %s #closed\n",client.getPort());
client.close();
break;
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}
}
}
NIO
No-Blocking
public class SocketNio {
public static void main(String[] args) throws Exception {
LinkedList clients = new LinkedList<>();
ServerSocketChannel ss = ServerSocketChannel.open();
ss.bind(new InetSocketAddress(9999));
ss.configureBlocking(false);
ByteBuffer buffer = ByteBuffer.allocateDirect(4096);
while(true){
SocketChannel client = ss.accept();
if(client == null){
}else{
client.configureBlocking(false);
int port = client.socket().getPort();
System.out.printf("client: %d #accepted\n", port);
clients.add(client);
}
Iterator iterator = clients.iterator();
while(iterator.hasNext()){
SocketChannel c = iterator.next();
buffer.clear();
int num = c.read(buffer);
if(num > 0){
buffer.flip();
byte[] tmp = new byte[buffer.limit()];
buffer.get(tmp);
System.out.printf("client %d #received: %s \n", c.socket().getPort(), new String(tmp));
}else if(num == -1){
System.out.printf("Client %d disconnected\n", c.socket().getPort());
iterator.remove();
c.close();
}
}
}
}
}
IO多路复用
看上面的BIO
和NIO
的代码,我们就可以发现:
BIO的缺点:
- 进程占据资源
- 上下文切换
- ...
NIO的缺点:
- 当连接多了之后,需要维护的东西很多。
于是可能会想到, 能否只用一个进程去维护多个socket呢?
select
如果有很多请求(但是select限制的最大请求上限是1024)连接到server,select
会对所有请求遍历,记录下有数据到达的请求IO,然后返回记录信息,客户端读取。
所以当请求的数量很多但是数据请求很少的时候, 会造成很多空读取。
int select(int maxfdp1,fd_set *readset,fd_set *writeset,fd_set *exceptset,const struct timeval *timeout)
参数说明
- mafdfp1 描述待轮讯的连接的个数
- fd_set: readset writeset exceptset 指定了让
kernel
读、写、异常的描述 - timeout 描述了一个就绪可以花费多长时间。
原理图(copy来的
poll
poll和select没有太大的区别,都是通过管理多个描述符来进行轮讯,但是poll没有最大连接数限制,与此同时,poll的一个缺点就是: 大量fd的数组被整体复制于用户态和内核的地址空间之间,而不论这些fd是否就绪,她的开销随着fd的数量增大线性增大。
# include
int poll ( struct pollfd * fds, unsigned int nfds, int timeout);
Epoll
epoll是在Linux-2.6中提出的,select
和poll
的增强版。
epoll使用一个文件描述管理多个fd,把用户关系的fd存放到kernel的一个事件表之中,这样用户空间和kernel的copy只需要一次。
大概就是: java端创建一个socket,fd是fd4,bind端口之后开始监听,这时候epoll会调用epoll_create(int size) 创建红黑树空间fd6,这个空间是被监听事件的数目,然后执行epoll_ctl,把fd6和fd4关联起来,当有链接有数据的时候,记录IO的编号,把这个编号存储在一个链表之中,当epoll调用epoll_wait的时候,就把这个事件相关的链表返回,之后调用程序只需要根据记录的信息,读写产生的事件IO即可。
#include
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
先写这么多吧,后续在补充
Muchas gracias. ?Como puedo iniciar sesion?
后续呢?急,在线等