socket、网络模型笔记
Socket是什么?
socket是一个文件,也是一个编程模型,本质上是一种管道文件
那么既然是一个文件,就会有文件描述符
同时也是编程接口(Stub)
客户端只有一种socket,即代表服务端的socket,而作为服务端,它有两种socket:
- Server Socket File
- 服务端Socket是虚拟抽象出来的概念,它里面存的是客户端Socket的文件描述符,是操作系统提供的编程模型
- listen(80)
- clientsocket=serversocket.accept()
- 服务端Socket是虚拟抽象出来的概念,它里面存的是客户端Socket的文件描述符,是操作系统提供的编程模型
- Client Socket File:代表着连接进来的客户端
I/O多路复用
-
单线程处理多个Socket
-
核心问题:内核分发消息
内核是非常清楚哪个请求发生变动的,当一个客户端连接进来时,内核是最了解的,如上图所示,当第1580号文件发生变动时,内核是很清楚的,但问题来了,内核怎么通知线程,哪个线程才是需要这个消息的线程呢?这时就内核需要分发,如果有了服务端socket文件这种东西,内核只需要把数据给服务端socket文件即可,但还有个问题就是还有很多客户端socket,如果是客户端socket发生了变动内核该通知哪个线程呢
-
select模型:内核不负责推消息,而由线程主动来内核查询,线程维护一个Socket FD的列表
- 周期性检查是否有FD变动,从客户端来说,有两个维度去思考这个问题,其一是看到Server Socket文件的变动,Server Socket本身就是一个管道文件,所以它只需要不断地从Server Socket中去读取就好了,而Client Socket就需要自行解决了
- 可以通过循环遍历来检查列表中什么元素状态发生了变化
-
epoll模型:内核维护一个高效的二叉搜索树
内核一旦知道什么socket发生了变动,它首先通过自己的红黑树找到这个socket的节点,然后再通过这个节点上描述的数据找到对应的事件、对应的线程,再通知对应的线程,效率非常高
- 已知某个FD发生变化的时候,可以快速知道哪个线程需要处理
-
-
通常来说,性能提升归根结底是算法和数据结构需要重新思考
BIO/NIO/AIO
Blocking IO
- API设计:操作会阻塞线程,而阻塞线程本质上是让线程休眠
- 如:等待Socket连接
- 原理:利用CPU中断
- 阻塞的线程进入休眠,将执行权限交给其他线程
- 优点:阻塞时不会占用系统资源;程序也好理解
- 缺点:高并发场景需要较高的线程数量(资源占用);线程切换有成本
- 适用范围:一般的高并发场景也可以考虑(并行量特别大的场景,如聊天除外)
- 性能是一个千层饼,是一个综合而复杂的问题,在没有压测之前不要形成任何定论(Jmeter可以用用)
Non-Blocking IO
-
API设计:操作不会阻塞线程
读不到就返回null
-
原理:由专门的数据结构负责统计发生的I/O,再允许程序监听变更
- I/O变更发生后,记录在某个特定的数据结构上,比如epoll的红黑树
- 线程可以在数据结构上注册自己关注的
- 文件描述符
- 消息类型(读、写…)
- 通常给线程提供一个方法可以一次性获得自己关注的事件
- 如果拿到:一个事件的集合
- 没有拿到:null
Asynchronous IO
-
API设计:异步编程(基于Future)
本质:异步转同步
-
原理:利用线程池技术、协程技术,调度所有Future的计算
- 通常结合epoll和directmemory技术
N(new)IO
-
NIO的channel设计
-
DirectMapping
因为经常需要将内核中的数据拷贝到用户空间,所以内核中的数据拷贝其实完全可以由内核做完,由内核创建一个堆外的内存,不是JVM堆里的那个内存,它再将这个内存给到JVM的进程,而这样当数据进到网卡,就由内核帮你将数据拷贝到对堆内存当中,需要用的时候就读这个堆外内存的缓冲区就可以了。
- 本质是堆外的缓冲区
- 优化点:减少一次拷贝
-
epoll模型比较适合Non-Blocking(事件驱动)