如果有不对的地方, 欢迎在评论区指正:
-
bio
1.1 请求-响应模型. 对于接收方, serverSocket.accept() 为每个请求(连接)安排一个线程
1.2浪费(阻塞占比大): socket.getInputStream().read()调用是阻塞的, 实际情况对于常见的web应用, 大家都是长连接, 同一时刻, 阻塞在此在线程会不少.
1.3 风险:线程很容易就上去满了
1.4 对于.1.3风险, 改良: 可以使用线程池, socket封装为task, 往线程池提交任务. 忙不过来就排到任务队列去
1.5 对于1.2 浪费: 没办法, 因为面向流的bio读数据的时候是阻塞的, 就算你同一个线程绑定多个连接, 总不能上一个连接read()不到数据的时候, 线程阻塞. 那这个线程上的其他连接都没戏了 -
nio
2.1 对于1.2浪费, 我们希望, 1个线程具有监听多个连接的能力, 当多个连接的任意一个具有可读数据的时候. 线程可以获取到数据. 这就是io多路复用
这里有几点需要说明:
2.2.1 1个线程监听m个连接(这个m是没有上限的. 你可能有疑惑, 那没有上限, 程序的上限在哪里呢, 这个在后文netty会说. 这个线程池中每个线程基本都均摊了m个不同的连接.) 这个就是多路复用
2.2.2 我们想要达到这个样子, 我给你分配一块内存, 当连接有数据可读的时候, 内核要自动帮我读到这块内存中去, 这个块内存就叫缓冲区吧. (如果缓冲区读满了还是没有读完连接上传来的此轮数据, 没关系, 方正我线程是自旋去读数据的, 下一个循环依然会有这个连接). 内核完成这个操作就减少了用户态内核态之间的切换与数据拷贝, 程序员编程也简单了. 这个缓冲区就不像bio了, bio没有缓冲区, bio是面向流的 单向的. 我这个buffer可读可写 , 维护两个指针就行了, 时分复用一块内存. 区分为可读区可写区. 这样就可以给写操作用了.
2.2.3 内核想要完成多路复用, 前提是系统提供的不阻塞读的系统调用. 如果连接没有数据读就立即返回. 当然, 这其中有基于遍历的 有基于事件回调的, epoll poll等实现的多路复用器 -
所以总结:
-
连接是绑定到固定的线程上的好处
4.1 1个线程监听m个连接, 连接是绑定到固定的线程上. 好处, 站在连接的角度, 这个连接上的数据处理是串行有序的, 是线程安全的, 是可以高效无锁的, 是方便编程的. 这也是netty优雅高效的设计之一.
进一步展开:
4.11 tcp是有序的, 只要连接是绑定到固定的线程上, 连接的处理就是串行的. 也就是说, 即便请求方并发的通过同一个连接发了多个请求过来, 这几个请求是不会乱序的.
4.12 这就解释了为什么 redisson/lettuce 同一个连接上发出的并发请求, 收到的响应依然是和请求同顺序的(redis单线程连接, 哪怕redis高版本的io是多线程的, 只要连接是绑定到固定的io线程上. 所以站在同一个连接上看, redis还是单线程的). redis通讯协议不支持加requestId啥的, 照样能高效(还减少了并发竞争与切换) 把请求和响应对应起来. 而且redis本身很快, 只要没有满命令, lettuce单个连接也是很强的
4.13 和2.2.1相关, 谈不上局限吧. 拿接收方来说, 1个线程监听m个连接, 并且绑定了这几个连接
既然io线程绑了多个连接, 自然要快点处理, 所以io线程不要挂上多余耗时的业务处理 不然把绑定上的其他连接rt增加了.
所以良好的设计是, io线程后面再跟一个业务线程池, io线程负责读写解码心跳等, 线程数量就设定cpu核数或者2倍(因为没有阻塞的情况下, 最大并行就这么多). 业务线程就看服务器性能了, 也和业务耗时有关, dubbo默认200 (为啥不是cpu数, 别闹, 我们还是要提高并发的, 不能要客户干等不是), 而且这种设计也是解耦的, 业务耗时归业务
4.14 业务线程池拿dubbo举例 如何让请求和响应对应呢?
业务线程池如果还是连接会绑定在固定线程上的话, 站在连接角度的一切还是串行的. 只不过是在几个线程之间接力串行.
但是业务线程池往往不会这么做, dubbo不是这样的, 因为每个可读连接都是一份急待处理的业务请求, 需要同优先度的处理, 而不是由于绑在同一个线程上而被迫等待上一个业务请求处理完才能处理. 所以需要一个请求id放在通信协议中. 响应报文也会带, 发送方收到响应后, 按缓存请求id和接下来要处理结果的对象 (一般封装的连接对象上是缓存一个map结构, 请求id为key, 上面挂一个future. 当请求方等待在future.get()的时候, 就可以被唤醒并取到结果了)
4.15 4.13说要加跟一个业务线程池, 那和bio相比强在哪里呢? bio渴望与所有连接数正相关, nio更渴望与同一时间并发的可读连接正相关(bio压根没可读事件等事件, 不阻塞读的话, 没法感知, nio能注册感兴趣的事件). bio是连接数维度的, nio是并发请求数维度的, 还是有一定百分比差距的. 当然如果你的业务就是所有连接都是大报文, 传大对象, 或者高io, 那nio的io线程反而就不行了, 因为nio是想省线程, 按之前说的io线程是cpu数. 不够用. 反而会 增加并发开销, 线程切换, 以及阻塞同线程下绑定的其他连接. 这也就是为啥说netty服务端(比如dubbo , 至于rocketmq要看下才知道)在大报文请求的高并发下, 会有下降的原因. tcp是有序的, 即便是同连接并发的请求, 也一定会等到上一个请求(不管被拆成多个包) 完全被io线程被解析成完整的报文, 丢给业务线程池后才会继续处理下一个请求
-
tomcat也是nio, 有和不同 (此段尚在研究验证中)
5.1 tomcat是servlet规范的实现. 虽然也是nio. 但是要为每一个可读的请求连接分配一个线程(不是多个同连接请求能绑定同一个线程).也就是io线程其实就是业务线程(所以springmvc你没有看到什么线程池). 用了线程池也用了nio, 至少不是bio, bio渴望与所有连接数正相关, nio至少能感知到连接的可读事件, 至少是说 这个连接可读了, 我才提交一个任务到线程池去, 执行完就就行. 下一次可读是下一次的事, 不会一个线程专门阻塞给一个线程服务不管他可读或者读完(bio读完还是自旋在哪里) 也就是说bio是连接数维度的, nio是并发请求数维度的.
5.2 tomcat vs netty 我的理解 (不使用aio情况下, 也不响应式 这些另开一篇)
5.2.1 tomcat 不会出现一个请求阻碍下一个请求
5.2.2 netty 更适合被二次开发, 比如服务端 加一个线程池就行了, 扩展性强
5.2.3 tomcat 是专业web服务器, netty能搞jsp吗, websocket? 如何维护session? 主流web展示层技术兼不兼容? 如何下文件等把这里搞定, 那netty又变成和另一个tomcat了. 专物专用, 都不要贬低. 如果只是http场景不是复杂的情况下, 并且是在小数据的请求上,此时netty优于tomcat, 可以用netty实现http服务器. 因为: https://zhuanlan.zhihu.com/p/433868436 -
nio是非阻塞吗
是的, 不是降低阻塞, 是非阻塞, 因为没数据可读时可以立即返回. (返回你可以干点别的事, 然后再去轮询)
如果你不想轮询, 那得aio了 (这个以后开一篇吧)
但是要注意, 大报文请求的高并发, 如果一个线程绑定了多个连接, 串行处理下, 有等待问题.