当前位置: 首页 > news >正文

网站开发要做什么电商联盟推广

网站开发要做什么,电商联盟推广,技术共享平台,网络优化概念计算机网络 网络协议初识协议分层OSI七层模型TCP/IP五层模型--初识 网络中的地址管理IP地址MAC地址 网络传输基本流程网络编程套接字预备知识网络字节序socket编程UDP socketTCP socket地址转换函数Jsoncpp 进程间关系与守护进程进程组会话控制终端作业控制守护进程 网络命令TC… 计算机网络 网络协议初识协议分层OSI七层模型TCP/IP五层模型--初识 网络中的地址管理IP地址MAC地址 网络传输基本流程网络编程套接字预备知识网络字节序socket编程UDP socketTCP socket地址转换函数Jsoncpp 进程间关系与守护进程进程组会话控制终端作业控制守护进程 网络命令TCP/IP五层模型应用层HTTP协议URLHTTP协议格式HTTP的方法HTTP的状态码HTTP的常见HeaderHTTP cookie与HTTP sessionHTTP cookieHTTP session HTTPS协议原理 传输层UDPTCPTCP协议端的格式确认应答机制超时重传机制连接管理机制建立连接断开连接 滑动窗口流量控制拥塞控制延迟应答粘包问题TCP全连接队列 tcpdump抓包 网络层协议格式IP报文的分片和组装网段划分网络路由 数据链路层以太网帧格式MTUARP协议ARP协议格式ARP欺骗 局域网转发 其他协议和技术NAT技术DNSICMP协议代理服务器正向代理反向代理NAT 和代理服务器 内网穿透内网打洞 高级IO五种IO模型IO多路转接selectpollepoll 网络协议初识 协议分层 使用网络进行通信的一般是2台不同的主机伴随着通信主机双方物理距离的变长势必会带来一系列的问题 ①数据处理问题通信双方拿到数据后该如何处理数据②可靠性问题如何保证数据在长距离传输过程中不会丢失同时保证对方接收到的数据顺序不会错乱③主机定位问题通信双方如何快速定位对方主机已将数据发送给对方④数据包局域网转发问题数据在网络中是一个节点接着一个节点往下转发最后到达目的主机的如何将数据从一个节点转发到下一个节点 基于以上问题人们提出了一系列的解决方案最后提出了网络协议该方案对网络进行了分层层与层之间是解耦合的可以随时替换和维护可拓展性强维护方便。 OSI七层模型 OSIOpen System Interconnection开放系统互连七层网络模型称为开放式系统互联参考模型是一个逻辑上的定义和规范把网络从逻辑上分为了7层每一层都有相关对应的物理设备比如路由器交换机等。OSI 七层模型是一种框架性的设计方法其只是提供了一种标准并没有提供最后的具体实现其最主要的功能使就是帮助不同类型的主机实现数据传输它的最大优点是将服务、接口和协议这三个概念明确地区分开来概念清楚理论也比较完整通过七个层次化的结构模型使不同的系统不同的网络之间实现可靠的通讯。 但是人们后来实践起来的时候发现将5、6、7层分开实现困难既复杂又不实用最后人们将这三层进行了合并统称为应用层这就是TCP/IP五层模型由于我们很少考虑物理层故其也称TCP/IP四层模型。 TCP/IP五层模型–初识 TCP/IP五层模型中每一层都有特定的功能以解决前面所说的数据在网络中传输的问题 物理层负责光/电信号的传递方式比如现在以太网通用的网线双绞线、早期以太网采用的同轴电缆现在主要用于有线电视、光纤现在的wifi无线网使用电磁波等都属于物理层的概念。物理层的能力决定了最大传输速率、传输距离、抗干扰性等。集线器Hub工作在物理层数据链路层用于解决问题④负责设备之间的数据帧的传送和识别。例如网卡设备的驱动、帧同步即从网线上检测到什么信号算作新帧的开始、冲突检测如果检测到冲突就自动重发、数据差错校验等工作有以太网、令牌环网、无线LAN等标准。交换机Switch工作在数据链路层。网络层用于解决问题③负责地址管理和路由选择。例如在IP协议中通过IP地址来标识一台主机并通过路由表的方式规划出两台主机之间的数据传输的线路路由。路由器Router工作在网路层现在不仅仅是在网络层工作传输层用于解决问题②负责两台主机之间的数据传输。如传输控制协议TCP能够确保数据可靠的从源主机发送到目标主机应用层用于解决问题①负责应用程序间沟通如简单电子邮件传输SMTP、文件传输协议FTP、网络远程访问协议Telnet等我们的网络编程主要就是针对应用层 一般而言一台主机实现了从传输层到物理层的全部内容对于一台路由器它实现了从网络层到物理层对于一台交换机它实现了从数据链路层到物理层对于集线器它只实现了物理层但是现在这些并不绝对很多交换机也实现了网络层的转发很多路由器也实现了部分传输层的内容比如端口转发。 Linux和Windows尽管是不同的操作系统但他们都遵循TCP/IP协议故二者之间可以直接进行通信协议的本质就是约定好的结构化的数据只要数据结构保持一致Linux和Windows对协议的具体实现可以不同。 网络中的地址管理 IP地址 IP协议有两个版本IPv4和IPv6我们这里只考虑IPv4。IP地址是在IP协议中用来标识网络中不同主机的地址对于IPv4来说, IP地址是一个4字节32位的整数我们通常也使用 “点分十进制” 的字符串表示IP地址例如 192.168.0.1 用点分割的每一个数字表示一个字节, 每一个数字范围是 0 - 255。 MAC地址 MAC地址用来识别数据链路层中相连的节点长度为48比特位6个字节一般用16进制数字加上冒号的形式来表示例如08:00:27:03:fb:19MAC地址在网卡出厂时就确定了不能修改通常是全球唯一的(虚拟机中的mac地址不是真实的mac地址可能会冲突也有些网卡支持用户配置mac地址 用户可以使用Linux指令 ifconfig 查看 MAC地址 IP 地址描述的是网络中一条路径的起点和终点MAC 地址描述的是路途上的相邻两个站点之间的起点和终点。 网络传输基本流程 不同的协议层对数据包有不同的称谓在应用层叫请求(request)/响应(respone)在传输层叫做段(segment)或数据段在网络层叫做数据报 (datagram)在链路层叫做帧(frame)或数据帧。应用层数据通过协议栈发到网络上时每层协议都要加上一个数据首部(header)也称为协议报头该过程称为封装(Encapsulation)首部信息中包含了一些类似于首部有多长、载荷(payload)有多长、上层协议是什么等信息数据封装成帧后发到传输介质上到达目的主机后每层协议再剥掉相应的首部根据首部中的 “上层协议字段” 将数据交给对应的上层协议处理该过程称为解包与分用解包即在当前协议层中将报头与有效载荷分离分用即将有效载荷交付上层协议处理。 网络编程套接字 预备知识 在认识网络套接字之前我们先来学习一下端口号现在我们知道可以利用ip地址将一个主机的数据发送给另一个主机但目的主机上有那么多的进程可以理解为有很多程序例如既打开了微信又打开了QQ目的主机接收到数据后该交给哪一个进程来处理呢我们这里使用端口号(port)来标识唯一一个进程其是一个2字节的16位整数用来告诉操作系统要将该数据交给哪一个进程通常一个进程通常可以绑定多个端口号但一个端口号只能唯一绑定一个进程因此利用ip地址和端口号就可以唯一确定计算机网络中的一个进程而在ip数据报报头中有2个IP地址分别叫做源ip地址、目的ip地址用来唯一标识互联网中的源主机和目的主机在传输协议层的数据段报头中有2个端口号分别叫做源端口号和目的端口号分别用来标识源主机的唯一进程和目的主机的唯一进程所以网络通信的本质就是进程间通信。需要注意的是尽管技术上可以使用进程的pid代替端口号但这无疑会增加网络的os的耦合度因此该方法不被采用。 其中0-1023被称为知名端口号Well-Know Port Number只有root用户可以绑定HTTP, FTP, SSH 等这些广为使用的应用层协议他们的端口号都是固定的一些常用的服务器都是用以下这些固定的端口号: ssh 服务器使用 22 端口ftp 服务器使用 21 端口http 服务器使用 80 端口https 服务器使用 443 用户执行以下命令就可以查看知名端口号 cat /etc/services1024-65535是客户端程序的端口号就是可以由os动态为进程分配的端口号。 这里再简单认识一些传输层协议 1.TCP协议 对TCP协议(Transmission Control Protocol 传输控制协议)的特点如下 传输层协议有连接可靠传输面向字节流 TCP协议保证可靠性一旦数据发送出错就会重发意味着TCP协议更为复杂同时其数据是面向字节流的即目的主机可能是先接收到数据的一部分接着再读取到另一部分需要自行对数据拼接其通常用于对数据出错丢包不容忍的场景如网络支付。 2.UDP协议 UDP协议(User Datagram Protocol 用户数据报协议)的特点如下 传输层协议无连接不可靠传输面向数据报 UDP协议不保证可靠性一旦数据发送出错不做任何处理意味着UDP协议较为简单同时其数据是面向数据报的即目的主机接收到的一定是一个完整的数据其通常用于对数据丢包可以容忍的场景如网络聊天。 网络字节序 我们已经知道内存中的多字节数据相对于内存地址有大端和小端之分磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分网络数据流同样有大端小端之分那么如何定义网络数据流的地址呢? 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出接收主机把从网络上接到的字节依次保存在接收缓冲区中也是按内存地址从低到高的顺序保存因此网络数据流的地址应这样规定先发出的数据是低地址后发出的数据是高地址。 TCP/IP协议规定网络数据流应采用大端字节序即低地址高字节不管这台主机是大端机还是小端机都会按照这个TCP/IP规定的网络字节序来发送/接收数据如果当前发送主机是小端就需要先将数据转成大端否则就忽略直接发送即可。 为使网络程序具有可移植性使同样的C代码在大端和小端计算机上编译后都能正常运行可以调用以下库函数做网络字节序和主机字节序的转换 #include arpa/inet.huint32_t htonl(uint32_t hostlong); uint16_t htons(uint16_t hostshort); uint32_t ntohl(uint32_t netlong); uint16_t ntohs(uint16_t netshort);这些函数名很好记h表示hostn表示networkl表示32位长整数s表示16位短整数例如htonl表示将32位的长整数从主机字节序转换为网络字节序例如将IP地址转换后准备发送。 如果主机是小端字节序这些函数将参数做相应的大小端转换然后返回如果主机是大端字节序这些函数不做转换将参数原封不动地返回。 socket编程 socket编程相关接口可以用于多种用途本地通信、跨网络通信、网络管理等只不过我们一般用于网络通信中其他用途可以使用其他的接口替换。 UDP socket 1.创建socket文件描述符 #include sys/types.h #include sys/socket.hint socket(int domain, int type, int protocol);如果成功socket() 函数返回一个非负整数即新创建的套接字的文件描述符。这个描述符用于后续的网络操作如绑定地址bind、监听连接listen仅对 SOCK_STREAM 套接字、接受连接accept、发送数据send 或 sendto、接收数据recv 或 recvfrom等。如果失败socket() 函数返回 -1并设置全局变量 errno 以指示错误原因。参数说明 domain指定套接字使用的协议族。常用的协议族有 AF_INETIPv4 地址和 AF_INET6IPv6 地址。 type指定套接字的类型。常用的类型有 SOCK_STREAM流式套接字如 TCPSOCK_DGRAM数据报套接字如 UDP以及 SOCK_RAW原始套接字允许对较低级别的协议如 IP 或 ICMP 直接访问。使用UDP协议时传 SOCK_DGRAM 即可 protocol指定使用的特定协议。大多数情况下该参数可以设置为 0让系统自动选择该类型套接字对应的默认协议。 无论是客户端还是服务端都需要先创建网络套接字的文件描述符。 2.绑定端口号 #include sys/types.h #include sys/socket.hint bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);成功时bind 函数返回0。失败时返回-1并设置全局变量 errno 以指示错误原因参数说明 sockfd要绑定的套接字的文件描述符。 addr指向特定协议的地址结构的指针该结构包含了要绑定的IP地址和端口号。 addrlenaddr 参数的长度通常使用 sizeof() 操作符来获取。 sockaddr结构体说明 sockaddr是一个通用的套接字地址结构体用于存储套接字的地址信息。它在 sys/socket.h 头文件中定义不过我们通常使用它的派生结构体 sockaddr_in 在使用时可以将派生结构体指针强转为 sockaddr 指针我们保证这种转换是安全的。 sockaddr_in #includenetinet/in.h #includearpa/inet.hstruct sockaddr_in { sa_family_t sin_family; // 地址族对于 IPv4 来说总是 AF_INET uint16_t sin_port; // 端口号网络字节序 struct in_addr sin_addr; // IPv4 地址 // 在某些系统上可能还有用于填充的字节以确保结构体大小与 sockaddr 一致 // 但这通常不是显式声明的字段 }; // 其中in_addr 结构体通常定义如下 struct in_addr { uint32_t s_addr; // IPv4 地址网络字节序 };sockaddr_in 是网络编程中用于表示 IPv4 套接字地址的结构体。它是在 netinet/in.h 或 arpa/inet.h取决于操作系统和编译器头文件中定义的是 sockaddr 结构体的一个派生或特定于协议的版本。sockaddr_in 结构体提供了存储 IPv4 地址、端口号以及地址族对于 IPv4 来说地址族总是 AF_INET的字段。 需要注意在填充 sockaddr_in 结构体时需要将主机字节序转为网络字节序同时也不推荐绑定任何一个确定的IP地址一台机器可以拥有多个ip地址而是使用任意绑定即将s_addr字段填充0或INADDR_ANY宏值为0相当于绑定了0.0.0.0表示任意绑定。如果我们绑定的ip地址是127.0.0.1则表示本地环回即数据只是走一遍网络协议栈并不发送出去通常用于代码测试。 为了安全起见可以在定义 sockaddr_in结构体后使用 bzero() 将该结构体置0 #include string.h void bzero(void *s, size_t n);sockaddr 还有其他的派生结构体如 sockaddr_in6 用于处理 IPv6 地址和端口号sockaddr_un用于本地通信。 服务端需要调用bind()显示绑定但客户端不需要这样显示绑定注意是不需要用户去手动显示绑定而不是不需要绑定os在底层会在客户端首次发送数据时自动帮我们进行绑定尽管用户也可以选择显示绑定但我们不建议这么做因为可能会出现多个进程显示绑定同一个端口号的情况就会导致只有一个进程可以绑定端口号从而导致异常。 如果用户用的是自己购买的云服务器进行socket编程需要用户自己到购买的云服务器开通端口号才可以对服务器的端口号进行绑定。 3.读写数据 #include sys/types.h #include sys/socket.h //向套接字写数据 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); ①sendto() 成功时sendto 返回发送的字节数失败时返回-1并设置全局变量 errno 以指示错误原因。 sockfd 是已打开的套接字描述符。 buf 指向要发送数据的缓冲区。 len 是要发送数据的长度。 flags 通常设置为0但可以用于控制发送操作的行为如设置消息边界。 dest_addr 是一个指向 sockaddr 结构体的指针包含了目标地址和端口信息。 addrlen 是 dest_addr 的长度。 ②recvfrom() 成功时recvfrom 返回实际接收到的字节数失败时返回-1并设置全局变量 errno 以指示错误原因。 sockfd 是接收数据的套接字描述符。 buf 是指向存储接收数据的缓冲区的指针。 len 是缓冲区的大小即最多可以接收多少字节的数据。 flags 通常设置为0但可以用于控制接收操作的行为如设置消息边界标志等。 src_addr 是一个指向 sockaddr 结构体的指针用于存储发送方的地址信息。调用前这个结构体可以是未初始化的调用后它将被填充为发送方的地址信息。 addrlen 是一个指向整数的指针它包含了src_addr结构体的大小。在调用之前应该设置为这个结构体的大小如sizeof(struct sockaddr_in)对于IPv4函数返回时它可能包含了实际存储在src_addr中的地址信息的大小。 TCP socket 1.创建socket文件描述符 #include sys/types.h #include sys/socket.hint socket(int domain, int type, int protocol);用于打开一个网络通讯端口成功返回一个文件描述符失败返回-1参数说明 domain 指定套接字使用的协议族。常用的协议族有 AF_INETIPv4 地址和 AF_INET6IPv6 地址。 type 指定套接字的类型。对于TCP协议传 SOCK_STREAM 即可 protocol 指定使用的特定协议。大多数情况下该参数可以设置为 0让系统自动选择该类型套接字对应的默认协议。 2.服务端设置地址复用 在旧的服务器进程终止后也可能是因为BUG导致服务器意外终止需要等待一段时间才能重新绑定相同的地址和端口号原因与TCP协议的设计有关特别是与TCP连接的终止过程和“TIME_WAIT”状态有关。地址复用Address Reuse是指在网络编程中允许在同一台主机上的多个套接字Socket绑定到同一个网络地址和端口。在TCP/IP网络中通常情况下一个端口在同一时间只能被一个套接字绑定。地址复用技术允许在特定条件下违反这一规则这样在服务器终止后其可以快速重启保持之前的端口号。 #includesys/socket.h int setsockopt(int sockfd, int level, int optname, const voi*optval, socklen_t optlen);setsockopt函数是一个用于设置套接字选项的函数如果设置套接字选项成功setsockopt函数返回0。这表示指定的套接字选项已经被成功设置,如果设置套接字选项失败setsockopt函数返回-1参数说明 sockfd套接字文件描述符用于标识要设置选项的套接字。 level指定选项的级别它决定了optname的解释。常见的级别包括SOL_SOCKET套接字级别选项、IPPROTO_TCPTCP协议级别选项等。 optname指定要设置的选项的名称如缓冲区大小、超时设置、广播选项等可以通过设置 SO_REUSEADDR 套接字选项来允许地址复用。 optval一个指向存储选项值的缓冲区的指针该值将被设置到套接字上。 optlenoptval缓冲区的长度用于确保传递了正确的数据大小。 使用示例 int sockfd; int opt 1; //用于将SO_REUSEADDR设置为1 如果要设置地址复用必须将SO_REUSEADDR 设置为1// 创建套接字省略了错误检查和细节 sockfd socket(AF_INET, SOCK_STREAM, 0); if (sockfd 0) { perror(socket creation failed); exit(EXIT_FAILURE); } // 设置SO_REUSEADDR选项 if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, opt, sizeof(opt)) 0) { perror(setsockopt); close(sockfd); // 出错时关闭套接字 exit(EXIT_FAILURE); } 3.绑定端口号 #include sys/types.h #include sys/socket.hint bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);与UDP的绑定用法一样其注意事项也与UDP的绑定一样 4.服务端开启监听 #include sys/types.h #include sys/socket.hint listen(int sockfd, int backlog);让sockfd处于监听状态用于服务端等待客户端connect()链接服务端成功返回0失败返回-1参数说明 sockfd指向一个已打开的套接字用于进行监听 backlog用于定义内核应该为相应套接字排队的最大连接数。即当服务器繁忙时新的连接请求会被放入队列中直到服务器准备好接受连接即调用 accept 函数。 5.客户端发起连接 #include sys/types.h #include sys/socket.hint connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);用于客户端向服务器发起连接以建立客户端与服务器之间的连接成功返回0失败返回-1参数说明 sockfd 这是客户端套接字的文件描述符它标识了一个打开的套接字。这个套接字之前应该通过 socket 函数创建但尚未与任何服务器建立连接。 addr 这是一个指向 sockaddr 结构体的指针该结构体包含了服务器的地址信息。由于 sockaddr 是一个通用结构体实际使用时通常会使用 sockaddr_in对于 IPv4 地址或 sockaddr_in6对于 IPv6 地址等更具体的结构体并通过类型转换后传递给 connect 函数具体可以参考前面UDP socket对struct sockaddr的介绍。 addrlen这个参数指定了 addr 指向的地址结构体的长度。对于 sockaddr_in 来说这个长度通常是 sizeof(struct sockaddr_in) 6.服务端获取链接 #include sys/types.h #include sys/socket.hint accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);用于服务端获取客户端使用connect()发起的连接成功返回一个文件描述符用于数据读写失败返回-1并设置对应错误码参数说明 sockfd这是监听套接字的文件描述符 addr 这是一个输出型参数用于接收客户端的地址信息。如果调用者对此信息不感兴趣可以将其设置为 NULL。如果提供了非 NULL 的地址accept 函数会将客户端的地址填充到该结构体中。 addrlen这是一个指向 socklen_t 变量的指针该变量在调用 accept 之前应该被设置为 addr 指向的地址结构体的长度。在 accept 返回时该变量会被更新为实际接收到的地址长度。 7.数据读写 #include unistd.h //向套接字写数据 ssize_t write(int fd, const void *buf, size_t count); //从套接字读数据但无法得到发送方的信息 ssize_t read(int fd, void *buf, size_t count);需要注意的是在TCP中并不是直接利用sockfd进行消息读写的而是对accept()获取连接成功后的文件描述符进行读写同时当客户端断开连接后服务端需要使用 close() 关闭该文件描述符以防止文件描述符泄漏。 地址复用 地址转换函数 在IPv4中IP地址有2中表示方法32位整型表示的IP地址和点分十进制风格的IP地址我们通常需要将IP地址在二者之间进行相互转换下面介绍一些转换函数 1.字符串风格转整型风格 ① #include sys/socket.h #include netinet/in.h #include arpa/inet.hin_addr_t inet_addr(const char *cp);用于将点分十进制的 IPv4 地址字符串如 “192.168.1.1”转换为 网络字节顺序 的整数in_addr_t 类型的函数如果输入字符串是一个有效的 IPv4 地址则 inet_addr 函数返回该地址的网络字节顺序表示in_addr_t 类型。如果输入字符串不是一个有效的 IPv4 地址或者包含无法识别的字符则函数返回 INADDR_NONE通常是 0xFFFFFFFF表示转换失败参数说明 cp指向一个字符串该字符串包含了需要转换的 IPv4 地址 ② #include sys/socket.h #include netinet/in.h #include arpa/inet.hint inet_aton(const char *cp, struct in_addr *inp);用于将点分十进制的 IPv4 地址字符串如 “192.168.1.1”转换成网络字节顺序的整型形式并存储在 struct in_addr 结构体中如果输入字符串是一个有效的 IPv4 地址则 inet_aton 函数返回非零值通常为 1如果输入字符串不是一个有效的 IPv4 地址则函数返回零参数说明 cp指向一个字符串该字符串包含了需要转换的 IPv4 地址 inp指向 struct in_addr 结构体的指针该结构体用于存储转换后的二进制形式的 IP 地址。struct in_addr 通常包含一个无符号 32 位整数uint32_t用于表示网络字节顺序的 IP 地址 struct in_addr 的具体实现可能因操作系统而异但最常见的定义如下所示 struct in_addr { uint32_t s_addr; // 这是一个无符号 32 位整数用于存储 IP 地址 };③ #include arpa/inet.hint inet_pton(int af, const char *src, void *dst);用于将点分十进制对于 IPv4或十六进制字符串对于 IPv6的 IP 地址转换为 网络字节顺序 的二进制形式的函数。这个函数比 inet_addr 函数更加灵活和强大因为它同时支持 IPv4 和 IPv6 地址并且提供了更明确的错误处理机制。如果转换成功函数返回 1如果输入的字符串不是一个有效的 IP 地址对于指定的地址族函数返回 0如果发生错误例如无效的地址族函数返回 -1并设置 errno 以指示错误原因。参数说明 af地址族Address Family指定要转换的地址类型。对于 IPv4这个值应该是 AF_INET对于 IPv6这个值应该是 AF_INET6。 src指向一个字符串该字符串包含了需要转换的 IP 地址例如对于 IPv4 是 “192.168.1.1”对于 IPv6 是 “2001:0db8:85a3:0000:0000:8a2e:0370:7334”。 dst指向用于存储转换后结果的缓冲区的指针。对于 IPv4 地址这个缓冲区应该足够大以存储一个 struct in_addr或等效的 uint32_t的值对于 IPv6 地址这个缓冲区应该足够大以存储一个 struct in6_addr 的值。 2.整型风格转字符串风格 ① #include sys/socket.h #include netinet/in.h #include arpa/inet.hchar *inet_ntoa(struct in_addr in);用于将网络字节顺序的 IPv4 地址存储在 struct in_addr 结构体中转换为点分十进制的字符串形式的函数。函数返回一个指向静态分配的字符数组的指针该数组包含了输入地址的点分十进制表示。需要注意的是这个返回的指针指向的是一个静态分配的内存区域这意味着每次调用 inet_ntoa 时它都会覆盖上一次调用的结果。参数说明 in 是一个 struct in_addr 类型的结构体它包含了一个网络字节顺序的 IPv4 地址。 需要注意的是inet_ntoa() 函数并不是线程安全的因为它使用了静态分配的缓冲区来存储结果字符串该静态缓冲区不需要用户去释放。在多线程环境中建议使用 inet_ntop() 函数作为替代该函数提供了更多的灵活性和线程安全性。 ② #include arpa/inet.hconst char *inet_ntop(int af, const void *src, char *dst, socklen_t size);用于将网络地址IPv4 或 IPv6从网络字节顺序转换为点分十进制对于 IPv4或十六进制字符串对于 IPv6的函数。这个函数比 inet_ntoa 更灵活且安全因为它允许你指定一个缓冲区来存储转换后的字符串从而避免了使用静态内存的问题这在多线程环境中尤为重要。返回一个指向 dst 的指针如果转换失败则返回 NULL并设置 errno 以指示错误原因。参数说明 af地址族AF_INET 表示 IPv4 地址AF_INET6 表示 IPv6 地址。 src指向包含网络字节顺序地址的缓冲区的指针。对于 IPv4它是一个指向 struct in_addr 的指针对于 IPv6它是一个指向 struct in6_addr 的指针。 dst指向用于存储转换后字符串的缓冲区的指针。 sizedst 缓冲区的大小以字节为单位。 这些函数也比较好记a表示地址(address)即点分十进制形式的IP地址n表示网络(network)即网络字节序的IP地址p译为表示(presentation)即人类可读的表示法可用于点分十进制的 IPv4 地址或冒号分隔的 IPv6 地址 同时需要注意前面这些所有函数包括字符串转整型所用到的或返回的整型风格的IP地址都是网络字节序的。 Jsoncpp 由于Tcp协议是面向字节流的不保证接收方能一次接收到一个完整的数据报因此需要用户在应用层将数据的格式进行约定即自定义协议或者私有协议对数据进行包装后再将数据发送给对方对方接收到数据化再按照约定对数据进行解析解析出一个完整的报文后才交付上层让上层对数据进行处理。考虑到系统复杂性和维护成本同时为了服务的拓展性一般发送方会将数据jason化将数据结构或对象转换为一种格式以便在网络上传输或存储到文件中再将数据按照自定义协议进行包装通过tcp协议发送到网络。 Jsoncpp 是一个用于处理 JSON 数据的 C 库。它提供了将 JSON 数据序列化为字符串以及从字符串反序列化为 C 数据结构的功能。Jsoncpp 是开源的广泛用于各种需要处理 JSON 数据的 C 项目中其安装方法如下 ubuntusudo apt-get install libjsoncpp-dev Centos: sudo yum install jsoncpp-devel在编译时需要添加选项 -ljsoncpp 1.序列化 ①使用 Json::Value 的 toStyledString 方法 #include jsoncpp/json/json.hJson::Value root; root[name] joe; root[sex] 男; std::string s root.toStyledString();该方法使用简单将 Json::Value 对象直接转换为格式化的 JSON 字符串 ②. 使用 Json::StreamWriter #include iostream #include string #include sstream #include memory #include jsoncpp/json/json.hint main() {Json::Value root;root[name] joe;root[sex] 男;Json::StreamWriterBuilder wbuilder; // StreamWriter 的工厂std::unique_ptrJson::StreamWriter writer(wbuilder.newStreamWriter());std::stringstream ss;writer-write(root, ss);std::cout ss.str() std::endl;return 0; }该方法提供了更多的定制选项如缩进、换行符等 ③使用 Json::FastWriter #include iostream #include string #include sstream #include memory #include jsoncpp/json/json.hint main() {Json::Value root;root[name] joe;root[sex] 男;Json::FastWriter writer;std::string s writer.write(root);std::cout s std::endl;return 0; }该方法比 StyledWriter 更快因为它不添加额外的空格和换行符。 2.反序列化 ①使用 Json::Reader #include iostream #include string #include jsoncpp/json/json.hJson::Reader reader; Json::Value root;// 从字符串s中读取 JSON 数据 if (reader.parse(s,root)) { // 解析成功访问 JSON 数据std::string name root[name].asString();int age root[age].asInt();std::string city root[city].asString(); }该方法提供详细的错误信息和位置方便调试。 ②使用 Json::CharReader 的派生类 在某些情况下可能需要更精细地控制解析过程可以直接使用Json::CharReader 的派生类。但通常情况下使用 Json::parseFromStream 或 Json::Reader 的 parse方法就足够了。 进程间关系与守护进程 进程组 每一个进程除了有一个进程 ID(PID)之外还属于一个进程组。进程组是一个或者多个进程的集合一个进程组可以包含多个进程。 每一个进程组也有一个唯一的进程组 ID(PGID)并且这个 PGID 类似于进程 ID同样是一个正整数可以存放在 pid_t 数据类型中。通常由一个进程组之间的进程相互配合完成某个特定的任务。bash进程是一个独立的进程组当用户新建一个会话时该会话也会有一个自己的-bash进程也是自成一个独立的进程组。 可以使用指令 ps -eo pid,pgid,ppid,comm 查看进程的相关信息包括进程id进程组id等信息其中 e 选项表示 every 的意思表示输出每一个进程信息o 选项以逗号操作符,作为定界符可以指定要输出的列 每一个进程组都有一个组长进程进程组组长可以创建一个进程组或者创建该组中的进程而进程组的生命周期为从进程组创建开始到其中最后一个进程离开为止即只要某个进程组中有一个进程存在则该进程组就存在这与其组长进程是否已经终止无关。 会话 会话其实和进程组息息相关当我们打开shell连接远端服务器时shell会为我们打开一个终端文件然后新启动一个-bash进程这就相当于建立了一个会话会话可以看成是一个或多个进程组的集合一个会话可以包含多个进程组其内部至少有一个进程即 -bash 进程每一个会话也有一个会话 ID(SID)一般为-bash进程的id。 在该会话下运行一个可执行程序时该可执行程序就会变成一个与该会话相关联的进程如果我们想另起一个新会话让当前会话下的可执行程序称为新会话下的进程从而与当前会话取关联可以使用函数 #include unistd.h pid_t setsid(void);创建会话成功返回 SID失败返回-1 调用该函数的进程会变成新会话的会话首进程此时新会话中只有唯一的一个进程而且进程会变成进程组组长新进程组 ID 就是当前调用进程 ID该进程没有控制终端如果在调用 setsid 之前该进程存在控制终端则调用之后会切断联系。 需要注意的是这个接口如果调用进程原来是进程组组长则会报错为了避免这种情况我们通常的使用方法是先调用 fork 创建子进程父进程终止子进程继续执行因为子进程会继承父进程的进程组 ID而进程 ID 则是新分配的就不会出现错误的情况。 控制终端 在 UNIX 系统中用户通过终端登录系统后得到一个 Shell 进程这个终端成为 Shell进程的控制终端。控制终端是保存在 PCB 中的信息我们知道 fork 进程会复制 PCB中的信息因此由 Shell 进程启动的其它进程的控制终端也是这个终端。默认情况下没有重定向每个进程的标准输入、标准输出和标准错误都指向控制终端进程从标准输入读也就是读用户的键盘输入进程往标准输出或标准错误输出写也就是输出到显示器上。 每一个终端都是一个文件他们在目录 /dev/pts/下如/dev/pts/0和/dev/pts/1都是一个终端文件可以像普通文件一样读写和重定向。 在同一个会话中任何时候都只允许运行一个前台进程组但可以同时运行多个后台进程组而只有前台进程组才可以与控制终端进行交互Linux中的命令就是通过创建一个前台进程去执行而bash进程转为后台进程了此时该命令执行期间我们再次向终端输入命令是不会有响应的因为只有前台进程可以与终端交互bash进程此时已经转为后台进程了没有接收到任何输入我们可以在命令后面加上表示在执行该命令的进程直接转为后台进程。 作业控制 作业是针对用户来讲的用户完成某项任务会启动一些进程一个作业既可以只包含一个进程也可以包含多个进程进程之间互相协作完成任务。 Shell 分前后台来控制作业或者进程组。一个前台作业可以由多个进程组成一个后台作业也可以由多个进程组成Shell 可以同时运⾏一个前台作业和任意多个后台作业这称为作业控制。 例如下列命令就是一个作业它包括两个命令在执⾏时 Shell 将在前台启动由两个进程组成的作业 cat /etc/filesystems | head -n 5放在后台执⾏的程序或命令称为后台命令可以在命令的后面加上符号从而让Shell 识别这是一个后台命令后台进程执行完后会返回一个作业号以及一个进程号PID。 可以使用指令 jobs 查看后台作业选项-l 则显示作业的详细信息选项-p 则只显示作业的 PID。以下是返回的一些符号和状态解释 : 表示该作业号是默认作业 -表示该作业即将成为默认作业 无符号 表示其他作业 对于一个用户来说只能有一个默认作业同时也只能有一 个即将成为默认作业的作业-当默认作业退出后该作业会成为默认作业。 作业状态 可以使用指令 fg 作业号 将该作业转为后台作业如果想将一个作业转为前台作业先使用ctrlz将当前作业暂停其就会自动转为后台作业接着使用指令 bg 作业号 启动该作业。 守护进程 当我们开发了一个服务端后我们是在一个会话下面启动运行该服务端的如果该会话退出会话内的任务也会退出因此我们就需要为该服务端进行新建一个会话该会话只有服务端这一个进程此时该进程就会称为一个守护进程也称为精灵进程本质就是一个孤儿进程该进程独立运行通常不与任何用户交互这样就保证了服务端运行时的稳定性。我们将一个进程变为守护进程的一般步骤为 忽略可能引起程序异常退出的信号 signal(SIGCHLD, SIG_IGN); signal(SIGPIPE, SIG_IGN);让自己不要成为组长 if (fork() 0) exit(0);设置让自己成为一个新的会话 后面的代码其实是子进程在走 setsid();每一个进程都有自己的 CWD可以选择将当前进程的 CWD 更改成为 /根目录 chdir(目录);已经变成守护进程啦不需要和用户的输入输出错误进行关联了可以直接关闭对应文件描述符 close(0); close(1); close(2);还可以选择重定向到文件dev_null中 int fd open(dev_null, O_RDWR); if (fd 0) {dup2(fd, 0);dup2(fd, 1);dup2(fd, 2);close(fd); }输入到文件dev_null的数据会被忽略从该文件读取数据会读到空如果需要推荐使用重定向这种方法。 网络命令 1. netstat netstatNetwork Status网络状态是一个用来查看网络状态的重要工具用法为 netstat [选项]常用选项 n 拒绝显示别名能显示数字的全部转化成数字l 仅列出有在 Listen (监听) 的服務状态p 显示建立相关链接的程序名t (tcp)仅显示tcp相关选项u (udp)仅显示udp相关选项a (all)显示所有选项默认不显示LISTEN相关 2. pidof 用于通过进程名查看进程id用法为 pidof [选项] [进程名称]-s仅返回一个进程号。如果有多个与指定名称匹配的进程在运行pidof将只返回第一个找到的进程的PID。-c仅显示具有相同“root”目录的进程。这个选项只对root用户有效它可以帮助用户筛选出具有特定根目录的进程。-x显示由脚本开启的进程。这个选项允许用户查找由shell脚本或其他程序启动的进程。-o指定不显示的进程ID。用户可以通过这个选项来排除某些特定的进程ID以避免它们在输出中出现 3. ping pingPacket Internet Groper网络包探测器是一个程序用于验证网络的连通性同时也会统计响应时间和 TTL(IP 包中的Time To Live生存周期)ping 命令基于 ICMP, 是在网络层使用时ping 命令会先发送一个 ICMP Echo Request 给对端对端接收到之后会返回一个 ICMP Echo Reply根本不关心端口号用法为 ping [选项] [目标主机的IP地址或域名]常用选项有 -t持续发送ping请求直到手动停止。 -n指定发送的ping请求次数。默认情况下ping命令会发送4个请求。例如“ping -n 10 192.168.1.1”将发送10个数据包进行测试。 -l或-s具体取决于操作系统设置发送的数据包大小。默认情况下ping命令发送的数据包大小为32字节。通过指定大小可以测试网络在不同数据包大小下的性能。 -a将IP地址解析为主机名。当目标主机的IP地址不容易记忆时可以使用这个参数将IP地址转换为更易于理解的主机名。例如“ping -a 192.168.1.1”可能会返回类似“Pinging example-PC [192.168.1.1]”的信息。 -w设置超时时间某些操作系统中可能不支持此参数。Ping命令默认的超时时间是一定的毫秒数如400毫秒。通过指定超时时间可以更灵活地控制ping命令的等待响应时间。例如“ping -w 1000 192.168.1.1”将超时时间设置为1000毫秒 4. traceroute tracerouteTrace Route路由追踪用于显示数据包从源主机到目的主机所经过的路由器路径它也是基于ICMP协议实现的不关心端口号用法为 traceroute [目标主机IP地址或域名]常用选项有 -n禁用域名解析只显示IP地址这可以加快命令的执行速度特别是在DNS解析较慢或不可用时。-m 跳数设置检测数据包的最大存活数值TTL的大小即限制追踪的最大跳数。-w 秒数设置等待每个路由器响应的超时时间。如果路由器在指定时间内没有响应则显示超时消息。-q 次数指定每个跳数发送的数据包数量。默认情况下Traceroute会发送多个数据包以获得更准确的延迟统计信息。-I仅Unix/Linux使用ICMP Echo请求代替UDP数据包进行追踪。这在某些情况下可能更有用特别是当目标主机对UDP数据包有特定过滤规则时。 5. telnet TelnetTErminaL NETwork是一种基于文本的远程登录协议允许用户通过网络连接到远程计算机并在远程计算机上执行命令用法为 telnet [IP地址] [端口号]它默认使用端口号是23 6.ssh SSHSecure Shell命令是一种用于远程登录、文件传输及远程命令执行的强大工具它通过网络在客户端和服务器之间建立安全的加密连接用法为 ssh [选项] 用户名主机名或IP地址默认使用的端口号是22 7.ARP ARP命令是用于操作和管理计算机网络中的ARPAddress Resolution Protocol地址解析协议缓存的命令。ARP协议的主要功能是将网络中的IP地址解析为目标硬件地址MAC地址以保证通信的顺利进行用法为 arp [选项] [参数]常用选项和参数 -a显示所有接口的当前ARP缓存表将列出计算机中存储的已解析IP地址和相应的MAC地址。-d InetAddr [IfaceAddr]删除指定IP地址的ARP缓存项。InetAddr代表要删除的IP地址IfaceAddr代表指派给该接口的IP地址可选。如果未指定IfaceAddr则使用第一个适用的接口。-s InetAddr EtherAddr [IfaceAddr]向ARP缓存添加静态项。InetAddr代表IP地址EtherAddr代表物理地址MAC地址IfaceAddr代表指派给该接口的IP地址可选。通过此命令添加的项属于静态项它们不会因ARP缓存超时而被删除。-g与-a相同用于显示本地ARP缓存表。在某些操作系统中可以使用-g选项命令来显示ARP缓存表。-n显示本地ARP缓存表但不解析主机名。这个命令可以显示ARP缓存表但不会尝试将IP地址解析为主机名。 TCP/IP五层模型 应用层 开发者写的一个个用于解决实际问题的网络程序都是在应用层进行开发由于其下一层传输层可能是基于TCP也可能是基于UDP因此应用层对不同的传输层协议有不同的处理方法对传输层协议为UDP时由于UDP是面向数据报的开发者不需要在应用层添加自定义协议保证接收到一个完整的数据报但为了维护方便通常会在应用层对数据进行json序列化和反序列化对传输层协议为TCP由于TCP是面向字节流的不保证接收方能一次就接收到一个完整的数据报因此需要开发者在应用层添加自定义协议保证接收到一个完整的数据报再进行处理为了维护方便其也要在应用层对数据进行json序列化和反序列化。 HTTP协议 我们经常说应用层的协议是由开发者自己定制的其实前人已经定制好了一些现成的且非常好用的协议可以直接供我们参考使用其中比较常用的就是HTTP协议即超文本传输协议https与http是类似的只不过https会对文本进行加密处理其是基于TCP的应用层协议。 URL 我们平时所说的网址其实就是URLUniform Resource Locator,统一资源定位符 片段标识符不是发送给服务器的而是由浏览器解析并在客户端内部使用以便直接定位到页面内的某个特定元素或执行与锚点相关联的JavaScript代码可以忽略。 总之除了服务器地址和目标文件外其余的都可以省略在HTML中服务器地址也可以省略表示请求服务器ip地址与当前HTML所属ip地址一致。 省略协议方案名则默认使用的协议是HTTP协议 省略端口号浏览器默认根据协议选择端口号HTTP协议默认访问的服务器端口号为80HTTPS协议默认访问的服务器端口号为443。 省略文件路径但目标文件名不能省表示请求的文件路径为‘/’服务端需要自己解析‘\’表示那个文件路径 像/?:等这样的字符已经被url当做特殊意义理解了在URL中?表示接下来是参数参数之间以分隔如果后面还有其他内容则用#分隔表示参数部分结束因此这些字符不能随意出现如果某个参数中需要带有这些特殊字符就必须先对特殊字符进行转义转义的规则如下 将需要转码的字符转为16进制然后从右到左取4位不足4位直接处理每2位做一位前面加上%编码成%XY格式例如的ASCII码是43被转义成 %2B。 HTTP协议格式 1.HTTP协议请求格式 [方法][空格][URL][空格][版本号][换行符][请求属性][换行符][空行(其实就是换行符)][请求正文] 其中版本号可以忽略不写请求属性也称为Header是用冒号分割的键值对如果有多组属性也需要使用换行符进行分割空行是用于表示Header部分结束如果没有在URL中指定请求资源的文件路径默认请求资源路径是“/”此时服务端如果解析到请求路径是“/”服务端开发者可以自定义返回一个主网页供客户端使用正文部分也称为BodyBody允许为空字符串如果Body存在则要在Header中加一个Content-Length属性来标识Body的长度。 例如以下一个http请求 POST http//example.edu.cn/main.html HTTP/1.1 Host: example.edu.cn Content-Length: 15 example_request 2.HTTP协议应答格式 [版本号][空格][状态码][空格][状态码解释][换行符][请求的属性][换行符][空行][应答正文] 正文部分也称为BodyBody允许为空字符串如果Body存在则要在Header中加一个Content-Length属性来标识Body的长度如果服务器返回了一个html页面HTML是一种用于创建网页内容的标记语言那么html页面内容就是在body中浏览器获取html内容后会根据获得的内容构建渲染网页呈现给用户关于HTML的学习和使用可以参考form表单 或 HTML 例如以下的一个http响应 HTTP/1.1 200 OK Content-language: zh-CN Content-Length: 16example_responseHTTP的方法 1.GET方法 用于请求URL指定的资源也可以以URL的形式向服务端提交参数即在URL中?表示接下来是参数参数之间以分隔使用这种方法时但不建议上传的参数太大。 2.POST方法: 用于传输实体的主体通常用于提交表单数据可以在正文中向服务器上传参数可以上传参数的大小比GET大且更加私密注意是私密不等同安全无论哪种方法上传参数都可以通过网络抓包工具抓到。 3.PUT方法 用于传输文件将请求报文主体中的文件保存到请求URL指定的位置在某些情况下如RESTfulAPI中可以用于更新资源 4.HEAD方法 与GET方法类似但不返回报文主体部分仅返回响应头,用于确认URL的有效性及资源更新的日期时间等 5.DELETE方法 用于删除文件按请求URL删除指定的资源是PUT的相反方法 6.OPTIONS方法 用于查询针对请求URL指定的资源支持的方法返回允许的方法如GET、POST等 这些方法中比较常用的方法是POST和GET。 HTTP的状态码 最常见的状态码比如200(OK)404(NotFound)403(Forbidden)302(Redirect,重定向)504(BadGateway) 以下是仅包含重定向相关状态码的表格 当服务器返回 HTTP 301 状态码时表示请求的资源已经被永久移动到新的位置在这种情况下服务器会在响应中添加一个 Location 头部用于指定资源的新位置。这个 Location 头部包含了新的 URL 地址浏览器会自动重定向到该地址例如 HTTP/1.1 302 Found\r\n Location: https://www.new-url.com\r\n 当服务器返回 HTTP 302 状态码时表示请求的资源临时被移动到新的位置同样地服务器也会在响应中添加一个 Location 头部来指定资源的新位置。浏览器会暂时使用新的 URL 进行后续的请求但不会缓存这个重定向例如 HTTP/1.1 302 Found\r\n Location: https://www.new-url.com\r\n总之无论是 HTTP 301 还是 HTTP 302 重定向都需要依赖 Location 选项来指定资源的新位置。这个 Location 选项是一个标准的 HTTP 响应头部用于告诉浏览器应该将请求重定向到哪个新的 URL 地址。 HTTP的常见Header Content-Type: 数据类型(text/html 等) 详细信息可以查看Content-Type Content-Length: Body 的长度 Host: 客户端告知服务器, 所请求的资源是在哪个主机的哪个端口上; User-Agent: 声明用户的操作系统和浏览器版本信息; referer 当前页面是从哪个页面跳转过来的; Location: 搭配 3xx 状态码使用, 告诉客户端接下来要去哪里访问; Cookie: 用于在客户端存储少量信息. 通常用于实现会话(session)的功能; 以上是HTTP常用的Header更多的Header如下 HTTP 中的 Connection 字段是 HTTP 报文头的一部分它主要用于控制和管理客户端与服务器之间的连接状态核心作用是管理持久连接也称为长连接持久连接允许客户端和服务器在请求/响应完成后不立即关闭 TCP 连接以便在同一个连接上发送多个请求和接收多个响应。 在 HTTP/1.1 协议中默认使用持久连接。当客户端和服务器都不明确指定关闭连接时连接将保持打开状态以便后续的请求和响应可以复用同一个连接。 在 HTTP/1.0 协议中默认连接是非持久的。如果希望在 HTTP/1.0 上实现持久连接需要在请求头中显式设置 Connection: keep-alive。 语法格式 Connection: keep-alive表示希望保持连接以复用 TCP 连接。Connection: close表示请求/响应完成后应该关闭 TCP 连接 如果用户通过浏览器访问服务端浏览器会自动发起以下请求获取网站图标 GET /favicon.ico HTTP/1.1favicon.ico 是一个网站图标通常显示在浏览器的标签页上、地址栏旁边或收藏夹中。这个图标的文件名 favicon 是 “favorite icon” 的缩写而 .ico 是图标的文件格式浏览器在发起请求的时候也会为了获取图标而专门构建 http 请求。 HTTP cookie与HTTP session Http协议本身是无状态和无连接的无连接即http协议在客户端收到服务端的应答之后就会断开连接后面引入了持久连接来解决该性能问题无状态即协议本身不缓存任何客户端历史上的请求信息如果服务端想要识别用户身份就需要用到cookie和session技术也合称为会话管理技术。 HTTP cookie HTTP Cookie也称为 Web Cookie、浏览器 Cookie 或简称 Cookie是服务器发送到用户浏览器并保存在浏览器上的一小块数据之后在浏览器向同一服务器再次发起请求时cookie被携带并发送到服务器上。通常它用于告知服务端两个请求是否来自同一浏览器如用户认证和会话管理、 跟踪用户行为、保持用户的登录状态、记录用户偏好等。 当用户第一次访问网站时服务器会在响应的 HTTP 头中设置 Set-Cookie字段用于发送 Cookie 到用户的浏览器浏览器在接收到 Cookie 后会将其保存起来可能是内存级存储也可能是文件级存储通常是按照域名进行本地存储在之后的请求中浏览器会自动在 HTTP 请求头中携带 Cookie 字段将之前保存的 Cookie 信息发送给服务器。 cookie有会话 Cookie与持久 CookiePersistent Cookie之分即内存级cookie和文件级cookie 会话 CookieSession Cookie在浏览器关闭时失效。持久 CookiePersistent Cookie带有明确的过期日期或持续时间可以跨多个浏览器会话存在可以长时间保存。 如果 cookie 是一个持久性的 cookie那么它其实就是浏览器相关的特定目录下的一个文件。但直接查看这些文件可能会看到乱码或无法读取的内容因为 cookie 文件通常以二进制或 sqlite 格式存储。一般在查看cookie时直接在浏览器对应的选项中直接查看即可。 HTTP 存在一个报头选项Set-Cookie, 可以用来进行给浏览器设置 Cookie值。在 HTTP 响应头中添加该header并设置好对应的值客户端如浏览器获取该响应后自行设置并保存如下设置一个cookie Set-Cookie: keyvalue; expiresThu, 18 Dec 2024 12:00:00 UTC; path/; domain.example.com; secure; HttpOnlykey和value时一对键值对key被用作cookie的名称value是cookie的值其余的是cookie的属性如果需要多个key–value键值对只能多次添加Set-Cookie并设置好对应属性而不能放在一个Set-Cookie中。cookie的属性是可选的每个 Cookie 属性都以分号;和空格 分隔属性和值之间使用等号分隔如果 Cookie 的名称或值包含特殊字符如空格、分号、逗号等则需要进行 URL 编码关于各个属性的解释 expiresdate 设置 Cookie 的过期日期/时间如果未指定此属性则 Cookie 默认为会话 Cookie即当浏览器关闭时cookie就过期。时间格式必须遵守 RFC 1123 标准具体格式样例 Tue, 01 Jan 2030 12:34:01 GMT/UTCGMT格林威治标准时间和 UTC协调世界时是两个不同的时间标准但它们在大多数情况下非常接近。 GMT 是格林威治标准时间的缩写它是以英国伦敦的格林威治区为基准的世界时间标准不受夏令时或其他因素的影响通常用于航海、航空、科学、天文等领域其计算方式是基于地球的自转和公转。 UTC 全称为“协调世界时”是国际电信联盟(ITU)制定和维护的标准时间UTC 的计算方式是基于原子钟而不是地球的自转因此它比 GMT 更准确现在用的时间标准多数全球性的网络和软件系统将其作为标准时间。因此我们推荐使用UTC。 pathsome_path 定义 Cookie 的作用范围。如果设置为根路径/意味着 Cookie对服务端域名下的所有路径都可用。 domaindomain_name 指定哪些主机可以接受该 Cookie默认为设置它的主机点前缀.表示包括所有子域名。 secure 仅当使用 HTTPS 协议时才发送 Cookie。这有助于防止 Cookie 在不安全的 HTTP 连接中被截获。 HttpOnly 标记 Cookie 为 HttpOnly意味着该 Cookie 不能被 客户端脚本如 JavaScript访问。这有助于防止跨站脚本攻击XSS。 HTTP session 使用cookie时由于这些用户私密数据在浏览器用户端保存非常容易被人盗取如果写入的是用户的私密数据比如用户名密码等就很容易被非法用户冒充用户登录服务端其次就是用户在cookie中的私密数据可以被解析读取从而造成私密信息泄漏。为了解决这些问题引入了HTTP session一般用于用户认证和会话管理、存储用户的临时数据如购物车内容、实现分布式系统的会话共享通过将会话数据存储在共享数据库或缓存中等。 HTTP Session 是服务器用来跟踪用户与服务器交互期间用户状态的机制。由于 HTTP协议是无状态的每个请求都是独立的因此服务器需要通过 Session 来记住用户的信息。当用户首次访问网站时服务器会为用户创建一个唯一的 Session ID并通过Cookie 将其发送到客户端。客户端在之后的请求中会携带这个 Session ID服务器通过 Session ID 来识别用户从而获取用户的会话信息。服务器通常会将 Session 信息存储在内存、数据库或缓存中。 总的来说就是服务端用一条记录记录了用户信息并用一个唯一值与该记录相关联接着服务端将该唯一值通过cookie保存在用户浏览器端当用户请求服务时客户端将该唯一值发给服务端服务端就可以利用该唯一值在自己的数据库中寻找用户的记录最后做对应的处理。 由于用户私密信息的记录是保存在服务端而服务端一般都有不错的安全防护因此这样基本可以解决用户私密信息泄漏问题。服务端还可以通过检测session是否异常以决定是否让session失效例如检测到访问用户的地址发生改变就判定session异常这样就算浏览器的cooki被盗取了服务端让该session失效非法用户就无法冒充用户了。 因此Cookie 是存储在客户端的而 Session 是存储在服务器端的。它们各有优缺点通常在实际应用中会结合使用以达到最佳的用户体验和安全性。 HTTPS协议原理 HTTP 协议内容都是按照文本的方式明文传输的明文数据会经过路由器、wifi 热点、通信服务运营商、代理服务器等多个物理节点如果信息在传输过程中被劫持传输的内容就完全暴露了。劫持者还可以篡改传输的信息且不被双方察觉这就是中间人攻击所以我们有对信息进行加密的需求由此产生了HTTPS协议。HTTPS 也是一个应用层协议是在 HTTP 协议的基础上引入了一个加密层。 加密就是把明文要传输的信息进行一系列变换生成密文解密就是把密文再进行一系列变换还原成明文在这个加密和解密的过程中往往需要一个或者多个中间的数据辅助进行这个过程这样的数据称为密钥。 常见的加密方式有2种对称加密和非对称加密 对称加密 对称加密就是采用单钥密码系统的加密方法只要同一个密钥就可以同时用作信息的加密和解密这种加密方法也称为单密钥加密其特征就是加密和解密所用的密钥是相同的。常见对称加密算法有DES、3DES、AES、TDEA、Blowfish、RC2 等采用这种加密方式有许多优点计算量⼩、加密速度快、加密效率高。 对称加密其实就是通过同一个 “密钥” , 把明文加密成密文, 并且也能把密文解密成明文例如以下一个简单的对称加密按位异或 假设明文 a 1234密钥 key 8888 则加密 a ^ key 得到的密文 b 为 9834. 然后针对密文 9834 再次进行运算 b ^ key得到的就是原来的明文 1234. (对于字符串的对称加密也是同理, 每一个字符都可以表⽰成一个数字)按位异或只是最简单的对称加密HTTPS 中并不是使用按位异或。 非对称加密 非对称加密就是需要两个密钥来进行加密和解密的加密方法这两个密钥分别是是公开密钥public key简称公钥允许被别人获取和私有密钥private key简称私钥绝对不能泄漏通常是一个用于加密另一个用于解密例如通过公钥对明文加密变成密文再通过私钥对密文解密变成明文这种用法下只有私钥可以对公钥加密出来的密文进行正确解密。也可以反着用通过私钥对明文加密变成密文通过公钥对密文解密变成明文这种用法下只有私钥加密出来的密文才能被公钥正确解密。常见非对称加密算法有RSADSAECDSA等其数学原理比较复杂涉及到一些数论相关的知识。这种加密方法算法强度复杂安全性依赖于算法与密钥加密解密速度没有对称加密解密的速度快通常是慢很多。 下面我们通过对比几种解决方案以便得到最佳解决方案 方案1只采用对称加密 双方想进行对称加密通信当客户端生成对称密钥C后由于服务端不知道该密钥因此客户端就必须要先将该密钥C发送给服务端此时中间人就可以直接窃取到该密钥C所以后续双方利用密钥C进行通信时中间人都可以利用窃取到的密钥C将密文解密成明文。因此这种方案不可行。 方案2只采用非对称加密 客户端生成公钥C和私钥C’后想用C进行加密C’进行解密于是客户端将公钥C发送给服务端服务端和中间人都得到了公钥C当服务端用得到的C对应答加密返回给客户端时尽管中间人有公钥C但其没有私钥C’就无法对该密文进行解密因此目前来看服务端到客户端的通信是安全的后面会说明其实这也并不安全但客户端到服务端的通信依旧可以被中间人窃取解读而且通信采用非对称加密通信速度慢因此这种方案不可行。 方案3双方都采用非对称加密 基于方案2我们可以让客户端生成公钥C和私钥C’服务端也生成自己的公钥S和私钥S’双方在通信前先进行公钥交换中间人进行得到了公钥C和S但由于没有对应的私钥自然就无法解读后续加密的密文因此目前来看服务端到客户端的通信是安全的同方案2一样后面会说明其实这也并不安全而且通信双方都采用非对称加密通信速度十分缓慢因此这种方案不可行。 方案4采用非对称加密和对称加密 服务端生成自己的公钥S和私钥S’客户端先获取服务端的公钥S接着生成对称密钥X通过S将对称密钥X加密发送给服务端服务端就可以利用S’对密文解密得到对称密钥X这样就只有客户端和服务端知道对称密钥X此后双方就可以利用对称密钥X进行通信了这种加密方式目前看来是安全的而且通信时采用对称加密通信速度快接下来说明为什么这种加密方式不安全 这也说明了为什么方案2和方案3的通信是不安全的因为中间人完全可以冒充客户端和服务器向另一方发送自己的公钥。 方案5采用非对称加密对称加密证书认证 现在我们清楚方案4的致命缺陷在于客户端不清楚一开始接收到的公钥是服务端的还是中间人的如果客户端可以甄别出接收到的公钥是服务端的再将对称密钥X利用得到的公钥加密发送给服务端那么中间人就无法解密出密钥X此后的客户端和服务端就可以安全的通信了。 在了解方案5之前我们需要知道什么是数字指纹、数据签名和证书。 数据指纹或者说数据摘要其基本原理是利用单向散列函数(Hash 函数)对信息进行运算生成一串固定长度的数字摘要两个不同的信息有可能算出相同的摘要但是概率非常低数字指纹并不是一种加密机制因为没有解密从摘要很难反推原信息而且源字符串只要改变一点点最终得到的数据摘要值都会差别很大通常用来进行数据对比以判断数据有没有被篡改摘要常见算法有MD5、SHA1、SHA256、SHA512 等。 数据签名就是签名者只有CA机构有资格成为签名者利用自己的私钥对一份数据一般是数据摘要这样可以缩小签名密文的长度加快数字签名的验证签名的运算速度进行加密形成的一份密文。 证书就是一份明文数据拼接上该数据对应的签名。 服务端在使用 HTTPS 前先生成公钥和私钥私有服务端持有然后服务端负责人向 CA 机构申领一份数字证书申请证书的时候需要在特定平台生成例如CSR会同时生成一对密钥对即公钥和私钥。这对密钥对就是用来在网络通信中进行明文加密以及数字签名的。其中公钥会随着 CSR 文件一起发给 CA 进行权威认证私钥服务端自己保留用来后续进行通信CA机构会根据申请者提交的域名、公钥等信息根据自己的私钥生成一份签名再将签名和提交的明文信息拼接就形成了一份CA证书。 当客户端首次向服务端发起请求时服务端直接给客户端返回一份CA证书客户端可以认为是浏览器一般内置了CA机构对应的公钥其会用该公钥对签名解密得到一份数据摘要A然后使用同样的哈希算法对明文数据进行运算得到一份数据摘要B客户端只需要比较A和B是否相等就可以知道该证书是否被修改未修改就可以根据证书得到服务端的公钥然后采用方案4的思路进行通信就可以了。 由于客户端使用的是CA机构的公钥解密因此中间人对证书的任何修改客户端都可以知道例如中间人向篡改明文数据中的公钥由于中间人没有CA机构的私钥自然就无法将篡改后的明文数据形成对应的签名。尽管中间人也可以向CA机构申请一份证书然后利用该申请的证书冒充服务端发给客户端但客户端只需要再检查对应的域名是否正确就可以识别出是否是服务端返回的证书。 因此方案5可以保证通信安全且通信速度也较快。 HTTPS 工作过程中涉及到的密钥有三组 第一组非对称加密:用于校验证书是否被篡改CA机构持有私钥客户端持有公钥操作系统包含了可信任的 CA 认证机构有哪些同时持有对应的公钥服务器在客户端请求时返回携带签名的证书客户端通过这个公钥进行证书验证保证证书的合法性进一步保证证书中携带的服务端公钥权威性。 第⼆组非对称加密用于协商生成对称加密的密钥客户端用收到的 CA 证书中的公钥是可被信任的给随机生成的对称加密的密钥加密传输给服务器服务器通过私钥解密获取到对称加密密钥。 第三组对称加密客户端和服务器后续传输的数据都通过这个对称密钥加密解密。 传输层 UDP UDP协议端的格式如下 源/目的端口号表示数据是从哪个进程来到哪个进程去 16位UDP长度表示整个数据报UDP 首部UDP 数据的长度以字节为单位其至少为8字节否则该UDP报文被视为无效可以知道整个UDP报文的最大长度是64K如果用户想要发送的数据超过了64K需要自己手动实现对报文进行分片然后手动实现拼装接收到的报文。 16位UDP检验和用于检验UDP报文是否出错如果校验和出错就会直接丢弃。其实就是发送方先按照一定的规则对这个UDP报文进行计算得到一个检验和接收方也按照相同的规则对接收到的报文进行计算如果如果计算出来的检验和与UDP数据报上携带的检验和不一致则认为UDP数据包传输出错。 UDP的特点如下 无连接知道目的的 IP 和端口号就直接进行传输不需要建立连接不可靠没有确认机制没有重传机制如果因为网络故障该段无法发到对方, UDP 协议层也不会给应用层返回任何错误信息面向数据报不能够灵活的控制读写数据的次数和数量应用层交给 UDP 多长的报文, UDP 原样发送既不会拆分也不会合并例如用 UDP传输100 个字节的数据如果发送端调用一次 sendto发送100个字节那么接收端也必须调用对应的一次 recvfrom接收100个字节而不能循环调用 10 次 recvfrom每次接收 10 个字节。 需要注意的是UDP 没有真正意义上的发送缓冲区调用 sendto 会直接交给内核由内核将数据传给网络层协议进行后续的传输动作但UDP 具有接收缓冲区但是这个接收缓冲区不能保证收到的 UDP 报的顺序和 发送 UDP 报的顺序一致如果缓冲区满了再到达的 UDP 数据就会被丢弃。 基于UDP的应用层协议有 NFS网络文件系统TFTP简单文件传输协议DHCP动态主机配置协议BOOTP启动协议用于无盘设备启动DNS域名解析协议 TCP tcp一个文件描述符对用2个缓冲区支持全双工通信udp不是全双工但可以在应用层添加特性来保证全双工。 TCP协议端的格式 TCP协议端的格式如下 源/目的端口号表示数据是从哪个进程来到哪个进程去 4位首部长度表示该 TCP 头部有多少个32位bit有多少个4字节所以TCP 头部最大长度是15 * 4 60字节 6位标志位 URGUrgent Pointer紧急指针是否有效ACKAcknowledgment确认序号是否有效PSHPush提示接收端应用程序立刻从 TCP 缓冲区把数据读走RSTReset对方要求重新建立连接我们把携带 RST 标识的报文称为复位报文段SYNSynchronize请求建立连接; 我们把携带 SYN 标识的称为同步报文段FINFinish通知对方本端要关闭了我们称携带 FIN 标识的报文为结束报文段 这些标志位是为了区分报文类型例如有的报文是为了建立连接有的是断开连接有的则是正常通信等以便对方决定接下来的动作 16位校验和发送端填充, CRC 校验. 接收端校验不通过, 则认为数据有问题. 此处的检验和不光包含 TCP 首部, 也包含 TCP 数据部分如果检验出错则丢弃当前报文在ACK应答中要求重新发送该报文 16位紧急指针紧急指针字段的值为紧急数据最后一个字节相对于TCP报文段首部的序列号的偏移量。通过将该偏移量与序列号相加可以计算出紧急数据最后一个字节的序号从而定位紧急数据在报文段中的位置由于紧急数据的处理可能会打断正常的TCP流量控制机制并且TCP协议本身并没有为紧急数据提供额外的保护如重传机制因此在实际应用中需要谨慎使用 40字节头部选项为TCP连接提供了额外的功能和灵活性使得TCP能够适应不同的网络环境和应用需求从而提高网络传输的效率和可靠性最大为40字节 其余字段后面作解释。 TCP的报文格式之所以比UDP的复杂很多就是因为TCP要保证可靠性传输其引入了各种机制来保证tcp的可靠性。 确认应答机制 现在主机A向主机B发送了一个tcp报文段那么主机A怎么确定主机B是否收到了报文呢最朴素的做法就是主机B在接收到报文后向主机A发送一个应答报文告诉主机A自己已经收到报文了如果主机A收到了主机B的应答主机A就可以保证主机B一定收到了报文。tcp的应答报文就是将报头的ACK标志位置1因此因此应答报文也称为ACK报文。 现在又来了一个新问题如果tcp接连发送了2个报文由于报文在网络中可能会选择不同的路径从而造成这2个报文到达主机B的顺序不一定与主机A的发送顺序一致那主机B如何保证接收报文的顺序呢这就需要用到tcp报头的32位序号和32位确认序号。 tcp层有两个缓冲区一个发送缓冲区一个接收缓冲区其实sendrecv等调用就是拷贝函数将数据拷贝到发送缓冲区或从接收缓冲区拷贝数据出去要发送的数据会提前放到发送缓冲区中我们把该缓冲区想象成一个大数组tcp会为每一个数据进行编号这就是序列号。 主机A在发送数据时会在报头的32位序号中填充该TCP报文段中第一个字节的序号主机B在收到报文后按照序号的大小排序就可以确定接收到的数据报的顺序同时主机B会在应答报文的32位确认序号中填充下一个期望接收的字节的序号即告诉主机A下次该从哪里发。 通过确认应答机制主机B就可以告诉主机A自己已经收到了那些数据下一次主机A应该从哪里开始发。 超时重传机制 主机A通过接收主机B的应答来确认B已经接收到的报文如果主机A没有接收到B的应答呢tcp的解决办法是如果超过一定的时间主机A没有接收到主机B的应答就对没有应答的报文进行重发那么该如何确定多长时间算超时呢 应对策略是找到一个最小的时间保证确认应答一定能在这个时间内返回但是这个时间的长短随着网络环境的不同是有差异的如果超时时间设的太长会影响整体的重传效率如果超时时间设的太短有可能会频繁发送重复的包。TCP 为了保证无论在任何环境下都能比较高性能的通信选择动态计算这个超时时间 Linux中BSD Unix 和 Windows 也是如此超时以 500ms 为一个单位进行控制每次判定超时重发的超时时间都是 500ms 的整数倍例如如果重发一次之后仍然得不到应答等待 2*500ms 后再进行重传如果仍然得不到应答就等待 4*500ms 进行重传依次类推以指数方式递增当累计到一定的重传次数, TCP 认为网络或者对端主机出现异常强制关闭连接。 现在又来了一个问题主机A没有收到应答不一定是主机A发送的数据段丢失了有可能是主机B接收到了A的报文但应答丢失了也有可能是应答正阻塞在某个路由中导致超时无论哪种情况主机A都会选择重发导致主机B收到多份重复的报文主机B该如何对报文去重呢 解决办法很简单根据接收到的报文的32位序号就可以做到去重的功能了。所有接收到的数据段都会先放在tcp层的接收缓冲区中如果数据段还在接收端的TCP缓冲区中但此时又收到了一个具有相同序列号的数据段TCP接收端会检查新收到的数据段的序列号。如果序列号与缓冲区中某个已接收但尚未被上层应用读取的数据段序列号相同则认为该数据段是重复的此时直接丢弃新来的数据段就可以了如果某个数据段已经被上层读走了由于TCP的32位序列号是一个连续的、递增的序列用于标识每个数据段的顺序即使某个数据段已经被上层应用读取TCP接收端仍然会跟踪序列号的状态如果数据段的序列号已经超过了它最近一次发送的ACK中的序列号tcp就认为该数据段是重复的直接丢弃这个数据段就可以了。 连接管理机制 我们知道tcp是面向连接的那么tcp是如何保证连接已建立好了的呢正常情况下tcp要经历3次握手建立连接4次挥手断开连接。 建立连接 tcp三次握手的流程为第一客户端先向服务端发送一个携带SYN标志位的报文以告诉服务端自己有建立连接的请求第二服务端接收到客户端请求建立连接的报文后发送一个携带ACK和SYN标志位的报文以对客户端发来的报文做应答同时表示自己也希望和客户端建立连接第三客户端收到服务端的报文后发送一个ACK报文以对服务端发来的报文做应答。 如果一开始报文①就丢失了导致服务端没有收到服务端自然就不会应答客户端超时收不到应答就会重新发送报文①如果第一次握手一直不成功达到最大重传次数客户端就会放弃连接。 如果报文②丢失了客户端收不到应答就会重新发送报文①如果客户端在达到最大重传次数后仍未收到服务端的报文②客户端会断开TCP连接。对于服务端来说服务端等不到客户端的应答③服务端就会重新发送报文②如果服务端在达到最大重传次数后仍未收到客户端的ACK报文③服务端会释放之前为这次连接分配的资源。 如果报文③丢失了服务端等不到客户端的应答服务端就会重新发送报文②这样客户端就会重发报文③如果服务端在达到最大重传次数后仍未收到客户端的ACK报文③它会释放之前为这次连接分配的资源并关闭连接。此时还存在一种情况由于此时客户端认为连接已经建立好了其可能会直接向服务端发送数据在这种服务端在没有收到ACK报文就接收到客户端的数据的情况下服务端会给客户端发送一个携带RST标志位的报文通知客户端重新建立连接当客户端收到RST报文时它会意识到连接已经被强制关闭并且通常会释放与该连接相关的所有资源如果客户端需要继续与服务端通信它将需要重新进行TCP的三次握手来建立一个新的连接。 如果三次握手顺利客户端和服务端都有一次报文的收发这样双方都可以确定自己发送的报文对方可以收到对方发送的报文自己也可以收到从而确认信道是健康的同时确保了双方的TCP都是愿意通信的。需要注意的是三次握手不是保证100%建立连接而是经历过三次握手之后双方都认为连接建立好了。而且第一次握手和第二次握手的报文不允许携带数据允许第三次握手的报文携带数据这样做一方面是第一次握手和第二次握手双方都不认为连接已经建立另一方面是为了阻止恶意攻击客户端可能会直接向服务端发送大量携带垃圾数据的SYN报文而服务端只能被动的接收这些垃圾数据从而使服务端遭受攻击。 至于我们为什么选择三次握手而不选择一次或两次握手是因为这种方式双方不能保证或者只有一方可以保证信道健康而不选择四次或者更多次握手单纯就是因为没有必要。 tcp在进行连接的过程中服务端和客户端都存在相应的状态的变化 服务端的状态变化 ① [CLOSED - LISTEN] 服务器端调用 listen 后进入 LISTEN 状态, 等待客户端连接② [LISTEN - SYN_RCVD] 一旦监听到连接请求同步报文段就将该连接放入内核等待队列中, 并向客户端发送 SYN ACK确认报文③ [SYN_RCVD - ESTABLISHED] 服务端一旦收到客户端的确认报文就进入ESTABLISHED 状态可以进行读写数据了 客户端的状态变化 ① [CLOSED - SYN_SENT] 客户端调用 connect发送同步报文段② [SYN_SENT - ESTABLISHED] connect 调用成功即客户端接收到服务端的确认报文则进入 ESTABLISHED 状态可以开始读写数据 断开连接 TCP四次挥手的过程为第一客户端先向服务端发送一个携带FIN标志位的报文以告诉服务端自己现在希望断开连接第二服务端收到客户端请求断开连接的报文后向客户端发送一个ACK应答报文表示自己已收到客户端断开连接的请求第三服务端也向客户端发送一个携带FIN标志位的报文表示自己要和客户端断开连接第四客户端收到服务端请求断开连接的报文后发送一个ACK报文表示自己已收到服务端断开连接的请求。 需要注意的是发送FIN报文只是表示自己在断开连接之前不再向对方发送数据了但这期间依然可以正常接收数据。 如果报文①丢失了导致服务端没有收到客户端希望断开连接的请求服务端自然就不会应答客户端超时收不到应答就会重新发送报文①如果重传次数达到了最大值但仍然没有收到ACK应答报文那么客户端可能会认为连接已经不可恢复于是会关闭连接。 如果报文②丢失了客户端触发重传机制重发报文①如果重传次数达到了最大值但仍然没有收到ACK应答报文那么客户端可能会认为连接已经不可恢复于是会关闭连接。 如果报文③丢失了对服务端来说它迟迟收不到客户端的应答则触发服务端的超时重传于是服务端重发报文③如果在重传多次后服务端仍然无法收到客户端的ACK报文服务端会断开连接。对客户端来说客户端会等待一段时间默认通常是60秒如果仍未收到服务端的报文③客户端会自动关闭连接。 如果报文④丢失了对服务端来说它迟迟收不到客户端的应答则触发服务端的超时重传于是服务端重发报文③如果在重传多次后服务端仍然无法收到客户端的ACK报文服务端会断开连接。对客户端来说客户端在发送ACK报文后它不会立即关闭连接而是通常会进入TIME-WAIT状态等待足够的时间通常是2倍的MSL即Maximum Segment Lifetime最大报文段生存时间以确保服务端能够收到ACK报文客户端如果又收到了服务端的FIN报文此时它会认为自己发送的ACK报文④丢失了于是忽略当前接收到的报文并重新发送一次ACK报文④以应答之前的FIN报文③然后再等待上一段时间当客户端在TIME-WAIT状态等待足够的时间后就会正常关闭连接。 如果四次挥手顺利双方都可以确认对方已经同意断开连接那为什么不选择一次、两次或者三次挥手呢 对于一次挥手万一报文①丢失了服务端无法知道客户端已经断开连接其仍会向客户端发送数据而客户端已经断开连接不会对这些报文作应答服务端就会不断重传直至到达最大重传次数这明显会造成网路资源的浪费而且万一服务端还有一些重要数据没有发送给客户端可鞥会对双方以后的运行造成影响。 对于二次挥手由于ACK应答并不是代表服务端也要断开连接仅仅只是服务端告诉客户端自己已经收到了客户端断开连接的请求因此服务端依然可能向客户端发送数据。 对于三次挥手尽管服务端也向客户端发送了FIN报文告诉客户端自己不在向它发送数据了同时自己也希望断开连接但服务端无法确认客户端是否真正接收到了自己的FIN报文。这可能导致双方对连接状态的理解不一致。如果该FIN报文丢失了客户端也不知道服务端已经断开连接了它依旧觉得服务端也许还有数据会发送给自己于是就会白白等待上一段时间才会知道服务端断开连接了然后自己也断开连接而且如果客户端收到了服务端的FIN报文却不做应答这不符合TCP协议的规定。总之三次挥手不符合TCP协议的全双工通信特性和确保双方都能有序、安全地关闭连接的需求。 最后还有一个问题那为什么不把第二次挥手和地三次挥手合并呢因为服务端要及时回复第二次挥手的ACK报文但其可能还有一些数据没有处理完服务端需要等数据处理完并发送给客户端之后才能断开连接因此第二次挥手和地三次挥手一般不能合并从这里也可以看出三次握手的本质是四次握手只不过第二次和第三次握手合并了这其实就是捎带应答即ACK应答与响应数据一同携带发送到发送端从而减少发送次数。 tcp在断开连接的过程中服务端和客户端都存在相应的状态的变化 服务端状态转化 ① [ESTABLISHED - CLOSE_WAIT] 当客户端主动关闭连接(调用 close)服务器会收到结束报文段, 服务器返回确认报文段并进入CLOSE_WAIT状态② [CLOSE_WAIT - LAST_ACK] 进入 CLOSE_WAIT后说明服务器准备关闭连接需要处理完之前的数据当服务器真正调用 close 关闭连接时会向客户端发送FIN报文此时服务器进入 LAST_ACK 状态等待最后一个 ACK 到来这个 ACK 是客户端确认收到了FIN报文③ [LAST_ACK - CLOSED] 服务器收到了客户端对 FIN 的 ACK应答彻底关闭连接 客户端状态变化 ① [ESTABLISHED - FIN_WAIT_1] 客户端主动调用 close 时向服务器发送结束报文段, 同时进入 FIN_WAIT_1状态② [FIN_WAIT_1 - FIN_WAIT_2] 客户端收到服务器对结束报文段的确认进入 FIN_WAIT_2状态开始等待服务器的结束报文段③ [FIN_WAIT_2 - TIME_WAIT] 客户端收到服务器发来的结束报文段进入TIME_WAIT状态并发出ACK应答报文④ [TIME_WAIT - CLOSED] 客户端要等待一个 2MSLMax Segment Life报文最大生存时间的时间才会进入 CLOSED 状态 关于 TIME_WAIT TCP 协议规定主动关闭连接的一方要处于 TIME_ WAIT 状态等待两个MSL(maximum segment lifetime)的时间后才能回到 CLOSED 状态如果是服务端终止了那么服务端就是是主动关闭连接的一方在 TIME_WAIT 期间其不能再次监听同样的 server 端口这也是为什么服务端要设置地址复用的原因。MSL 在 RFC1122 中规定为两分钟但是各操作系统的实现不同在 Centos7 上默认配置的值是 60s可以通过 cat /proc/sys/net/ipv4/tcp_fin_timeout 查看 msl 的值。 为什么是 TIME_WAIT 的时间是 2MSL呢? MSL 是 TCP 报文的最大生存时间如果 TIME_WAIT 持续存在 2MSL 的话就能保证在两个传输方向上的尚未被接收或迟到的报文段都已经消失否则服务器立刻重启可能会收到来自上一个进程的迟到的数据但是这种数据很可能是错误的同时也是在理论上保证最后一个报文可靠到达假设最后一个 ACK 丢失那么服务器会再重发一个 FIN. 这时虽然客户端的进程不在了但是 TCP 连接还在仍然可以重发 ACK应答。 关于 CLOSE_WAIT 对于服务器上出现大量的 CLOSE_WAIT 状态原因就是服务器没有正确的关闭socket导致四次挥手没有正确完成这是一个 BUG只需要加上对应的 close 即可解决问题。 滑动窗口 在确认应答机制下对每一个发送的数据段都要给一个 ACK 确认应答收到 ACK 后再发送下一个数据段这样做明显导致数据传输效率低性能较差尤其是数据往返的时间较长的时候。于是我们想到既然这样一发一收的方式性能较低那么我们可以一次发送多条数据就可以大大的提高性能其实是将多个段的等待时间重叠在一起了而接收方则对接收到的数据排序后放入其接收缓冲区中。 滑动窗口是TCP层发送缓冲区的一部分窗口大小表示无需等待确认应答而可以继续发送数据的最大值只有收到对方的ACK后滑动窗口才会根据确认应答序号做相应的向右滑动 滑动窗口在内核中其实就是用两个指针来进行维护 start_win指向窗口左侧等于收到的确认应答的32位确认序号需要注意如果对方回复了某个确认序号那么就表示确认序号之前的数据对方都已经收到。 end_win等于start_win加上窗口大小该值大小为应答中的16窗口大小字段是对方基于自己接收缓冲区还可以接收数据的能力填充的 因此滑动窗口左侧的数据就表示发送方已经确定对方已经接收到的数据在滑动窗口中的数据就表示可以发送或已经发送但还未确认对方已经收到的数据滑动窗口右侧的数据表示待发送的数据。 如果发送的数据包丢失了该包的32位序号是1001 那么接收方接下来的所有ACK应答的确认序号都是1001表示1001之前的数据我都已经收到了接下来请从1001开始发当发送方连续三次收到了同样一个 “1001” 这样的应答就会将序列号为1001的数据包重新发送这个时候接收端收到了 1001 之后再次返回的 ACK 就是 7001 了因为 2001 - 7000的数据接收端其实之前就已经收到了被放到了接收端操作系统内核的接收缓冲区中。这种机制被称为“高速重发控制”也叫 “快重传”当快重传之后依旧收到和之前相同的确认应答序号时发送方会意识到可能存在问题并采取相应的措施来确保数据的可靠传输和网络的稳定包括继续重传丢失的报文段、关注接收方的窗口大小信息、采用拥塞控制机制以及根据网络状况调整发送策略等如果问题无法解决并且网络状况持续恶化那么TCP最终可能会决定关闭连接。 快重传速度比超时重传快因为其不需要等到超时没有收到ACK应答时才意识到数据丢了需要注意的是接收方只有收到至少3次相同的确认序号时才会触发快重传由超时重传进行兜底。 如果部分ACK应答丢失了其通常是不要紧的因为可以通过后续的ACK应答知道接收方已经接收了那些数据从而决定是否重发如果接收方成功接收了某个数据报文段并且后续还有数据报文段被成功接收那么接收方会发送包含更高确认序号的ACK应答这个ACK应答可以间接地告诉发送方之前丢失ACK的那个数据报文段已经被成功接收了如果发送方在一段时间内没有收到某个数据报文段的ACK应答并且后续也没有收到包含更高确认序号的ACK应答那么发送方会认为这个数据报文段可能丢失了并触发超时重传或快重传机制来重新发送这个数据报文段。 到这里我们也许还存在一些疑惑即既然发送方已经知道接收方还可以接收多少数据发送方为什么不把要发送的数据按接收方的最大接收限度一次发送过去而是要分成几个小报文段再发送呢 因为数据链路层规定单次收发的有效数据不超过一个MTU大小该值一般为1500字节可以通过指令 ifconfig 查看当网络层发现上一层交付给它的数据较大时它就会对数据进行分片由接收方网络层对分好的片进行组装而一旦某一个片丢失了接收端的网络层就认为整个数据段都丢失了发送方就需要重发所有的片网络层分片过多明显会增加丢包的概率导致重发浪费资源因此需要在传输层提前将数据分成小段避免网络层过多分片。 流量控制 接收端处理数据的速度是有限的如果发送端发的太快导致接收端的缓冲区被打满这个时候如果发送端继续发送就会造成丢包继而引起不必要的丢包重传等等一系列连锁反应造成网络资源浪费因此 TCP 支持根据接收端的处理能力来决定发送端的发送速度这个机制就叫做流量控制Flow Control。 接收端将自己可以接收的缓冲区大小放入 TCP 首部中的 “16位窗口大小” 字段, 通过 ACK 应答通知发送端16 位数字最大表示 65535实际上TCP 首部 40 字节选项中还包含了一个窗口扩大因子 M实际窗口大小是窗口字段的值左移 M 位窗口大小字段越大说明网络的吞吐量越高当接收端一旦发现自己的缓冲区快满了就会将窗口大小设置成一个更小的值通知给发送端发送端接受到这个窗口之后就会减慢自己的发送速度如果接收端缓冲区满了就会将窗口置为 0这时发送方不再发送数据但是需要定期发送一个窗口探测数据段使接收端把窗口大小告诉发送端在极端情况下如果连接长时间无法恢复发送方可能会选择关闭连接。 需要注意TCP滑动窗口的大小并不总是等于对方接收缓冲区可以接收数据的大小。虽然接收窗口的大小通常基于接收缓冲区的大小来设置但它还会受到其他因素的影响如应用程序的读取速度和网络拥塞状况等。因此在实际的网络通信中发送方需要动态地调整自己的发送窗口大小以适应接收方的接收能力和网络状况的变化因此TCP滑动窗口的大小是一个动态变化的参数它反映了接收方当前愿意接收的数据量以及网络拥塞状况等多个因素的综合影响。 拥塞控制 如果在刚开始阶段就发送大量的数据可能会引发问题因为网络上有很多的计算机如果当前的网络状态已经比较拥堵在不清楚当前网络状态下贸然发送大量的数据很有可能导致网络状况雪上加霜为此TCP 引入慢启动机制先发少量的数据探探路摸清当前的网络拥堵状态再决定按照多大的速度传输数据。 TCP引入了一个称为拥塞窗口的概念其实就是一个数字当单次发送数据的大小小于该值时不会导致网络阻塞如果大于该值则可能会导致网络堵塞因此每次发送数据包的时候将拥塞窗口和接收端主机反馈的窗口大小做比较取较小的值作为实际发送的窗口。 我们希望拥塞窗口慢启动后可以随传输轮次较快增长但又不至于增长过快因此我们把拥塞窗口的增长进行分段先以指数增长超过某个慢启动阈值ssthresh后以线性方式增长。 故发送方开始发送的时候拥塞窗口大小为 1在TCP连接建立之初慢启动阈值通常被设置为一个相对较大的值如接收窗口的大小在每次超时重发的时候慢启动阈值会变成原来的一半同时将传输轮次置0相当于拥塞窗口置回1慢启动阈值不会无限减小通常会设定某个最小值在某些TCP拥塞控制算法如CUBIC算法中当网络状况良好且没有发生拥塞时慢启动阈值可能会随着时间的推移而逐渐增加。 拥塞控制需要在有限的资源和无限的需求之间找到平衡点归根结底是TCP协议想尽可能快的把数据传输给对方但是又要避免给网络造成太大压力的折中方案。 延迟应答 如果接收数据的主机立刻返回ACK应答这时候返回的窗口可能比较小也许等一会儿接收方就把数据从缓冲区取走了那么这时可以返回的滑动窗口的值就比较大。当然了不是所有的包都可以延迟应答其是有一定的限制的 数量限制每隔N个包就应答一次时间限制超过最大延迟时间就应答一次 具体的数量和超时时间操作系统不同之间存在差异一般N取2延迟时间取200ms保证理想情况下在该延迟时间内应答不会触发发送方的超时重传机制。 粘包问题 首先要明确粘包问题中的包是指的应用层的数据包在 TCP 的协议头中没有如同 UDP 一样的 “报文长度” 这样的字段但是有一个序号这样的字段站在传输层的角度TCP 是一个一个报文过来的按照序号排好序放在接收缓冲区中但站在应用层的角度看到的只是一串连续的字节数据那么应用层该如何将一连串的字节数据分割成一个个完整的请求呢 解决思路很简单只需要明确两个请求之间的边界即可。对于定长的请求只需要保证每次都按固定大小读取即可当然了一般客户端请求都是变长的对于变长的包, 可以在包头的位置约定一个包总长度的字段从而就知道了包的结束位置还可以在包和包之间使用明确的分隔符例如使用自定义的应用层协议只要保证分隔符不和正文冲突即可。 对于 UDP 其不存在粘包问题因为如果还没有上层交付数据UDP 是有报文长度的有很明确的数据边界如果站在应用层的站在应用层的角度使用 UDP 的时候要么收到完整的 UDP 报文要么不收不会出现“半个”的情况。 TCP全连接队列 在建立TCP连接时如果三次握手已经完成但服务器正处于繁忙状态无暇立即调用accetp获取连接此时该连接就会被放到TCP全连接队列当中而全连接队列的长度是有限的由服务端开启listen监听时的第二个参数backlog决定全连接队列的长度为 backlog1 如果全连接队列已经满了服务器依旧无暇将连接获取走此时又来了一个新连接当前连接就无法进入 established 状态则该连接就会失败。需要注意的是所有的连接都会先被放到全连接队列中再由上层调用accept将该连接获取走因此全连接队列的长度不宜过小否则服务端一忙只有几个连接可以被放到全连接队列中剩余的连接都会失败这对大部分客户来说就是一请求服务就失败客户尝试几次之后就会放弃请求此后尽管服务端空闲了但客户可能已经没有请求服务的意愿了从而增加服务器的闲置率但全连接队列的长度也不宜过长如果队列很长处于队列后面的连接需要很久才可以被accept可客户不会等这么久大部分等了一会就直接断开连接了但这期间为了维护这些连接服务端是花费了资源的例如内存、cpu等。 其实Linux 内核协议栈为一个TCP连接管理使用两个队列 半链接队列用来保存处于 SYN_SENT 和 SYN_RECV 状态的请求全连接队列accpetd 队列用来保存处于 established 状态但是应用层没有调用 accept 取走的请求 一些不完整的连接会被放到半连接队列中连接完整后就放到全连接队列中等待上层accept。 下面我们从内核角度简单理解一下全连接队列 当新建立一个连接时就是创建了一个struct tcp_sock它不需要理会自己的全连接队列对象并将该对象放到Listen套接字的全连接队列中当上层调用accept时则就分配一个文件描述符把该连接对象放到该文件描述符下面的struct socket中最后将该文件描述符返回上层上层只需要对文件描述符操作就可以进行数据收发了。 tcpdump抓包 我们可以使用tcpdump这个工具可以对网络进行抓包包括UDP报文在使用时需要root用户权限。 在Ubuntu中安装指令如下 sudo apt-get update sudo apt-get install tcpdump在 Red Hat 或 CentOS 系统中可以使用以下命令安装 sudo yum install tcpdump常见的使用方法如下 使用 tcpdump 的时候有些主机名会被云服务器解释成为随机的主机名如果不想要就添加 -n 选项 捕获所有网络接口上的 TCP 报文 sudo tcpdump -i any tcp-i any 字段指定捕获所有网络接口上的数据包i 可以理解成为 interface 的意思tcp 字段指定捕获 TCP 协议的数据包。 捕获特定源或目的 IP 地址的 TCP 报文 使用 host 关键字可以指定源或目的 IP 地址同时可以使用 and 关键字连接两个条件 //捕获源 IP 地址为192.168.1.100 的 TCP 报文 sudo tcpdump src host 192.168.1.100 and tcp//捕获目的 IP 地址为 192.168.1.200 的 TCP 报文 sudo tcpdump dst host 192.168.1.200 and tcp//同时指定源和目的 IP 地址 sudo tcpdump src host 192.168.1.100 and dst host 192.168.1.200 and tcp捕获特定端口的 TCP 报文 使用 port 关键字可以指定端口号 //捕获端口号为 80 的 TCP 报文 sudo tcpdump port 80 and tcp保存捕获的数据包到文件 使用 -w 选项可以将捕获的数据包保存到文件中以便后续分析 sudo tcpdump -i eth0 port 80 -w data.pcap把捕获到的 HTTP 流量保存到名为 data.pcap 的文件中pcap 后缀的文件通常与 PCAPPacket Capture文件格式相关是一种用于捕获网络数据包的文件格式。 从文件中读取数据包进行分析 使用 -r 选项可以从文件中读取数据包进行分析 tcpdump -r data.pcap捕获指定网络接口上的 TCP 报文 //捕获某个特定网络接口如 eth0上的 TCP 报文 sudo tcpdump -i eth0 tcpwireshark 是 windows 下的一个网络抓包工具虽然 Linux 命令行中有 tcpdump 工具同样能完成抓包但是 tcpdump 是纯命令行界面使用起来不如 wireshark 方便如果需要可以在wireshark下载下载 wireshark具体使用教程自行网上参考。 在TCP小节的最后我们考虑一些TCP异常情况 进程终止进程终止会释放文件描述符仍然可以发送 FIN和正常关闭没有什么区别机器重启: 和进程终止的情况相同机器掉电/网线断开其中一端一开始不会意识到对方出现问题会认为连接还在一旦该端有写入操作就会发现连接已经不在了就会发送RST报文段来异常关闭连接与TCP的正常关闭过程不同发送RST报文段关闭连接时不需要等待缓冲区的包都发送出去也不需要接收端发送ACK报文段来确认即使没有写入操作TCP 自己也内置了一个保活定时器一般为2个小时没有结合到应用层具体的服务因此显得不合理如果双方在保活定时器设置的时间没有数据交换TCP就会认为连接可能已经不再有效或对方可能已经异常TCP会发送一个探查报文段也称为保活探测报文给对方以检查对方是否仍然在线并能够响应如果对方在线并正常响应了这个探查报文段那么TCP会收到回应并重新复位保活定时器以继续维持连接的有效性。如果对方没有响应这个探查报文段TCP可能会继续发送多个探查报文段具体的次数和间隔时间取决于TCP的实现和配置如果在发送完所有探查报文段后仍未收到任何响应TCP就会认为连接已经失效并主动释放这个TCP连接。 另外应用层的某些协议也有一些这样的检测机制例如 HTTP 长连接中也会定期检测对方的状态例如使用 QQ时在 QQ 断线之后也会定期尝试重新连接。 总之TCP 之所以这么复杂就是因为其要保证可靠性同时又尽可能的提高性能。 通过以下机制提供可靠性 校验和序列号按序到达确认应答超时重发连接管理流量控制拥塞控制 通过以下机制提高性能: 滑动窗口快速重传延迟应答捎带应答 基于 TCP 应用层协议 HTTPHTTPSSSHTelnetFTPSMTP 网络层 网络层的主要作用为根据目标主机的IP在网络中进行路由选择即寻找一条合适的传输路径其有很大概率将报文转运到目标主机配合上TCP的重发策略就可以保证数据传输的可靠性。 协议格式 4 位版本号(version)指定 IP 协议的版本对于 IPv4 来说是 4对于 IPv6 来说是 6 4 位头部长度(header length)IP 头部的长度是多少个 32bit, 也就是 4*length 个字节其中length4bit表示最大的数字是 15因此 IP 报头最大长度是 60 字节 8 位服务类型(Type Of Service)3 位优先权字段已经弃用4 位 TOS 字段1 位保留字段必须置为 0)以下是文心一言对这些字段的解释 16 位总长度(total length)整个IP 数据报的长度以字节为单位 16 位标识(id)用于唯一的标识主机发送的报文确保了每个IP数据报在网络中的唯一性从而避免了混淆和冲突。如果传输层交付下来的报文被网络层分片了那么每一个片里面的这个 id 都是相同的此时还可以确保接收端能够准确地识别出哪些分片属于同一个原始数据报。 3 位标志字段第一位保留现在不用以后可能要用到第二位DFDon’t Fragment位置为 1 表示禁止分片这时候如果报文长度超过 MTU该报文就会被丢弃并向发送方发送一个ICMP消息通知第三位MFMore Fragment位用于指示当前数据报是否是分片数据报序列中的最后一个分片如果是最后一个分片则置0否则置1以帮助接收端正确地重组分片数据报从而恢复原始的IP数据报 13 位分片偏移(framegament offset)是分片相对于原始 IP 报文开始处的偏移其实就是在表示当前分片在原报文中处在哪个位置实际偏移的字节数是这个值乘8得到的. 因此除了最后一个报文之外其他报文的长度必须是 8 的整数倍否则报文就不连续了 8 位生存时间(Time To Live, TTL)数据报到达目的地的最大报文跳数该值一般是64. 每次经过一个路由, TTL 就减1如果一直减到 0 还没到达目标主机那么就直接丢弃该报文这个字段主要是用来防止出现路由循环。 8 位协议表示上层协议的类型 16 位头部校验和使用 CRC 进行校验来鉴别头部是否损坏数据部分由上层传输层检查以减少计算量 32 位源地址和 32 位目标地址表示发送端和接收端的IP地址 选项字段(不定长, 最多 40 字节)用于提供了额外的功能允许发送方在IP数据报中嵌入一些可选的信息 IP报文的分片和组装 数据链路层规定网络层交付给它的单个报文长度不能超过MTUMaximum Transmission Unit该值一般是1500字节除去IP报头的20字节网络层单次接收的传输层交付给它的报文长度如果超过1480字节网络层就要对报文进行分片再由接收方的网络层进行组装但如果其中任何一个分片丢失了接收方的网络层就无法组装这个报文自然不会将报文交付传输层最后发送方只能进行重传。因此网络层分片会增加丢包重传的概率不建议让网络层进行分片一般是在传输层就控制好报文的长度。 分片 网络层分片时 每一个分片都要有自己的IP报头报头中的16位标识符填充的是相同的值这样接收方网络层就知道哪些分片是属于同一个报文。每一个分片的3位标志位的MF标志位置1但最后一个分片的MF标志位置0这样接收方网络层就知道哪个分片是在报文末尾每一个分片的13位分片偏移填充该分片的有效载荷在原始报文中的偏移量除以8因此每一个分片的有效载荷的长度必须都是8的整数倍最后一个分片除外这样做是因为13位分片偏移字段只有13位而IP报文的总长度有16位偏移量除以8 就可以保证13位分片偏移可以覆盖整个报文。 组装 接收方网络层接收到报文后只需要判断MF标志位为1或13片偏移不为0就可以知道当前报文是一个分片需要进行组装。 组装时只需要按照片偏移对所有的片从小到大进行排序如果找不到片偏移为0的片就可以确定原始报文的第一片丢了如果最后一片的MF标志位不为0就可以确定原始报文的最后一片丢了如果前一片的13位偏移加上前一片的有效载荷除以8不等于当前片的13位偏移就可以确定中间的某一片丢失了。如果确认没有片丢失就可以直接组装排好序的片交付上层了。 网段划分 IP 地址分为两个部分网络号和主机号 网络号保证相互连接的两个网段具有不同的标识主机号同一网段内主机之间具有相同的网络号但是必须有不同的主机号 不同的子网其实就是把网络号相同的主机放到一起如果在子网中新增一台主机则这台主机的网络号和这个子网的网络号一致但是主机号必须不能和子网中的其他主机重复因此通过合理设置主机号和网络号就可以保证在相互连接的网络中每台主机的 IP 地址都不相同。但手动管理子网内的 IP是一个相当麻烦的事情为此产生了有一种叫做 DHCP的技术能够自动的给子网内新增主机节点分配 IP 地址避免了手动管理 IP 的不便一般的路由器都带有 DHCP 功能因此路由器也可以看做一个 DHCP 服务器。 那么问题来了IP地址只有32个比特位给网络号和主机号分配多少个比特位才是合适的呢即怎么进行网段划分呢 常见的网段划分有静态划分、子网划分等 静态划分 过去曾经提出一种划分网络号和主机号的方案把所有 IP 地址分为五类 A 类 0.0.0.0 到 127.255.255.255B 类 128.0.0.0 到 191.255.255.255C 类 192.0.0.0 到 223.255.255.255D 类 224.0.0.0 到 239.255.255.255E 类 240.0.0.0 到 247.255.255.255 随着 Internet 的飞速发展这种划分方案的局限性很快显现出来大多数组织都申请 B 类网络地址导致 B 类地址很快就分配完了而 A 类却浪费了大量地址且申请了一个 B 类地址理论上一个子网内能允许 6 万 5 千多个主机A 类地址的子网内的主机数更多然而实际网络架设中不会存在一个子网内有这么多的情况. 因此大量的 IP 地址都被浪费掉了。 无分类编址CIDR 针对以上情况后面提出了新的划分方案称为 CIDRClassless Interdomain Routing。 引入一个额外的子网掩码subnet mask来区分网络号和主机号子网掩码也是一个 32 位的正整数在二进制中0和1不允许交替出现即连续的1之后必须是连续的0其中0或1的数量可以为0将 IP 地址和子网掩码进行“按位与”操作得到的结果就是网络号而网络号和主机号的划分与这个 IP 地址是 A 类、B 类还是 C 类无关例如 可见只要对IP 地址与子网掩码做与运算可以得到网络号主机号从全 0 到全 1 就是子网的地址范围。IP 地址和子网掩码还有一种更简洁的表示方法例如 140.252.20.68/2表示 IP 地址为140.252.20.68, 子网掩码的高 24 位是 1也就是 255.255.255.0。 这样通过增缩掩码的宽度即增减掩码中1的数量就可以控制该网段中的IP的数量以合理分配IP。 由此出现了一些特殊的IP地址 将 IP 地址中的主机地址全部设为 0就成为了网络号代表这个局域网用于标识一个特定的网络或子网将 IP 地址中的主机地址全部设为 1就成为了广播地址用于给同一个链路中相互连接的所有主机发送数据包127.*的 IP 地址用于本机环回loop back测试通常是 127.0.0.1 主机号为全0或全1的IP地址有特殊用途不能分配给主机使用。 网络路由 路由在复杂的网络结构中找出一条通往终点的路线在了解路由之前我们先了解一下私有IP和公网IP。我们将IP地址分为私有IP和公网IP并允许私有IP在不同网段内可以重复但不允许在同一个网段中出现相同的私有IP而公网IP在网络中必须是唯一的同时也不允许在公网中出现私有IP。 RFC 1918 规定了以下类型的IP地址为私有 IP 地址 10.*共 16,777,216 个地址172.16.*到 172.31.*共 1,048,576 个地址192.168.*共 65,536 个地址 包含在这个范围中的都是私有 IP其余的则称为全局 IP或公网 IP原则上处于不同的子网中的2台私有IP的主机不能直接通信必须通过公网上的服务器进行转发例如QQ的服务器。 我们的网路世界中的主机之间并不是一张任意连接的图这些主机是有一定的层次结构的类似于国家、省、市、镇、乡一样的层次结构一般是由运营商进行合理规划 一个路由器一般配置有两个 IP 地址更通常的情况是拥有一个WAN口和多个LAN口一个是WANWide Area Network口IP一个是LANLocal Area Network口IP也称为子网IP路由器的子网IP其实都是一样的通常都是192.168.1.1其中路由器LAN 口IP与其下一级的路由器的WAN口IP或普通主机的IP处于同一网段中相当于它们在同一个局域网中之间可以互相通信WAN口IP与上一级路由器的LAN口IP相连或该WAN口IP直接就是一个公网IP这样路由器就连接了2个不同的子网。每一个家用路由器其实都是运营商路由器的子网中的一个节点而运营商路由器可能会有很多级最外层的运营商路由器的WAN 口 IP 就是一个公网 IP。 子网内的主机需要和外网进行通信时路由器将 IP 首部中的 IP 地址进行替换(替换成 WAN 口 IP)这样逐级替换最终数据包中的 IP 地址成为一个公网 IP。这种技术称为 NATNetwork Address Translation网络地址转换技术这样公网中就不会出现私有IP了因此如果希望我们自己实现的服务器程序能够在公网上被访问到就需要把程序部署在一台具有公网IP 的服务器上而这样的服务器可以在阿里云或腾讯云上进行购买。 路由的过程就是不断地问路一跳一跳Hop by Hop的往下一站走当 IP 数据包到达路由器时路由器会先查看目的 IP以决定这个数据包是直接发送给目标主机还是需要发送给下一个路由器如此反复一直到达目标 IP 地址为止。 那么路由器是怎么知道数据包是直接发送给目标主机还是需要发送给下一个路由器呢 路由器自己内部会维护一张路由表类似于下面的表可以使用指令 route 查看 Destination目的网络地址 Gateway下一跳地址为*意味着目标主机与本机在同一子网中可以直接访问 Genmask子网掩码 FlagsU 标志表示此条目有效可以禁用某些条目G标志表示此条目的下一跳地址是某个路由器的地址没有 G 标志的条目表示目的网络地址是与本机接口直接相连的网络不必经路由器转发。 Metric用以确定到达目的地的最佳路径的计量标准用于在多个可能的路由中选择最佳路径根据Metric值来判断哪条路径是最优的 Ref指示有多少其他路由项引用了这个路由项 Use表示该路由项被路由软件查找或使用的次数 Iface发送接口 当路由器得到一个报文后它会用报文中的目的IP地址与子网掩码Genmask做与运算如果结果与Destination一致就通过其对应的发送接口将数据发送出去否则继续往下匹配如果路由表中其它行都不匹配时就按缺省路由条目规定的接口发送到下一跳地址。 当路由器新增一个LAN口子网或WAN口子网时路由表会为该子网生成一个新增一条记录路由表可以由网络管理员手动维护静态路由也可以通过一些算法自动生成动态路由例如距离向量算法、LS 算法、Dijkstra 算法等。 现在我们更加具体的看看数据包路由的过程 现在手机的数据链路层有一个数据包目标主机是公网中的京东的服务器该数据会先被发送到与手机相连的路由器中路由器拿到该数据包后取出目的IP地址根据路由表决定向上层路由器交付还是交付到子网中的某个主机如此层层交付到达服务商的公网出口路由器此时数据包的源IP地址会被替换层该路由器的WAN口IP地址继续向上交付直至目标主机目标主机应答时根据数据包的源IP地址就可以将数据返回到请求时的服务商的公网出口路由器通过NAT技术后面讲就可以把数据返回到手机上。 数据链路层 现在我们已经知道在复杂的网络结构中是如何找出一条通往终点的路线的了那么数据是如何在线路中两个节点进行转发的呢 以太网是当前应用最广泛的局域网技术它不是一种具体的网络而是一种技术标准既包含了数据链路层的内容也包含了一些物理层的内容例如它规定了网络拓扑结构、访问控制方式、传输速率等以及以太网中的网线必须使用双绞线和以太网并列的还有令牌环网、无线LAN 等。 以太网帧格式 源地址和目的地址当前主机网卡和下一跳主机网卡的硬件地址也叫 MAC 地址该地址的长度是 48 位是在网卡出厂时固化的 类型帧协议类型字段有三种值分别对应 IPInternet Protocol、ARPAddress Resolution Protocol、RARPReverse Address Resolution Protocol IP表示该帧封装的是IP数据包ARP表示该帧封装的是ARP数据包用于将IP地址解析为对应的MAC地址。ARP数据包包括ARP请求包和ARP应答包两种类型分别用于发送查询请求和接收应答回复。RARP表示该帧封装的是RARP数据包RARP协议是ARP协议的逆向过程用于将MAC地址解析为对应的IP地址现在RARP协议已经逐渐被DHCP等更先进的协议所取代因此在现代网络中遇到RARP数据包的可能性较低 CRC循环冗余校验Cyclic Redundancy Check用于检验整个数据帧是否传输出错 MTU 以太网规定以太网帧中的数据长度即有效载荷不包括帧头最小为46字节最大为1500字节ARP 数据包的长度不够46字节的要在后面补填充位这个最大值1500称为以太网的最大传输单元MTUMaximum Transmission Unit,不同的网络类型有不同的MTU可以用指令 ifconfig 查看MTU、MAC地址和IP地址。如果链路层接收到一个超过MTU限制的报文时它通常会通知网络层该报文的大小超过了MTU限制并请求网络层对报文进行分片处理。 MTU 对 IP 协议的影响 由于数据链路层 MTU 的限制对于较大的 IP 数据包要进行分包即将较大的 IP 包分成多个小包并给每个小包打上标签到达对端时再将这些小包会按顺序重组拼装到一起返回给传输层一旦这些小包中任意一个小包丢失接收端的重组就会失败但是 IP 层不会负责重新传输数据而是由传输层进行重发。 MTU 对 UDP 协议的影响 一旦 UDP 携带的数据超过 1472(1500 - 20(IP 首部) - 8(UDP 首部))那么就会在网络层分成多个 IP 数据报这多个 IP 数据报有任意一个丢失都会引起接收端网络层重组失败整个报文就丢失了因此如果 UDP 数据报在网络层被分片整个数据被丢失的概率会大大增加。 MTU 对于 TCP 协议的影响 TCP 的一个数据报也受制于 MTU单个TCP数据报的最大消息长度称为 MSS(Max Segment Size) 通信双方在三次握手发送 SYN 报文的时会进行 MSS 协商以确保数据传输的顺利进行。最理想的情况下, MSS 的值正好是在网络层不会被分片处理的最大长度双方在 TCP 头部写入自己能支持的 MSS 值然后双方得知对方的 MSS 值之后选择较小的作为最终 MSS而MSS 的值就是在 TCP 首部的 40 字节变长选项中kind2。 ARP协议 在链路的2个节点网络通信时时数据包首先是被网卡接收到再到上层协议的如果接收到的数据帧的硬件地址与本机不符则直接丢弃该数据帧因此在通讯前必须获得目的主机的硬件地址MAC地址。 ARPAddress Resolution Protocol地址解析协议 不是一个单纯的数据链路层的协议而是一个介于数据链路层和网络层之间的协议它建立了主机 IP 地址 和 MAC 地址 的映射关系。 ARP协议格式 ARP协议的格式如下 硬件类型数据链路层的网络类型1 为以太网; 协议类型要转换的地址类型,0x0800 为 IP 地址 硬件地址长度对于以太网地址为 6 字节 协议地址长度对于 IP 地址为 4 字节 op字段为 1 表示 ARP 请求op 字段为 2 表示 ARP 应答 注意到源 MAC 地址、目的 MAC 地址在以太网首部和 ARP 请求中各出现一次对于链路层为以太网的情况是多余的但如果链路层是其它类型的网络则有可能是必要的。 当前主机获取下一个节点的MAC地址的过程为 源主机发出 ARP 请求在以太网帧首部的以太网目的地址填充FF:FF:FF:FF:FF:FF表示广播这个请求会被广播到本地网段以询问IP地址是下一跳主机的硬件地址是多少目的主机接收到广播的 ARP 请求会先查看OP字段以判断该报文是ARP请求还是ARP应答接着发现其中的目的 IP 地址与本机相符它会将自己的硬件地址填写在应答包中然后将该ARP应答数据包给源主机这样源主机就获取了下一跳的MAC地址。 为了网络性能每台主机都维护一个 ARP 缓存表可以用指令 arp -a 查看缓存表中的表项是有过期时间的一般为 20 分钟如果 20 分钟内没有再次使用某个表项则该表项失效下次需要的话要发 ARP 请求来获得目的主机的硬件地址。 ARP欺骗 正常情况下网络通信是这样的 主机A在自己的ARP缓存表中记录IP地址ipR和macR的映射关系如果有报文的下一跳IP地址是ipR直接向macR主机发送即可同时路由器R在自己的ARP缓存表中记录IP地址ipA和macA的映射关系如果有报文的下一跳IP地址是ipA直接向macA主机发送即可。 但此时来了一个中间人主机MIP地址是ipMMAC地址是macM它大量向主机A发送ARP应答报文告诉主机A现在ipR对应的MAC地址是macM主机A对此不知情就对自己的ARP缓存表进行了更新同理中间人M对路由器R进行相同的操作R就认为ipA对应的MAC地址是macM 这样主机A发往路由器R和路由器R返回给主机A的报文都要经过中间人MM就成功窃取到它们的通信数据 局域网转发 同一个局域网中的主机是可以直接进行通信的当一台主机发送数据帧时该局域网的所有主机都可以接收到但这些主机会拿得到的数据帧的帧头的MAC地址与自己的MAC地址做比对以决定是忽略还是接收这个数据帧最终只有MAC地址匹配的主机会选择接收这个数据帧。 在局域网中是不允许多台主机同时发送消息的任何时候都只允许一台主机向局域网中发送数据帧相当于局域网就是一个临界资源否则就会发生数据碰撞导致数据混淆无法区分。不同网络对该问题的解决方案不同以以太网为例以太网允许数据碰撞的发生发送数据的主机会检测是否发送了碰撞如果检测到了数据碰撞当前主机就会暂停数据的发送等待一个合适的时间之后重新发送数据帧。相比之下令牌环网的解决方案就比较简单它为当前的局域网分配一个令牌只有持有令牌的主机才可以向局域网中发送数据这类似于一个互斥锁。 至于数据帧是如何发送到局域网中以什么信号形式怎么传输到目标主机目标主机又是如何将这些信号识别成一个数据帧的等等问题这些都是物理层的内容本文不做讨论。 其他协议和技术 NAT技术 我们知道IP 地址IPv4是一个 4 字节 32 位的正整数一共只有 2的32 次方 个 IP地址大概是 43 亿左右而 TCP/IP 协议规定每个主机都需要有一个 IP 地址难道只有 43 亿台主机能接入网络吗?而且实际上由于一些特殊的 IP 地址的存在数量远不足 43 亿另外 IP 地址也并非是按照主机台数来配置的而是每一个网卡都需要配置一个或多个 IP 地址。尽管CIDR 在一定程度上缓解了 IP 地址不够用的问题提高了利用率, 减少了浪费但是 IP地址的绝对上限并没有增加 IP地址仍然不是很够用目前时候有三种方式来解决该问题 动态分配 IP 地址只给接入网络的设备分配 IP 地址因此同一个 MAC 地址的设备每次接入互联网中得到的 IP 地址不一定是相同的这种方法任然只是缓解了该问题没有从根本上解决问题。 IPv6IPv6 并不是 IPv4 的简单升级版它们是互不相干的两个协议彼此并不兼容IPv6 用 16 字节 128 位来表示一个 IP 地址该方法几乎从根本上解决了问题但遗憾的是目前 IPv6 还没有普及。 NAT Network Address Translation网络地址转换技术 在路由器内部有一张路由器自动生成的用于地址转换的表当数据包到达某个路由器后如果是源主机第一次向目的主机发送数据该表就会新增一条记录记录的是数据包的源IP地址和目的IP地址的映射关系接着数据包的源IP地址会被替换成当前路由器的WAN口IP如此层层替换直到数据包的源IP地址被替换成一个公网IP运营商公网出口路由器的WAN口IP。这样当数据包返回时利用该表就可以溯源上一个主机层层溯源就可以将数据包返回到原始发送数据的主机。 NAT 路由器将源地址从 10.0.0.10 替换成全局的 IP 202.244.174.37当服务器的数据包返回到路由器时它会把目标 IP 从 202.244.174.37 替换回10.0.0.10这样数据包就成功返回到原始发送数据的主机了。 如果局域网内有多个主机都访问同一个外网服务器那么对于服务器返回的数据中目的 IP 都是相同的那么 NAT 路由器如何判定将这个数据包转发给哪个局域网的主机? 这时候 NAPT Network Address Port Translation网络地址端口转换是NAT的变体来解决这个问题它使用 IPport 来建立这个映射关系。 当源主机第一次向目的主机发送数据时它会拿数据包中的源IP地址、源端口号与该路由器的WAN口IP地址与新端口号该新端口号路由器自行分配但保证唯一性做映射生成一条新纪录添加到转换表中接着把数据包的源IP地址和端口号替换成路由器的WAN口IP地址与新端口号。这样数据包返回时就可以找到原始发送数据的主机。 例如有下面这样一个网络 M和B在一个子网中N和C在一个子网中三个路由器A、B、C在同一个子网中其路由器A的WAN口IP是一个公网IP服务器处于公网中其端口号为11M和N的私有IP相同它们的端口号也相同都是7现在他们都向服务器S发起请求M发出的报文会经过B到A最后抵达SN发出的报文会经过C到A最后抵达S他们建立的转换表大致如下 这种映射关系也是由路由器自动维护的例如在 TCP 的情况下建立连接时就会生成这个表项在断开连接后就会删除这个表项。 由于NAT依赖转换表所以它存在一定的缺陷 无法从NAT外部向内部服务器建立连接装换表的生成和销毁都需要额外开销通信过程中一旦NAT设备异常即使存在热备一种备份策略它在主系统正常运行时备份系统也处于运行状态并且随时准备接管主系统的工作所有的TCP连接也都会断开 通过NAT技术就允许了私有IP的重复极大的缓解了IPv4的IP地址不足问题但公网IP数量依旧是有限的因此可以预见IPv6仍将是未来的主流。 DNS TCP/IP 中使用 IP 地址和端口号来确定网络上的一台主机的一个程序但是 IP 地址不方便记忆于是人们想到用主机名即一串字符串来标识网络中的IP并且使用 hosts 文件来描述主机名和 IP 地址的关系。 最初是通过互连网信息中心(SRI-NIC)来管理这个 hosts 文件的但如果一个新服务主机要接入网络或者某个服务主机 IP 变更都需要到信息中心申请变更 hosts 文件其他计算机也需要定期下载更新新版本的 hosts 文件才能正确上网这样就显得十分不方便于是产生了 DNS 系统。 DNSDomain Name System域名系统是一整套从域名映射到 IP 的系统如果新计算机接入网络则将这个信息注册到数据库中用户输入域名的时候会自动查询 DNS 服务器由 DNS 服务器检索数据库得到对应的 IP 地址。但是至今我们的计算机上仍然保留了 hosts 文件在域名解析的过程中仍然会优先查找hosts 文件的内容。一般一个组织的系统管理机构自己会维护系统内的每个主机的 IP 和主机名的对应关系。 域名是分级的各个部分使用 . 连接例如域名www.baidu.com com顶级域名com 表示这是一个企业域名同级的还有 net (网络提供商)org (非盈利组织) 等baidu一级域名或称为顶级子域用于标识其在互联网上的身份和位置一般是公司名或者组织名wwwWorld Wide Web万维网二级域名这只是一种习惯用法之前人们在使用域名时往往命名成类似于ftp.xxx.xxx或者www.xxx.xxx 这样的格式来表示主机支持的协议 域名结构是树状结构 树的最顶端代表根服务器根的下一层就是由我们所熟知的.com、.net、.cn等通用域和.cn、.uk等国家域组成被称为顶级域。网上注册的域名基本都是二级域名2个层级比如http://baidu.com、http://taobao.com等等二级域名它们基本上是归企业和运维人员管理接下来是三级或者四级域名这里不多赘述。 域名解析的过程如图 解析成功后系统或者浏览器一般都会进行DNS缓存。 可以使用dig工具分析DNS过程Centos下安装dig工具 yum install bind-utils使用方法很简单可以参考dig使用例如 dig www.baidu.comICMP协议 ICMP协议Internet Control Message Protocol互联网控制消息协议是一个网络层协议主要用于在IP网络中发送错误报告以及其他需要注意的信息。 ICMP 主要功能包括 确认 IP 包是否成功到达目标地址通知在发送过程中 IP 包被丢弃的原因 ICMP 也是基于 IP 协议工作的它只能搭配 IPv4 使用如果是 IPv6 需要使用 ICMPv6 在源主机向目标主机发送报文时当目标主机或中途某个节点出现某些错误如目标主机断电关机而源主机是无法获取这些情况的如果源主机希望了解这些情况就需要距离故障节点最近的节点向源主机返回一个汇报情况的报文 ICMP协议格式如下 类型的值如下 可以大致将报文分为2类一类是通知出错原因一类是用于诊断查询。 代理服务器 代理服务器又分为正向代理和反向代理是一种应用比较广的技术例如 翻墙广域网中的代理负载均衡局域网中的代理 正向代理用于请求的转发(例如借助代理绕过反爬虫)反向代理往往作为一个缓存。 正向代理 正向代理Forward Proxy是一种常见的网络代理方式它位于客户端和目标服务器之间代表客户端向目标服务器发送请求。正向代理服务器接收客户端的请求然后将请求转发给目标服务器最后将目标服务器的响应返回给客户端。通过这种方式正向代理可以实现多种功能如提高访问速度、隐藏客户端身份、实施访问控制等。 工作原理 客户端将请求发送给正向代理服务器正向代理服务器接收请求并根据配置进行处理如缓存查找、内容过滤等然后将处理后的请求转发给目标服务器目标服务器对接收到的请求做处理并将响应返回给正向代理服务器最后正向代理服务器将响应返回给客户端。 功能特点 缓存功能正向代理服务器可以缓存经常访问的资源当客户端再次请求这些资源时可以直接从缓存中获取提高访问速度。内容过滤正向代理可以根据预设的规则对请求或响应进行过滤如屏蔽广告、阻止恶意网站等。访问控制通过正向代理可以实现对特定网站的访问控制如限制员工在工作时间访问娱乐网站。隐藏客户端身份正向代理可以隐藏客户端的真实 IP 地址保护客户端的隐私。负载均衡在多个目标服务器之间分配客户端请求提高系统的可扩展性和可靠性。 应用场景 企业网络管理企业可以通过正向代理实现对员工网络访问的管理和控制确保员工在工作时间内专注于工作避免访问不良网站或泄露公司机密。公共网络环境在公共场所如图书馆、学校等提供的网络环境中通过正向代理可以实现对网络资源的合理分配和管理确保网络使用的公平性和安全性。内容过滤与保护家长可以通过设置正向代理来过滤不良内容保护孩子免受网络上的不良信息影响。提高访问速度对于经常访问的网站或资源正向代理可以通过缓存机制提高访问速度减少网络延迟。跨境电商与海外访问对于跨境电商或需要访问海外资源的企业和个人正向代理可以帮助他们突破网络限制顺畅地访问海外网站和资源。 反向代理 反向代理服务器是一种网络架构模式其作为 Web 服务器的前置服务器接收来自客户端的请求并将这些请求转发给后端服务器然后将后端服务器的响应返回给客户端。这种架构模式可以提升网站性能、安全性和可维护性等 基本原理 反向代理服务器位于客户端和 Web 服务器之间当客户端发起请求时它首先会到达反向代理服务器。反向代理服务器会根据配置的规则将请求转发给后端的 Web服务器并将 Web 服务器的响应返回给客户端。在这个过程中客户端并不知道实际与哪个 Web 服务器进行了交互它只知道与反向代理服务器进行了通信。CDNContent Delivery Network内容分发网络就是采用了反向代理的原理。 应用场景 负载均衡反向代理服务器可以根据配置的负载均衡策略将客户端的请求分发到多个后端服务器上以实现负载均衡。这有助于提升网站的整体性能和响应速度特别是在高并发场景下。安全保护反向代理服务器可以隐藏后端 Web 服务器的真实 IP 地址降低其被直接攻击的风险。同时它还可以配置防火墙、访问控制列表ACL等安全策略对客户端的请求进行过滤和限制以保护后端服务器的安全。缓存加速反向代理服务器可以缓存后端 Web 服务器的响应内容对于重复的请求它可以直接从缓存中返回响应而无需再次向后端服务器发起请求。这可以大大减少后端服务器的负载提升网站的响应速度。内容过滤和重写反向代理服务器可以根据配置的规则对客户端的请求进行过滤和重写例如添加或删除请求头、修改请求路径等。这有助于实现一些特定的业务需求如 URL 重写、用户认证等。动静分离在大型网站中通常需要将静态资源和动态资源分开处理。通过将静态资源部署在反向代理服务器上可以直接从反向代理服务器返回静态资源的响应而无需再次向后端服务器发起请求。这可以大大提升静态资源的访问速度。 NAT 和代理服务器 路由器往往都具备 NAT 设备的功能通过 NAT 设备进行中转完成子网设备和其他子网设备的通信过程。代理服务器看起来和 NAT 设备有一点像。客户端向代理服务器发送请求代理服务器将请求转发给真正要请求的服务器服务器返回结果后代理服务器又把结果回传给客户端那么 NAT 和代理服务器的区别有哪些呢? 从应用上讲, NAT 设备是网络基础设备之一解决的是 IP 不足的问题代理服务器则是更贴近具体应用比如通过代理服务器进行翻墙另外像迅游这样的加速器也是使用代理服务器从底层实现上讲, NAT 是工作在网络层直接对 IP 地址进行替换代理服务器往往工作在应用层从使用范围上讲NAT 一般在局域网的出口部署代理服务器可以在局域网做也可以在广域网做也可以跨网从部署位置上看NAT 一般集成在防火墙路由器等硬件设备上代理服务器则是一个软件程序需要部署在服务器上. 内网穿透 如果我在自己家里的电脑上部署了一个小服务S如果我现在在学校我该怎么取得家里电脑的服务呢直接拿家里电脑的私有IP进行访问肯定是不行的此时我们需要在公网上部署一个服务M先让家里的小服务S向服务M发起一个TCP请求这样S和M之间就有了一条通信链路此后其他客户端向M发起的请求都可以转发给SS处理请求后将请求结果返回M再由M转交给客户端。像M这种服务现在已经有很多现成的了如frps等。 其实利用一个中间服务器的转发功能将请求从一个网络转发到另一个网络。 内网打洞 现在有2个客户端主机A和BA和B都向公网中的服务器M发起tcp连接这样A和B到各自出口路由器的链路上都建立起了NAT转换表此时服务器M做了这样一件事将A的出口路由器的IP和端口号告诉B同时也将B的出口路由器的IP和端口号告诉A此后A和B都直接向得到的IPport发送报文A和B就实现了互相通信而且通信不必再经过服务器M这就极大的减轻了服务器M的压力QQ通信就是利用了这个原理。 高级IO IO的本质就是等待条件就绪数据拷贝如果单位时间内等的比重越低那么IO的效率就越高。 五种IO模型 阻塞IO 阻塞 IO就是在内核将数据准备好之前系统调用会一直等待所有的套接字默认都是阻塞方式陷入阻塞IO的线程不占用CPU资源此期间线程无法往下推进执行后续代码。 非阻塞IO 非阻塞 IO就是如果内核还未将数据准备好系统调用会直接返回并且返回EWOULDBLOCK 错误码此时线程就可以往后继续执行其他代码。但非阻塞 IO 往往需要程序员循环的方式反复尝试读写文件描述符这个过程称为轮询这对 CPU 来说是较大的浪费一般只有特定场景下才使用。 文件描述符默认都是阻塞IO我们通过系统调用 fcntl 将某个文件描述符设置为非阻塞IO。 #include unistd.h #include fcntl.hint fcntl(int fd, int cmd, ... /* arg */ );fcntl的返回值与执行的命令有关。如果操作成功则返回特定于命令的值如新的文件描述符、文件描述符标记、文件状态标记等。如果操作失败则返回-1并设置errno以指示错误类型参数说明 fd需要设置的文件描述符 cmd操作指令取值如下 F_DUPFD复制一个现有的描述符cmdF_GETFD 或 F_SETFD获得/设置文件描述符标记、F_GETFL 或 F_SETFL获得/设置文件状态标记F_GETOWN 或 F_SETOWN获得/设置异步 I/O 所有权F_GETLK,F_SETLK 或 F_SETLKW获得/设置记录锁 如下我们就实现了将一个文件描述符设置为非阻塞 void setNonBlock(int fd) { int fl fcntl(fd, F_GETFL); // 获取文件描述符的当前状态标记 if (fl 0) { perror(fcntl); return; } fcntl(fd, F_SETFL, fl | O_NONBLOCK); // 设置文件描述符为非阻塞模式 } 信号驱动IO 信号驱动 IO就是内核将数据准备好的时候使用 SIGIO 信号通知应用程序进行 IO操作。此期间线程可以往后继续执行其他代码等到 SIGIO 信号来临时再进行IO和数据处理。 IO多路转接 IO 多路转接也称为IO多路复用能够同时等待多个文件描述符的就绪状态如果其中任何一个文件描述符就绪了系统调用就直接返回。 异步IO 异步IO就是由内核在数据拷贝完成时通知应用程序而信号驱动是告诉应用程序何时可以开始拷贝数据其将数据从内核空间拷贝到用户空间仍需要一段时间期间线程时处于阻塞状态的。此期间线程可以往后继续执行其他代码等到内核通知线程时直接进行数据处理。 在前面的IO模型中前四种属于同步IO其中IO多路转接的方式效率最高因为它一次等待多个文件描述符平均下来单位时间内等的比重较低。 纪录锁、系统 V 流机制、I/O 多路转接、readv 和writev 函数以及存储映射 IOmmap都统称为高级IO我们主要讨论I/O 多路转接。 IO多路转接 select 系统提供 select 函数来实现多路复用输入/输出模型它可以用来让我们的程序监视多个文件描述符的状态变化的当程序执行到select时程序会停在这里等待直到被监视的文件描述符有一个或多个发生了状态改变。 #include sys/select.h int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set*exceptfds, struct timeval *timeout);其返回值如下 正值当select函数返回正值时表示有相应数量的文件描述符已经就绪如果此次数据没有读完下次select会立即就绪可以进行I/O操作。这个正值就是就绪文件描述符的总数。调用者可以通过检查传入的文件描述符集合readfds、writefds、exceptfds来确定哪些文件描述符已经就绪。零如果select函数返回0则表示在指定的超时时间内没有任何文件描述符就绪。这通常意味着所有的I/O操作都在等待中或者已经设置的超时时间已经到期。负值当select函数返回-1时表示调用过程中发生了错误。此时通常会设置全局变量errno来指示具体的错误类型。例如如果select函数被信号中断errno可能会被设置为EINTR。 参数说明 nfds要监视的文件描述符集合中的最大值加1。文件描述符是从0开始计数的整数因此这个参数实际上是文件描述符集合中最大文件描述符的值加1。 readfds用于指定要监视其读状态变化的文件描述符集合。如果对某个文件描述符的读状态感兴趣则应该将其添加到这个集合中。 writefds用于指定要监视其写状态变化的文件描述符集合。如果对某个文件描述符的写状态感兴趣则应该将其添加到这个集合中。 exceptfds用于指定要监视其异常状态变化的文件描述符集合。如果某个文件描述符的异常状态感兴趣则应该将其添加到这个集合中。 timeout指向timeval结构的指针用于指定select函数的超时时间。timeval结构包含两个成员tv_sec秒和tv_usec微秒取值如下 NULL值表示select函数将一直阻塞直到有文件描述符就绪或发生错误。0值如果将tv_sec和tv_usec都设置为0则select函数将立即返回而不进行任何阻塞。正值如果tv_sec和/或tv_usec设置为正值则select函数将在指定的超时时间内阻塞等待文件描述符就绪。如果在超时时间内有文件描述符就绪则select函数将返回否则在超时后返回。 其中fd_set结构体的定义如下 其实这个结构就是一个整数数组更严格的说是一个 “位图”使用位图中对应的位来表示要监视的文件描述符系统提供了一组操作 fd_set 的接口以方便用户操作这个位图 void FD_CLR(int fd, fd_set *set); // 用来清除set中fd的位 int FD_ISSET(int fd, fd_set *set); // 用来检测fd是否在set中 void FD_SET(int fd, fd_set *set); // 用来将fd设置到set中 void FD_ZERO(fd_set *set); // 用来清除set的全部位select的特点 可监控的文件描述符个数取决于 sizeof(fd_set)的值一般 sizeof(fd_set)512每 bit 表示一个文件描述符则支持的最大文件描述符是 512*84096可以通过某些手段调整fd_set 的大小这可能涉及到重新编译内核select返回后fd_set结构体中对应事件依旧没有就绪的位会被置0因此将 fd 加入 select 监控集的同时还要再使用一个数据结构 array 保存放到 select监控集中的 fd一是用于在 select 返回后array 作为源数据和 fd_set 进行 FD_ISSET 判断二是 select 返回后会把以前加入的但并无事件发生的 fd 清空则每次开始select 前都要重新从 array 取得 fd 逐一加入fd_set结构体(应先用FD_ZERO将该结构体清空)扫描 array 的同时取得 fd 最大值 maxfd用于 select 的第一个参数。 select的缺点 每次调用 select都需要手动设置 fd 集合本质原因就是对应的fd_set结构体既作为输入参数又作为输出参数输入和输出重合从接口使用角度来说也非常不便每次调用 select都需要把 fd 集合从用户态拷贝到内核态这个开销在 fd 很多时会很大因为文件描述符集合是保存在用户态的内存空间中的。但是为了检查这些文件描述符是否有I/O事件发生内核需要在其自己的内存空间中访问这些文件描述符同时每次调用 select 都需要在内核遍历传递进来的所有 fd这个开销在 fd 很多时也很大如果遍历完所有被监视的文件描述符后没有发现任何I/O事件发生并且没有指定超时或者超时时间未到那么select函数通常会进入阻塞状态进程会挂起即进入睡眠状态直到某个被监视的文件描述符上有I/O事件如可读、可写或异常事件发生或者超时时间到达select 支持的文件描述符数量太小 最后我们看看网络中socket文件描述符的读写就绪和异常就绪情况 socket读就绪 socket 内核中接收缓冲区中的字节数大于等于低水位标记 SO_RCVLOWAT此时可以无阻塞的读该文件描述符并且返回值大于 0socket TCP 通信中对端关闭连接此时对该 socket 读则返回 0监听的 socket 上有新的连接请求socket 上有未处理的错误 socket写就绪 socket 内核中发送缓冲区中的可用字节数发送缓冲区的空闲位置大小大于等于低水位标记 SO_SNDLOWAT此时可以无阻塞的写并且返回值大于 0socket 的写操作被关闭(close 或者 shutdown)对一个写操作被关闭的 socket进行写操作会触发 SIGPIPE 信号socket 使用非阻塞 connect 连接成功或失败之后socket 上有未读取的错误 异常就绪 socket 上收到带外数据 poll 同select一样poll是系统提供的用来实现多路复用输入/输出模型的函数可以让我们的程序监视多个文件描述符的状态变化。当程序执行到poll时程序会停在这里等待直到被监视的文件描述符有一个或多个发生了状态改变。 #include poll.h int poll(struct pollfd *fds, nfds_t nfds, int timeout);// pollfd 结构 struct pollfd {int fd; /* file descriptor 文件描述符 */short events; /* requested events 要监控的事件类型 */short revents; /* returned events 实际发生的事件类型 */ };该函数调用成功时poll 返回发生事件的文件描述符数量。如果在调用时所有文件描述符都不满足条件并且 timeout 毫秒已经过去则返回 0。如果调用失败则返回 -1并设置 errno 以指示错误类型。参数说明 fds 是一个 poll 函数监听的结构列表每一个元素中包含了三部分内容文件描述符, 监听的事件集合, 返回的事件集合 nfds 表示 fds 数组的长度 timeout表示 poll 函数的超时时间单位是毫秒(ms) events 和 revents 的取值 我们一般只关心POLLIN和POLLOUT即读事件和写事件。 poll的优点 不同于 select 使用三个位图来表示三个 fdset 的方式poll 使用一个 pollfd 的指针实现pollfd 结构包含了要监视的事件和发生的事件不再使用 select“参数-值”传递的方式接口使用比 select 更方便poll 并没有最大数量限制 (但是数量过大后性能也是会下降) poll的缺点 和 select 函数一样poll 返回后需要轮询 pollfd 来获取就绪的描述符当监听的文件描述符数目增多时十分影响性能每次调用 poll 都需要把大量的 pollfd 结构从用户态拷贝到内核中同时连接的大量客户端在一时刻可能只有很少的处于就绪状态因此随着监视的描述符数量的增长其效率也会下降 总之poll不需要像select一样在每次调用时都重新初始化文件描述符集也没有文件描述符数量限制解决了select的大部分缺点问题但随着文件描述符数量的增加所带来的监视性能下降问题依旧没有解决。 epoll epoll是为处理大批量句柄而作了改进的 poll它是在 2.5.44 内核中被引进的几乎具备了之前所说的一切优点被公认为 Linux2.6 下性能最好的多路 I/O 就绪通知方法。 1. 创建一个epoll句柄 #include sys/epoll.h int epoll_create(int size);成功时epoll_create 返回一个非负的文件描述符该描述符用于后续对 epoll 实例的操作如添加、删除监控的文件描述符以及等待事件。失败时返回 -1并设置 errno 以指示错误原因。参数说明 size 在较新的 Linux 内核版本中该参数已经被忽略它曾经用于指示期望监控的文件描述符数量。即使现在被忽略许多程序仍然传递一个值如 1024作为 size以保持向后兼容性。 需要注意在使用完之后必须调用 close()关闭返回的文件描述符。 2. epoll事件注册 #include sys/epoll.h int epoll_ctl(int epfd, int op, int fd, struct epoll_event*event);成功时epoll_ctl 返回 0失败时返回 -1并设置 errno 以指示错误原因参数说明 epfd创建epoll句柄时返回的文件描述符 op要执行的操作可以是以下三个值之一 EPOLL_CTL_ADD向 epoll 实例中添加一个新的文件描述符进行监控。EPOLL_CTL_DEL从 epoll 实例中删除一个文件描述符停止对其的监控。EPOLL_CTL_MOD修改一个已经存在于 epoll 实例中的文件描述符的监控事件。 fd要添加、删除或修改的文件描述符。 event指向一个 epoll_event 结构体的指针该结构指定了要监控的事件类型及与文件描述符相关联的数据对于 EPOLL_CTL_ADD 和 EPOLL_CTL_MOD 操作。对于 EPOLL_CTL_DEL 操作此参数可以是 NULL。 其中 struct epoll_event 结构体定义如下 struct epoll_event { __uint32_t events; // 要监控的事件类型如读、写、异常等 epoll_data_t data; // 与文件描述符相关联的数据可以是 void *、int 或 uint32_t 类型 }; typedef union epoll_data { void *ptr; int fd; uint32_t u32; uint64_t u64; } epoll_data_t;events 即我们想要监视的文件描述符的事件的集合它的取值有 EPOLLIN : 表示对应的文件描述符可以读 (包括对端 SOCKET 正常关闭)EPOLLOUT : 表示对应的文件描述符可以写EPOLLPRI : 表示对应的文件描述符有紧急的数据可读 (这里应该表示有带外数据到来)EPOLLERR : 表示对应的文件描述符发生错误EPOLLHUP : 表示对应的文件描述符被挂断EPOLLET : 将 EPOLL 设为边缘触发(Edge Triggered)模式, 这是相对于水平触发(Level Triggered)来说的EPOLLONESHOT只监听一次事件, 当监听完这次事件之后, 如果还需要继续监听这个 socket 的话, 需要再次把这个 socket 加入到 EPOLL 队列里 data 字段是一个epoll_data_t类型的联合体允许我们存储与文件描述符相关联的任意类型的数据这个数据在事件发生时会被返回以便我们识别是哪个文件描述符触发了事件并据此执行相应的操作也可以让开发者实现复杂的事件处理逻辑并优化程序的性能和响应速度。它提供了四种方式来存储与文件描述符相关联的数据 ptr一个通用指针可以指向任何类型的数据。fd一个文件描述符这在某些情况下很有用比如当你想要将事件与另一个文件描述符相关联时。u32提供了32位的无符号整数存储可以用于存储任何可以表示为整数的数据。u64提供了64位的无符号整数存储可以用于存储任何可以表示为整数的数据。 3. epoll事件等待 #include sys/epoll.h int epoll_wait(int epfd, struct epoll_event * events, intmaxevents, int timeout);如果成功epoll_wait 返回就绪事件的数量这个值可能小于或等于 maxevents如果 timeout 到期且没有事件发生则返回 0失败时返回 -1并设置 errno 以指示错误原因。参数说明 epfd创建epoll句柄时返回的文件描述符 events指向一个 epoll_event 结构数组的指针该数组用于存储就绪事件的信息调用者需要分配足够的空间来存储最多 maxevents 个事件 maxeventsevents 数组可以存储的最大事件数。这个值也限制了 epoll_wait 可以一次性返回的最大事件数 timeout等待事件的超时时间以毫秒为单位如果 timeout 为 -1epoll_wait 将无限期地阻塞直到有事件发生如果 timeout 为 0epoll_wait 将立即返回即使没有任何事件发生这种情况下返回值将是 0如果 timeout 大于 0epoll_wait 将阻塞最多 timeout 毫秒然后返回。 下面简单介绍一下epoll的工作原理 每一个 epoll 句柄都有一个独立的 eventpoll 结构体当我们使用epoll_create创建一个epoll句柄时Linux内核就会创建一个evenpoll对象该结构体比较重要成员就是rdllist一个就绪队列链表和rbr一颗红黑树。当我们使用epoll_ctl将一个新的文件描述符添加到epoll实例中时就是在该红黑树中新增一个节点epitem对象因此使用epoll_ctl对epoll实例的操作就是对该红黑树进行增删查改这样重复添加的事件就可以通过红黑树而高效的识别出来红黑树的插入时间效率是 lgn其中 n 为树的高度。所有添加到 epoll 中的事件都会与设备(网卡)驱动程序建立回调关系当响应的事件发生时会调用这个回调方法内核中这个回调方法叫 ep_poll_callback它会将发生的事件严格按照发生的先后顺序添加到 rdllist 双链表中。 struct epitem{struct rb_node rbn;//红黑树节点struct list_head rdllink;//双向链表节点struct epoll_filefd ffd; //事件句柄信息struct eventpoll *ep; //指向其所属的 eventpoll 对象struct epoll_event event; //期待发生的事件类型 }我们注意到epitem中有一个成员struct list_head rdllink如果回调函数想将事件就绪节点加入到就绪链表只需要将利用rdllink指针就可以将当前节点链入到就绪队列中从而省去了拷贝的开销如果是监视多个事件但只有某些事件就绪了内核并会创建一个简化的结构体或数据结构通常包含事件类型和对应的文件描述符来表明当前就绪的这些事件并将其链入到就绪队列中。 当调用 epoll_wait 检查是否有事件发生时只需要检查 eventpoll 对象中的 rdlist 双链表中是否有 epitem 元素即可如果 rdlist 不为空则把发生的事件复制到用户态同时将事件数量返回给用户由于就绪链表中的节点数量通常相对较少这个操作的时间复杂度近似O(1)。 epoll的优点 3. 接口使用方便虽然拆分成了三个函数但是反而使用起来更方便高效不需要每次循环都设置关注的文件描述符也做到了输入输出参数分离开 4. 数据拷贝轻量只在合适的时候调用 EPOLL_CTL_ADD 将文件描述符结构拷贝到内核中例如当添加新的文件描述符到监视列表时这个操作并不频繁而 select/poll 都是每次循环都要进行拷贝 5. 事件回调机制避免使用遍历而是使用回调函数的方式将就绪的文件描述符结构加入到就绪队列中epoll_wait 返回直接访问就绪队列就知道哪些文件描述符就绪这个操作时间复杂度 O(1)即使文件描述符数目很多效率也不会受到影响 6. 没有数量限制文件描述符数目无上限 epoll的工作方式 epoll 有 2 种工作方式水平触发LT和边缘触发ET 水平触发 Level Triggered 工作模式 当 epoll 检测到某个文件描述符上事件就绪的时候可以不立刻进行处理或者只处理一部分例如读缓冲区有2K数据可以只读 1K 数据缓冲区中还剩 1K 数据在第二次调用epoll_wait 时epoll_wait 会立刻返回并通知该文件描述符读事件就绪直到缓冲区上所有的数据都被处理完epoll_wait 才不会立刻返回epoll 默认状态下就是 LT 工作模式。 边缘触发 Edge Triggered 工作模式 当 epoll 检测到某个文件描述符上事件就绪时必须立刻处理如上面的例子中如果只读了 1K 的数据缓冲区还剩 1K 的数据在第二次调用epoll_wait 的时候epoll_wait 不会立刻返回了此后如果没有通知用户不敢贸然读取缓冲区中剩下的数据因为用户只知道自己读了1K的数据但不确定自己读的这1K究竟是缓冲区数据的一部分还是全部除非缓冲区中数据变多epoll_wait 才会返回。也就是说ET 模式下文件描述符上的事件就绪后只有一次处理机会注意不是指一次read的机会。 Nginx 一个高性能的HTTP和反向代理web服务器默认采用ET 模式使用 epoll 使用 ET 能够减少 epoll_wait 触发的次数因此ET 的性能比 LT 性能更高这也意味着ET 的代码复杂程度更高其实在 LT 情况下如果能做到每次就绪的文件描述符都立刻处理不让这个就绪被重复提示的话它们的性能是一样的。 最后需要注意的是使用 ET 模式的 epoll需要将文件描述设置为非阻塞这个不是接口上的要求而是 “工程实践” 上的要求LT工作方式没有这个要求可以对文件描述符的阻塞读写和非阻塞读写因为ET模式下epoll只通知我们一次缓冲区中有没有数据可以读写在使用read读数据时我们不能保证一次就把缓冲区的数据刚好读完需要循环多次读取如果该文件描述符是阻塞读取的话最后一次读取必然会阻塞如果我们将该文件描述符设为非阻塞最后一次读取如果没有出错返回值必定是0只要检查错误码errno EWOULDBLOCK || errno EAGAIN就可以知道是否是出错返回。如果在一个设置为非阻塞模式的文件描述符读写失败时就会将错误码设置为 EWOULDBLOCK如果在非阻塞模式下尝试读取一个当前没有数据的文件或套接字或者向一个当前无法写入数据的文件或套接字写入数据就会将错误码设置为 EAGAIN。而且每一个线程都有自己的错误码error此时就不会被阻塞接下来就可以继续调用epoll_wait等待下次缓冲区数据到来。如果是LT模式只有epoll_wait返回表示事件就绪我们才进行读写无论该文件描述符是阻塞还是非阻塞我们都不会被阻塞。 在使用epoll时对于读我们要先设置对某个文件描述符的监视读条件就绪了再读对于写我们一般是直接向文件描述符写如果写完了那么此次写就结束了如果写出错返回在非阻塞模式下写缓冲区满会返回EAGAIN或EWOULDBLOCK错误码当然也有极小的概率此时数据刚刚好写完了意味着写条件不满足此时再用epoll设置对该文件描述符写事件的监视写完后就解除对该写事件的监视。这样做是因为一般缓冲区大部分时间都是空的直接读很可能会读到空而直接写很可能立即就成功了。 epoll 的使用场景 epoll 的高性能是有一定的特定场景的如果场景选择的不适宜epoll 的性能可能适得其反。对于多连接且多连接中只有一部分连接比较活跃时比较适合使用 epoll例如典型的一个需要处理上万个客户端的服务器、各种互联网 APP 的入口服务器就很适合 epoll。如果只是系统内部服务器和服务器之间进行通信只有少数的几个连接这种情况下用epoll 就并不合适epoll在内核中需要维护一些数据和结构在连接数较少时这些开销是不值得的此时使用epoll并不能带来性能上的显著提升因此要根据具体需求和场景特点来决定使用哪种 IO 模型。
http://www.w-s-a.com/news/788320/

相关文章:

  • 鳌江哪里有做网站百度短链接在线生成
  • 有没有什么做水利资料的网站杭州建设信用平台
  • 电子商务网站建设及推广方案论文wordpress无法显示文章
  • 建设工程监理网站前端和后端分别需要学什么
  • 公司网站制作效果国内最好的在线网站建设
  • 徐州好点的做网站的公司有哪些wordpress 工具插件下载
  • 如何用云服务器建设网站微网站免费开发平台
  • 官网的网站设计公司做网站需要准备哪些东西
  • 程序员和做网站那个好找工作wordpress二维码 插件
  • 湖南城市建设技术学院官方网站青海省建设局网站
  • 响应式网站有什么区别百度网站官网
  • 金华企业自助建站系统长沙建站公司模板
  • 云主机 做网站友情链接网站
  • 定制型网站设计天津网站模板建站
  • 为什么公司网站打开很慢wordpress汉化插件
  • 用dw做教学网站做网站用什么配置笔记本
  • 秦皇岛网站制作服务无网站无产品链接如何做SOHO
  • 国际婚恋网站做翻译合法吗南宁网络推广有限公司
  • 济南做网站公司排名销售市场规划方案
  • 营销型网站定制珠海建站网站
  • 企业网站代码wordpress页面重定向循环
  • 厦门网站建设哪家便宜用wordpress做企业网站
  • 网站备案有幕布python 做网站速度
  • 旅游网站模板psd网站后台维护主要做什么
  • 晋江做任务的网站网站如何设置关键词
  • 呼伦贝尔网站建设呼伦贝尔ps网页设计心得体会
  • 字母logo设计网站动画设计方案及内容
  • 怎样做网站建设方案wordpress 附件预览
  • 网站内容编辑wordpress cron原理
  • 户外商品网站制作建筑网络图片