1、先浅谈同步和异步:
同步和异步关注的是消息通信机制
所谓同步,就是在发出一个”请求或者调用“时,在没有得到结果之前,这个"请求或者调用"就不返回。但是一旦调用返回,那就是肯定得到返回值
所谓异步,"请求或者调用"发出之后,就直接返回了,不会有任何返回值,返回值由被调用者,通过状态、通知、回调函数等等方式来通知调用者
沿用网上众多通俗例子之一:
阻塞调用是指:调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回
非阻塞调用是指:不能立刻得到结果之前,该调用不会阻塞当前线程
沿用上面例子:
你打电话问书店老板有没有《分布式系统》这本书:
如果是阻塞式调用,你会一直把自己“挂起”,直到得到这本书有没有的结果,
如果是非阻塞式调用,你不管老板有没有告诉你,你自己先一边去玩了, 当然你也要偶尔过几分钟check一下老板有没有返回结果
将一个socket文件描述符,调用recv
设置为阻塞时间为1秒、或5秒、或无限(-1):内核发现当前没有数据,等待1秒、5秒、一直等
设置为非阻塞,不管有没有都立即返回。
3、浅谈同步异步与阻塞非阻塞:
阻塞与非阻塞本身,与是否同步异步无关,这是两个事情。但确有影响。
如上例:你的如何等待(阻塞与非阻塞),跟老板通过什么方式回答你结果(同步或异步),无关。但你是会等还是不等,怎么选择呢。
1、同步:一定要等到结果,势必会阻塞。不存在所谓“同步非阻塞”
2、异步:不需要立即获取到结果,完全没必要阻塞。不存在所谓"异步阻塞"
4、一个网上的最恰到好处的例子:
小张喜欢喝咖啡,同时养了好多狗;
出场:
1. 小张:相当于我们的客户端进程
2. 小狗大黑:阻塞处理的IO函数
3. 小狗大黄:非阻塞处理的IO函数
4. 小狗大白、大红:异步处理的IO函数
同步阻塞:小张派大黑去看咖啡煮好没,大黑等咖啡煮开了才回来;
伪代码:read(fd, recv, recv_len, -1);
同步非阻塞:小张派大黄去看咖啡煮好没,大黄看了一眼就回来了,过了一会,大黄再去看看咖啡煮好没;
伪代码:
while (1) {
res = read(fd, recv, recv_len, 0);
if (res) {
break;
}
sleep(1);
}
"异步"非阻塞:小张派大白和大红去看咖啡煮好没,大白和大红到了厨房后,大白就回来告诉小张,大红已经到厨房啦;
过了一会咖啡煮好了,大红喊大白,大白再告诉小张;
伪代码:select/poll/epoll的伪代码
此“异步”非文中1中的异步
"异步"阻塞:小张派大白和大红去看咖啡煮好没,大白和大红到了厨房后,一起在那等着;
过了一会咖啡煮好了,大红大白一起回到客厅告诉小张;
其他的情况
结论:
1、只有异步IO不会阻塞线程。但平时用的select、poll、epoll,还是直接的read/write,都是同步IO。
Linux的网络应用基本上不涉及异步IO(AIO未关注),也就是说数据的最终读写都得是同步的。
2、问题根源是同步阻塞会拉住线程不放,同步非阻塞则显得很低效
5、select、poll、epoll:
select、poll、epoll,事实上更多解决的是很多个网络IO的统一管理问题,即“多路IO复用”的解决方案;
试想如有10个客户端会请求我们的服务器,并假设没有select、poll、epoll这些系统调用,那么必须自己设法形成一种机制,
至少能不断的检查10个socket文件fd,是否处于可读、可写的状态。而select、poll、epoll就是这个机制的实现。
select的方式:
1、应用程序阻塞的调用select,将服务器监听的端口、全部已创建tcp连接的客户端的端口,即fd_set大集合,通过select系统调用,复制给内核;
2、内核遍历这些端口,找到在协议栈里是活跃的fd们。方式为,置位,细节见fd_set数据结构;
3、内核回复给应用程序fd_set大集合,select遍历fd_set大集合,处理可能发生了的listen、recv事件
缺点:
1、fd_set大集合的传递:用户态和内核态的复制,慢
2、内核需要遍历全部fd_set大集合的每一个fd,用这种方式判断哪些fd是就绪的,慢
3、因为上面1和2都慢,再加上阻塞的select调用,导致应用程序线程也慢,慢
poll的方式:
除以下,与select基本相同。
1、解除了select的最大连接数仅为1024的限制;
2、增加了水平触发机制,即内核回复了某fd的就绪事件,如应用程序未处理,下次还会报。这相当于epoll的边沿处理
epoll的方式:
1、epoll_create:为应用程序,mmap映射一个内存区域,在该内核创建一个"节点":并在该"节点":
1.1、创建红黑树用于存储接下来epoll_ctl系统调用传来的socket fd;
1.2、建立一个双向链表,用于存储准备就绪的事件
1.3、返回给应用程序一个fd,作为这个"节点"标识
2、epoll_ctl:2.1、把需要listen(也包括后面需要接收的)的socket fd,放到epoll文件系统里file对象对应的红黑树上
2.2、内核中断处理程序会注册一个回调函数,当这个句柄的中断到了,就把它放到准备就绪该"节点"的双向链表里
3、epoll_wait:阻塞的系统调用,和select、poll一样的,自己设置阻塞超时时间。
3.1、观察"节点"的双向链表里有没有数据。有数据就返回,没有数据就sleep;
3.2、超时时间到,发现还没数据,返回。
3.3、超时时间到之前,发现有数据,返回有数据的socket fd们。然后依旧是同步的accept、read这些socket fd。并不是"自动"accept、read的。
优点(核心把握1和2):
1、不再每次复制和接收回传全部的fd_set大集合,改为:1.1、需要加入一个新的待listen、recv的socket fd时,通过epoll_ctl通知下内核,从此以后不必再次通知;
1.2、如果epoll_wait发现有数据,仅返回有数据的socket fd,而不是全部fd_set从内核再传回来再慢慢判断
2、内核也不再遍历全部的fd并挨个判断是否就绪,而是依靠回调函数,在socket fd在内核协议栈就绪时,调用回调函数将socket fd事件放入双向链表。这个是epoll总被认为是“异步”的最大原因。
3、epoll加入了边沿触发,即就绪的socket fd被返回给过应用程序后,不论应用程序是否处理,不会再多通知
4、mmap,即内存映射一个内存区域,用户态到内核态的数据复制速度大幅加快
6、总结 那么nginx、libevent之类,他们其实也都是使用epoll,为什么说它们是异步的呢,异步机制体现在哪里呢?
答:它们是在更高层次,体现事件的异步处理性。简单说,由epoll引领read、write的这一套数据收发机制,相当于,对于事件级别的,异步。或者说,nginx、libevent的网络事件的定义及处理,是异步,底层的数据收发,依然是同步。真正的底层数据的异步,像Linux应该是AIO之类的东西。
7、rpc与消息队列:
现实互联网业务开发中,随着各种牛逼东西的诞生,似乎越来越不太多关注epoll、select、poll、accept、read、write了,更多的似乎是“更大的架构”的开发,哈哈哈, 但原理一定要懂的,必须要透彻,否则,libevent、libev、nginx、rpc、kafka。。。。。就会遇到坑再懂了。
1、希望同步得到结果的场合、使用方式模拟本地调用,RPC最合适,RPC不要做所谓异步的事情;
2、如果是一个生产者消费者的业务场景,那就不适合RPC了更适合消息队列,因为这样会限制生产者的生产的速度;
按理说rpc和消息队列没有直接关联,但实际互联网业务开发中,分别代表着同步和异步的业务处理方式,所以在这里放一起描述。
rpc更多是一种基于业务间通讯方式的演变的业务思路。消息队列在互联网业务中的功能如同是一级级的大坝,最大作用就是缓冲,具体到业务,就演变成了:按业务解耦、削波峰、均分流量等等的功能。
8、tcpip:
针对互联网偏业务、工程架构,略掉估计不会考或者平时用的太少的知识(7层协议、4层协议基本知识应该已会,不会自己看,如果有公司问什么太细节的东西说明进去也没什么意思),只看如下方面,往往和日常工作有点关系:
8.1、握手与挥手:
握手:1、客户端发送一个带SYN标志的TCP报文到服务器;
2、服务器端回应客户端,同时带ACK标志和SYN标志。表示对刚才客户端SYN报文的回应;同时询问客户端是否准备好进行数据通讯
3、客户再次回应服务段一个ACK报文
挥手:由于TCP连接是全双工的,因此每个方向都必须单独进行关闭,也就是,当一方完成它的数据发送任务后,就能发送一个FIN来终止这个方向的连接
1、客户端发送一个FIN,用来关闭客户到服务器的数据传送
2、服务器收到这个FIN,它发回一个ACK,确认序号为收到的序号加1,至此客户端无法再发送
3、服务器关闭客户端的连接,发送一个FIN给客户端
4、客户段发回ACK报文确认,并将确认序号设置为收到序号加1,至此服务端也无法再发送
8.2、socket的各个状态:
核心是time_wait和close_wait,有时出问题通过查看是否这两个状态特别多进而差代码是否有问题
1、CLOSED:初始状态
2、LISTEN:服务端监听时状态
3、SYN_RCVD:任何一端接收到SYN
4、SYN_SENT:任何一端发送SYN之后
5、ESTABLISHED:tcp连接创建成功
6、FIN_WAIT_1:当前是ESTABLISHED时,某端主动关闭连接(close),向对方发送FIN之后
7、FIN_WAIT_2:在6的基础上,收到了对端的ACK后
8、TIME_WAIT:在7的基础上,等待2MSL时间的状态。正常情况下下一个状态是回到CLOSED
等待2MSL时间是因为,在6的基础上,某端发送的ACK,对端未收到,此时对端还会重发FIN;
这个2MSL时间就是包容这种未收到的情况,直到让它收到为止。一般为几分钟。
对于一个处理大量短连接的服务器,如果是由服务器主动关闭客户端的连接(非常不推荐服务端主动关闭,应该是客户端主动关闭),
将导致服务器端存在大量的处于TIME_WAIT状态的socket,
甚至比处于Established状态下的socket多的多,严重影响服务器的处理能力,甚至耗尽可用的socket,挂了。
解决方法为:1、服务端避免主动发起关闭;2、设置操作系统,缩短这个时间。
另外,要开启端口复用,即,如果端口忙,但TCP状态位于TIME_WAIT ,那么可以重用端口,否则重用时返回“正在被使用”
9、CLOSING:基本不用管。这个状态是双方同时发送FIN时的情况极罕见。
10、CLOSE_WAIT:在收到对方的FIN且发送ACK后,等待本方应用程序也调用close时的状态,本方应用程序未调用close之前,本方处于此状态
这就是为什么挥手要4次,因为本端可能还得继续发送呢,tcp是双向的,所以得等着本方应用程序也close,不能说对方不发了,本方也跟着结束了
服务端应用程序不要忘记执行close(),否则无法由CLOSE_WAIT到LAST_ACK,导致服务端里边大量CLOSE_WAIT
11、LAST_ACK:在10的基础上,本方应用程序调用close了,等待对方的ACK,期间处于此状态
9、长短连接和连接池
总得连接着的东西用长连接,比如数据库;如果预知是哪些客户端,服务端改用连接池
像一下一下的,典型如网站里普通的各种业务http接口,短连接