Ruby Ruby socket 库中的 BasicSocket#recv 方法的一个疑问

lululau · September 03, 2013 · Last by skandhas replied at September 03, 2013 · 6144 hits
Topic has been selected as the excellent topic by the admin.

之前发现自己一遇到涉及到 TCP/IP 基础知识的问题时就抓瞎,所以决定好好学习一下 TCP/IP 的基础知识,把 UNP 的基础编程这部分粗略过了一遍,然后想回头过一遍 Ruby 的 socket 库的文档,并且希望能把 Ruby socket 库的用法和 UNP 中讲述的内容对应起来。当看到 BasicSocket#recv(maxlen, flags) 这个方法时,遇到了个问题。recv 方法的 flags 参数是一些标识选项,用于控制 socket 的行为,UNP 对 recv(2) 系统服务的 flags 的描述如下:

flags MSG_DONTWAIT

UNP 中提到可能不是所有系统都支持这个 MSG_DONTWAIT 选项,我顺便查下了手册,OS X / Linux 中关于 recv(2) 的手册都没有提到 MSG_DONTWAIT 选项,Linux 的 recv(2) 手册中还提到 POSIX 只规定了 MSG_OOB, MSG_PEEK, and MSG_WAITALL 中三个选项,但是实际上 OS X 和 Linux 都定义了 MSG_DONTWAIT(grep MSG_DONTWAIT /usr/include/linux/socket.h /usr/include/sys/socket.h),并且以下C语言和 Python 实现的服务器程序在设置 DONTWAIT 选项调用 recv 时,确实是表现出了 non-block 的行为(立即返回并抛出一个 EAGAIN):

C 版:

#include <sys/socket.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>


int main(int argc, char const *argv[])
{
    int server_fd, client_fd;
    socklen_t len;
    struct sockaddr_in server_addr, client_addr;

    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(6789);
    inet_aton("127.0.0.1", &server_addr.sin_addr);

    server_fd = socket(AF_INET, SOCK_STREAM, 0);
    bind(server_fd, (struct sockaddr *) &server_addr, sizeof(server_addr));
    listen(server_fd, 5);

    while (1) {
        len = sizeof(client_addr);
        client_fd = accept(server_fd, (struct sockaddr *) &client_addr, &len);
        char buf[10];
        recv(client_fd, buf, 10, MSG_DONTWAIT);
        buf[9] = '\0';
        printf("data: %s\n", buf);
    }
}

Python 版:

#!/usr/bin/env python

from socket import *

server_sock = socket(AF_INET, SOCK_STREAM)
server_sock.bind(("127.0.0.1", 6789))
server_sock.listen(5)

while True:
    client_sock, client_addr = server_sock.accept()
    data = client_sock.recv(10, MSG_DONTWAIT)
    print "data: %s" % data
    client_sock.close()
server_sock.close()

而 Ruby 版的程序在调用 recv 时,却没有表现出来 non-block 的特性,而是一直阻塞在 recv 调用中知道接受到客户端发送过来数据为止:

#!/usr/bin/env ruby

require "socket"

server = TCPServer.new '127.0.0.1', 6789
server.listen 5
while client = server.accept    
    data = client.recv(10, Socket::MSG_DONTWAIT)
    # begin
    #     data = client.recv_nonblock 10
    # rescue Errno::EAGAIN
    #     retry
    # end
    puts "data: #{data.inspect}"
    client.close
end

so, why ?

顺便附上 Client 的代码:

#!/usr/bin/env ruby

require "socket"

sock = TCPSocket.new "127.0.0.1", 6789
while line = gets
    sock.puts line
end

非阻塞的话,用 recv_nonblock 应该可以吧。

Ruby Socket 的 recv 与 我们通常说的 Unix Socket 的 recv 行为是不同的。 Ruby Socket 的 recv 是调用 rsock_s_recvfrom 来实现。说到底,是用了 recvfrom 来实做,而不是使用 recv。 rsock_s_recvfrom 在内部调用了 rb_io_wait_readable 来处理 EAGAIN。如果 errno 是 EAGAIN,则就继续 wait 这个 fd。

while (rb_io_check_closed(fptr),
      rb_thread_wait_fd(arg.fd),
      (slen = BLOCKING_REGION_FD(recvfrom_blocking, &arg)) < 0) {
       if (!rb_io_wait_readable(fptr->fd)) {
           rb_sys_fail("recvfrom(2)");
       }
   if (RBASIC(str)->klass || RSTRING_LEN(str) != buflen) {
       rb_raise(rb_eRuntimeError, "buffer string modified");
   }
   }

对 EAGAIN 的处理时在 rb_io_wait_readable 中:

int
rb_io_wait_readable(int f)
{
    if (f < 0) {
    rb_raise(rb_eIOError, "closed stream");
    }
    switch (errno) {
      case EINTR:
#if defined(ERESTART)
      case ERESTART:
#endif
    rb_thread_check_ints();
    return TRUE;

      case EAGAIN:
#if defined(EWOULDBLOCK) && EWOULDBLOCK != EAGAIN
      case EWOULDBLOCK:
#endif
    rb_thread_wait_fd(f);
    return TRUE;

      default:
    return FALSE;
    }
}

可以用 http://bogomips.org/socket_dontwait/ ,这个 gem 用 recv 实现了一遍。

#2 楼 @kenshin54 我之前 Google 的时候也搜到了这个,但是没认真看。。。谢谢!

#1 楼 @skandhas 非常感谢,明白了,实际上这个 sock 确实是被设成 non-block 了,但是 BasicSocket#recv 的内部实现在调用 recvfrom 的后面加了个 while-select。那还是不明白,Ruby 为什么要这么做,它的意思是你要非阻塞的话,就必须去调 xxxx_nonblock() 方法吗?(另外,通过 strace/dtruss 我还发现 recv(2) 实际上也不是真正的系统服务,它最终也是调的 recvfrom。)

#4 楼 @lululau 嗯。我和你的看法相同。Ruby 的 Socket 是特意将 recv 做成阻塞的。然后又提供 xxxx_nonblock() 方法 来适应 非阻塞的场合。

You need to Sign in before reply, if you don't have an account, please Sign up first.