网络常用函数

大小端转换#

#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保存了被挂起信号的信号集。

作者:cwtxx

出处:https://www.cnblogs.com/cwtxx/p/18718221

版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。

posted @   cwtxx  阅读(6)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?
more_horiz
keyboard_arrow_up dark_mode palette
选择主题
menu
点击右上角即可分享
微信分享提示