前言

在网络的初期,网民很少,服务器完全没有压力,通常用一个线程追踪处理一个请求。

其实代码实现大家都知道,就是服务器上有个ServerSocket在某个端口监听,接收到客户端的连接后,会创建一个Socket,并把它交给一个线程进行后续处理。

线程主要从Socket读取客户端传过来的数据,然后进行业务处理,并把结果再写入Socket传回客户端。

由于网络的原因,Socket创建后并不一定能立刻从它上面读取数据,可能需要等一段时间,此时线程也必须一直阻塞着。在向Socket写入数据时,也可能会使线程阻塞。

一个小示例

客户端:创建20个Socket并连接到服务器上,再创建20个线程,每个线程负责一个Socket。

服务器端:接收到这20个连接,创建20个Socket,接着创建20个线程,每个线程负责一个Socket。

为了模拟服务器端的Socket在创建后不能立马读取数据,让客户端的20个线程分别休眠5-10之间的一个随机秒数。

客户端的20个线程会在第5秒到第10秒这段时间内陆陆续续的向服务器端发送数据,服务器端的20个线程也会陆陆续续接收到数据。

public class BioServer {static AtomicInteger counter = new AtomicInteger(0);static SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss"); public static void main(String[] args) {try {ServerSocket ss = new ServerSocket();ss.bind(new InetSocketAddress("localhost", 8080));while (true) {Socket s = ss.accept();processWithNewThread(s);}} catch (Exception e) {e.printStackTrace();}}static void processWithNewThread(Socket s) {Runnable run = () -> {InetSocketAddress rsa = (InetSocketAddress)s.getRemoteSocketAddress();System.out.println(time() + "->" + rsa.getHostName() + ":" + rsa.getPort() + "->" + Thread.currentThread().getId() + ":" + counter.incrementAndGet());try {String result = readBytes(s.getInputStream());System.out.println(time() + "->" + result + "->" + Thread.currentThread().getId() + ":" + counter.getAndDecrement());s.close();} catch (Exception e) {e.printStackTrace();}};new Thread(run).start();}static String readBytes(InputStream is) throws Exception {long start = 0;int total = 0;int count = 0;byte[] bytes = new byte[1024];//开始读数据的时间long begin = System.currentTimeMillis();while ((count = is.read(bytes)) > -1) {if (start < 1) {//第一次读到数据的时间start = System.currentTimeMillis();}total += count;}//读完数据的时间long end = System.currentTimeMillis();return "wait=" + (start - begin) + "ms,read=" + (end - start) + "ms,total=" + total + "bs";}static String time() {return sdf.format(new Date());}
}
public class Client {public static void main(String[] args) {try {for (int i = 0; i < 20; i++) {Socket s = new Socket();s.connect(new InetSocketAddress("localhost", 8080));processWithNewThread(s, i);}} catch (IOException e) {e.printStackTrace();}}static void processWithNewThread(Socket s, int i) {Runnable run = () -> {try {//睡眠随机的5-10秒,模拟数据尚未就绪Thread.sleep((new Random().nextInt(6) + 5) * 1000);//写1M数据,为了拉长服务器端读数据的过程s.getOutputStream().write(prepareBytes());//睡眠1秒,让服务器端把数据读完Thread.sleep(1000);s.close();} catch (Exception e) {e.printStackTrace();}};new Thread(run).start();}static byte[] prepareBytes() {byte[] bytes = new byte[1024*1024*1];for (int i = 0; i < bytes.length; i++) {bytes[i] = 1;}return bytes;}
}
时间->IP:Port->线程Id:当前线程数
15:11:52->127.0.0.1:55201->10:1
15:11:52->127.0.0.1:55203->12:2
15:11:52->127.0.0.1:55204->13:3
15:11:52->127.0.0.1:55207->16:4
15:11:52->127.0.0.1:55208->17:5
15:11:52->127.0.0.1:55202->11:6
15:11:52->127.0.0.1:55205->14:7
15:11:52->127.0.0.1:55206->15:8
15:11:52->127.0.0.1:55209->18:9
15:11:52->127.0.0.1:55210->19:10
15:11:52->127.0.0.1:55213->22:11
15:11:52->127.0.0.1:55214->23:12
15:11:52->127.0.0.1:55217->26:13
15:11:52->127.0.0.1:55211->20:14
15:11:52->127.0.0.1:55218->27:15
15:11:52->127.0.0.1:55212->21:16
15:11:52->127.0.0.1:55215->24:17
15:11:52->127.0.0.1:55216->25:18
15:11:52->127.0.0.1:55219->28:19
15:11:52->127.0.0.1:55220->29:20时间->等待数据的时间,读取数据的时间,总共读取的字节数->线程Id:当前线程数
15:11:58->wait=5012ms,read=1022ms,total=1048576bs->17:20
15:11:58->wait=5021ms,read=1022ms,total=1048576bs->13:19
15:11:58->wait=5034ms,read=1008ms,total=1048576bs->11:18
15:11:58->wait=5046ms,read=1003ms,total=1048576bs->12:17
15:11:58->wait=5038ms,read=1005ms,total=1048576bs->23:16
15:11:58->wait=5037ms,read=1010ms,total=1048576bs->22:15
15:11:59->wait=6001ms,read=1017ms,total=1048576bs->15:14
15:11:59->wait=6016ms,read=1013ms,total=1048576bs->27:13
15:11:59->wait=6011ms,read=1018ms,total=1048576bs->24:12
15:12:00->wait=7005ms,read=1008ms,total=1048576bs->20:11
15:12:00->wait=6999ms,read=1020ms,total=1048576bs->14:10
15:12:00->wait=7019ms,read=1007ms,total=1048576bs->26:9
15:12:00->wait=7012ms,read=1015ms,total=1048576bs->21:8
15:12:00->wait=7023ms,read=1008ms,total=1048576bs->25:7
15:12:01->wait=7999ms,read=1011ms,total=1048576bs->18:6
15:12:02->wait=9026ms,read=1014ms,total=1048576bs->10:5
15:12:02->wait=9005ms,read=1031ms,total=1048576bs->19:4
15:12:03->wait=10007ms,read=1011ms,total=1048576bs->16:3
15:12:03->wait=10006ms,read=1017ms,total=1048576bs->29:2
15:12:03->wait=10010ms,read=1022ms,total=1048576bs->28:1

从列子得出心得

可以看到服务器端确实为每个连接创建一个线程,共创建了20个线程。

客户端进入休眠约5-10秒,模拟连接上数据不就绪,服务器端线程在等待,等待时间约5-10秒。

客户端陆续结束休眠,往连接上写入1M数据,服务器端开始读取数据,整个读取过程约1秒。

可以看到,服务器端的工作线程会把时间花在“等待数据”和“读取数据”这两个过程上。

有两个不好的点

一是有很多客户端同时发起请求的话,服务器端要创建很多的线程,可能会因为超过了上限而造成崩溃。
二是每个线程的大部分时光中都是在阻塞着,无事可干,造成极大的资源浪费。

开头已经说了那个年代网民很少,所以,不可能会有大量请求同时过来。至于资源浪费就浪费吧,反正闲着也是闲着。

再来个简单的小例子

饭店共有10张桌子,且配备了10位服务员。只要有客人来了,大堂经理就把客人带到一张桌子,并安排一位服务员全程陪同。

即使客人暂时不需要服务,服务员也一直在旁边站着。可能觉着是一种浪费,其实非也,这就是尊贵的VIP服务。

其实,VIP映射的是一对一的模型,主要体现在“专用”上或“私有”上。

真正的多路复用技术

又可以举个小列子

一条小水渠里水在流,在一端往里倒入大量乒乓球,在另一端用网进行过滤,把乒乓球和水流分开。

这就是一个比较“土”的多路复用,首先在发射端把多种信号或数据进行“混合”,接着是在通道上进行传输,最后在接收端“分离”出自己需要的信号或数据。

相信大家都看出来了,这里的重点其实就是处理好“混合”和“分离”,对于不同的信号或数据,有不同的处理方法。

比如以前的有线电视是模拟信号,即电磁波。一家一般只有一根信号线,但可以同时接多个电视,每个电视任意换台,互不影响。

这是由于不同频率的波可以混合和分离。(当然,可能不是十分准确,明白意思就行了。)

再比如城市的高铁站一般都有数个站台供高铁(同时)停靠,但城市间的高铁轨道单方向只有一条,如何保证那么多趟高铁安全运行呢?

很明显是分时使用,每趟高铁都有自己的时刻。多趟高铁按不同的时刻出站相当于混合,按不同的时刻进站相当于分离。

总结一下,多路指的是多种不同的信号或数据或其它事物,复用指的是共用同一个物理链路或通道或载体。

您先看着,我一会再过来

一对一服务是典型的有钱任性,虽然响应及时、服务周到,但不是每个人都能享受的,毕竟还是“屌丝”多嘛,那就来个共享服务吧。

所以实际当中更多的情况是,客人坐下后,会给他一个菜单,让他先看着,反正也不可能立马点餐,服务员就去忙别的了。

可能不时的会有服务员从客人身旁经过,发现客人还没有点餐,就会主动去询问现在需要点餐吗?

如果需要,服务员就给你写菜单,如果不需要,服务员就继续往前走了。

这种情况饭店整体运行的也很好,但是服务员人数少多了。现在服务10桌客人,4个服务员绰绰有余。(这节省的可都是纯利润呀。)

因为10桌客人同时需要服务的情况几乎是不会发生的,绝大部分情况都是错开的。如果真有的话,那就等会好了,又不是120/119,人命关天的。

回到代码里,情况与之非常相似,完全可以采用相同的理论去处理。

连接建立后,找个地方把它放到那里,可以暂时先不管它,反正此时也没有数据可读。

但是数据早晚会到来的,所以,要不时的去询问每个连接有数据没有,有的话就读取数据,没有的话就继续不管它。

其实这个模式在Java里早就有了,就是Java NIO,这里的大写字母“N”是单词“New”,即“新”的意思,主要是为了和上面的“一对一”进行区分。

先铺垫一下吧

现在需要把Socket交互的过程再稍微细化一些。客户端先请求连接,connect,服务器端然后接受连接,accept,然后客户端再向连接写入数据,write,接着服务器端从连接上读出数据,read。

和打电话的场景一样,主叫拨号,connect,被叫接听,accept,主叫说话,speak,被叫聆听,listen。主叫给被叫打电话,说明主叫找被叫有事,所以被叫关注的是接通电话,听对方说。

客户端主动向服务器端发起请求,说明客户端找服务器端有事,所以服务器端关注的是接受请求,读取对方传来的数据。这里把接受请求,读取数据称为服务器端感兴趣的操作。

在Java NIO中,接受请求的操作,用OP_ACCEPT表示,读取数据的操作,用OP_READ表示。

我决定先过一遍饭店的场景,让首次接触Java NIO的同学不那么迷茫。就是把常规的场景进行了定向整理,稍微有点刻意,明白意思就行了。

1、专门设立一个“跑腿”服务员,工作职责单一,就是问问客人是否需要服务。

2、站在门口接待客人,本来是大堂经理的工作,但是他不愿意在门口盯着,于是就委托给跑腿服务员,你帮我盯着,有人来了告诉我。

于是跑腿服务员就有了一个任务,替大堂经理盯梢。终于来客人了,跑腿服务员赶紧告诉了大堂经理。

3、大堂经理把客人带到座位上,对跑腿服务员说,客人接下来肯定是要点餐的,但是现在在看菜单,不知道什么时候能看好,所以你不时的过来问问,看需不需要点餐,需要的话就再喊来一个“点餐”服务员给客人写菜单。

于是跑腿服务员就又多了一个任务,就是盯着这桌客人,不时来问问,如果需要服务的话,就叫点餐服务员过来服务。

4、跑腿服务员在某次询问中,客人终于决定点餐了,跑题服务员赶紧找来一个点餐服务员为客人写菜单。

5、就这样,跑腿服务员既要盯着门外新过来的客人,也要盯着门内已经就坐的客人。新客人来了,通知大堂经理去接待。就坐的客人决定点餐了,通知点餐服务员去写菜单。

事情就这样一直循环的持续下去,一切,都挺好。角色明确,职责单一,配合很好。

大堂经理和点餐服务员是需求的提供者或实现者,跑腿服务员是需求的发现者,并识别出需求的种类,需要接待的交给大堂经理,需要点餐的交给点餐服务员。

哈哈,Java NIO来啦

代码的写法非常的固定,可以配合着后面的解说来看,这样就好理解了,如下:

public class NioServer {static int clientCount = 0;static AtomicInteger counter = new AtomicInteger(0);static SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss"); public static void main(String[] args) {try {Selector selector = Selector.open();ServerSocketChannel ssc = ServerSocketChannel.open();ssc.configureBlocking(false);ssc.register(selector, SelectionKey.OP_ACCEPT);ssc.bind(new InetSocketAddress("localhost", 8080));while (true) {selector.select();Set<SelectionKey> keys = selector.selectedKeys();Iterator<SelectionKey> iterator = keys.iterator();while (iterator.hasNext()) {SelectionKey key = iterator.next();iterator.remove();if (key.isAcceptable()) {ServerSocketChannel ssc1 = (ServerSocketChannel)key.channel();SocketChannel sc = null;while ((sc = ssc1.accept()) != null) {sc.configureBlocking(false);sc.register(selector, SelectionKey.OP_READ);InetSocketAddress rsa = (InetSocketAddress)sc.socket().getRemoteSocketAddress();System.out.println(time() + "->" + rsa.getHostName() + ":" + rsa.getPort() + "->" + Thread.currentThread().getId() + ":" + (++clientCount));}} else if (key.isReadable()) {//先将“读”从感兴趣操作移出,待把数据从通道中读完后,再把“读”添加到感兴趣操作中//否则,该通道会一直被选出来key.interestOps(key.interestOps() & (~ SelectionKey.OP_READ));processWithNewThread((SocketChannel)key.channel(), key);}}}} catch (Exception e) {e.printStackTrace();}}static void processWithNewThread(SocketChannel sc, SelectionKey key) {Runnable run = () -> {counter.incrementAndGet();try {String result = readBytes(sc);//把“读”加进去key.interestOps(key.interestOps() | SelectionKey.OP_READ);System.out.println(time() + "->" + result + "->" + Thread.currentThread().getId() + ":" + counter.get());sc.close();} catch (Exception e) {e.printStackTrace();}counter.decrementAndGet();};new Thread(run).start();}static String readBytes(SocketChannel sc) throws Exception {long start = 0;int total = 0;int count = 0;ByteBuffer bb = ByteBuffer.allocate(1024);//开始读数据的时间long begin = System.currentTimeMillis();while ((count = sc.read(bb)) > -1) {if (start < 1) {//第一次读到数据的时间start = System.currentTimeMillis();}total += count;bb.clear();}//读完数据的时间long end = System.currentTimeMillis();return "wait=" + (start - begin) + "ms,read=" + (end - start) + "ms,total=" + total + "bs";}static String time() {return sdf.format(new Date());}
}

它的大致处理过程如下:
1、定义一个选择器,Selector。

相当于设立一个跑腿服务员。

2、定义一个服务器端套接字通道,ServerSocketChannel,并配置为非阻塞的。

相等于聘请了一位大堂经理。

3、将套接字通道注册到选择器上,并把感兴趣的操作设置为OP_ACCEPT。

相当于大堂经理给跑腿服务员说,帮我盯着门外,有客人来了告诉我。

4、进入死循环,选择器不时的进行选择。

相当于跑腿服务员一遍又一遍的去询问、去转悠。

5、选择器终于选择出了通道,发现通道是需要Acceptable的。

相当于跑腿服务员终于发现门外来客人了,客人是需要接待的。

6、于是服务器端套接字接受了这个通道,开始处理。

相当于跑腿服务员把大堂经理叫来了,大堂经理开始着手接待。

7、把新接受的通道配置为非阻塞的,并把它也注册到了选择器上,该通道感兴趣的操作为OP_READ。

相当于大堂经理把客人带到座位上,给了客人菜单,并又把客人委托给跑腿服务员,说客人接下来肯定是要点餐的,你不时的来问问。

8、选择器继续不时的进行选择着。

相当于跑腿服务员继续不时的询问着、转悠着。

9、选择器终于又选择出了通道,这次发现通道是需要Readable的。

相当于跑腿服务员终于发现了一桌客人有了需求,是需要点餐的。

10、把这个通道交给了一个新的工作线程去处理。

相当于跑腿服务员叫来了点餐服务员,点餐服务员开始为客人写菜单。

11、这个工作线程处理完后,就被回收了,可以再去处理其它通道。

相当于点餐服务员写好菜单后,就走了,可以再去为其他客人写菜单。

12、选择器继续着重复的选择工作,不知道什么时候是个头。

相当于跑腿服务员继续着重复的询问、转悠,不知道未来在何方。

相信你已经看出来了,大堂经理相当于服务器端套接字,跑腿服务员相当于选择器,点餐服务员相当于Worker线程。

参考

参考

20200321——IO 多路复用相关推荐

  1. 漫谈五种IO模型(主讲IO多路复用)

    首先引用levin的回答让我们理清楚五种IO模型 1.阻塞I/O模型 老李去火车站买票,排队三天买到一张退票. 耗费:在车站吃喝拉撒睡 3天,其他事一件没干. 2.非阻塞I/O模型 老李去火车站买票, ...

  2. Python:通过一个小案例深入理解IO多路复用

    通过一个小案例深入理解IO多路复用 假如我们现在有这样一个普通的需求,写一个简单的爬虫来爬取校花网的主页 import requests import timestart = time.time()u ...

  3. 聊一个不常见的面试题:为什么数据库连接池不采用 IO 多路复用?

    欢迎关注方志朋的博客,回复"666"获面试宝典 今天我们聊一个不常见的 Java 面试题:为什么数据库连接池不采用 IO 多路复用? 这是一个非常好的问题.IO多路复用被视为是非常 ...

  4. 为什么数据库连接池不采用 IO 多路复用?

    欢迎关注方志朋的博客,回复"666"获面试宝典 接着,今天我们聊一个不常见的 Java 面试题:为什么数据库连接池不采用 IO 多路复用? 这是一个非常好的问题.IO多路复用被视为 ...

  5. Linux IO多路复用之Select简史

    内容目录 前言早期的UnixTCP/IP诞生后终端复用套接字章节回顾结论引用 前言 最近我一直在思考 Linux 中的多路复用,即 epoll(7)[1]系统调用.我很好奇 epoll与Windows ...

  6. 为什么数据库连接池不采用IO多路复用?

    文章来源:https://sourl.cn/q8fbw3 今天我们聊一个不常见的 Java 面试题:为什么数据库连接池不采用 IO 多路复用? 这是一个非常好的问题.IO多路复用被视为是非常好的性能助 ...

  7. 【NIO】IO多路复用

    上节,我们讲解了阻塞和非阻塞,我们今天讲第三种IO模型,这就是IO多路复用. 引入多路复用的原因 实际上,在服务端与客户端一对一通信的时候,同步阻塞式地读写并不会有太大的问题,最典型的就是两个对等机器 ...

  8. IO模式和IO多路复用

    前言 前天看redis相关的博文里面提到了epoll,就搜了一下,发现这篇文章 Linux IO模式及 select.poll.epoll详解,讲的很好,收获很大.这里根据自己的理解总结一下. IO模 ...

  9. IO多路复用之poll

    1.基本知识 poll的机制与select类似,与select在本质上没有多大差别,管理多个描述符也是进行轮询,根据描述符的状态进行处理,但是poll没有最大文件描述符数量的限制.poll和selec ...

最新文章

  1. 长征五号复飞成功:史上最重最大,2020月岩采样火星探测都要靠它
  2. PIL库自我学习总结及应用(美白,磨皮,搞笑图片处理)
  3. 如何提高Google Adsense单价:点击率篇
  4. cmw500综合测试仪使用_山西优质三相直流电阻测试仪图片-南电合创
  5. windows 7搭建流媒体服务
  6. 基于LINQ to SQL的WEB开发三层架构(2)
  7. 收藏 | 使用合成数据集做目标检测
  8. PHP代码更新后画面不更新,为什么我的PHP代码不能更新SQL
  9. Bootstrap排版——HTML元素的样式重定义
  10. uCOS-II中的任务切换机制
  11. linux mysql 开发_Linux64下mysql安装和开发
  12. 计算机病毒发展简史,计算机病毒分析_计算机病毒简史
  13. C语言中逻辑非和取反的不同
  14. python学习笔记(6)
  15. 杰理之ANS 参数【篇】
  16. 一文获取36个Python开源项目,平均Star 1667,精选自5000个项目
  17. “云上进化——2022全球AI生物制药大赛”来袭,30万奖池等你来战!
  18. 穿透路由器,解决内网远程桌面等
  19. 程序员为维持游戏开发被迫炒股,一年内反赚1600万;雷军退任天星银行董事会主席;华为开源Karmada将捐赠CNCF...
  20. 毕业设计 单片机风速测量系统 - 物联网 嵌入式 stm32 arduino

热门文章

  1. python自动输入密码_Python实现自动输密码
  2. eclipse下载安装、配置tomcat、Maven、lombok
  3. torch基本功能介绍
  4. EKL日志平台架构概括
  5. 为什么美团股价大跌:疫情影响、阿里竞争与模式弊病
  6. python控制键盘输入_神技能:Python控制键盘鼠标
  7. 使用AutoJS实现2019天猫双11喵币自动领取
  8. EasyExcel自定义表头
  9. 重要知识:身份证复印件的正确使用方法
  10. 2021SC@SDUSC Zxing开源代码(十七)Zxing代码解析——一维码