一直有个疑问,关于 IO 多路复用的。
假设应用程序进程监听在 localhost:80 。这实际上是一个套接字吧。 那么,有需要用 IO 多路复用么?监听在多个套接字上才有必要用 IO 多路复用吧。
不知道哪出了问题,没想明白。请有明白的解释一下。 谢谢。
1
SoloCompany 2016-03-25 10:31:42 +08:00
直接说 socket 不行吗
不要把 server socket 和 client socket 的概念搞混了 |
2
ubear1991 OP 查资料看到了原来监听之后 accept 会生成新的套接字。
|
3
ubear1991 OP @SoloCompany 嗯,大概明白了。我以为就一种套接字。
|
4
ubear1991 OP 又有一个问题。当一个链接建立之后,客户端的数据还是发往 80 端口么,或者是发往新的套接字的端口?
|
5
DuckJK 2016-03-25 11:04:51 +08:00 1
我查了下资料:
服务端 socket 流程是这样的: 先创建 socket()---bind()---listen()--循环 accept() 在 accept 的时候可能会产生阻塞(blocking),,所以又有一种叫做非阻塞(non-blocking),就是在等待客户端访问的时候, CPU 处理其他的事情,这种情况下如果要返回处理客户端,继续要不断的轮询,会浪费 CPU 的时间。然后又有一种新的方法比如(select 、 polll)可以同时监控多个 socket 进行轮询,比如哪些 socket 可以读取,哪些可以写入,这种是最基本的多路复用。 以上可能有错误,我查到的是这么个情况。参考资料: https://translate.googleusercontent.com/translate_c?depth=1&hl=zh-CN&prev=search&rurl=translate.google.com.sg&sl=zh-TW&u=http://beej-zhtw.netdpi.net/07-advanced-technology/7-2-select&usg=ALkJrhg0zwDET71E2ri1ObOL_Xb3et8b8w http://www.keakon.net/2011/09/28/%E5%85%B3%E4%BA%8Esocket%E7%9A%84%E4%B8%80%E4%BA%9B%E5%88%9D%E6%AD%A5%E7%A0%94%E7%A9%B6 http://www.cnblogs.com/Anker/p/3254269.html |
6
wy315700 2016-03-25 11:05:29 +08:00
端口 != 套接字
|
7
ubear1991 OP @DuckJK
我有一点不理解。 accept 就算是阻塞了,服务器的 CPU 不也是在干活,比如处理其他进程。 accept 进程在阻塞队列里等待唤醒事件。 这里面的非阻塞只并不阻塞这个进程,而 CPU 也在处理比如说其他 socket 的通信事件? |
8
hitmanx 2016-03-25 12:35:30 +08:00
@ubear1991 对的,进程阻塞以后操作系统会把这个进程设为 blocked ,依照唤醒需要的资源丢到对应的队列里,以便在将来资源有的时候(在这个例子里就是 server 端端口收到io的时候)唤醒。然后会在状态为 ready 的进程队列里会挑选一个进程运行,并把它的状态设置为 running 。
|
10
mhycy 2016-03-25 12:54:32 +08:00
<论底层基础的重要性>
首先, Accept 这个过程之前必定有一次 bind 的操作 这个操作是通知内核,进程要绑定某个端口,以后进程会使用这个端口进行通讯 然后在 listen 的时候开启一个队列, accept 只是读取这个队列。 如果是阻塞状态,那么队列没数据自然就等待在那了。 如果有新连接进入,那么内核会进行预处理 (判断是否合法,如果合法就分配资源生成一个 socket ,扔到队列) 自然而然的,队列有数据了,就会通知上层的应用进行调度。 (至于这个队列在底层的原理以及 CPU 在等待的时候刚啥,这个涉及到硬件层面的调度,例如中断。。) 而一个链路的唯一标记是包含地址在内的四元组( local_ip, local_port, remote_ip, remote_port ) bind 的过程只是定义了本地的 local_ip, local_port 。 远端而来的 ip/端口是不固定的 |
11
hitmanx 2016-03-25 12:55:36 +08:00
@ubear1991 我的回复是针对你问阻塞对于 CPU (操作系统)、进程是什么关系。 V2EX 上 at 人看不出 at 的是哪一楼这是个问题。关于阻塞和非阻塞的 socket 的用法应该能搜到大量的教程和资源。
|
12
msg7086 2016-03-25 13:00:24 +08:00
顺便你可以多个进程监听同一个端口。
|
13
DuckJK 2016-03-25 13:12:51 +08:00
@ubear1991 阻塞简单来说就是 “ sleep 的技术术语”,我发给你的第一个链接里面有,第三个链接里面会把它具体化容易理解。
|
14
jy01264313 2016-03-25 13:46:46 +08:00
看看五个 IO 模型。
|
15
iMouseWu 2016-03-25 16:01:39 +08:00
@mhycy 我的理解是。如果按照这种 accept()创建了 socket 连接以后,还会有一个接受信息的阻塞过程(数据从内核缓冲区到用户缓冲器)。而 I/O 多路复用拿到的连接不需要这一阻塞步骤,因为内核会把数据从内核缓冲区到用户缓冲器拷贝完成以后才会通知 select 的线程。
可以这么理解嘛? |
17
DuckJK 2016-03-25 16:48:09 +08:00
@iMouseWu 我认为你说的这种是 I/O 多路复用搭配 non-blocking 的问题, https://www.zhihu.com/question/37271342 ,里面的回答是这样的:
Under Linux, select() may report a socket file descriptor as "ready for reading", while nevertheless a subsequent read blocks. This could for example happen when data has arrived but upon examination has wrong checksum and is discarded. There may be other circumstances in which a file descriptor is spuriously reported as ready. Thus it may be safer to use O_NONBLOCK on sockets that should not block. 里面说的是 select 和 read , select 和 read 是两个独立的系统调用,当 select 可读的时候, read 不一定可读。我没有查 accept 的 man 手册,但是我觉得 accept 也是这样的。(我感觉 accept 这句话有问题) 最后结果就是 I/O 多路复用搭配 non-blocking 。至于你说的数据从内核缓冲区 copy 到用户缓冲区,这个我不太了解。 |
18
DuckJK 2016-03-25 16:56:22 +08:00
@iMouseWu http://kenby.iteye.com/blog/1159621 我找到一个 tornado 的例子,里面是 accept 完成之后返回客户端的 socket ,然后创建了两个缓冲区,_read_buffer 和_write_buffer
|
20
iMouseWu 2016-03-25 17:27:28 +08:00
|
21
DuckJK 2016-03-25 17:49:52 +08:00
@iMouseWu 你说的第二个是这样的, I/O 多路复用搭配非阻塞 IO ,也就是说 select 一个 socket ,创建一个线程处理这个 socket ,这时候可以设置 read 是非阻塞(我觉得关键点在这里,这种叫做非阻塞 I/O )。
阻塞 I/O :每次只能调用一次 read 或者 accept ,因为多路复用只会告诉 fd 对应的 socket 可读,但不会告诉有多少数据。所以在处理 handle_read/handle_accept 只能 read/accept 一次,无法确定下一次是否阻塞。所以只能再次循环。 非阻塞 I/O 是循环的 read 或 accept ,直到读完所有的数据(抛出 EWOULDBLOCK 异常) 上面两个都是我从那个知呼里面找到的。 |
22
current 2016-03-25 19:34:56 +08:00
IO multiplexing 在某些情况下必须搭配 Non-blocking socket 使用,在另外一些情况下可以使用 Blocking Sockets ,但不会带来任何好处,也不会降低编程的复杂度
|
23
current 2016-03-25 19:38:34 +08:00
@DuckJK 你举出的那种情况是客观存在的,网卡收包以后会通过驱动通知相关的 socket ,但是如果后续发现是错包的话会将这个包丢掉,然而 io multiplexer 已经发出了 fd 可读事件,这时候使用阻塞读会造成阻塞,但是这应该是几率很低的一种情况,我个人觉得不具有说服力。
我的理解中,必须使用 non-blocking socket 的情况只有 epoll 的 ET mode |
24
current 2016-03-25 19:43:27 +08:00
@iMouseWu IO multiplexing 和后续的并发处理是完全不相关的两件事, select 出一个 fd 以后,是当场处理还是丢到线程池里面处理,还是用更加猥琐的回调+协程方式去处理都是有可能的,这属于并发处理的范畴
IO Multiplexing 的意义在于提供了一种机制让你可以同时监听大量 socket 对于 blocking sockets, 显然你直接去尝试 read 是不可行的,因为不知道会阻塞在哪儿 对于 non-blocking sockets ,不用 io multiplexing 的话,就只能通过 busy polling 去探查 socket 是否可读,这也是不太能接受的做法 |
25
current 2016-03-25 19:49:03 +08:00 1
@DuckJK V2 的回复不能直达楼层好蛋疼。。。
IO multiplexing 在使用的时候必须配合应用层的 buffer ,这是 TCP 的本质决定的, TCP 是一个字节流协议,没有包的概念,不能保证你每次 read 读到的都是一个完整的『应用层的包』,因此通常人们使用类似 readn, readline, readUntil 这类函数来处理 socket 读 在 IO multiplexing 的场合下,针对 blocking sockets 使用这类函数显然是不科学的,如果你希望读到一个 1024bytes 的应用层包,而 socket 上只有 512bytes 数据,那么整个 IO 线程就阻塞了,直到读满了 1024bytes ,科学的做法是 socket 里面有多少数据就读多少,读出多少就写进应用层 buffer ,应用层再从 buffer 里面读应用层的包,上层的应用逻辑通过 buffer 和 TCP 打交道,这和 non-blocking socket 的处理模式是一样的,也就是我在上面说的『在其他场合可以使用 non-blocking sockets ,但不会带来任何好处』 |
26
current 2016-03-25 19:50:17 +08:00
再插一句, epoll 在 ET 模式下的饥饿问题是类似原因
|
27
bicoff9527 2016-03-25 20:27:28 +08:00
套接字是五元组,有一个不同就是别的套接字了,其它的感觉楼上都说得很清楚了,但是我觉得 LZ 实际编程写一下会有更透彻的了解,这里面坑很深
|
28
mhycy 2016-03-25 21:22:45 +08:00 1
|
29
iMouseWu 2016-03-25 22:59:59 +08:00
@current 感谢。
这里有个疑问. "IO Multiplexing 的意义在于提供了一种机制让你可以同时监听大量 socket " 请问下这里监听的 socket 指的是多个服务端 socket 嘛? 如果是服务端,那么可以理解,但是如果是客户端的话,我觉得普通的 accept 也可以达到相同的效果,这个是我一直不理解的地方 |
32
DuckJK 2016-03-26 00:16:37 +08:00
@current 谢谢,另外问一个问题:如果我使用 Python 的 requests 从网上下载压缩文件,然后解压缩:
1. 下载的文件太大,直接采用 reponse.raw.read ,把 socket 当作 file 来操作,然后 zlib 解压缩这样会产生什么样的后果,因为一次 read 的话是读取不完的。 2.直接解压缩 reponse.content 这种是不是和上一个不同? 麻烦了,谢谢。想了好几天。 |
33
mhycy 2016-03-26 00:28:14 +08:00
@DuckJK
zlib 如果支持流解压,那么你只是把数据从一个缓冲区搬到另一个缓冲区而已 如果 zlib 不支持流解压,那么你提供给 zlib 的必须是一个合法的压缩数据 response.content 的动作应该会在 HTTP 头有提供长度的情况下阻塞缓存完整数据 在未提供长度的情况下阻塞到链接断开。 如果是解包时候的处理,同上一段 |
34
DuckJK 2016-03-26 00:36:32 +08:00
@mhycy 请问网卡接收数据,这个数据的流程是怎么样的,先到网卡,然后是到内核处理还是怎么样子?如果 zlib 支持流解压缩,那就是跟压缩数据大小没关系,只要是合法压缩数据,都可以解压缩,只是从 readbuffer 到 zlib 的 buffer ,是不是可以这么理解。
reponse.content 在未提供长度的情况下阻塞到链接断开,这个链接断开是跟 read 的长度有关系么?还是到什么样的情况下链接断开?谢谢啦 |
35
iMouseWu 2016-03-26 10:45:32 +08:00
@current 一般我们在 socket 编码的时候,是先创建一个 serviceSocket 然后 accept,如果有客户端连接的话就产生一个 SocketChannel
那我可不可以理解为,IO 多路复用的优势是在创建两个 serviceSocket 这种场景么? |
36
current 2016-03-26 11:56:20 +08:00
@iMouseWu
我理解错你的意思了,我以为你说的是客户端进行 accept server 监听一个 listen socket ,每次 accept 会产生一个 client socket 使用 IO multiplexing 的时候,将 listen socket 和每次 accept 产生的 client socke 都添加到监听集合中 除此之外, io multiplexing 也可以监听 timerfd , eventfd 等,用于监听计时器事件和计数器事件 |
37
current 2016-03-26 12:00:27 +08:00
@iMouseWu
while 1 { ready_set = select(fd_set) for item in ready_set if item is listen_sock client_sock = accept(listen_sock) fd_set.append(client_sock) else buf = read(item) } 大致这样子 |
38
mhycy 2016-03-26 12:19:53 +08:00 1
@DuckJK
网卡收到的数据通过中断的方式通知内核处理,数据的传递一般是 DMA (不可能 CPU 直接读不然负载太高了) ( DMA 直接拷贝数据到系统内存,当然也有可能是网卡上的缓冲区,这个说不准) (通知或许除了中断还有别的处理方式,这个不了解,因为频繁的中断会非常耗费 CPU 资源) 所有的 Socket 都是内核分配管理的,网卡在通知内核收到包以后,内核就去获取这一个包然后解析 (可以理解成中间有个缓冲区,内核从缓冲区取数据) 内核处理包的过程会验证包是否合法,然后路由到合适的地方 (这个路由包含多种意思,一个是数据包的路由另一个是转发到合适的处理程序[的缓冲区]) 如果是 TCP ,底层的会进行拥塞控制处理(组包、验证、依据情况发回 ACK 信息各种各样) (驱动能在拥塞控制之前接管所有 TCP 处理,这也是锐速的原理) 组成合法的流数据以后再发往给上层应用的缓冲区,通知上层应用 “如果 zlib 支持流解压缩,那就是跟压缩数据大小没关系,只要是合法压缩数据,都可以解压缩,只是从 readbuffer 到 zlib 的 buffer ,是不是可以这么理解。 ” 就是这么回事,我并不知道 zlib 是否支持流解压,也没查。。这个依据实际情况判断 (推广到别的编码方式也一样) “ reponse.content 在未提供长度的情况下阻塞到链接断开,这个链接断开是跟 read 的长度有关系么?还是到什么样的情况下链接断开?谢谢啦” 这个原因是 HTTP 头未提供数据长度的情况下客户端并不知道数据有多大 只能阻塞到 TCP 链路主动断开才能获知已经收完数据 (如果有防火墙的地方,这个数据很可能是损坏的,因为防火墙有可能提前掐断这个链路) |
39
iMouseWu 2016-03-26 13:49:00 +08:00
@current 看了你的解释,大致明白了一点,说下我的理解
我的理解是,按照这种方式对于 clinet 和 service 需要进行多次通信的,I/O 多路复用确实有很大的优势,但是如果是基于 http 这种协议,只需要一次通信的,感觉优势就没有那么的大了。 PS:redis 是不是也是只需要进行一次通信的,但是 redis 也用了 I/O 多路复用,很是不解 |
40
xueqt 2016-08-24 14:25:51 +08:00
借楼插个问题
我看不少人说 “ accept 产生的新的 fd ,端口可以是 listen 的端口,也可以不是,具体由客户端决定” 但是,我看的新的 fd 用的就是 listen 的那个端口,这个可以由 client 指定?如果可以怎么指定 |