大小端转换
1 2 3 4 5 6 #include <netinet/in.h> unsigned long int htonl (unsigned long int host) ; unsigned long int ntohl (unsigned long int net) ; unsigned short int htons (unsigned short int host) ; unsigned short int ntohs (unsigned short int net) ;
socket地址结构
通用socket地址
所有操作系统接口使用都是sockaddr
。下面的几种地址结构在使用时,只要且必须要强转成sockaddr
。
1 2 3 4 5 6 #include <bits/socket.h> struct sockaddr { sa_family_t sa_family; char sa_data[14 ]; };
但是14字节的sa_data
不够存放有的协议类型的地址值,所以linux中定义了新的通用地址结构:
1 2 3 4 5 6 7 #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域
1 2 3 4 5 6 #include <sys/un.h> struct sockaddr_un { sa_family_t sin_family; char sun_path[108 ]; };
IPv4和IPv6
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 struct sockaddr_in { sa_family_t sin_family; u_int16_t sin_port; struct in_addr sin_addr ; }; struct in_addr { u_int32_t a_addr; } struct sockaddr_in6 { sa_family_t sin_family; u_int16_t sin6_port; uint32_t sin6_flowinfo; struct in6_addr sin6_addr ; u_int32_t sin6_scope_id; }; struct in6_addr { unsigned char sa_addr[16 ]; };
IP地址转换函数
通常会习惯用字符串表示IPv4,用16进制表示IPv6,但在网络编程中需要转换成二进制使用;而在读取时,比如写入日志,为了方便查找,又希望转换成可读的字符串。所以有以下函数用于转换。
1 2 3 4 5 #include <arpa/inet.h> in_addr_t inet_addr (const char * strptr_t ) ; int inet_aton (const char * cp, struct in_addr* inp) ; char * inet_ntoa (struct in_addr* inp) ;
socket
创建socket
1 2 3 4 #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的类型,但还没有和地址进行绑定,只有在绑定后客户端才能知道如何连接它。
1 2 3 4 #include <sys/types.h> #include <sys/socket.h> int bind (int sockfd, const struct sockaddr* addr, socklen_t addrlen) ;
监听
在绑定地址后,客户端就可以通过ip地址和端口号与socket进行连接,但是这边还是没有准备好接受连接,需要创建一个监听队列来存放待处理的客户端连接。
1 2 3 #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,绑定了客户端的地址。
1 2 3 4 #include <sys/types.h> #include <sys/socket.h> int accept (int sockfd, struct sockaddr* clientaddr, socklen_t * clientaddrlen) ;
发起连接
客户端通过调用connect向服务器发起连接。
1 2 3 4 #include <sys/types.h> #include <sys/socket.h> int connect (int sockfd, const struct sockaddr* serv_addr, socklen_t addrlen) ;
连接成功后,sockfd就绑定了服务器的地址,通过对sockfd读写就可以与服务器通信。
读写操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 #include <sys/types.h> #include <sys/socket.h> 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) ;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; socklen_t msg_namelen; struct iovec * msg_iov ; int msg_iovlen; void * msg_control; socklen_t msg_controllen; int msg_flags; }; struct iovec { void * iov_base; size_t iov_len; }
flags控制了socket读写的操作逻辑。
地址信息函数
有时候我们想知道一个socket连接的本地socket地址和远端socket地址。
1 2 3 4 #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选项
1 2 3 4 #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
域名函数
1 2 3 4 5 6 7 8 9 10 11 12 #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; };
获取服务函数(对应端口号)
1 2 3 4 5 6 7 8 9 10 11 #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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 #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
1 2 3 #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]写,从而实现了两个进程之间的通信。
1 2 3 #include <unistd.h> int pipe (int fd[2 ]) ;
dup和dup2函数
1 2 3 4 #include <unistd.h> int dup (int filedes) ;int dup2 (int filedes, int filedes2) ;
创建一个文件描述符,复制filedes的所有属性。dup2返回的是不小于filedes2的文件描述符。
readv和writev
readv:分散读,将文件描述符中的内容分散到若干的iovec内存块中;
wirtev:集中写,将若干iovec内存块的内容集中写到文件描述符中。
1 2 3 #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实现了在两个文件描述符间直接传递数据(一直在内核中操作),避免了内核态和用户态之间的数据拷贝。
1 2 3 #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是一对用于内存分配和释放的函数,申请到的内存可以直接将文件描述符映射到其中,常作为开辟进程间通信的共享内存。
1 2 3 4 #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用于两个文件描述符之间进行数据移动,也是零拷贝的操作。
1 2 3 #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函数不消耗数据,被传输的数据仍可以做后续的读操作。
1 2 3 #include <fcntl.h> ssize_t tee (int fd_in, int fd_out, size_t len, unsigned int flags) ;
fcntl函数
fcntl是对文件描述符的控制函数,功能十分强大,有很多的flags控制各种属性和行为。
1 2 3 #include <fcntl.h> int fcntl (int fd, int cmd, ...) ;
在网络编程中,fcntl主要用于设置文件描述符是阻塞的还是非阻塞的。
I/O复用
select API
轮询所有文件描述符,查看设置的文件描述符是否被修改,修改就意味着对应的文件描述符触发了相应的事件并处于就绪状态。
1 2 3 #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一样也是轮询。
1 2 3 4 5 6 7 8 #include <poll.h> int poll (struct pollfd* fds, nfds_t nfds, int timeout) ;struct pollfd { int fd; short events; short reevents; };
epoll
通过在内核中创建一个事件表,而无需反复传入文件描述符集和事件集。
1 2 3 4 5 6 7 8 9 10 #include <sys/epoll.h> int epoll_create (int 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_data_t data; }
通过epoll_create
创建事件表,并返回指向该事件表的文件描述符epfd;
epoll_ctl
通过op的宏EPOLL_CTL_*控制向事件表中注册的事件;
epoll_wait
监听事件,一旦有事件触发,就将事件从内核写入events中,maxevents表示最多监听多少事件,返回值是触发的文件描述符数量。
信号函数
1 2 3 4 5 6 7 8 9 #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在现有进程的基础上指定了一组信号,这些信号不会被发送给本进程,通过一组函数来设置信号集。
1 2 3 4 5 6 7 #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) ;
被挂起的信号
在进程设置了掩码后,相关的信号就被屏蔽了,如果给进程发送一个被屏蔽的信号,则操作系统会将该信号挂起,如果取消对挂起信号的屏蔽,则我们会立刻收到一条该信号。
查看挂起信号的函数
1 2 3 #include <signal.h> int sigpending (sigset_t * set ) ;
set保存了被挂起信号的信号集。