Looyao's Blog

记录一些点滴

阻塞方式的socket, Select判断读就绪后读多少字节不会阻塞?

| Comments

编写网络服务器时, 我们需要并发处理多个请求, 所以不能阻塞在一个连接上, select就提供了一种解决方案, I/O Multiplexing. 那么当select返回, 我们收到可读通知之后, read多少个字节不会阻塞呢? 答案是只读一次不会阻塞, 读多少无所谓, 一般来说读一个固定的buffer size就可以. 如果多次调用read就可能阻塞程序了. 看下边的demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>
#include <signal.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/select.h>
#include <sys/time.h>
#include <unistd.h>

int init_listen_sock(int port, const char *ip)
{
    int listen_fd;
    struct sockaddr_in servaddr;
    socklen_t reuse = 1;

    listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (listen_fd < 0) {
        fprintf(stderr, "socket error\n");
        return -1;
    }

    /* 设置重用, 防止程序退出后一段时间不能再次运行绑定 */
    if (setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) < 0) {
        fprintf(stderr, "setsockopt SO_REUSEADDR error\n");
        return -1;
    }


    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(port);
    if (!ip) {
        servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    } else {
        inet_pton(AF_INET, ip, &servaddr.sin_addr);
    }

    if (bind(listen_fd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
        fprintf(stderr, "bind error\n");
        return -1;
    }

    if (listen(listen_fd, 256) < 0) {
        fprintf(stderr, "listen error\n");
    }

    return listen_fd;
}

int main(void)
{
    int listen_fd = init_listen_sock(8080, NULL);

    if (listen_fd < 0) {
        return 1;
    }

    fd_set rfds, rfds_sel, wfds, wfds_sel;
    FD_ZERO(&rfds);
    FD_ZERO(&wfds);

    FD_SET(listen_fd, &rfds);

    /* 忽略SIGPIPE, 否则连接断掉时write会终止程序 */
    signal(SIGPIPE, SIG_IGN);

    for ( ;; ) {
        memcpy(&rfds_sel, &rfds, sizeof(fd_set));
        memcpy(&wfds_sel, &wfds, sizeof(fd_set));

        int nfds = select(FD_SETSIZE, &rfds_sel, &wfds_sel, NULL, NULL);
        if (nfds <= 0) {
            if (nfds < 0) {
                if (errno == EINTR) {
                    continue;
                } else {
                    fprintf(stderr, "select error\n");
                    exit(1);
                }
            }
        } else if (nfds > 0) {
            int n = 0;
            int fd = 0;
            for ( ; fd <= FD_SETSIZE && n < nfds; fd++) {
                if (FD_ISSET(fd, &rfds_sel)) { /* 可读 */
                    if (fd == listen_fd) { /* 如果是监听的fd, accept新连接 */
                        struct sockaddr_in cliaddr;
                        socklen_t socklen = sizeof(struct sockaddr);
                        int new_fd = accept(listen_fd, (struct sockaddr *)&cliaddr, &socklen);
                        char *ip = inet_ntoa(cliaddr.sin_addr);
                        fprintf(stdout, "accept fd %d, ip %s\n", new_fd, ip);

                        FD_SET(new_fd, &rfds);
                    } else {
                        char buf[4096];
                        int nread = read(fd, buf, sizeof(buf)); /* 读一次不会阻塞 */
                        /* nread = read(fd, buf, 128); 测试, 这里如果再次读可能会阻塞 */

                        /* TODO: 这里需要分析一下请求HTTP报文, 读到\r\n\r\n代表请求报文头结束, 作为例子, 就不实现了 */
                        if (nread <= 0) { /* nread为0代表连接断了 */
                            FD_CLR(fd, &rfds);
                            FD_CLR(fd, &wfds);
                            close(fd);
                        } else {
                            FD_SET(fd, &wfds);
                        }
                    }
                    n++;
                } else if (FD_ISSET(fd, &wfds_sel)) {
                    char *wbuf = "HTTP/1.1 200 OK\r\nServer: select\r\nContent-Type: text/plain\r\nContent-Length: 5\r\n\r\nhello";
                    int nwrite = write(fd, wbuf, strlen(wbuf));
                    FD_CLR(fd, &wfds);
                    FD_CLR(fd, &rfds);
                    close(fd);

                    n++;
                }
            }
        }
    }

    return 0;
}

这个demo算是简陋的实现了一个HTTP Server, 访问localhost:8080会返回hello, 可以通过curl 命令或者浏览器测试.

上边说读一次不会阻塞也不严谨, man page 里的bugs有这么一条

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编写网络服务程序的时候, 非阻塞socket + select才是比较安全的用法.

Comments