尽管有轮询文件描述符,但是不可靠的http客户端

时间:2015-02-23 13:31:02

标签: c sockets ocaml unix-socket

我正在尝试在OCaml中编写一个简单的HTTP客户端。我明白使用像cohttp等库更容易。我这样做是为了我自己,所以不需要提出建议。

这是我的代码。

module Connection = struct
    let sock_fd =
        let s_fd = Unix.socket Unix.PF_INET Unix.SOCK_STREAM 0 in
        Unix.setsockopt s_fd Unix.TCP_NODELAY true;
        s_fd

    let read_timeout = 10.0

    let read_from_sock () =
        let buff_size = 4096 in
        let buff = Bytes.create buff_size in
        let rec read_all response =
            let (read_fds, _, _) = Unix.select [sock_fd] [] [] read_timeout in
            match read_fds with
            | [] -> response
            | (read_fd :: _) -> begin
                let _ = Unix.read read_fd buff 0 buff_size in
                let current_response = response ^ buff in
                read_all current_response
            end in
        read_all ""

    let write_to_sock str =
        let len = String.length str in
        let _ = Unix.write sock_fd str 0 len in ()

    let make_request request serv_addr =
        Unix.connect sock_fd serv_addr;
        write_to_sock request

    class connection address port =
        object
            val serv_addr = Unix.ADDR_INET (Unix.inet_addr_of_string address, port)

            method get_response (request: string) =
                make_request request serv_addr;
                let response = read_from_sock () in
                Printf.printf "%s\n" response;
                Unix.shutdown sock_fd Unix.SHUTDOWN_ALL;
                Unix.close sock_fd
        end

    let create address port = new connection address port
end

let connection = Connection.create "54.175.219.8" 80;;
connection#get_response "GET / HTTP/1.1\r\nHost: www.httpbin.org\r\n\r\n"

正如我之前发布的那样 - 如果你发现它有用 - 我会想象一个(非常粗糙的)C等价物是这样的:

int sock_fd = socket(PF_INET, SOCK_STREAM);
setsockopt(sock_fd, TCP_NODELAY, 1);

serv_addr addr {"54.175.219.8", 80};
connect(sock_fd, &serv_addr);
write(sock_fd, "GET / HTTP/1.1\r\nHost: www.httpbin.org\r\n\r\n");

char buffer[512];

while (sock_fd = select(sock_fd, 10.0)) {
    if (!sock_fd) break;
    read(sock_fd, &buffer);
    printf("%s\n", buffer);
    flush(stdout);
}

shutdown(sock_fd, SHUTDOWN_ALL);
close(sock_fd);

当我执行此操作时,我得到的结果非常多变。有一次,我确实得到了整个页面。但大多数时候,它会在整个页面中被切断大约80%。我尝试增加超时无济于事。

我想如果我对文件描述符进行了轮询,我就能够可靠地知道何时没有像这个博客suggests这样的数据。看起来这个方法是对循环的改进,直到读取大小小于buffer_size,但我猜不是吗?我错过了什么?

更新

我编辑了我的代码以检查读取大小是否小于缓冲区大小。但是,这似乎是多余的。如果有更多要读取,select将返回文件描述符。如果没有更多要阅读,它将不会,我将只返回我读过的内容。这是新代码:

let r = Unix.read read_fd buff 0 buff_size in
let current_response = response ^ buff in
if r < buff_size
then current_response
else read_all response

但实际上这是错的。这完全取消了轮询文件描述符的点。也许问题仍然是阅读不到buff_size数据...但我真的不知道我可能处理它的任何其他方式。无论读取什么(无论&lt; buff_size与否),仍将附加到响应中。 read_all将尝试完成读取,直到select不再返回文件描述符,此时,不再需要阅读。

最终解决方案(感谢@ivg):

let read_from_sock () =
    let buff_size = 4096 in
    let buff = Bytes.create buff_size in
    let rec read_all response =
        let (read_fds, _, _) = Unix.select [sock_fd] [] [] read_timeout in
        let rec read_all_helper current_response =
            match read_fds with
            | [] -> current_response
            | (read_fd :: _) -> begin
                let r = Unix.read read_fd buff 0 buff_size in
                let current_response = response ^ (String. sub buff 0 r) in
                if r < buff_size then read_all current_response
                else read_all_helper current_response
            end in
        read_all_helper response in
    read_all ""

1 个答案:

答案 0 :(得分:2)

是的,根据您以前的帖子,这实际上就是我期待的代码问题。这是邪恶的根源:

let _ = Unix.read read_fd buff 0 buff_size in

您无法忽略阅读结果,因为无法保证,read调用将准确读取buff_size,它可以返回更少的数据(所谓的&#34;短读&#34)。 write调用也是同样的问题。因此,您需要仔细使用缓冲区,以便在短读取后重建数据。另一个问题是,呼叫可以被信号中断,但我认为你现在没有打到这个。