IO的演进:从BIO到--->NIO再到-->多路复用器-->延伸出netty
BIO的代码验证:
public class Demo {
public static void main(String[] args) throws Exception {
ServerSocket server = new ServerSocket(8090);
System.out.println("step 1 new serverSocket");
while (true){
Socket client = server.accept();
System.out.println("step 2 client"+client.getPort());
new Thread(new Runnable() {
Socket ss;
public Runnable setSs(Socket ss) {
this.ss = ss;
return this;
}
@Override
public void run() {
try {
InputStream inputStream = ss.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
while (true){
System.out.println(reader.readLine());
}
} catch (IOException e) {
e.printStackTrace();
}
}
}.setSs(client)).start();
}
}
}
BIO 在系统调用的具体过程:代码如下
整体流程,在BIO代码中ServerSocket server = new ServerSocket(8090);这个过程中,整体内部函数的调用是,
第一步:kernel创建文件描述符socket(PF_INET,SOCK_STREAM,IPPROTO_IP) = 3,其中3就是文件描述符fd,
第二步,需要绑定端口8090,系统函数调用bind(3..........,sin_port=htons(8090)),其中3为第一步中的文件描述符,8090为端口号
第三步:服务端等待客户端的连接,java代码Socket client = server.accept();系统函数的调用为,accept(3.........,3为文件描述符,上图展示accept(3之后没有任何响应标识阻塞
下面我们看一下bind的系统调用的具体实现:为啥要说这个呢,因为有现成的DEMO,现成的DEMO,本着节约不浪费的原则,我们瞅瞅写的系统函数
上图知道,
第一步是sfd = socket(.......),返回值是sfd,也就是上文中咱们看到的3
第二步:bind(sfd,.......,也就是上文中的bind(3...,是不是匹配上了。
客户端连接:此时客户端发起连接
上文中accept(3...不再阻塞,开始执行,并且生成了文件描述符5
、
客户端阻塞等待客户端传递数据,其中recv(5....,5是上图中的文件描述符
BIO的整体流程如下图所示:
以上就是BIO过程,每个线程对应一个连接
优势:可以接收很多连接
弊端:创建多个线程造成内存的消耗,cpu在线程间切换调度消耗比较大
根源:accept、recv这个系统调用时阻塞的。
这些弊端主要应为内核的函数调用时阻塞的,因此内核提供nonblocking 非阻塞的函数调用
NIO调用的代码
@Test
public void test() throws IOException, InterruptedException {
LinkedList<SocketChannel> clients = new LinkedList<>();
ServerSocketChannel ss = ServerSocketChannel.open();
ss.bind(new InetSocketAddress(9090));
ss.configureBlocking(false);//设置 os 系统调用为 nonblocking
while (true){
Thread.sleep(1000);
SocketChannel client = ss.accept();
if(client == null){
System.out.println("client is null");
}else {
client.configureBlocking(false);
int port = client.socket().getPort();
System.out.println("client port is "+port);
clients.add(client);
}
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
for (SocketChannel channel : clients) {
int read = channel.read(buffer);
if(read > 0){
buffer.flip();
byte[] bytes = new byte[buffer.limit()];
buffer.get(bytes);
String string = new String(bytes);
System.out.println(channel.socket().getPort()+"\t"+string);
buffer.clear();
}
}
}
}
下面是两个客户端同时连接9090的验证:
两个连接,NIO程序依然在继续执行 ↓↓↓↓↓↓↓↓
BIO代码不会继续执行结果↓↓↓↓↓↓↓
NIO的原理基本上和BIO的差不多,只是在函数调用发生变化,有原来的accept(fd,recv(fd.... 返回fd -1阻塞的过程,变成了非阻塞
NIO优势:规避开多个线程造成的cpu频繁切换的问题
弊端:有上图所知,socketchannel每次都要遍历一遍,假设有1000个连接,for循环要循环1000次,但是并不是所有的连接都有数据需要接收
思考:我们只是一次系统调用,把所有的fd都发送给系统,然后监听这些fd,那些fd有信息返回给我们不就完事了
方法:系统调用提供了select函数,可以传递所有fds,返回给我们有用的fds
select函数的实现:Linux下select()系统调用笔记
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *errorfds, struct timeval *timeout);
参数:
nfds:要测试的描述符范围,0~(nfds-1)
返回值:
1.readfds集合中有描述符可读,writefds集合中有描述符可写,errorfds集合中有描述符遇到错误条件时select将返回1,失败返回-1;
2.如果三种情况都没有发生,select将在到达timeout超时时间后,返回0;
3.如果timeout为0,则select将一直阻塞,直到有情况发生。(之前说但凡是阻塞就是浪费CPU资源,但是此处的阻塞,也算是牺牲小我,成就大我了,因为一个阻塞同时监控着所有文件描述符的变化,跟每个描述符都阻塞相比,效率还是高高的……)
关键:
select()函数,就可以用一个进程同时监视很多个文件描述符到输入、输出、错误。select函数返回后,通过FD_ISSET()函数检测是哪个文件描述符状态发生了变化,在进行相应的操作,从而省去了对这么文件描述符的等待操作,提高了CPU的利用率
void FD_ZERO(fd_set *fdset); 将fdset集合清零(初始化)
void FD_CLR(int fd, fd_set *fdset); 清除fd文件描述符
void FD_SET(int fd, fd_set *fdset); 添加fd文件描述符
int FD_ISSET(int fd, fd_set *fdset); 检测fd是否为fdset文件描述符集合中的元素,是返回非0值,否返回0;
其中:数据结构“fd_set”是由打开的文件描述符构成的集合。上面的四个函数用来控制这个集合。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/time.h>
#include <fcntl.h>
#include <sys/ioctl.h>
int main(void)
{
char buffer[128];
int result, nread;
fd_set inputs, testfds;
struct timeval timeout;
FD_ZERO(&inputs);
FD_SET(0,&inputs);
while(1)
{
testfds = inputs;
timeout.tv_sec = 2;
timeout.tv_usec = 500000;
result = select(FD_SETSIZE, &inputs, (fd_set *)NULL, (fd_set *)NULL, &timeout);//@1
switch(result)
{
case 0:printf("timeout\n");break;
case -1:perror("select");exit(1);
default:
if(FD_ISSET(0,&inputs)) //@2
{
ioctl(0,FIONREAD,&nread);
if(nread == 0)
{
printf("keyboard done\n");
exit(0);
}
nread = read(0,buffer,nread);
buffer[nread] = 0;
printf("read %d from keyboard: %s", nread, buffer);
}
break;
}
}
}
select指示有活动发生,我们便可以用FD_ISSET()函数来遍历所有可能的文件描述符,以检查是哪个发生了活动。select与poll属于一类
优点:解决了NIO的缺点,主要就是减少了NIO对于系统的调用
缺点:上面代码@1发现每次循环都要全部的将fds传递给内核,内核并不存储这些fds,
思考:内核存储了这些fds,我们就不用重复传递
策略:epoll
欢迎小伙伴的关注