UNIX Network Programming
Table of Contents
基本概念
网络中进程之间要通信首先需要标识自己,本地是通过PID标识的,而网络中则是通过IP地址标识主机,通过协议和端口标识进程。使用TCP/IP协议的应用程序通常采用套接字通信,套接字源于UNIX。
在网络编程中还需要注意字节序,网络字节序为大端,因此将CPU数据传递给网络接口的时候,需要进行字节序转换。常用的也就四个函数:hton[sl]()、ntoh[sl]()。几个字母的含义是Host、Network、Short、Long。
网际网协议族和OSI模型如下图所示:
UDP不保证到达目的地,不保证顺序不变,不保证只达一次,不需要两端长期连接,数据报包含长度信息。
TCP需要建立连接,三次握手,基于字节流,提供确认机制,全双工。
SCTP和TCP最大区别是提供了多宿主连接,四次握手,支持多个流,基于消息流。
TCP机制
基本概念
连接三次握手
- 服务端:socket、bind、listen被动打开
- 客户端:connect主动打开,发送SYN,握手开始
- 服务端:accept确认ACK并发送SYN
- 客户端:确认服务端SYN,此时客户端connect返回,服务端accept返回
断开四次握手
- A主动关闭:close发送FIN
- B被动关闭:确认FIN
- B主动关闭:close发送FIN
- A确认关闭:确认FIN
应用程序接口
数据结构
struct sockaddr_in { sa_family_t sin_family; // e.g. AF_INET, AF_INET6 in_port_t sin_port; // e.g. htons(3490) struct in_addr sin_addr; // see below }; struct in_addr { uint32_t s_addr; // load with inet_pton() }; struct sockaddr_in6 { sa_family_t sin6_family; // address family, AF_INET6 in_port_t sin6_port; // port number, Network Byte Order uint32_t sin6_flowinfo; // IPv6 flow information struct in6_addr sin6_addr; // IPv6 address uint32_t sin6_scope_id; // Scope ID }; struct in6_addr { unsigned char s6_addr[16]; // load with inet_pton() }; struct sockaddr_un { sa_family_t sun_family; // AF_UNIX char sun_path[UNIX_PATH_MAX]; // pathname };
作为参数传递时要使用通用套接字sockaddr
,相当于是一个通用结构。之所以这么麻烦,是因为指定标准时还没有void
类型。
struct sockaddr { unsigned short sa_family; // 2 bytes address family, AF_xxx char sa_data[14]; // 14 bytes of protocol address };
创建套接字
int socket(int domain, int type, int protocol);
参数domain指定地址类型。
- AF_INET
- IPv4地址加端口
- AF_INET6
- IPv6地址加端口
- AF_UNIX
- 绝对路径
参数type指定套接字的类型。
- SOCK_DGRAM
- 固定长度,无连接,不可信赖消息,使用UDP协议,无连接的含义是不需要建立连接,直接发送数据包即可
- SOCK_STREAM
- 序列化、可信赖、双向面向连接的字节流
- SOCK_RAW
- IP报文接口
- SOCK_SEQPACKET
- 固定长度、序列化、可信赖、面向连接的消息
参数protocol指定协议,包括:IPPROTO_IP、IPPROTO_IPV6、 IPPROTO_ICMP、IPPROTO_RAW、IPPROTO_TCP、IPPROTO_UDP等。
要注意类型和协议是不能随意组合的,为了避免人工错误选择,可以设置协议为0,这样会自动选择匹配的协议。
绑定套接字
int inet_pton(int af, const char *str, void *addr); const char *inet_ntop(int af, const void *addr, char *str, socklen_t size); int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
在服务启动的时候需要绑定一个已知的IP地址和端口,客户就可以利用它来连接服务器,客户端不用自己分配,在调用connect的时候系统会自动随机分配一个。
由inet_pton
和inet_ntop
完成in_addr
与字符串IP地址之间的转换。
监听和连接
int listen(int sockfd, int backlog);
参数backlog用于指定最大连接个数。
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
客户端通过connect来建立连接。
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数addr用于获取客户端的协议地址,addrlen是客户端协议地址长度,如果接受成功,返回内核生成的全新描述字。注意区分参数中的sockfd是监听套接字,而返回的是已连接套接字。
断开连接
int close(int sockfd);
当使用完成之后关闭即可断开连接,关闭之后就不能继续使用该描述符。实际行为是将引用计数减1,当计数为0时才会真正去关闭套接字。
int shutdown(int sockfd, int how);
注意shutdown()不会影响引用计数,只会影响行为,由参数how来控制:
SHUT_RD
SHUT_WR
SHUT_RDWR
。
数据传输
#include <unistd.h> ssize_t read(int fd, void *buf, size_t count); ssize_t write(int fd, const void *buf, size_t count);
这一组读写方法和UNIX文件读写方法是完全相同的接口,需要注意的是凡是读写都要检查返回值,返回为0的时候表示没有读写到信息,没有读写到信息也可能是套接字被关闭,返回负数的时候表示有错误发生。
#include <sys/types.h> #include <sys/socket.h> ssize_t send(int sockfd, const void *buf, size_t len, int flags); ssize_t recv(int sockfd, void *buf, size_t len, int flags); ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen); ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen); ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags); ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
这一组中的sendto/recvfrom可以用于未连接的报文套接字,事实上对于报文套接字也可以调用connect()函数,这样做的好处就是可以用send/recv进行收发,调用send/recv并不会改变协议类型,但是可以自动帮我们加上目标地址。
IO模型
阻塞传输
阻塞式IO处理方法是当资源没有准备好的时候一直等待。
非阻塞传输
实际上就是不断的轮询,当资源没准备好的时候直接返回错误。
复用模型
select和poll是该模型基本命令,select用于确定哪些资源已经准备好,对于准备的好的资源就可以进行传输。复用模型的好处是可以一次查询多个资源。
函数select能够监视多个套接字,告诉你哪些可以读,哪些可以写等等。
int select(int numfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
该函数也有一些局限性,就是最大可测试数目限制为FD_SETSIZE
,这通常比进程可打开文件描述符小很多。
信号驱动模型
让内核在资源准备好的时候向用户空间发送一个信号,收到信号之后开始数据传输。
异步模型
异步模型就是向内核提交一个数据传输然后返回,当传输完成之后内核发送一个信号。这种方法和信号驱动模型很相似,只不过发送信号的时间推迟到传输完成而已。