V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
V2EX 提问指南
ubear1991
V2EX  ›  问与答

关于套接字的疑问

  •  
  •   ubear1991 · 2016-03-25 09:55:54 +08:00 · 3137 次点击
    这是一个创建于 3158 天前的主题,其中的信息可能已经有所发展或是发生改变。

    一直有个疑问,关于 IO 多路复用的。

    假设应用程序进程监听在 localhost:80 。这实际上是一个套接字吧。 那么,有需要用 IO 多路复用么?监听在多个套接字上才有必要用 IO 多路复用吧。

    不知道哪出了问题,没想明白。请有明白的解释一下。 谢谢。

    40 条回复    2016-08-24 14:25:51 +08:00
    SoloCompany
        1
    SoloCompany  
       2016-03-25 10:31:42 +08:00
    直接说 socket 不行吗
    不要把 server socket 和 client socket 的概念搞混了
    ubear1991
        2
    ubear1991  
    OP
       2016-03-25 10:32:04 +08:00
    查资料看到了原来监听之后 accept 会生成新的套接字。
    ubear1991
        3
    ubear1991  
    OP
       2016-03-25 10:33:42 +08:00
    @SoloCompany 嗯,大概明白了。我以为就一种套接字。
    ubear1991
        4
    ubear1991  
    OP
       2016-03-25 10:51:55 +08:00
    又有一个问题。当一个链接建立之后,客户端的数据还是发往 80 端口么,或者是发往新的套接字的端口?
    DuckJK
        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
    wy315700
        6
    wy315700  
       2016-03-25 11:05:29 +08:00
    端口 != 套接字
    ubear1991
        7
    ubear1991  
    OP
       2016-03-25 11:42:11 +08:00
    @DuckJK

    我有一点不理解。 accept 就算是阻塞了,服务器的 CPU 不也是在干活,比如处理其他进程。 accept 进程在阻塞队列里等待唤醒事件。

    这里面的非阻塞只并不阻塞这个进程,而 CPU 也在处理比如说其他 socket 的通信事件?
    hitmanx
        8
    hitmanx  
       2016-03-25 12:35:30 +08:00
    @ubear1991 对的,进程阻塞以后操作系统会把这个进程设为 blocked ,依照唤醒需要的资源丢到对应的队列里,以便在将来资源有的时候(在这个例子里就是 server 端端口收到io的时候)唤醒。然后会在状态为 ready 的进程队列里会挑选一个进程运行,并把它的状态设置为 running 。
    ubear1991
        9
    ubear1991  
    OP
       2016-03-25 12:43:31 +08:00
    @hitmanx

    这是阻塞的方式吧?还有非阻塞。
    mhycy
        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/端口是不固定的
    hitmanx
        11
    hitmanx  
       2016-03-25 12:55:36 +08:00
    @ubear1991 我的回复是针对你问阻塞对于 CPU (操作系统)、进程是什么关系。 V2EX 上 at 人看不出 at 的是哪一楼这是个问题。关于阻塞和非阻塞的 socket 的用法应该能搜到大量的教程和资源。
    msg7086
        12
    msg7086  
       2016-03-25 13:00:24 +08:00
    顺便你可以多个进程监听同一个端口。
    DuckJK
        13
    DuckJK  
       2016-03-25 13:12:51 +08:00
    @ubear1991 阻塞简单来说就是 “ sleep 的技术术语”,我发给你的第一个链接里面有,第三个链接里面会把它具体化容易理解。
    jy01264313
        14
    jy01264313  
       2016-03-25 13:46:46 +08:00
    看看五个 IO 模型。
    iMouseWu
        15
    iMouseWu  
       2016-03-25 16:01:39 +08:00
    @mhycy 我的理解是。如果按照这种 accept()创建了 socket 连接以后,还会有一个接受信息的阻塞过程(数据从内核缓冲区到用户缓冲器)。而 I/O 多路复用拿到的连接不需要这一阻塞步骤,因为内核会把数据从内核缓冲区到用户缓冲器拷贝完成以后才会通知 select 的线程。
    可以这么理解嘛?
    ubear1991
        16
    ubear1991  
    OP
       2016-03-25 16:30:03 +08:00
    @iMouseWu

    unix 网络编程里面说 这个是异步 I/O 。
    DuckJK
        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 到用户缓冲区,这个我不太了解。
    DuckJK
        18
    DuckJK  
       2016-03-25 16:56:22 +08:00
    @iMouseWu http://kenby.iteye.com/blog/1159621 我找到一个 tornado 的例子,里面是 accept 完成之后返回客户端的 socket ,然后创建了两个缓冲区,_read_buffer 和_write_buffer
    iMouseWu
        19
    iMouseWu  
       2016-03-25 16:59:31 +08:00
    @ubear1991 抱歉,我记错了。这个是算异步的,阻塞应该是发生在内核到数据完成准备这个阶段
    iMouseWu
        20
    iMouseWu  
       2016-03-25 17:27:28 +08:00
    @DuckJK 不好意思,前面的内核缓存区和用户缓存区有误感谢 @ubear1991 指出,具体看 19 楼。
    我一开始也是如你 17 楼所说的去理解,select 和 accept 可读,read 不一定可读,但是如果这样子的话。我的理解是:
    1.直接 accept,为每个 accept 的 socket 创建一个线程处理这个 socket,在这些线程里面可能会 read 阻塞
    2.I/O 多路复用,select 出来一个 socket,创建一个线程处理这个 socket,在这些线程里面可能会 read 阻塞
    所以感觉 I/O 多路复用没有优势。
    望指正
    DuckJK
        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 异常)

    上面两个都是我从那个知呼里面找到的。
    current
        22
    current  
       2016-03-25 19:34:56 +08:00
    IO multiplexing 在某些情况下必须搭配 Non-blocking socket 使用,在另外一些情况下可以使用 Blocking Sockets ,但不会带来任何好处,也不会降低编程的复杂度
    current
        23
    current  
       2016-03-25 19:38:34 +08:00
    @DuckJK 你举出的那种情况是客观存在的,网卡收包以后会通过驱动通知相关的 socket ,但是如果后续发现是错包的话会将这个包丢掉,然而 io multiplexer 已经发出了 fd 可读事件,这时候使用阻塞读会造成阻塞,但是这应该是几率很低的一种情况,我个人觉得不具有说服力。

    我的理解中,必须使用 non-blocking socket 的情况只有 epoll 的 ET mode
    current
        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 是否可读,这也是不太能接受的做法
    current
        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 ,但不会带来任何好处』
    current
        26
    current  
       2016-03-25 19:50:17 +08:00
    再插一句, epoll 在 ET 模式下的饥饿问题是类似原因
    bicoff9527
        27
    bicoff9527  
       2016-03-25 20:27:28 +08:00
    套接字是五元组,有一个不同就是别的套接字了,其它的感觉楼上都说得很清楚了,但是我觉得 LZ 实际编程写一下会有更透彻的了解,这里面坑很深
    mhycy
        28
    mhycy  
       2016-03-25 21:22:45 +08:00   ❤️ 1
    @current
    针对 23 楼的回复,网卡收包以后是先通知内核,内核处理完了再通知应用层。
    应用层收到的 TCP 数据不一定可靠,但绝对是合法数据。
    (不可靠的原因是校验码碰撞)
    iMouseWu
        29
    iMouseWu  
       2016-03-25 22:59:59 +08:00
    @current 感谢。
    这里有个疑问.
    "IO Multiplexing 的意义在于提供了一种机制让你可以同时监听大量 socket "
    请问下这里监听的 socket 指的是多个服务端 socket 嘛?
    如果是服务端,那么可以理解,但是如果是客户端的话,我觉得普通的 accept 也可以达到相同的效果,这个是我一直不理解的地方
    current
        30
    current  
       2016-03-25 23:53:49 +08:00
    @mhycy 谢谢,这个问题我确实不太了解
    current
        31
    current  
       2016-03-25 23:54:21 +08:00
    @iMouseWu 客户端出于什么情况需要 accept 呢? :)
    DuckJK
        32
    DuckJK  
       2016-03-26 00:16:37 +08:00
    @current 谢谢,另外问一个问题:如果我使用 Python 的 requests 从网上下载压缩文件,然后解压缩:
    1. 下载的文件太大,直接采用 reponse.raw.read ,把 socket 当作 file 来操作,然后 zlib 解压缩这样会产生什么样的后果,因为一次 read 的话是读取不完的。
    2.直接解压缩 reponse.content 这种是不是和上一个不同?

    麻烦了,谢谢。想了好几天。
    mhycy
        33
    mhycy  
       2016-03-26 00:28:14 +08:00
    @DuckJK
    zlib 如果支持流解压,那么你只是把数据从一个缓冲区搬到另一个缓冲区而已
    如果 zlib 不支持流解压,那么你提供给 zlib 的必须是一个合法的压缩数据

    response.content 的动作应该会在 HTTP 头有提供长度的情况下阻塞缓存完整数据
    在未提供长度的情况下阻塞到链接断开。
    如果是解包时候的处理,同上一段
    DuckJK
        34
    DuckJK  
       2016-03-26 00:36:32 +08:00
    @mhycy 请问网卡接收数据,这个数据的流程是怎么样的,先到网卡,然后是到内核处理还是怎么样子?如果 zlib 支持流解压缩,那就是跟压缩数据大小没关系,只要是合法压缩数据,都可以解压缩,只是从 readbuffer 到 zlib 的 buffer ,是不是可以这么理解。

    reponse.content 在未提供长度的情况下阻塞到链接断开,这个链接断开是跟 read 的长度有关系么?还是到什么样的情况下链接断开?谢谢啦
    iMouseWu
        35
    iMouseWu  
       2016-03-26 10:45:32 +08:00
    @current 一般我们在 socket 编码的时候,是先创建一个 serviceSocket 然后 accept,如果有客户端连接的话就产生一个 SocketChannel
    那我可不可以理解为,IO 多路复用的优势是在创建两个 serviceSocket 这种场景么?
    current
        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 等,用于监听计时器事件和计数器事件
    current
        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)
    }

    大致这样子
    mhycy
        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 链路主动断开才能获知已经收完数据
    (如果有防火墙的地方,这个数据很可能是损坏的,因为防火墙有可能提前掐断这个链路)
    iMouseWu
        39
    iMouseWu  
       2016-03-26 13:49:00 +08:00
    @current 看了你的解释,大致明白了一点,说下我的理解
    我的理解是,按照这种方式对于 clinet 和 service 需要进行多次通信的,I/O 多路复用确实有很大的优势,但是如果是基于 http 这种协议,只需要一次通信的,感觉优势就没有那么的大了。
    PS:redis 是不是也是只需要进行一次通信的,但是 redis 也用了 I/O 多路复用,很是不解
    xueqt
        40
    xueqt  
       2016-08-24 14:25:51 +08:00
    借楼插个问题
    我看不少人说 “ accept 产生的新的 fd ,端口可以是 listen 的端口,也可以不是,具体由客户端决定”
    但是,我看的新的 fd 用的就是 listen 的那个端口,这个可以由 client 指定?如果可以怎么指定
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2683 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 27ms · UTC 01:56 · PVG 09:56 · LAX 17:56 · JFK 20:56
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.