socket 编程

include: co/co.h.

#Socket APIs

co 提供了常用的协程化的 socket API,以支持基于协程的网络编程。

大部分 API 形式上与原生的 socket API 保持一致,这样可以减轻用户的学习负担,熟悉 socket 编程的用户可以轻松上手。

这些 API 大部分需要在协程中使用,它们在 I/O 阻塞或调用 sleep 等操作时,调度线程会挂起当前协程,切换到其他等待中的协程运行,调度线程本身并不会阻塞。借助这些 API,用户可以轻松的实现高并发、高性能的网络程序

#术语约定

阻塞

在描述 co 中的一些 socket API 时,会用到阻塞一词,如 accept, recv,文档中说它们会阻塞,是指当前的协程会阻塞,而当前的调度线程并不会阻塞(可以切换到其他协程运行)。用户看到的是协程,而不是调度线程,因此从用户的角度看,它们是阻塞的。实际上,这些 API 内部使用 non-blocking socket,并不会真的阻塞,只是在 socket 上没有数据可读或者无法立即写入数据时,调度线程会挂起当前协程,当 socket 变为可读或可写时,调度线程会重新唤起该协程,继续 I/O 操作。

non-blocking socket

co 中的 socket API 必须使用 non-blocking socket,在 windows 平台还要求 socket 支持 overlapped I/O,win32 API 创建的 socket 默认都支持 overlapped I/O,用户一般不需要担心这个问题。为了叙述方便,这里约定文档中说到 non-blocking socket 时,同时也表示它在 windows 上支持 overlapped I/O。

#co::socket

1. sock_t socket(int domain, int type, int proto);
2. sock_t tcp_socket(int domain=AF_INET);
3. sock_t udp_socket(int domain=AF_INET);
  • 创建 socket。
  • 1, 形式上与原生 API 完全一样,在 linux 系统可以用 man socket 查看参数详情。
  • 2, 创建一个 TCP socket。
  • 3, 创建一个 UDP socket。
  • 参数 domain 一般是 AF_INET 或 AF_INET6,前者表示 ipv4,后者表示 ipv6。
  • 这些函数返回一个 non-blocking socket。发生错误时,返回值是 -1,可以调用 co::error() 获取错误信息。

#co::accept

sock_t accept(sock_t fd, void* addr, int* addrlen);
  • 在指定 socket 上接收客户端连接,参数 fd 是之前调用 listen() 监听的 non-blocking socket,参数 addr 与 addrlen 用于接收客户端的地址信息,*addrlen 的初始值是 addr 所指向 buffer 的长度。如果用户不需要客户端地址信息,可以将 addr 与 addrlen 设置为 NULL。
  • 此函数必须在协程中调用
  • 此函数会阻塞,直到有新的连接进来,或者发生错误。
  • 此函数成功时返回一个 non-blocking socket,发生错误时返回 -1,可以调用 co::error() 获取错误信息。

#co::bind

int bind(sock_t fd, const void* addr, int addrlen);
  • 给 socket 绑定 ip 地址,参数 addr 与 addrlen 是地址信息,与原生 API 相同。
  • 此函数成功时返回 0,否则返回 -1,可以调用 co::error() 获取错误信息。

#co::close

int close(sock_t fd, int ms=0);
  • 关闭 socket。
  • 在 2.0.0 及之前的版本中,此函数必须在进行 I/O 操作的线程中调用。从 2.0.1 版本开始,此函数可以在协程或非协程中调用。
  • 参数 ms 大于 0 时,会先调用 co::sleep(ms) 将当前协程挂起一段时间,再关闭 socket。一般只在 server 端将 ms 设置为大于 0 的值,可以在一定程度上缓解非法的网络攻击。
  • 此函数内部已经处理了 EINTR 信号,用户无需考虑。
  • 此函数成功时返回 0,否则返回 -1,可以调用 co::error() 获取错误信息。

#co::connect

int connect(sock_t fd, const void* addr, int addrlen, int ms=-1);
  • 在指定 socket 上创建到指定地址的连接,参数 fd 必须是 non-blocking 的,参数 addr 与 addrlen 是地址信息,参数 ms 是超时时间,单位为毫秒,默认为 -1,永不超时。
  • 此函数必须在协程中调用
  • 此函数会阻塞,直到连接完成,或者超时、发生错误。
  • 此函数成功时返回 0,超时或发生错误返回 -1,用户可以调用 co::timeout() 判断是否超时,调用 co::error() 获取错误信息。

#co::listen

int listen(sock_t fd, int backlog=1024);
  • 监听指定的 socket,参数 fd 是已经调用 bind() 绑定 ip 及端口的 socket。
  • 此函数成功时返回 0,否则返回 -1,可以调用 co::error() 获取错误信息。

#co::recv

int recv(sock_t fd, void* buf, int n, int ms=-1);
  • 在指定 socket 上接收数据,参数 fd 必须是 non-blocking 的,参数 buf 是用于接收数据的 buffer,参数 n 是 buffer 长度,参数 ms 是超时时间,单位为毫秒,默认为 -1,永不超时。
  • 此函数必须在协程中调用
  • 在 Windows 平台,此函数只适用于 TCP 等 stream 类型的 socket。
  • 此函数会阻塞,直到有数据进来,或者超时、发生错误。
  • 此函数成功时返回接收的数据长度(可能小于 n),对端关闭连接时返回 0,超时或发生错误返回 -1,用户可以调用 co::timeout() 判断是否超时,调用 co::error() 获取错误信息。

#co::recvn

int recvn(sock_t fd, void* buf, int n, int ms=-1);
  • 在指定 socket 上接收指定长度的数据,参数 fd 必须是 non-blocking 的,参数 buf 是用于接收数据的 buffer,参数 n 是要接收数据的长度,参数 ms 是超时时间,单位为毫秒,默认为 -1,永不超时。
  • 此函数必须在协程中调用
  • 此函数会阻塞,直到 n 字节的数据全部接收完,或者超时、发生错误。
  • 此函数成功时返回 n,对端关闭连接时返回 0,超时或发生错误返回 -1,用户可以调用 co::timeout() 判断是否超时,调用 co::error() 获取错误信息。

#co::recvfrom

int recvfrom(sock_t fd, void* buf, int n, void* src_addr, int* addrlen, int ms=-1);
  • co::recv 类似,只是可以用参数 src_addr 与 addrlen 接收源地址信息,*addrlen 的初始值是 src_addr 所指向 buffer 的长度,如果用户不需要源地址信息,可以将 addr 与 addrlen 设置为 NULL。
  • 一般建议只用此函数接收 UDP 数据

#co::send

int send(sock_t fd, const void* buf, int n, int ms=-1);
  • 向指定 socket 上发送数据,参数 fd 必须是 non-blocking 的,参数 buf 与 n 是要发送的数据及长度,参数 ms 是超时时间,单位为毫秒,默认为 -1,永不超时。
  • 此函数必须在协程中调用
  • 在 Windows 平台,此函数只适用于 TCP 等 stream 类型的 socket。
  • 此函数会阻塞,直到 n 字节的数据全部发送完,或者超时、发生错误。
  • 此函数成功时返回 n,超时或发生错误返回 -1,用户可以调用 co::timeout() 判断是否超时,调用 co::error() 获取错误信息。

#co::sendto

int sendto(sock_t fd, const void* buf, int n, const void* dst_addr, int addrlen, int ms=-1);
  • 向指定的地址发送数据,当 dst_addr 为 NULL,addrlen 为 0 时,与 co::send 等价。
  • 一般建议只用此函数发送 UDP 数据
  • fd 是 UDP socket 时,n 最大是 65507。

#co::shutdown

int shutdown(sock_t fd, char c='b');
  • 此函数一般用于半关闭 socket,参数 c 为 'r' 时表示关闭读,为 'w' 时表示关闭写,默认为 'b',关闭读与写。
  • 一般建议在进行 IO 操作的线程中调用此函数。
  • 此函数成功时返回 0,否则返回 -1,可以调用 co::error() 获取错误信息。

#———————————

#co::getsockopt

int getsockopt(sock_t fd, int lv, int opt, void* optval, int* optlen);
  • 获取 socket option 信息,与原生 API 完全一样,man getsockopt 看详情。

#co::setsockopt

int setsockopt(sock_t fd, int lv, int opt, const void* optval, int optlen);
  • 设置 socket option 信息,与原生 API 完全一样,man setsockopt 看详情。

#co::set_nonblock

void set_nonblock(sock_t fd);
  • 给 socket 设置 O_NONBLOCK 选项。

#co::set_reuseaddr

void set_reuseaddr(sock_t fd);
  • 给 socket 设置 SO_REUSEADDR 选项,一般 server 端的 listening socket 需要设置这个选项,防止 server 重启后 bind 失败。

#co::set_recv_buffer_size

void set_recv_buffer_size(sock_t fd, int n);
  • 设置 socket 的接收缓冲区大小,必须在 socket 连接前调用此函数。

#co::set_send_buffer_size

void set_send_buffer_size(sock_t fd, int n);
  • 设置 socket 的发送缓冲区大小,必须在 socket 连接前调用此函数。

#co::set_tcp_keepalive

void set_tcp_keepalive(sock_t fd);
  • 给 socket 设置 SO_KEEPALIVE 选项。

#co::set_tcp_nodelay

void set_tcp_nodelay(sock_t fd);
  • 给 socket 设置 TCP_NODELAY 选项。

#co::reset_tcp_socket

int reset_tcp_socket(sock_t fd, int ms=0);
  • 重置 TCP 连接,与 co::close 类似,但主动调用方不会进入 TIME_WAIT 状态。
  • 一般只有 server 端会调用此函数,用于主动关闭客户端连接,同时避免进入 TIME_WAIT 状态。

#———————————

#co::addr2str

1. fastring addr2str(const struct sockaddr_in* addr);
2. fastring addr2str(const struct sockaddr_in6* addr);
3. fastring addr2str(const void* addr, int len);
  • 将 sockaddr 地址转换成 "ip:port" 形式的字符串。

  • 1 用于 ipv4 地址,2 用于 ipv6 地址,3 根据 len 选择调用版本 1 或版本 2。

  • 示例

struct sockaddr_in addr;
co::init_addr(&addr, "127.0.0.1", 80);
co::addr2str(&addr);                // "127.0.0.1:80"
co::addr2str(&addr, sizeof(addr));  // "127.0.0.1:80"

#co::init_addr

1. bool init_addr(struct sockaddr_in* addr, const char* ip, int port);
2. bool init_addr(struct sockaddr_in6* addr, const char* ip, int port);
  • 用 ip 及 port 初始化 sockaddr 结构。

  • 1 用于 ipv4 地址,2 用于 ipv6 地址。

  • 示例

union {
    struct sockaddr_in  v4;
    struct sockaddr_in6 v6;
} addr;

co::init_addr(&addr.v4, "127.0.0.1", 7777);
co::init_addr(&addr.v6, "::", 7777);

#co::init_ip_addr

bool init_ip_addr(struct sockaddr_in* addr, const char* ip, int port);
bool init_ip_addr(struct sockaddr_in6* addr, const char* ip, int port);
  • v3.0.1 标记为 deprecated,建议用 co::init_addr 取代之。

#co::peer

fastring peer(sock_t fd);
  • 获取 peer 端的地址信息,返回值是 "ip:port" 形式的字符串。