从 Socket 编程谈谈 IO 模型(三)
快过年啦,估计很多朋友已在摸鱼的路上。而我为了兄弟们年后的追逐,却在苦苦寻觅、规划,导致文章更新晚了些,各位猿粉谅解。
上期分享,我们结合新春送祝福的场景,通过一坨坨的代码让 BIO、NIO 编程过程呈现的淋漓尽致。
本期分享,通过画几张图,再聊 IO 之 Socket 编程的哪些事儿(小猿舞剑,上期意在代码,这期意在图)。
Socket 翻译为插口、槽,名字很有意义,一旦插入网线进行连接,我们的代码便能够通讯。
如图示意,每个 Socket 都包含两条线,也就是两个流(输入流和输出流)。其实建立网络连接就类似电话系统,一端给另一端打电话(port 就像电话号码),而且一直在监听是不是通了,是不是说话啦。
了解过网络底层协议的都知道,通过使用 Socket,使得开发变得简单了很多,因为 Socket 隐藏了大量的网络开发的细节。其实,对于 Java 程序而言,网络的 IO 就像操作顺序文件的 IO 一样。
如上图示意,Socket 编程模型,倒是不复杂,我们拆开去说。
服务端编程步骤,其实图说的已经很清晰,再絮叨两句。
第一步:创建 ServerSocket 对象;
第二步:接受来自客户端的连接请求并返回 Socket;
第三步:从 Socket 获取输入流/输出流;
第三步:根据数据类型将原始输入流/输出流封装为高级流(可选);
第四步:从流中接收/发送消息;
第五步:释放资源。
客户端与服务端编程步骤差不多,也不复杂。
客户端编程步骤图中也说的很清楚了,不过还是要简单归纳一下。
第一步:创建 Socket;
第二步:从连接的 Socket 获取输入流和输出流;
第三步:根据数据类型将原始输入流/输出流封装为高级流(可选);
第四步:从流中接收/发送消息;
第五步:释放资源。
Socket 编程模型总体来说很简单,所以稍微花点功夫,轻轻松松就能照猫画虎撸出能聊天的程序。
接下来我们再画几张图,一起看看 Socket 编程的演进。
如图所示,服务端首先通过 accept() 方法接受客户端的链接,然后创建线程进行处理客户端的请求,最后把响应发给客户端。这种模型坊间称之为传统 BIO 编程模型图。
聪明的你们会发现,这种模型,如果在客户端访问过多的情况下,服务端需要频繁的启动、销毁线程,那么势必会有性能上的开销;另外,线程数过多,也有可能会拖垮服务器,于是就引入了线程池进行改进。
如上图所示,服务端接收到请求之后,封装成 Task 对象,然后交给线程池去执行任务,最后给客户端响应。
通过引入线程池,来管理工作线程的数量,进而避免频繁创建、销毁线程带来的开销,在实际研发中若是并发量较小的应用,这种设计已经足矣,这种模型在坊间称为伪异步 IO 编程模型图。
但是,这种模型,恰恰由于线程池限制了线程的数量,在高并发场景下,请求超过线程池的最大数量时,那么就只能等待,直到线程池中的有空闲的线程才可以被复用。那么,在网络较差、传输较大文件时,是不是就出现了链接超时?!
这或许就是 BIO(同步阻塞) 的劣势吧,那该怎么办呢?于是就有了 NIO 编程模型。
如图示意,NIO 利用了单线程管理一个Selector,而一个Selector管理多个 SocketChannel,也就是管理多个连接,那么就不用为每个连接都创建一个线程,可以有效避免高并发情况下,频繁线程切换等带来的问题。
举个贴近生活的场景,加深一下理解。当下猪肉的行情着实不错,不妨就举个养猪场的栗子。
养猪场有个看护猪圈的人,他每天要做的事儿,就是不停的检查猪圈,如果有猪生病、有猪生仔、有猪跑出去等等,他就把相应的情况记录下来,如果猪场的老板想知道具体情况,只需要问看护猪圈的那个人就行啦。
容我抽象一下,看护猪圈的人就相当于 Selector,每个猪圈就相当于 SocketChannel,而猪场的老板就相当于线程Thread,他可以通过一个Selector管理多个 SocketChannel,进而服务多个 Socket。
好了,今天就扯这么多,没有引入新知识点,一方面在于把前期的知识点系统的串一串;重点是为了,给找我请教的热情粉丝,一个交代,后续有时间会把 Socket 往深里挖一挖,请大家期待。