NIO学习系列(一) |
您所在的位置:网站首页 › socket底层的io模型 › NIO学习系列(一) |
一. 理解Socket
一个简单的服务器 public static void main(String[] args) throws IOException { //监听9876端口 ServerSocket serverSocket = new ServerSocket(9876); while (true) { try { //等待客户端连接,此时处于阻塞状态 Socket socket = serverSocket.accept(); doBusiness(socket); } catch (Exception e) { e.printStackTrace(); } } } private static void doBusiness(Socket socket) { try { System.out.println("request started"); InputStream inputStream = socket.getInputStream(); byte[] bytes = new byte[1024]; //2.读取数据,阻塞状态。 inputStream.read(bytes); System.out.println("receive:" + new String(bytes)); PrintWriter writer = new PrintWriter(socket.getOutputStream(), true); writer.println("HTTP/1.1 200 OK"); writer.println("Content-Type:text/html;"); writer.println(); writer.println("hello nio"); writer.close(); socket.close(); System.out.println("request end"); } catch (IOException e) { e.printStackTrace(); } }一个简单的客户端 public static void main(String[] args) throws IOException { //连接9876端口号 Socket socket = new Socket("localhost", 9876); OutputStream outputStream = socket.getOutputStream(); Scanner scanner = new Scanner(System.in); while (scanner.hasNext()) { String nextLine = scanner.nextLine(); // 向服务端发送数据 outputStream.write(nextLine.getBytes()); } }平常开发过程中,我们其实不太会接触到socket,都是直接发起http请求。但是呢,java中http请求底层也是使用socket进行网络通信,因为我们都会利用现成的工具如Tomcat,Tomcat的底层也是socket编程,也就是说和网络打交道,就离不开socket。 socket是什么呢?socket是在应用层和传输层之间的一个抽象层,它把TCP复杂的操作抽象为几个简单的接口供应用层调用,而开发者就无需关心TCP连接相关细节,就可以进行网络编程。简而言之,Socket就是解决服务器和客户端之前进程通信连接的。看下它的核心方法: accept流程 创建ServerSocket:创建一个用于监听某端口号的socket,同时在内核中创建syn队列和accept队列 serverSocket.accept():线程阻塞在此,等待请求进来 client发起connect请求:这是TCP三次握手的第一次syn请求,会将此请求添加到syn队列 当TCP三次握手完成,将此请求从syn队列移到accept队列,此时会唤醒监控socket所在线程 serverSocket.accept()会将请求从accept队列取出,创建新的socket和client进行读写操作参考理解socket connect和accept的实现细节,可见accept就把TCP三次握手的操作封装了起来,而用户进程只需要关心请求来的数据。 inputStream.read()也就是大名鼎鼎的IO,上面例子实现的是传统IO,也叫Blocking IO(BIO)。先简述通用的IO过程,主要分为两步: 等待数据从物理设备复制到内核缓冲区( 等待数据准备好) 将内核缓冲区的数据复制到用户缓冲区为什么没有直接将数据复制到用户缓冲区? 为保证系统安全,禁止应用程序直接访问或操作内核数据,只能通过操作系统指令进行操作。 我们调用read的时候,线程会阻塞在此,等待上面两步执行完成之后,才能继续执行业务代码。而我们都知道IO操作是比CPU慢很多的,如果每个连接进来都需要等待IO操作,那么应用可处理的连接也就太差了。 这里有一个小知识点:read的过程中,如果你打印线程状态的话,其实是RUNNABLE,accept的时候也是RUNNABLE状态,众所周知,传统IO是阻塞IO,应该是BLOCKED或者WAITING都比较好理解。这是为什么呢?我们先看下RUNNABLE的javaDoc:处在runnable状态的线程是在jvm中执行,但是可能是在等待操作系统中如cpu的资源。 所以Java中定义线程的状态和操作系统的状态是有一定区别的,现代操作系统一般都是时分处理器,时间片极短,很难区分ready和running状态,所以Java将其合并为runnable。 IO操作比CPU慢,那当CPU执行到IO操作时,会立马切换,执行调度队列中的其他线程任务,而当前线程会被放到等待队列中,所以此时线程是阻塞状态,不占用CPU。当IO操作完成后,会发出中断信号,CPU会进入中断处理流程,此时之前等待IO的线程会被重新放入调度队列,等待CPU调度。以上是一个简单描述,详细可参考Java线程状态RUNNABLE详解 /** * Thread state for a runnable thread. A thread in the runnable * state is executing in the Java virtual machine but it may * be waiting for other resources from the operating system * such as processor. */ RUNNABLE,对于一个高并发的应用来说,不能够在等待网络IO的过程中浪费时间。那么我们是不是能对这个read方法进行一些优化呢。我们先来分析一下,read方法是同步的,所以每次请求进来都需要等待之前的请求执行结束,也就是并发数是1了。 初步优化方式就是在io读写时开启多线程,这样可以保证多个连接同时处理。这个代码我就不举例了,一般来说都是两种方式,1是每个请求进来直接创建一个新的线程处理,2是使用固定线程池处理。1的问题在于线程的资源相对是昂贵的,在高并发的场景下,会创建大量的线程,在并发下降后又会有线程的销毁,线程创建和销毁都是消耗系统资源的。2的问题是在面对并发非常高的情况下,仍然没有办法应对。那有更进一步的优化方式嘛? 仔细思考一下,问题其实是在io读写的时候是个同步过程,线程必须等待。我们结合前面提到IO过程的两步 如果能够:当内核将这两步都执行结束之后,再通知用户进程,进行读取(这就是AIO) ,那应用处理效率就是最理想化了。不过我们不能一口气吃成个胖子,我们先看看能否把第一步变成异步,第二步不变(这就是NIO) ;若第一步是异步的话,那我们就需要一个线程去管理所有的连接,若有连接第一步完成之后,再通知用户进程,进行读取(这就是IO多路复用) 。这样我们就是把IO模型都引入进来了。 二. I/O模型一般来说IO模型分为两个维度,同步和阻塞。 阻塞:第一步调用recv/recvfrom时,当数据未准备好时是否返回,返回则为非阻塞,反之则为阻塞 同步:第二步等待数据从内核空间复制到用户空间,若未复制完成就返回为异步,反之则是同步 BIO(blocking io):同步阻塞IO 其缺点显而易见:当数据未复制到用户空间时,调用read方法的线程就会一直阻塞。 NIO(non blocking io)同步非阻塞IO 相对于阻塞IO,当用户线程发起read请求时,当内核数据未准备好时,系统会直接返回error,此时用户线程可以先去做些其他事情,只要定时轮询数据准备好即可。其缺点当然就是会产生大量的系统调用,因为不确定数据何时准备好,所以只有不停的轮询。 IO multiplexing:IO多路复用
AIO:异步非阻塞IO
目前网络框架中应用最广的就是IO多路复用,而IO多路复用离不开系统底层提供的几个指令,那就是select/poll/epoll,这里就再学习一下这几个指令。 select关键代码 int select (int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); // fd_set定义 #include //fd_setsize 定义为了1024 #define FD_SETSIZE 1024 #define NFDBITS (8 * sizeof(unsigned long)) #define __FDSET_LONGS (FD_SETSIZE/NFDBITS) typedef struct { unsigned long fds_bits[__FDSET_LONGS]; } fd_set; void FD_SET(int fd, fd_set *fdset) //将fd添加到fdset void FD_CLR(int fd, fd_set *fdset) //从fdset中删除fd void FD_ISSET(int fd, fd_set *fdset) //判断fd是否已存在fdset void FD_ZERO(fd_set *fdset) //初始化fdset内容全为0一般来说,网络请求只关心n 和 fd_set *readfds,n是最大的文件描述符值+1,readfds是个数组,保存对读事件感兴趣的socket的fd(文件描述符:当程序打开一个现有文件(socket)或者创建一个新文件(socket)时,内核向进程返回一个文件描述符,在UNIX、Linux的系统调用中,大量的系统调用都是依赖于文件描述符)。使用select的demo如下: while(1){ //将文件描述符数组每一位全都置为0 FD_ZERO(&rset); //每次while循环都要重新设置要监控的socket for (i = 0; i< 5; i++ ) { FD_SET(fds[i],&rset); } //一直阻塞直到有读事件已ready select(max+1, &rset, NULL, NULL, NULL); for(i=0;i |
今日新闻 |
推荐新闻 |
CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3 |