C / socket · 2022年3月25日 0

C语言 异步Socket编程

常见的socket编程的api有

  • socket() 创建socket fd标识符
  • setsocketopt() 设置socket工作参数
  • bind() 绑定socket到本地的ip + port
  • listen() 启动服务 监听socket fd
  • accept() 接受一个socket连接,阻塞等待socket连接

socket函数

创建一个socket的内核对象 返回的是一个fd(file descriptor),关联于当前进程的所有fd的一个自增的id号,映射到内核是一个socket的对象地址

创建一个IPv4的TCP Socket方法如下

// PF_INET 使用IPv4协议
// SOCK_STREAM 使用顺序、可靠的双向数据流
// IPPROTO_TCP 使用TCP协议
int server_socket_fd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);

setsocketopt函数 设置socket工作参数

该函数可以设置任意socket的所有可配置参数详细可查看手册

设置socket address重用,如果不设置的话重启的时候会报错

错误代码 EADDRINUSE

提示信息 Address already in use

为什么会这样呢? 是因为TCP的连接断开后 TCP的资源并没有立即释放,而是进入了TIME_WAIT状态,该状态是为了保证TCP的可靠关闭,该状态会持续2分钟

如果服务器异常终止,TCP并不会直接被回收,因为TCP是一个可靠的服务,所以内核会自动维护TCP进入TIME_WAIT状态,向当前连接的客户端继续应答TCP关闭的ACK消息

int isReuse = 1;
setsockopt(server_socket_fd, SOL_SOCKET, SO_REUSEADDR, (const char *) &isReuse, sizeof isReuse);

bind函数

绑定socket到一个特定的地址+端口和协议上,如果不绑定的话 调用listen、connect函数内核会自动分配一个随机的地址和端口号,服务端需要绑定固定的端口所以listen前一定需要绑定一个固定的端口协议上

如下代码是绑定socket到 本地的0.0.0.0:80端口上

// 确定本地服务地址 + 端口号
struct sockaddr_in server_address = {0};
server_address.sin_family = AF_INET;
server_address.sin_addr.s_addr = htonl(INADDR_ANY);
server_address.sin_port = htons(8082);

bind(server_socket_fd, (const struct sockaddr *) &server_address, sizeof(server_address));

listen函数

启动对socket指定的ip+port的监听,可以接受远端的客户端连接

MAX_CONNECTIONS 是自己定义的服务可以接受的最大连接数

errno = listen(server_socket_fd, MAX_CONNECTIONS);

accept函数

该函数是阻塞式的,调用该函数如果没有客户端的话,则进程会被阻塞,该函数不会返回,直到有一个客户端连接才会返回对应客户端的socket fd

构建一个IPv4的TCP地址结构,然后传递给accept函数,该函数会一直等待客户端连接才返回

struct sockaddr_in client_address = {0};
int client_socket_length = sizeof(struct sockaddr_in);
int client_fd = accept(master_socket_fd, &client_address, &client_socket_length);

异步Socket编程如何实现?

linux提供了一个select函数通过轮询一批指定的fd状态来实现异步的socket操作相关的api如下

  • fd_set fd集合数据类型 只能通过下面的FD_XX()宏操作
  • FD_ZERO(&fdset) 清空指定的fd合集
  • FD_SET(fd, &fdset) 向指定合集添加fd
  • FD_ISSET(fd, &fdset) 检查指定fd 是否存在于fd合集中
  • FD_COPY(&src, &dist) 复制src合集到dist中
  • FD_CLR(fd, &fdset) 从指定合集中删除指定fd
  • select 检查fd合集状态

select 函数

该函数用于检查给定的fd合集是否可操作,而且可以设置超时时间

select函数会修改传入的三个fd合集内容,函数执行成功后,会返回一个可执行操作的fd数量,传入的集合中只会保留下需要操作的fd

所以select函数后就可以通过FD_ISSET宏来检查对应的fd是否可以操作了

如果返回值等于0则没有fd可操作,如果返回值小于0则select函数出错

函数原型如下

int select(
    int nfds,
    fd_set *restrict readfds,
    fd_set *restrict writefds,
    fd_set *restrict errorfds,
    struct timeval *restrict timeout
);

nfds 检查次数

指的是 readfds、writefds、errorfds、三个合集中所有的fd的最大值 + 1,表示select函数对传入的每个合集检查的次数 – 1

假设readfds中设置了两个fd,10和20,writefds中设置了1个fd 30,这时候nfds传入的值应该是30 + 1个

readfds、writefds、errorfds 检查的fd合集

需要检查的三种类型的fd合集,如果某个没有直接传入NULL即可,该集合会在函数执行完成后被修改为只有需要操作的fd集合

timeout 超时时间

如果该参数传入NULL的话 select函数会阻塞

如果该参数传入一个空的 struct timeval 结构 则select会检查完所有的fd集合并返回变动的fd数量

如果传入的结构限制了执行时间的话,超时select也会返回,但是可能会有些集合没有遍历完成

demo如下

#include <stdio.h>
#include <libc.h>

#define printf(...) printf(__VA_ARGS__); printf("\n");

#define MAX_CONNECTIONS 10

struct session {
    // socket 读写端
    int socket_fd;
    // 会话链表
    struct session *next;
};

struct session *root_session = NULL;


struct session *disconnect(struct session *s) {
    struct session *prev = NULL, *ps = root_session;
    while (ps != NULL) {
        if (ps == s) {
            if (prev == NULL) {
                root_session = ps->next;
            } else {
                prev->next = ps->next;
            }
            close(ps->socket_fd);
            free(ps);
            break;
        }
        prev = ps;
        ps = ps->next;
    }

    return root_session;
}

struct session *new_session() {
    struct session *s = malloc(sizeof(struct session));
    memset(s, 0, sizeof(struct session));
    return s;
}

int main() {
    // 创建socket对象
    int server_socket_fd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
// 确定本地服务地址 + 端口号
    struct sockaddr_in server_address = {0};
    server_address.sin_family = AF_INET;
    server_address.sin_addr.s_addr = htonl(INADDR_ANY);
    server_address.sin_port = htons(8082);

    int isReuse = 1;

    int errno = setsockopt(server_socket_fd, SOL_SOCKET, SO_REUSEADDR, (const char *) &isReuse, sizeof isReuse);
    if (errno < 0) {
        printf("SO_REUSEADDR socket error = %d", errno);
        return errno;
    }

    errno = bind(server_socket_fd, (const struct sockaddr *) &server_address, sizeof(server_address));
    if (errno < 0) {
        printf("bind socket error = %d", errno);
        return errno;
    }

    errno = listen(server_socket_fd, MAX_CONNECTIONS);

    if (errno < 0) {
        printf("listen socket error = %d", errno);
        return errno;
    }

    printf(
            "socket server listened to %d",
            ntohs(server_address.sin_port)
    );
    fflush(stdout);

    fd_set read_fds;
    fd_set write_fds;
    struct timeval timeout = {0}; // 初始化一个空的超时结构
    int max_fd = server_socket_fd; // maxfd等于服务器的fd

    // 死循环操作等待socket连接
    while (1) {
        FD_ZERO(&read_fds);
        FD_ZERO(&write_fds);
        FD_SET(server_socket_fd, &read_fds);
        max_fd = server_socket_fd;

        struct session *next, *session = root_session;

        // 遍历链表把socket fd全部放入fdset中
        while (session != NULL) {
            if (session->socket_fd == -1) {
                next = session->next;
                disconnect(session);
                session = next;
                continue;
            } else {
                FD_SET(session->socket_fd, &read_fds);
                FD_SET(session->socket_fd, &write_fds);
                max_fd = session->socket_fd;
            }
            session = session->next;
        }

        // 检查 读取 写入 错误 三种类型的文件是否可以操作
        errno = select(max_fd + 1, &read_fds, &write_fds, NULL, &timeout);

        if (errno == 0) { // 没有变化 跳过此次循环
            continue;
        }

        if (errno < 0) { // socket 错误退出进程
            printf("socket select error = %d", -1);
            return -1;
        }

        // 检查client socket 读写
        session = root_session;
        while (session != NULL) {
            // socket 可读
            if (FD_ISSET(session->socket_fd, &read_fds)) {
                char read_buf[1024] = {0};
                int read_size = read(session->socket_fd, read_buf, sizeof(read_buf));
                if (read_size < 0) {
                    printf("socket client error fd = %d", session->socket_fd);
                    session->socket_fd = -1;
                    continue;
                }

                if (read_size == 0) {
                    printf("socket client disconnected fd = %d", session->socket_fd);
                    session->socket_fd = -1;
                    continue;
                }

                char real_buf[1024] = {0};
                strncpy(real_buf, read_buf, read_size);
                printf("socket client read = %s", real_buf);

                if (FD_ISSET(session->socket_fd, &write_fds)) {
                    // DO THE ECHO
                    errno = write(session->socket_fd, real_buf, read_size);
                    if (errno < 0) {
                        printf("socket client write error = %d", errno);
                    }
                } else {
                    printf("socket client can't write data");
                }
            }

            session = session->next;
        }

        // 有新客户端连接
        if (FD_ISSET(server_socket_fd, &read_fds)) {
            struct sockaddr_in client_address = {0};
            int client_socket_length = sizeof(struct sockaddr_in);
            int client_fd = accept(server_socket_fd, &client_address, &client_socket_length);
            if (client_fd < 0) {
                printf("socket client accept error = %d", client_fd);
                continue;
            }

            struct session *new_client = new_session();

            new_client->socket_fd = client_fd;

            printf(
                    "client socket address = %s  port = %d",
                    inet_ntoa(client_address.sin_addr),
                    ntohs(client_address.sin_port)
            );

            // 添加新的socket到链表中
            if (root_session == NULL) {
                root_session = new_client;
            } else {
                session = root_session;
                while (session->next != NULL) {
                    session = session->next;
                }
                session->next = new_client;
            }
        }

        // 强制刷新printf数据到控制台上
        fflush(stdout);
    }
}

可以使用telnet或者nc命令测试以上服务