Linux 网络编程入门:API 详解

QQ截图20221120132804

字节序/网络序

为了保证机器无关,需要保证在网络上传输的字节序是一致的。所以某些关键信息(例如 IP),需要经过字节序的转化,确保所有机器都能正常解析,网络上采用大端方式的字节序。

所以有一系列的 API 负责字节序的转化,例如 htons,其中 h 代表 host,主机,n 代表 network,网络,s 代表转化的是 short 型,如果是 ntohl 就是网络序转为本地主机序,转化的类型是 long

这部分 API 属于周边设施,也比较简单,不做详解。

socket()

创建套接字

#include <sys/socket.h>
int socket(int domain, int type, int protocol);
// 成功返回套接字描述符, 失败返回 -1
  1. domain:协议族
    • AF_INET:IPv4 协议
    • AF_INET6:IPv6 协议
  2. type:套接字类型
    • SOCK_STREAM:TCP
    • SOCK_DGRAM:UDP
  3. protocol:传输协议
    • 0:协议族的默认值
    • IPPROTO_TCP:TCP
    • IPPROTO_UPD:UDP

关于套接字类型的其他知识:

  • TCP 类型的套接字不存在数据边界,发送方调用多次 send(),接受方可以只调用一次 recv(),一次拿到全部
  • UDP 类型的套接字存在数据边界,发送方调用 send() 的次数和接受方调用 recv() 的次数需要一致

套接字描述符在进程内唯一,跨进程不唯一,例如进程 A 创建个套接字,其描述符是 4,进程 B 创建个套接字,其描述符也可以是 4,但在底层,这两个套接字是不一样的

bind()

将一个本地地址和端口赋予一个套接字

套接字由一个四元组确定:[本地: ip, port; 对方: ip, port],该函数设定的是本地 ip 和端口

#include <sys/socket.h>
int bind(int sockfd, struct sockaddr* addr, socklen_t addrlen);
// 成功返回 0, 失败返回 -1
  1. sockfd:套接字描述符
  2. addr:存有地址信息的结构体
  3. addrlen:地址结构体变量的长度

服务端:必须要调用 bind 进行绑定
客户端:非必须调用,如不调用,在 connect 时会自动分配端口和地址

  • 需要在建立连接前就知道端口的话,需要 bind
  • 需要通过指定的端口来通讯的话,需要 bind

IPv4 地址结构体:

struct sockaddr_in {
    short   sin_family;         // 地址族
    u_short sin_port;           // 端口号
    struct  in_addr sin_addr;   // IPv4 地址
    char    sin_zero[8];        // 不使用, 为了和 sockaddr 内存布局一致
};
struct in_addr {
	u_int32_t s_addr;           // IPv4 地址, 用网络字节序(大端)表示
};

// 与 sockaddr_in 等价,内存布局一致,只不过 sockaddr_in 可读性更好
// 另外 sockaddr_in 专为 IPv4 设计, 但 sockaddr 需要兼容 IPv6, 为了
// 二者等价, 才会在 sockaddr_in 中有不使用的数据
struct sockaddr {
    u_short sa_family;   // 地址族
    char    sa_data[14]; // 端口和 IP 地址
};

// 经常有 bind(sockfd, (struct sockaddr*)&addr, addrlen) 的用法
// 就是因为使用 addr(sockaddr_in) 更方便,但实际上是等价的

listen()

创建一个监听队列存放待处理的连接

#include <sys/socket.h>
int listen(int sock, int backlog);
// 成功返回 0, 失败返回 -1
  1. sock:进入等待连接请求的套接字描述符(监听套接字)
  2. backlog:连接请求等待队列的长度(参考资料

请求等待队列可以简单解释成,在服务端收到客户端连接请求之后,到服务端调用 accept() 之前,这部分连接就放在请求等待队列。

服务端调用 accept() 会从请求等待队列中拿出一个连接,所以 backlog 并不代表服务器所能承载的连接数,只是意味着等待处理的连接的队列的长度,另外,该队列长度和 backlog 相关,但具体并不一定相等。

connect()

客户端向服务端请求连接,如果套接字未绑定 IP 和地址,则会自动分配

#include <sys/socket.h>
int connect(int sock, struct sockaddr* addr, socklen_t* addrlen);
// 成功返回 0, 失败返回 -1
  1. sock:客户端套接字描述符
  2. addr:目标服务器的地址信息
  3. addrlen:地址结构体变量的长度

调用 connect() 之后会阻塞,当以下情况时才会返回:

  • 服务器端接收连接请求
  • 发生断网等异常情况

服务器端接收请求并不代表会进行请求处理,接受连接请求意味着与服务器端完成了 TCP 连接,并等待服务器端调用 accept() 进行处理,所以此时服务器端并不一定执行了 accept() 进行请求的处理。

accept()

调用 listen() 之后,有新的连接建立完成后,accept 会返回一个新创建的套接字,该套接字与客户端的套接字连接。

有阻塞和非阻塞两种模式,取决于监听套接字

#include <sys/socket.h>
int accept(int sock, struct sockaddr* addr, socklen_t* addrlen);
// 成功返回创建套接字描述符, 失败返回 -1
  1. sock:服务端监听套接字的描述符
  2. addr:用于保存客户端地址信息
  3. addrlen:地址结构体变量的长度

调用后 addr 会储存客户端地址信息,addrlen 会储存结构体的长度

send()

向目标地址发送数据,需要完成 TCP 连接

#include <sys/socket.h>
size_t send(int sockfd, const void* buf, size_t nbytes, int flags);
// 成功返回发送的字节数, 失败返回 -1
  1. sockfd:与目标连接的套接字描述符
  2. buf:待传输数据的地址
  3. nbytes:待传输的字节数
  4. flags:可选项信息
    • 0:无
    • MSG_OOB:传输带外数据
    • MSG_DONTROUTE:告诉内核,目标主机在本地网络,不用查路由表
    • MSG_DONTWAIT:调用 I/O 函数不阻塞

MSG_OOB 的含义是该数据应当优先处理,这个是在应用层面的,在 TCP 层面没有什么影响,需要和客户端进行配合。

在 MSG_OOB 的数据到达之后,在客户端会产生 SIGURG 信号,随后调用该信号所绑定的处理事件,以完成应当优先处理的需求

sendto()

向目标地址发送数据,走 UDP 协议,不需要连接

#include <sys/socket.h>
size_t sendto(int sockfd, void* buf, size_t nbytes, int flags,
								     struct sockaddr* addr, socklen_t addrlen);
// 成功返回发送的字节数, 失败返回 -1
  1. sockfd:用于传输数据的套接字描述符,不需要连接,只是用于发送数据
  2. buf:待传输数据的地址
  3. nbytes:待传输的字节数
  4. flags:可选项信息
    • 0:无
    • MSG_OOB:传输带外数据
    • MSG_DONTROUTE:告诉内核,目标主机在本地网络,不用查路由表
  5. addr:发送的目标地址
  6. addrlen:地址结构体变量的长度

UDP 不需要调用 connect() 函数连接服务器,sendto() 函数会检测套接字是否绑定地址,如果没有会调用 bind,将套接字和地址进行绑定,此时会自动分配 IP 和端口(绑定的是本地 IP 和端口)

UDP 套接字过程:sendto() 函数

  1. 向 UDP 套接字注册目标 IP 和端口号
  2. 传输数据
  3. 删除 UPD 套接字的注册信息

每次调用 sendto() 都会执行如上操作,而如果需要长时间通信,则有些浪费,所以可以有连接的 UDP 套接字,调用 connect() 函数即可,但实际上并不会与对方服务器进行连接,只是告诉 sendto(),不要再注册和删除套接字,已经提前设定好了

recv()

#include <sys/socket.h>
size_t recv(int sockfd, void* buf, size_t nbytes, int flags);
// 成功返回发送的字节数, 失败返回 -1
  1. sockfd:与目标连接的套接字描述符
  2. buf:保存数据的地址
  3. nbytes:可接收的最大字节数
  4. flags:可选项信息
    • 0:无
    • MSG_OOB:传输带外数据
    • MSG_PEEK:读取可读的数据,在读取后不会删除缓冲区的内容
    • MSG_DONTWAIT:调用 I/O 函数时不阻塞
    • MSG_WAITALL:阻塞,直到接收完设定的字节数

MSG_OOB:需要与服务端配合

MSG_PEEK 通常和 MSG_DONTWAIT 搭配使用,以完成检查输入缓冲是否存在待接收的数据的目的。在使用该组合检测完之后,再调用非 MSG_PEEK 的 recv(),真正将数据接收,并清除缓冲区的内容。

recvfrom()

从目标地址接收数据,走 UDP 协议,不需要连接

#include <sys/socket.h>
size_t recvfrom(int sockfd, void* buf, size_t nbytes, int flags,
								       struct sockaddr* addr, socklen_t addrlen);
// 成功返回发送的字节数, 失败返回 -1
  1. sockfd:用于接收数据的套接字描述符,不需要连接,只是用于接收数据
  2. buf:保存数据的地址
  3. nbytes:可接收的最大字节数
  4. flags:可选项信息
    • 0:无
    • MSG_OOB:传输带外数据
    • MSG_PEEK:读取可读的数据,在读取后不会删除缓冲区的内容
  5. addr:发送的目标地址
  6. addrlen:地址结构体变量的长度

close()

关闭套接字,调用并不一定会销毁套接字,当套接字的描述符存在多个副本时(fork 操作),关闭一个套接字会使计数减 1,当套接字描述符的计数为 0 时,操作系统才会真正销毁套接字。

在多进程环境中,需要保证正确的 close,否则会导致套接字无法真正关闭

#include <sys/socket.h>
int close(int sockfd);
// 成功返回 0, 失败返回 -1
  1. sockfd:要关闭的套接字描述符

close() 函数是单方面完全关闭连接,即无法继续接收数据也无法发出数据,如果想关闭输出流(完成输出任务,但可能仍然需要接收对方的数据),同时又保持输入流的打开,close() 则无法做到,此时要用 shutdown()

shutdown()

半关闭套接字,选择关闭一个流或都关闭

在多进程中,如果要关闭套接字并向对方发出断开连接请求,则需要保证所有进程都关闭该套接字,实现起来比较麻烦,而 shutdown() 只需要一个进程执行,即可发出断开连接的请求。

#include <sys/socket.h>
int shutdown(int sockfd, int opt);
// 成功返回 0, 失败返回 -1
  1. sockfd:要半关闭的套接字描述符
  2. opt:关闭模式
    • SHUT_RD:关闭读入流
    • SHUT_WR:关闭输出流
    • SHUT_RDWR:同时关闭输入流和输出流

需要注意,shutdown() 并不会销毁套接字,即使是 SHUT_RDWR,后续仍然需要调用 close() 关闭套接字,这样操作系统才会销毁套接字

DNS

根据域名获得 IP 地址或根据 IP 地址获得域名

#include <netdb.h>
struct hostent* gethostbyname(const char* hostname);
// 成功返回 hostent 结构体, 失败返回 NULL
  1. hostname:域名
struct hostent* gethostbyaddr(const char* addr, socklen_t len, int family);
// 成功返回 hostent 结构体, 失败返回 NULL
  1. addr:IP 地址信息的 in_addr 结构体指针,转为字节流传入
  2. len:地址信息的字节数,IPv4 4,IPv6 16
  3. family:地址族,IPv4 AF_INET,IPv6 AF_INET6
struct hostent {
	char* h_name;       // 官方域名
	char** h_aliases;   // 别名列表
	int h_addrtype;     // 地址族类型, IPv4/IPv6
	int h_length;       // IP 地址长度, IPv4 4, IPv6 16
	char** h_addr_list; // 以整数形式保存的 IP 地址
}

套接字可选项

查看套接字可选项的值

#include <sys/socket.h>
int getsockopt(int sock, int level, int optname, void* optval, socklen_t* optlen);
// 成功返回 0, 失败返回 -1
  1. sock:要查看的套接字的描述符
  2. level:查看的协议层
  3. optname:查看的可选项名
  4. optval:用于储存查看结果的地址
  5. optlen:optval 的缓冲大小,调用后改为查看的结果的字节数

设置套接字可选项的值

#include <sys/socket.h>
int setsockopt(int sock, int level, int optname, const void* optval, socklen_t* optlen);
// 成功返回 0, 失败返回 -1
  1. sock:要查看的套接字的描述符
  2. level:要改变的协议层
  3. optname:要改变的可选项名
  4. optval:要设置的值
  5. optlen:要设置的值的字节大小

IPPROTO_TCP 层中的 TCP_NODELAY:
Nagle 算法:当收到 ACK 时再把缓冲区的数据一并发出;非 Nagle,缓冲区一旦有数据就发出,在大文件数据中,可以禁用 Nagle 算法,实现更快的传输效率

I/O 多路复用

本文只罗列了 API 相关的内容,具体多路复用的意义和 select、epoll 的优劣比较,参考 详解 I/O 多路复用

select()

#include <sys/select.h>
#include <sys/time.h>

int select(int maxfd, fd_set* readset, fd_set* writeset, fd_set* exceptset, 
										const struct timeval* timeout);
// 成功返回发生事件的描述符数量, 超时返回 0, 失败返回 -1
  1. maxfd:监听套接字的数量
  2. readset:存在待读取数据事件的套接字描述符位图
  3. writeset:存在可传输无阻塞数据事件的套接字描述符位图
  4. exceptset:发生异常状态的套接字描述符位图
  5. timeout:当无事件发生时,阻塞多久
struct timeval {
	long tv_sec;  // 秒
	long tv_usec; // 毫秒
}

// fd_set 实际上是一个位图,通过一系列宏来进行操作
FD_ZERO(fd_set* fdset);          // 初始化为 0
FD_SET(int fd, fd_set* fdset);   // 向 fdset 中注册套接字 fd
FD_CLR(int fd, fd_set* fdset);   // 向 fdset 中注销套接字 fd
FD_ISSET(int fd, fd_set* fdset); // 判断 fdset 中是否包含该套接字

select() 最多支持监听 1024 个套接字,位图的工作方式是,如果套接字 fd 发生事件,则位图在下标 fd 位置置 1,其他则置 0

另外 timeout 每次都需要重新设置,在阻塞过程中会不断减小 timeout 结构体中的数值完成计时,所以需要重新设定数值

epoll()

创建 epoll

#include <sys/epoll.h>
int epoll_create(int size);
// 成功返回 epoll 文件描述符, 失败返回 -1
  1. size:Linux 内核版本大于 2.6.8 没有意义

向 epoll 中注册、注销或修改套接字的监听事件

#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);
// 成功返回 0, 失败返回 -1
  1. epfd:epoll 的文件描述符
  2. op:监听对象的添加、删除或更改等操作
    • EPOLL_CTL_ADD:注册监听对象到 epoll
    • EPOLL_CTL_DEL:从 epoll 中删除监听对象
    • EPOLL_CTL_MOD:修改监听对象的监听事件
  3. fd:监听对象的文件描述符
  4. event:监听对象的事件类型
    • event.events:监听的事件类型
      • EPOLLIN:监听数据到来
      • EPOLLOUT:输出缓存为空,可以发送数据
      • EPOLLPRI:收到 OOB 数据
      • EPOLLRDHUP:断开连接或半关闭
      • EPOLLERR:发生错误
      • EPOLLET:设置为边沿触发模式
      • EPOLLONESHOT:设置的套接字事件只会通知一次,后续需要再通知则需要用 EPOLL_CTL_MOD 修改
    • event.data.fd:套接字描述符
struct epoll_event {
	__unit32_t events; // 监听的事件类型
	epoll_data_t data; // 数据
}

typedef union epoll_data {
	void* ptr;      // 用户自定义数据
	int fd;         // 文件描述符
	__uint32_t u32;
	__uint64_t u64;
} epoll_data_t;

等待事件发生

#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);
// 成功返回发生事件的描述符数量, 失败返回 -1
  1. epfd:epoll 的文件描述符
  2. events:保存发生事件的文件描述符集合
  3. maxevents:可以保存的最大事件数
  4. timeout:等待时间,-1 表示一直等待直到事件发生

需要注意,第二个参数的地址需要动态分配

struct epoll_event* ep_events = malloc(sizeof(struct epoll_event) * EPOLL_SIZE);
int event_cnt = epoll_wait(epfd, ep_events, EPOLL_SIZE - 1);
上一篇 下一篇

评论 | 0条评论