网络常用函数
大小端转换#
#include <netinet/in.h>
unsigned long int htonl(unsigned long int host); // 本地转网络字节序 32bit
unsigned long int ntohl(unsigned long int net); // 网络转本地字节序
unsigned short int htons(unsigned short int host); // 本地转网络字节序 16bit
unsigned short int ntohs(unsigned short int net); // 网络转本地字节序
socket地址结构#
通用socket地址#
所有操作系统接口使用都是sockaddr
。下面的几种地址结构在使用时,只要且必须要强转成sockaddr
。
#include <bits/socket.h>
struct sockaddr {
sa_family_t sa_family; // AF_UNIX/AF_INET/AF_INET6
char sa_data[14];
};
但是14字节的sa_data
不够存放有的协议类型的地址值,所以linux中定义了新的通用地址结构:
#include <bits/socket.h>
struct sockaddr_storage {
sa_family_t sa_family;
unsigned long int __sa_align;
char __ss_padding[128 - sizeof(__ss_align)];
};
专用socket地址#
unix域
#include <sys/un.h>
struct sockaddr_un {
sa_family_t sin_family;
char sun_path[108]; // 文件路径名
};
IPv4和IPv6
//v4
struct sockaddr_in {
sa_family_t sin_family;
u_int16_t sin_port; // 端口号,网络字节序
struct in_addr sin_addr; //IPv4地址,网络字节序
};
struct in_addr {
u_int32_t a_addr;
}
//v6
struct sockaddr_in6 {
sa_family_t sin_family;
u_int16_t sin6_port; // 同上
uint32_t sin6_flowinfo; // 流信息,置0
struct in6_addr sin6_addr; // 同样要网络字节序
u_int32_t sin6_scope_id; // 还没流行使用到
};
struct in6_addr {
unsigned char sa_addr[16];
};
IP地址转换函数#
通常会习惯用字符串表示IPv4,用16进制表示IPv6,但在网络编程中需要转换成二进制使用;而在读取时,比如写入日志,为了方便查找,又希望转换成可读的字符串。所以有以下函数用于转换。
#include <arpa/inet.h>
in_addr_t inet_addr(const char* strptr_t); // 点分十进制字符串转换成in_addr结构,失败时返回 INADDR_NONE
int inet_aton(const char* cp, struct in_addr* inp); // 完成和上一行同样的功能,成功返回1,失败返回0
char* inet_ntoa(struct in_addr* inp); // 将网络字节序地址转换成点分十进制字符串
socket#
创建socket#
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol); // 成功时返回描述符
type现在可以和SOCK_NONBLOCK/SOCK_CLOEXECX相与。
SOCK_NONBLOCK表示socket不阻塞;SOCK_CLOEXECX表示fork开辟子进程时在子进程中关闭这个socket。
绑定#
创建时指定了socket的类型,但还没有和地址进行绑定,只有在绑定后客户端才能知道如何连接它。
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr* addr, socklen_t addrlen);
监听#
在绑定地址后,客户端就可以通过ip地址和端口号与socket进行连接,但是这边还是没有准备好接受连接,需要创建一个监听队列来存放待处理的客户端连接。
#include <sys/socket.h>
int listen(int sockfd, int backlog);
backlog表示可以有多少客户端能够处于完全连接状态。
ESTABILISHED = backlog + 1
以前还要加上半连接状态SYN_RECV,linux内核2.2之后,SYN_RECV由/proc/sys/net/ipv4/tcp_max_syn_backlog
参数维护。
接受连接#
在有客户端来连接后,socket通过accept获取客户端的地址,成功返回一个新的socket,绑定了客户端的地址。
#include <sys/types.h>
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr* clientaddr, socklen_t* clientaddrlen);
发起连接#
客户端通过调用connect向服务器发起连接。
#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr* serv_addr, socklen_t addrlen);
连接成功后,sockfd就绑定了服务器的地址,通过对sockfd读写就可以与服务器通信。
读写操作#
#include <sys/types.h>
#include <sys/socket.h>
// tcp
ssize_t recv(int sockfd, void* buf, size_t len, int flags);
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
// udp
ssize_t recvfrom(int sockfd, void* buf, size_t len, int flags, struct sockaddr* src_addr, socklen_t* addrlen);
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, struct sockaddr* dest_addr, socklen_t addrlen);
// 通用
ssize_t recvmsg(int sockfd, struct msghdr* msg, int flags);
ssize_t sendmsg(int sockfd, struct msghdr* msg, int flags);
// 封装了结构体
struct msghdr {
void* msg_name; // socket地址
socklen_t msg_namelen; // socket地址长度
struct iovec* msg_iov; // 分散的内存块
int msg_iovlen; // 内存块数量
void* msg_control; // 指向辅助数据的起始位置
socklen_t msg_controllen; // 辅助数据大小
int msg_flags;
};
// 每个iovec维护了一个内存块的首地址和大小
struct iovec {
void* iov_base;
size_t iov_len;
}
flags控制了socket读写的操作逻辑。
地址信息函数#
有时候我们想知道一个socket连接的本地socket地址和远端socket地址。
#include <sys/socket.h>
int getsockname(int sockfd, struct sockaddr* addr, socklen_t* addrlen);
int getpeername(int sockfd, struct sockaddr* addr, socklen_t* addrlen);
socket选项#
#include <sys/socket.h>
int getsockopt(int sockfd, int level, int option_name, void* option_value, socklen_t* restrict option_len);
int setsockopt(int sockfd, int level, int option_name, void* option_value, socklen_t option_len);
通过这两个函数设置socket文件描述符的属性。
网络信息API#
域名函数#
#include <netdb.h>
struct hostent* gethostbyname(const char* name);
struct hostent* gethostbyaddr(const void* addr, size_t len, int type);
struct hostent {
char* h_name; // 主机名,注册的域名
char** h_aliases; // 主机别名列表
int h_addrtype; // 地址类型
int h_length; // 地址长度
char** h_addr_list; // 网络字节序的IP地址列表
};
获取服务函数(对应端口号)#
#include <netdb.h>
struct servent* getservbyname(const char* name, const char* proto);
struct servent* getservbyport(int port, const char* proto);
struct servent {
char* s_name; // 服务名称
char** s_aliases; // 服务的别名列表
int s_port; // 服务的端口号
char* s_proto; // 服务类型
};
getaddrinfo#
#include <netdb.h>
int getaddrinfo(const char* hostname, const char* service, const struct addrinfo* hints, struct addrinfo* result);
struct addrinfo {
int ai_flags;
int ai_family;
int ai_socktype;
int ai_protocol;
socklen_t ai_addrlen;
char* ai_canonname;
struct sockaddr* ai_addr;
struct addrinfo* ai_next;
};
getnameinfo#
#include <netdb.h>
int getnameinfo(const struct sockaddr* sockaddr, socklen_t addrlen, char* host, socklen_t hostlen, char* serv, socklen_t servlen, int flags);
高级I/O函数#
pipe函数#
创建一对文件描述符,也就是管道,fd[0]读,fd[1]写,从而实现了两个进程之间的通信。
#include <unistd.h>
int pipe(int fd[2]);
dup和dup2函数#
#include <unistd.h>
int dup(int filedes);
int dup2(int filedes, int filedes2);
创建一个文件描述符,复制filedes的所有属性。dup2返回的是不小于filedes2的文件描述符。
readv和writev#
readv:分散读,将文件描述符中的内容分散到若干的iovec内存块中;
wirtev:集中写,将若干iovec内存块的内容集中写到文件描述符中。
#include <sys/uio.h>
ssize_t readv(int fd, const struct iovec* vector, int count);
ssize_t writev(int fd, const struct iovec* vector, int count);
第二个参数是iovec结构的数组;第三个参数是第二个参数数组的大小,描述了数组中有多少个iovec。
sendfile函数#
sendfile实现了在两个文件描述符间直接传递数据(一直在内核中操作),避免了内核态和用户态之间的数据拷贝。
#include <sys/sendfile.h>
ssize_t sendfile(int out_fd, int in_fd, off_t* offset, size_t count);
offset描述了开始读取时的偏移量,0就从in_fd指向文件的开头开始读取;
count描述了读取多少的字节。
需要明确的是,in_fd(读文件描述符)必须是指向了真实文件的,不能是socket或者管道这种;out_fd则必须是socket。
mmap和munmap函数#
mmap和munmap是一对用于内存分配和释放的函数,申请到的内存可以直接将文件描述符映射到其中,常作为开辟进程间通信的共享内存。
#include <sys/mman.h>
void* mmap(void* start, size_t length, int prot, flags, int fd, off_t offset);
int munmap(void* start, size_t length);
允许用户指定内存的起始位置,如果start为null,则系统会随机分配一个地址。
prot指定了这块内存的权限。
offset指定了fd从这块内存的什么位置开始映射。
splice函数#
splice用于两个文件描述符之间进行数据移动,也是零拷贝的操作。
#include <fcntl.h>
ssize_t splice(int fd_in, loff_t* off_in, int fd_out, loff_t* off_out, size_t len, unsigned int flags);
tee函数#
tee用于两个管道文件描述符之间复制数据,也是零拷贝的操作。而且,tee函数不消耗数据,被传输的数据仍可以做后续的读操作。
#include <fcntl.h>
ssize_t tee(int fd_in, int fd_out, size_t len, unsigned int flags);
fcntl函数#
fcntl是对文件描述符的控制函数,功能十分强大,有很多的flags控制各种属性和行为。
#include <fcntl.h>
int fcntl(int fd, int cmd, ...);
在网络编程中,fcntl主要用于设置文件描述符是阻塞的还是非阻塞的。
I/O复用#
select API#
轮询所有文件描述符,查看设置的文件描述符是否被修改,修改就意味着对应的文件描述符触发了相应的事件并处于就绪状态。
#include <sys/select.h>
int select(int nfds, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, struct timeval* timeout);
FD_ZERO(fd_set* fd_set):清除fd_set所有位;
FD_SET(int fd, fd_set* fd_set):设置fd_set的fd位
FD_CLR(int fd, fd_set* fd_set):清除fd_set的fd位
FD_ISSET(int fd, fd_set* fd_set):检查fd_set设置的fd位是否被修改
poll#
poll和select一样也是轮询。
#include <poll.h>
int poll(struct pollfd* fds, nfds_t nfds, int timeout);
struct pollfd {
int fd; // 文件描述符
short events; // 注册的事件
short reevents; // 实际发生的事件,由内核修改
};
epoll#
通过在内核中创建一个事件表,而无需反复传入文件描述符集和事件集。
#include <sys/epoll.h>
int epoll_create(int size); // size只是个建议值,无更多用处
int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);
int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);
struct epoll_event {
uint32_t events; // epoll事件
epoll_data_t data; // 用户数据,是个union,基本只用fd
}
通过epoll_create
创建事件表,并返回指向该事件表的文件描述符epfd;
epoll_ctl
通过op的宏EPOLL_CTL_*控制向事件表中注册的事件;
epoll_wait
监听事件,一旦有事件触发,就将事件从内核写入events中,maxevents表示最多监听多少事件,返回值是触发的文件描述符数量。
信号函数#
#include <signal.h>
int sigaction(int sig, const struct sigaction* act, struct sigaction* oact);
struct sigaction {
__sighandler_t sa_handler; // 信号的处理函数
__sigset_t sa_mask; // 信号的掩码
int sa_flags; //
void (*sa_restorer)(void); // 过时了,不用
};
注册信号,当sig信号发生时,默认会调用act中的sa_handler信号处理函数, oct保存旧的sigaction结构。
信号集#
sa_mask在现有进程的基础上指定了一组信号,这些信号不会被发送给本进程,通过一组函数来设置信号集。
#include <signal.h>
int sigemptyset(sigset_t* __set); // 清空信号集
int sigfillset(sigset_t* __set); // 设置所有信号
int sigaddset(sigset_t* __set, int __signo); // 添加某个信号
int sigdelset(sigset_t* __set, int __signo); // 删除某个信号
int sigismemberset(const sigset_t* __set, int __signo); // 判断某个信号是否在信号集中
被挂起的信号#
在进程设置了掩码后,相关的信号就被屏蔽了,如果给进程发送一个被屏蔽的信号,则操作系统会将该信号挂起,如果取消对挂起信号的屏蔽,则我们会立刻收到一条该信号。
查看挂起信号的函数
#include <signal.h>
int sigpending(sigset_t* set);
set保存了被挂起信号的信号集。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?