字节序/网络序
为了保证机器无关,需要保证在网络上传输的字节序是一致的。所以某些关键信息(例如 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
- domain:协议族
- AF_INET:IPv4 协议
- AF_INET6:IPv6 协议
- type:套接字类型
- SOCK_STREAM:TCP
- SOCK_DGRAM:UDP
- 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
- sockfd:套接字描述符
- addr:存有地址信息的结构体
- 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
- sock:进入等待连接请求的套接字描述符(监听套接字)
- backlog:连接请求等待队列的长度(参考资料)
请求等待队列可以简单解释成,在服务端收到客户端连接请求之后,到服务端调用 accept()
之前,这部分连接就放在请求等待队列。
服务端调用 accept()
会从请求等待队列中拿出一个连接,所以 backlog 并不代表服务器所能承载的连接数,只是意味着等待处理的连接的队列的长度,另外,该队列长度和 backlog 相关,但具体并不一定相等。
connect()
客户端向服务端请求连接,如果套接字未绑定 IP 和地址,则会自动分配
#include <sys/socket.h>
int connect(int sock, struct sockaddr* addr, socklen_t* addrlen);
// 成功返回 0, 失败返回 -1
- sock:客户端套接字描述符
- addr:目标服务器的地址信息
- addrlen:地址结构体变量的长度
调用 connect()
之后会阻塞,当以下情况时才会返回:
- 服务器端接收连接请求
- 发生断网等异常情况
服务器端接收请求并不代表会进行请求处理,接受连接请求意味着与服务器端完成了 TCP 连接,并等待服务器端调用
accept()
进行处理,所以此时服务器端并不一定执行了accept()
进行请求的处理。
accept()
调用 listen()
之后,有新的连接建立完成后,accept
会返回一个新创建的套接字,该套接字与客户端的套接字连接。
有阻塞和非阻塞两种模式,取决于监听套接字
#include <sys/socket.h>
int accept(int sock, struct sockaddr* addr, socklen_t* addrlen);
// 成功返回创建套接字描述符, 失败返回 -1
- sock:服务端监听套接字的描述符
- addr:用于保存客户端地址信息
- addrlen:地址结构体变量的长度
调用后 addr 会储存客户端地址信息,addrlen 会储存结构体的长度
send()
向目标地址发送数据,需要完成 TCP 连接
#include <sys/socket.h>
size_t send(int sockfd, const void* buf, size_t nbytes, int flags);
// 成功返回发送的字节数, 失败返回 -1
- sockfd:与目标连接的套接字描述符
- buf:待传输数据的地址
- nbytes:待传输的字节数
- 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
- sockfd:用于传输数据的套接字描述符,不需要连接,只是用于发送数据
- buf:待传输数据的地址
- nbytes:待传输的字节数
- flags:可选项信息
- 0:无
- MSG_OOB:传输带外数据
- MSG_DONTROUTE:告诉内核,目标主机在本地网络,不用查路由表
- addr:发送的目标地址
- addrlen:地址结构体变量的长度
UDP 不需要调用 connect()
函数连接服务器,sendto()
函数会检测套接字是否绑定地址,如果没有会调用 bind
,将套接字和地址进行绑定,此时会自动分配 IP 和端口(绑定的是本地 IP 和端口)
UDP 套接字过程:sendto()
函数
- 向 UDP 套接字注册目标 IP 和端口号
- 传输数据
- 删除 UPD 套接字的注册信息
每次调用 sendto()
都会执行如上操作,而如果需要长时间通信,则有些浪费,所以可以有连接的 UDP 套接字,调用 connect()
函数即可,但实际上并不会与对方服务器进行连接,只是告诉 sendto()
,不要再注册和删除套接字,已经提前设定好了
recv()
#include <sys/socket.h>
size_t recv(int sockfd, void* buf, size_t nbytes, int flags);
// 成功返回发送的字节数, 失败返回 -1
- sockfd:与目标连接的套接字描述符
- buf:保存数据的地址
- nbytes:可接收的最大字节数
- 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
- sockfd:用于接收数据的套接字描述符,不需要连接,只是用于接收数据
- buf:保存数据的地址
- nbytes:可接收的最大字节数
- flags:可选项信息
- 0:无
- MSG_OOB:传输带外数据
- MSG_PEEK:读取可读的数据,在读取后不会删除缓冲区的内容
- addr:发送的目标地址
- addrlen:地址结构体变量的长度
close()
关闭套接字,调用并不一定会销毁套接字,当套接字的描述符存在多个副本时(fork 操作),关闭一个套接字会使计数减 1,当套接字描述符的计数为 0 时,操作系统才会真正销毁套接字。
在多进程环境中,需要保证正确的 close,否则会导致套接字无法真正关闭
#include <sys/socket.h>
int close(int sockfd);
// 成功返回 0, 失败返回 -1
- sockfd:要关闭的套接字描述符
close()
函数是单方面完全关闭连接,即无法继续接收数据也无法发出数据,如果想关闭输出流(完成输出任务,但可能仍然需要接收对方的数据),同时又保持输入流的打开,close()
则无法做到,此时要用 shutdown()
shutdown()
半关闭套接字,选择关闭一个流或都关闭
在多进程中,如果要关闭套接字并向对方发出断开连接请求,则需要保证所有进程都关闭该套接字,实现起来比较麻烦,而 shutdown()
只需要一个进程执行,即可发出断开连接的请求。
#include <sys/socket.h>
int shutdown(int sockfd, int opt);
// 成功返回 0, 失败返回 -1
- sockfd:要半关闭的套接字描述符
- 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
- hostname:域名
struct hostent* gethostbyaddr(const char* addr, socklen_t len, int family);
// 成功返回 hostent 结构体, 失败返回 NULL
- addr:IP 地址信息的 in_addr 结构体指针,转为字节流传入
- len:地址信息的字节数,IPv4 4,IPv6 16
- 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
- sock:要查看的套接字的描述符
- level:查看的协议层
- optname:查看的可选项名
- optval:用于储存查看结果的地址
- optlen:optval 的缓冲大小,调用后改为查看的结果的字节数
设置套接字可选项的值
#include <sys/socket.h>
int setsockopt(int sock, int level, int optname, const void* optval, socklen_t* optlen);
// 成功返回 0, 失败返回 -1
- sock:要查看的套接字的描述符
- level:要改变的协议层
- optname:要改变的可选项名
- optval:要设置的值
- 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
- maxfd:监听套接字的数量
- readset:存在待读取数据事件的套接字描述符位图
- writeset:存在可传输无阻塞数据事件的套接字描述符位图
- exceptset:发生异常状态的套接字描述符位图
- 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
- 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
- epfd:epoll 的文件描述符
- op:监听对象的添加、删除或更改等操作
- EPOLL_CTL_ADD:注册监听对象到 epoll
- EPOLL_CTL_DEL:从 epoll 中删除监听对象
- EPOLL_CTL_MOD:修改监听对象的监听事件
- fd:监听对象的文件描述符
- event:监听对象的事件类型
- event.events:监听的事件类型
- EPOLLIN:监听数据到来
- EPOLLOUT:输出缓存为空,可以发送数据
- EPOLLPRI:收到 OOB 数据
- EPOLLRDHUP:断开连接或半关闭
- EPOLLERR:发生错误
- EPOLLET:设置为边沿触发模式
- EPOLLONESHOT:设置的套接字事件只会通知一次,后续需要再通知则需要用 EPOLL_CTL_MOD 修改
- event.data.fd:套接字描述符
- event.events:监听的事件类型
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
- epfd:epoll 的文件描述符
- events:保存发生事件的文件描述符集合
- maxevents:可以保存的最大事件数
- 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);