关于网络和IO

鉴于最近课上已经有点开始懵,所以在这里普及一下关于网络和IO的基础知识

网络基础知识

本来我写了感想,结果码成天书,故删除.推荐一个视频地址https://www.bilibili.com/video/av53978866/?p=28

IO

BIO线程模型:

传统BIO的阻塞点:

  • socket.accept();
  • inputstrean.read();

缺点:单线程,同一时间只能处理一个客户端,如果同时连上两个客户端,只能处理第一个客户端,第二个客户端虽然能连上但是服务器端不做处理

解决办法,拿到socket之后,把它交给另一个线程做单独处理

但是服务器端的线程并不是无限的,如果来了很多客户端,服务端会因为起过多线程,资源耗尽

解决方法,服务器端线程池,由线程池去拉起线程

Executors.newFixedThreadPool(100);

本质依然是去new Thread,只不过对数量可以做一个限制,超过了就丢掉或者队列

BIO模型总结

优点:一个线程为一个客户端服务,服务的质量好

缺点:需要为每一个客户端分配一个线程,并发连接的数量巨大时,县城所有用资源和cpu线程切换带来的开销巨大

NIO线程模型

服务器端只关心socket,客户端关心的是io事件

由此,在服务器端抽象出一个selector,为socket管理者,意思是将大门交给selector看着,监听是否有accpet事件

listenSelector采用轮询的方式监听selector上是否由需要处理的事件

nio模型中没有new任何线程的概念,只有一个线程在处理

selector按照事件来处理,就是事件驱动,也就是多路复用模型

缺点:selector只有一个太忙,需要处理所有的socket,所以衍生出了netty框架

IO模型概念

阻塞:客户端发来请求(在收到服务器请求之前必须等着,不能去干其他活)->服务器端收到请求->服务器端读取数据->服务器端读取完成->返回给客户端->客户端拿到数据去继续自己的任务

个人理解:除非svip,一般人没这待遇.

非阻塞:客户端发来请求(在收到服务器请求之前没必要非得等着,可以去干其他活)->服务器端收到请求->服务器端读取数据->客户端不停的问:我要的数据准备好了吗?服务器:还没呢,等一会.客户端不停的问:我要的数据准备好了吗?服务器:还没呢,等一会.客户端不停的问:我要的数据准备好了吗?服务器:准备好了,你拿走吧->返回给客户端->客户端拿到数据去继续自己的任务

个人理解:服务器的内心独白:客户端你是sb么?问个没完你不累?

复用模型selector:客户端发来请求(在收到服务器请求之前没必要非得等着,可以去干其他活)->服务器端收到请求->服务器端找个selector给你->客户端不停的问selector:我要的数据准备好了吗?selector去服务器申请干活并回答:还没呢,等一会,客户端不停的问selector:我要的数据准备好了吗?selector去服务器申请干活并回答:还没呢,等一会,客户端不停的问selector:我要的数据准备好了吗?selector:准备好了,你拿走吧->返回给客户端->客户端拿到数据去继续自己的任务->返回给客户端->客户端拿到数据去继续自己的任务

个人理解:除了抽象出一个selector,剩下与非阻塞没什么区别,在非阻塞中是每次都新拉起一个线程去回答客户端,这次是由线程池管理服务器端的线程,仅此而已

复用模型epoll:客户端发来请求(在收到服务器请求之前没必要非得等着,可以去干其他活)->服务器端收到请求->服务器端找个epoll给你->epoll说了:我懂你要干什么了客户端,你回家吧,请你闭嘴->服务器端开始读取数据->读取好了之后返回数据给客户端-

个人理解:客户端不必非得一直问一直问,相当于减轻了客户端的压力,变相的提升了服务器端的压力

对于一次IO访问,数据会先被拷贝到内核的缓冲区中,然后才会从内核的缓冲区拷贝到应用程序的地址空间。需要经历两个阶段:

  • 准备数据
  • 将数据从内核缓冲区拷贝到进程地址空间

由于存在这两个阶段,Linux产生了下面五种IO模型(以socket为例)

阻塞式IO:

当用户进程调用了recvfrom等阻塞方法时,内核进入IO的第1个阶段:准备数据(内核需要等待足够的数据再拷贝)这个过程需要等待,用户进程会被阻塞,等内核将数据准备好,然后拷贝到用户地址空间,内核返回结果,用户进程才从阻塞态进入就绪态

Linux中默认情况下所有的socket都是阻塞的

非阻塞式IO:

当用户进程发出read操作时,如果kernel中的数据还没有准备好,那么它并不会block用户进程,而是立刻返回一个error。

用户进程判断结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作

一旦kernel中的数据准备好了,并且又再次收到了用户进程的system call,那么它马上就将数据拷贝到了用户内存,然后返回

非阻塞IO模式下用户进程需要不断地询问内核的数据准备好了没有

IO多路复用:

通过一种机制,一个进程可以监视多个文件描述符(套接字描述符)一旦某个文件描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作(这样就不需要每个用户进程不断的询问内核数据准备好了没)

常用的IO多路复用方式有selectpollepoll

信号驱动IO:

内核文件描述符就绪后,通过信号通知用户进程,用户进程再通过系统调用读取数据。

此方式属于同步IO(实际读取数据到用户进程缓存的工作仍然是由用户进程自己负责的)

异步IO:

用户进程发起read操作之后,立刻就可以开始去做其它的事。内核收到一个异步IO read之后,会立刻返回,不会阻塞用户进程。

内核会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,内核会给用户进程发送一个signal告诉它read操作完成了

拓展:为啥出现IO多路复用

如果一个I/O流进来,我们就开启一个进程处理这个I/O流。那么假设现在有一百万个I/O流进来,那我们就需要开启一百万个进程一一对应处理这些I/O流(这就是传统意义下的多进程并发处理)。思考一下,一百万个进程,你的CPU占有率会多高,这个实现方式及其的不合理。所以人们提出了I/O多路复用这个模型,一个线程,通过记录I/O流的状态来同时管理多个I/O,可以提高服务器的吞吐能力