1 信息格式:在计算机系统通信的过程中,为了保证信息能够被处理,信息也会被做成特定的格式,而且要保证处理者能够明白这种格式。常用的信息格式如下:
XML JSON ProtocalBuffer 甚至是自己自定义格式 (类似与两个人用同一种语言沟通)
2 网络协议 是TCP还是HTTP协议(声音传播的载体—空气)
3 通信方式 异步IO 同步IO,阻塞或非阻塞(沟通的模式)
首先来说下阻塞IO的模型,就是socket的模式,它是利用的TCP/UDP的网络协议,通信方式是阻塞
当服务端建立了一个socket链接后,就会阻塞在那里,直到接受到来自客户端的请求才会去执行,同样的,客户端向服务端发送请求后也会阻塞在那里,直到有请求的返回才会去执行下面的逻辑
这样的模式当然不符合在高并发下的业务需求。于是很自然的想到利用多线程。在服务端这里,让多个线程去分别执行请求,在客户端这里,让一个线程去专门发送请求,其他线程同时去执行其他的逻辑。
我们可以通过查看 cat /proc/sys/kernel/threads-max的方式看到linux系统最大支持的线程数,并可以增加这个值,可是线程数越多,cpu用于线程切换的时间就越长,用于真正业务处理的资源就越少。
当然我们也可以用threadPoolExcuter的方式来减少cup线程的创建和切换,但是最终会发现,通过增加线程的方式来处理处理高并发,只是治标没有治本。
问题的关键是为什么accept()方法会阻塞,答案就是它的内部实现是使用操作系统级别的同步网络IO模型
PS:
阻塞式IO和非阻塞式IO:这两个概念是程序级别的。主要描述当程序请求操作系统IO后,如果网络IO没有准备好,那程序该如何处理,前者等待,后者继续执行(使用线程一直轮询,直到IO资源准备好了)
同步IO和非同步IO:这两个概念是操作系统级别的。主要描述的是操作系统在收到程序请求网络IO操作后,如果网络IO没有准备好,该如何响应程序的问题:前者不响应,直到网络IO资源准备好,后者返回一个标记(好让程序和自己
知道以后的数据往哪里通知),当网络IO资源准备好以后,在用事件机制返回给程序。
通过上面的分析,对阻塞IO进行改进:
第一次改进:通过让TCP连接和数据读取的这两个过程加上socket.setSoTimeout(10) ,并通过for循环的方式来探知是否有网络IO的到来。把阻塞变成了非阻塞。但这都是同步的
我们知道accept()和read()方法阻塞的根本原因是底层接受数据时采用了操作系统级别的同步IO的工作方式。这里只是在程序层面将阻塞方式变成了非阻塞方式。
第二次改进:加上多线程的支持,但是改进效果有限。
第三次改进:(考虑下一个服务员,多份菜单,由客人自己写菜单,由一个服务员统一收取菜单)多路复用IO模型在应用层工作效率比BIO模型快的本质原因是,前者不再使用操作系统级别的
同步IO模型。多路复用IO目前具体的实现主要包括 select poll epoll kqueue 后两种比前两种效率要好
多路复用IO技术最适合高并发场景,1毫秒内有成千上万个连接请求。其他场景下发挥不了多路复用IO技术的优势。
而且实际上客户点是否使用多路复用IO技术对整个服务端系统架构性能提升相关性不大。
多路复用IO的重要概念:
1 channel
channel通道,是一个用来完成应用程序和操作系统交互事件,传递内容的渠道,注意是连接到操作系统。一个通道会有一个专属的文件状态描述符。既然是和操作系统进行内容传递,
那就说明应用程序可以通过通道从操作系统读取数据,也可以通过通道向操作系统写数据。
所有被selector注册的通道,只能是继承了selectableChannel类的子类,其中有几个关键的Channel通道实现。
ServerSocketChannel:应用服务器端程序的监听通道,只有通过这个通道,应用程序才能向操作系统注册支持多路复用IO的端口监听,它同时支持UDP协议和TCP协议。
SocketChannel : TCP Socket套接字的监听通道,一个Socket套接字对应了一个客户端(IP和端口)到服务器端(IP和端口)的通信连接
DatagramChannel:UDP数据报文的监听通道。
2 Buffer
为了保证每个通道的数据读写速度,java NIO框架为每一种需要支持数据读写的通道集成了Buffer的支持。
例如ServerSocketChannel通道值支持对OP_ACCEPT事件的监听,它是不能直接进行网络数据内容的读写的,所以ServerSocketChannel是没有集成Buffer的
Buffe有两种工作模式:写模式和读模式,在读模式下,应用程序只能从Buffer中读取数据,不能进行写操作,但是在写模式下,应用程序可以进行读操作,这就表示可能会出现
脏读的情况。所以一旦决定要从Buffer中读取数据,就一定要将Buffer的状态改为读模式。
position 缓存区目前正在操作的数据位置
limit 缓存区最大可以进行操作的位置 缓存区的读写状态正式由这个属性控制的
capacity :缓存去的最大容量。这个容量是在缓存区创建的时候指定的,由于高并发时的通道的数量往往会很庞大,所以每个缓存区的容量最好不要过大。
3 Selector
selector的作用如下:
事件订阅和channel管理:应用程序将向selector对象注册需要它关注的channel,以及具体的某一个channl会对哪些IO事件感兴趣。selector中也会维护一个已经注册的channel的容器。
轮询代理:应用层不再通过阻塞模式或者非阻塞模式直接询问系统事件有没有发生,而是由selector代其询问。
实现不同操作系统的支持:作为上层的JVM,必须要为不同操作系统的多路复用IO实现编写不同的代码
目前介绍的阻塞式IO,非阻塞式IO,多路复用IO,这些都是基于操作系统级别对同步IO的实现。所谓同步IO就是:只要上层系统询问我是否有某个事件发生了,否则我不会主动告诉上层系统事件发生了。
异步IO又成为AIO,则是采用 订阅-通知的工作模式,即应用程序向操作系统注册IO监听,然后继续做自己的事情。当操作系统发生IO事件,并且准备好数据后,再主动通知应用程序,触发相应的函数。
在java NIO框架中,说到了一个重要概念是Selector ,它负责代替应用查询所有已注册的通道到操作系统中进行IO事件轮询,管理当前注册的通道集合,定位发生事件的通道等操作,但是java AIO框架中,由于应用程序不是
轮询的方式,而是订阅-通知的方式,所以不再需要selector了,改由channel通道直接到操作系统注册监听了
在java AIO框架中,只实现了两种网络IO通道,AsynchronousServerSocketChannel 服务器监听通道,AsynchronousSocketChannel socket套件字通道
java NIO 和javaAIO框架,除了因为操作系统的实现不一样而去掉了selector,其他重要概念都是相似的。
------------------------------------------
为什么使用线程池。
线程是一个操作系统级别的概念,也就是说操作系统负责线程的创建,挂起,运行,阻塞和终止操作。而操作系统创建线程,切换线程状态,终结线程都要进行CPU调度。这是一个耗费时间和系统资源的事情。
另一方面,目前在大多数生产环境上所面临的技术背景一般是,处理某一次请求的时间是非常短暂的,但是请求数量却是非常巨大。在这种技术背景下,如果我们为每一个请求都单独创建一个线程,那么物理机上所有的资源都被操作系统创建线程,切换线程状态,销毁线程这些操作所占用,用于业务请求处理的资源反而减少了。所以最理想的处理方式是,将处理请求的线程数量控制在一个范围内。