写在前面: 最近在看Nginx具体接口的实现,发现一些网络接口不是很熟悉,大概看了下Unix网络编程,发现上面都有具体介绍。后续这段时间攻读下这本教程。
记于 2018-1-30
第三章 套接字编程简介
1. IPv4套接字地址结构
IPv4套接字地址结构通常也称为“”网际套接字地址结构“”, 他以sockaddr_in命名, 定义在<netinet/in.h>中
struct in_addr { in_addr_t s_addr; //32-bit IPv4 address,network byte ordered };
struct sockaddr_in { uint8_t sin_len; //length of structure(16) sa_family_t sin_family; //AF_INET in_port_t sin_port; //16-bit TCP or UDP port number struct in_addr sin_addr; char sin_zero; //unused };
2. 通用套接字地址结构
当作为一个参数传递进任何套接字函数时, 套接字地址结构总是以引用形式(也就是指向该结构的指针)来传递,然而以这样的指针作为参数之一的任何套接字函数必须处理来自所支持的任何协议族的套接字地址结构
//<sys/socket.h> struct sockaddr { uint8_t sa_len; sa_family_t sa_family; //address family:AF_xxx value char sa_data[14]; //portocol-specific address };
3. IPv6 套接字地址结构
//<netinet/in.h> struct in6_addr{ uint8_t s6_addr[16]; //128-bit IPv6 address network byte ordered }; #define SIN6_LEN; //required for compile-time tests struct sockaddr_in6{ uint8_t sin6_len; //length of this struct sa_family_t sin6_family; //AF_INET6 in_port_t sin6_port; //transport layer port //network byte ordered uint32_t sin6_flowinfo; //flow information undefined struct in6_addr sin6_addr; //IPv6 address uint32_t sin6_scope_id; //set of interfaces for a scope };
4. 套接字地址结构是在进程和内核之间传递的
从进程到内核传递套接字地址结构的函数有3个:bind 、connect、sendto
从内核到进程传递套接字地址结构的函数有4个: accept、recvfrom、getsockname、getpeername
5. 术语“小端”和“大端”表示多个字节值的哪一端(小端或大端)存储在该值的起始位置
我们把某个给定系统所用的字节序称为主机字节序(host byte order)
主机字节序和网络字节序转换函数
#include<netinet/in.h> uint16_t htons(uint16_t host16bitvalue); uint32_t htonl(uint32_t host32bitvalue); //均返回:网络字节序的值 uint16_t ntohs(uint16_t net16bitvalue); uint32_t ntohl(uint32_t net32bitvalue); //均返回:主机字节序的值其中: h代表host,n代表network, s 代表short,l 代表long
6. 字节操作函数
名字以b开头的函数起源于4.2BSD,名字以mem(表示内存)开头的函数起源于ANSI C标准
下面是Berkeley函数:
#include<strings.h> void bzero(void* dest,size_t nbytes); void bcopy(const void* src,void* dest,size_t nbytes); void bcmp(const void* ptr1,const void* ptr2,size_t nbytes); //返回:若相等则为0, 否则为非0
下面是ANSI C函数:
#include<string.h> void* memset(void* dest,int c,size_t len); void* memcpy(void* dest,const void* src,size_t nbytes); int memcmp(const void* ptr1,size_t nbytes); //若相等则为0,否则为>0或<0
7. inet_aton 、inet_addr、 inet_ntoa 函数
#include<arpa/inet.h> int inet_aton(const char* strptr,struct in_addr* addrptr); //返回:若字符串有效则为1, 否则为0 in_addr_t inet_addr(const char* strptr); //返回:若字符串有效则为32位二进制网络字节序的IPv4地址,否则为INADDR_NONE char* inet_ntoa(struct in_addr inaddr); //返回:指向一个点分十进制数串的指针
inet_aton将strptr所指C字符串转换成一个32位的网络字节序二进制值,并通过指针addrptr来存储。
8. inet_pton 和 inet_ntop 函数
这两个函数是随IPv6出现的新函数。函数名中p和n分别代表表达(presentation)和数值(numeric)。
地址的表达式格式通常是ANSII字符串,数值格式则是存放到套接字地址结构中的二进制值
#include<arpa/inet.h> int inet_pton(int family,const char* strptr,void* addrptr); //返回:若成功则为1, 若输入不是有效的表达式格式则为0, 若出错则为-1 const char* inet_ntop(int family,const void* addrptr,char* strptr,size_t len); //返回:若成功则为指向结果的指针,若出错则为NULL
注: 表达格式就是 点分十进制格式, 数值格式就是in_addr{} 格式
表达格式就是在一个IPv4 的点分十进制数串之后,或者在一个括以方括号的IPv6地址的十六进制数串格式之后,跟一个终止符(分号),再跟一个十进制的端口号,最后跟一个空字符。
因此,缓冲区大小对于IPv4至少为INET_ADDRSTRLEN加上6个字节(16+6=22),对于IPv6至少为INET6_ADDRSTRLEN加上8个字节(46+8=54)
9. sock_ntop和相关函数
#include"unp.h" char* sock_ntop(const struct sockaddr* sockaddr,socklen_t addlen);
sockaddr指向一个长度为addrlen的套接字地址结构。
10. 字节流套接字
字节流套接字(如TCP套接字)上的read和write函数所表现的行为不同于通常的文件I/O, 字节流套接字上调用read和write输入或输出的字节数可能比请求的数量少,然而这不是出错的状态,这个现象的原因在于内核中用于套接字的缓冲区可能已经达到了极限。此时所需的是调用者再次调用read和write函数。
第四章 基本TCP套接字编程
1. socket函数
#include<sys/socket.h> int socket(int family,int type,int portocol); //返回:若成功则为非负描述符,若出错则为-1
family 参数指明协议族, type指明套接字类型。 protocol设为某个协议类型常值,或者设为0
family | 说明 |
AF_INET | IPv4协议 |
AF_INET6 | IPv6协议 |
AF_LOCAL | UNIX域协议 |
AF_ROUTE | 路由套接字 |
AF_KEY | 秘钥套接字 |
type | 说明 |
SOCK_STREAM | 字节流套接字 |
SOCK_DGRAM | 数据报套接字 |
SOCK_SEQPACKET | 有序分组套接字 |
SOCK_RAW | 原始套接字 |
protocol | 说明 |
IPPROTO_TCP | TCP传输协议 |
IPPROTO_UDP | UDP传输协议 |
IPPROTO_SCTP | STCP传输协议 |
2. connect函数
#include<sys/socket.h> int connect(int sockfd,const struct sockaddr* seraddr,socklen_t addrlen);
返回: 若成功则为0, 若出错则为-1
按照TCP状态转换图,connect函数导致当前套接字从CLOSED 状态(该套接字自从由socket函数创建以来一直所处的状态)转移到SYN_SENT状态,若成功则再转移到ESTABLISHED状态。
若connect失败则该套接字不再可用,必须关闭,我们不能对这样的套接字再次调用connect函数。
3. bind函数
#include<sys/socket.h> int bind(int sockfd,const struct sockaddr* myaddr,socklen_t addrlen);
对于TCP,调用bind函数可以指定一个端口号,或指定一个IP地址,也可以两者都指定,还可以都不指定
如果一个TCP客户或者服务器未曾调用bind捆绑一个端口,当调用connect或listen时,内核就要为相应的套接字选择一个临时端口。让内核来选择临时端口对于TCP客户来说是正常的,除非应用需要一个预留端口。
进程指定 | 结果 | |
IP地址 | 端口 | |
通配地址 | 0 | 内核选择IP地址和端口 |
通配地址 | 非0 | 内核选择IP地址, 进程指定端口 |
本地IP地址 | 0 | 进程指定IP地址,内核选择端口 |
本地IP地址 | 非0 | 进程指定IP地址和端口 |
4. listen函数
根据TCP状态转换图,调用listen导致套接字从CLOSED状态转换到LISTEN状态
#include<sys/socket.h> int listen(int sockfd,int backlog); //返回:若成功返回0,失败返回-1
内核为任何一个给定的监听套接字维护两个队列:
* 未完成连接队列
* 已完成连接队列
5. accept函数
accept函数由TCP服务器调用,用于从已完成连接队列头返回下一个已完成连接
#include<sys/socket.h> int accept(int sockfd,struct sockaddr* cliaddr,socklen_t* addrlen); //返回:若成功则为非负描述符,若出错则为-1
cliaddr 和addrlen 用来返回对方进程(客户)的协议地址。也就是返回客户端的IP地址
我们称第一个参数sockfd为监听套接字(listening socket)描述符(由socket创建,随后用作bind和listen的第一个参数的描述符)
称它的返回值为已连接套接字(connected socket)描述符。
一个服务器通常仅仅创建一个监听套接字,它在该服务器的生命期内一直存在。内核为每个由服务器进程接收的客户创建一个已连接套接字(也就是说对于它的三次握手过程已经完成)。当服务器完成对某个给定客户的服务时,相应的已连接套接字就会关闭。
如果accept函数的第二个和第三个参数都是空指针,表示我们队客户的身份不感兴趣
6. fork和exec函数
#include<unistd.h> pid_t fork(); 返回:在子进程中为0,在父进程中为子进程Id,-1--出错
在调用进程(成为父进程),它返回一次,返回值是新派生进程(成为子进程)的进程ID;在子进程它还返回一次,返回值为0.
因此可以用返回值来判断当前进程是子进程还是父进程
fork 在子进程返回0而不是父进程ID,原因是:子进程只有一个父进程,它总是可以调用getppid来得到
fork有两个典型应用:
1. 一个进程可以为自己创建一个拷贝,这样,当一个拷贝处理一个操作时,其他的拷贝可以执行其他的任务。这是非常典型的网络服务器
2. 一个进程想执行其他的程序,由于创建进程的唯一方法是调用fork,进程首先调用fork来生成一个拷贝,然后其中一个拷贝(通常为子进程)调用exec来代替自己去执行新程序。
#include<unistd.h> int execve(const char* pathname,char* argv[],char* const envp[] ); 返回:-1--出错, 无返回--成功
7. close函数
#include<unistd.h> int close(int sockfd); return: 0-ok,-1--error
8. getsockname 和getpeername函数
#include<sys/socket.h> int getsockname(int sockfd,struct sockaddr* localaddr,socklen_t* addrlen); int getpeername(int sockfd,struct sockaddr* peeraddr,socklen_t* addrlen); return : 0-ok,-1-error
getsockname : 返回与套接字关联的本地协议地址
getpeername: 返回与套接字关联的远程协议地址
当一个服务器由调用accept的进程调用exec启动执行时,它获得客户身份的唯一途径就是调用getpeername
第七章 套接字选项
7.1 概述
* getsockopt 和setsockopt
* fcntl
* ioctl
7.2 getsockopt 和setsockopt 函数
#include<sys/socket.h> int getsockopt(int sockfd,int level,int optname,void* optval,socklen_t* optlen); int setsockopt(int sockfd,const void* optval,socklen_t* optlen); return : 0-ok,-1-error
sockfd: 指向一个打开的套接字描述符
level:
SOL_SOCKET :
IPPROTO_TCP :
optname:
SO_REUSEADDR : 允许重用本地地址
SO_REUSEPORT :允许重用本地地址
SO_KEEPALIVE : 如果2 小时内在此套接字的任一方向都没有数据交换,TCP就自动给对方发一个keepalive probe
SO_RCVBUF : 接收缓冲区大小
SO_SNDBUF:发送缓冲区大小
SO_SETFIB :
SO_SNDLOWAT : 发送低潮限度, select函数使用
SO_ACCEPTFILTER :
SO_UPDATE_ACCEPT_CONTEXT
SO_BINDANY :
SO_LINGER : 若有数据待发送则延迟关闭
TCP_FASTOPEN :
TCP_NODELAY : 禁止Nagle 算法
TCP_DEFER_ACCEPT :
TCP_NOPUSH
TCP_CORK
IP_RECVDSTADDR : 返回目的ip地址
IP_PKTINFO :
IP_BIND_ADDRESS_NO_PORT :
IP_TRANSPARENT :
IPV6_RECVPKTINFO :
IPV6_V6ONLY:
IPV6_TRANSPARENT:
IPV6_BINDANY :
管道的容量称为带宽-延迟积(bandwidth-delay product),我们可以将带宽(位/秒)乘上RTT(秒),并将结果由位转换成字节来计算得到。