2 minute read

Socket小知识

创建socket要三个参数,

第一个是它的domain:一般来说是AF_INET或者AF_UNIX。前者可以连internet,后者连本地unix文件系统里的socket。前者的address是sockaddr_in,后者的address是sockaddr_un.

第二个是它的数据类型:一般来说是SOCK_STREAM或者SOCK_DGRAM。众所周知TCP是字节流(stream),UDP是报文(DataGRAM)。所以分别对应了两个socket type。

第三个是protocol类型,一般置0,表示自动选择。

服务端基本流程

先自己搞个socket(参数和上面一样)。socket()

再给socket取个名字(bind一个标识符,like ip地址+port)。bind()

这个时候就可以listen()了。我之前一直理解错了,以为listen()会block。其实是不会的,它的唯一作用就是标记socket为“passive”(即用来监听),并且建立一个监听队列。超过队列容量的请求会refuse掉。

真正建立连接是之后的accept()。这个时候如果没有要接收的新连接,才会block。

server v1: AF_UNIX

如下定义socket和server_address。

sockaddr_un server_address;
server_sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
server_address.sun_family = AF_UNIX;
strcpy(server_address.sun_path, my_server);

连接建立之后会在当前目录创一个my_server文件。也印证了AF_UNIX domain是UNIX本地文件系统相关。

server v2: AF_INET

只是server_address定义不太一样了。

sockaddr_in addr;
int sock_fd = socket(AF_INET, SOCK_STREAM, 0);
addr.sin_addr.s_addr = inet_addr("127.0.0.1");
addr.sin_family = AF_INET;
addr.sin_port = htons(8080);

inet_addr()可以方便地将ip字符串转化为32位无符号整数。

htons()是关于byte order的。它将host的byte-order转换为网络中的byte-order。比如x86是little-endian,但是网络是big-endian.

可以用netstats -A inet -n查看所有的tcp/udp连接。在刚刚启动完client之后看,会有一个TIME_WAIT状态的连接,这就是我们刚才的连接。可以看到port是8080,和我们设置的一样。但是如果不加htons()的话就会变成另外一个port。

timed_wait

server v3: multi-connection

考虑新建一个子进程来处理accept之后的流程(也是工作最多的流程),主进程直接继续listen。

signal(SIGCHLD, SIG_IGN);

while (true) {
    printf("server waiting\n");
    // listen does NOT block.
    listen(sock_fd, 5);
    sockaddr client_addr;
    socklen_t client_addr_len;
    // accept BLOCKS if there's no new connections.
    int client_fd = accept(sock_fd, &client_addr, &client_addr_len);
    if (fork() == 0) {
        sleep(5);
        char out[] = "haha"; 
        write(client_fd, out, sizeof(out));
        close(client_fd);
        exit(0);
    }
    else close(client_fd);
}

其实用pthread也行,但懒得搞了。

server v4: select

所谓select,就是监听一堆fd,如果其中有fd是readable / writable / exception的话就通知你。

可以通过fd_set (一个bitset)来指定你要监听哪些fd。

在下面的程序中,我们先监听sock_fd(服务器socket)。

  1. 当有新连接请求时,sock_fd会触发readable。这时accept并且将新的client_fd加入监听。
  2. client_fd触发readable的时候说明client传来了信息。
    1. 信息可能是EOF。这时read不到(nread == 0)。那就可以移除对该client_fd的监听,并且关闭连接。
    2. 信息不是EOF。转入正常处理逻辑。

这里还有一个点:select函数是会block的,直到有事件到来。当然,你也可以设置block的timeout。

fd_set fds, test_fds;
FD_ZERO(&fds);
FD_SET(sock_fd, &fds);

while (true) {
    test_fds = fds;
    printf("server waiting\n");
    //  If timeout is specified as NULL, select() blocks
    //  indefinitely waiting for a file descriptor to become
    //  ready.
    //  By syh: An incoming connection on sock_fd is also seen as a READABLE activity.
    //  Hence it's captured by `select`.
    int res = select(FD_SETSIZE, &test_fds, NULL, NULL, NULL);
    if (res < 1) {
        printf("error!\n");
        return -1;
    } 
    for (int i = 0; i < FD_SETSIZE; i++) {
        if (!FD_ISSET(i, &test_fds)) continue;
        if (i == sock_fd) {
            sockaddr client_addr;
            socklen_t client_addr_len;
            int client_fd = accept(sock_fd, &client_addr, &client_addr_len);
            FD_SET(client_fd, &fds);
            printf("adding client %d to fds\n", client_fd);
        } else {
            int nread;
            ioctl(i, FIONREAD, &nread);
            if (!nread) {
                printf("removing connection %d\n", i);
                FD_CLR(i, &fds);
                close(i);
            } else {
                char buf[128];
                read(i, &buf, nread); buf[nread] = 0;
                printf("received from connection %d: %s\n", i, buf);
                char out[] = "haha"; 
                write(i, out, sizeof(out));
            }
        }
    }
}

server v5: epoll

epoll与select不同,先注册一些events。当任意event被触发时,epoll_wait()返回,并填充预先设置的event数组。之后只要遍历这些event就可以了。

比select明显好的地方在于不需要遍历整个fd_set了。另外看上去和select也没啥区别。

void register_event(int epfd, int ops, int target_fd) {
    struct epoll_event event;
    event.events = ops;
    event.data.fd = target_fd; // 别忘了这句!!!
    epoll_ctl(epfd, EPOLL_CTL_ADD, target_fd, &event);
}
void unregister_event(int epfd, int ops, int target_fd) {
    struct epoll_event event;
    event.events = ops;
    epoll_ctl(epfd, EPOLL_CTL_DEL, target_fd, &event);
}

struct epoll_event ep_events[20];
int main() {
    sockaddr_in addr;
    int sock_fd = socket(AF_INET, SOCK_STREAM, 0);
    addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    addr.sin_family = AF_INET;
    addr.sin_port = htons(8080);
    bind(sock_fd, (const sockaddr*)&addr, sizeof(addr));

    listen(sock_fd, 5); // listen does NOT block
    int epfd = epoll_create(10);
    register_event(epfd, EPOLLIN, sock_fd);

    while (true) {
        printf("server waiting\n");
        int num_events = epoll_wait(epfd, ep_events, 5, -1); // timeout = -1,即block
        //printf("??");
        if (num_events < 0) {
            printf("error!\n");
            return -1;
        } 
        for (int i = 0; i < num_events; i++) {
            int fd = ep_events[i].data.fd;
            if (fd == sock_fd) {
                sockaddr_in client_addr;
                socklen_t client_addr_len = sizeof(client_addr);
                int client_fd = accept(sock_fd, (sockaddr*)&client_addr, &client_addr_len);
                register_event(epfd, EPOLLIN, client_fd);
                printf("adding client %s:%d\n", inet_ntoa(client_addr.sin_addr), client_addr.sin_port);

            } else {
                char buf[128];
                int nread = read(fd, &buf, 128);
                if (nread < 0) {
                    perror("read error!\n");
                    close (fd);
                    return -1;
                }
                else if (!nread) {
                    printf("removing connection %d\n", i);
                    unregister_event(epfd, EPOLLIN, fd);
                    close(fd);
                } else {
                    buf[nread] = 0;
                    printf("received from connection %d: %s\n", fd, buf);
                    char out[] = "haha"; 
                    write(fd, out, sizeof(out));
                }
            }
        }
    }
}