网络通信IO的演进过程

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

欢迎小伙伴的关注

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

技术王老五

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值