计算机网络基础总结

网络协议分层: 物理层->链路层->网络层->运输层->应用层

  1. 物理层: 为网络中主机提供物理连接, 信息传输通过某种介质(双绞线/光纤/无线电波等).

  2. 链路层: 封装了物理网络的连接细节,主机之间通过链路层彼此发送以帧为单位的数据, 帧的传输并不可靠, 物理介质的损坏或信号干扰等都会导致信息传输失败, 而链路层对此毫无察觉, 并不作为.
    链路层主要职责体现在:

  • 定义主机的唯一标识方法, 方便帧数据对接收方进行编址;
  • 定义帧的格式, 包括目的地址的格式和所传输数据的格式;
  • 定义帧的长短, 以便确定上层每一次传输所能发送的数据大小;
  • 定义一种将帧转换为电子信号的物理方法, 以便数据可以通过物理层进行传输, 并被接收方接收
    链路层常用的协议有ARP(Address Resolve Protocol)地址解析协议和RARP(Reverse Address Resolve Protocol)逆地址解析协议, 用来实现IP地址和机器物理地址(MAC地址)之间的相互转换.
  1. 网络层: 在链路层基础上提供一套逻辑地址的基础设施, 封装了网络连接的细节.
    网络层最常用的协议为IPv4: 其定义了一个为每台主机单独标识的逻辑寻址系统, 一个定义地址空间的逻辑分段作为物理子网的子网系统, 一个在子网之间转发数据的路由系统. IP协议是TCP/IP协议族的动力, 它为上层协议提供无状态 无连接 不可靠的服务.
    无状态是指IP通信双方不同步传输数据的状态信息, 因为所有IP数据包的发送传输和接受都是相互独立 没有上下文关系的. 这种服务最大的缺点是无法处理乱序和重复的IP数据包. 优点是简单高效, 无须为保持通信的状态而分配内核资源, 也无须每次传输数据时都携带状态信息.
    无连接是指IP通信双方都不长久地维持对方的任何信息, 这样, 上层协议每次发送数据时, 都必须明确指定对方的IP地址.
    不可靠是指IP协议不能保证IP数据报被准确地到达接收端, 它只能承诺尽最大努力.
    另外一个重要的协议是ICMP(Internet Control Message Protocol)因特网控制报文协议: 它是对IP协议的重要补充, 主要用来检测网络连接.

  2. 传输层: 实现主机上进程之间的通信, 为应用程序封装了一条端到端的逻辑通信.
    传输层最常用的协议是TCP和UDP:

  • TCP(Transmission Control Protocol)传输控制协议: 为应用层提供可靠的 面向连接和基于字节流的服务.
    可靠是指TCP协议使用超时重传 数据确认等方式来确保数据包被正确地发送到目的地.
    面向连接是指使用TCP协议通信的双方必须先建立连接, 然后才能开始数据的读写. 双方都必须为该连接分配必要的内核资源, 以管理连接的状态和连接上数据的传输, TCP是全双工的, 即双方的数据可以通过一个连接进行, 完成数据的交换之后, 通信双方都必须断开连接以释放系统资源.
    基于字节流的服务是指当发送端应用程序连续执行多次写操作时, TCP模块先将这些数据放入TCP发送缓冲区中, 当TCP模块真正开始发送数据时, 发送缓冲区中这些等待发送的数据可能被封装成一个或多个TCP报文段发出, 所以TCP模块发出的TCP报文段的个数和应用程序执行的写操作次数之间没有固定的数量关系.
    当接收端收到一个或多个TCP报文段后, TCP模块将它们携带的应用程序数据按照TCP报文段的序号一次放入TCP接收缓冲区中, 并通知应用程序读取数据. 接收端应用程序可以一次性将TCP接收缓冲区的数据全部读出, 也可以分多次读取, 这取决于用户指定的应用程序读缓冲区的大小. 因此, 应用程序执行读操作次数和TCP模块接收到TCP报文段个数之间也没有固定的数量关系.
    发送端执行写操作次数和接收端执行读操作次数之间没有任何数量关系, 这就是字节流的概念: 应用程序对数据的发送和接收是没有边界限制的.
  • UDP(User Datagram Protocol)用户数据报协议: UDP为应用层提供不可靠 无连接和基于数据报的服务.
    不可靠意味着UDP协议无法保证数据从发送端正确的传送到目的端. 如果数据在中途丢失, 或者目的端通过数据校验发现数据错误而将其丢弃, 则UDP协议只是简单的通知应用程序发送失败, 因此, 使用UDP协议的应用程序通常需要自己处理数据的确认, 超时重传等逻辑.
    无连接意味着通信双方不会保持一个长久的联系, 因此应用程序每次发送数据都要明确指定接收端的地址(IP地址等信息).
    基于数据报的服务是指发送端每执行一次写操作, UDP模块就将其封装为一个UDP数据报并发送之, 接收端必须针对每一个UDP数据报执行读操作, 否则就会丢包, 并且如果用户没有指定足够的应用程序缓冲区来读取UDP数据, 这个UDP数据将被截断.
  1. 应用层
    数据链路层 网络层和传输层负责处理网络通信细节, 这部分必须稳定高效, 因此它们是在内核空间实现的. 应用层负责处理应用程序的逻辑, 在用户空间实现.
    应用层常用的协议有:
    telnet协议: 是一种远程登录协议, 使得能够在本地完成远程任务.
    DNS(Domain Name System)域名系统: 协议能够将域名和子域名翻译为IP地址.

IP详解

IP头部结构

字段 描述
版本号(Version) 表示目前采用的IP协议版本号, 如IPv4的版本号为4
IP数据包头长度(Header Length) IP包头长度, 以4字节位单位, 所以包头长度最长为60, 除选项信息外的必要信息占20字节, 所以该字段取值至少为5
服务类型(Type of Service) 用于从拥塞控制到差异化服务识别的各种目的(详情查阅)
IP数据包总长(Total Length) 以1字节位单位计算的IP数据包长度, 包括头部和数据, 所以IPv4所能携带的数据包中数据部分最大长度为65535-20=65512字节, 由于MTU的限制, 长度超过MTU的数据报都将被分片传输.
片标识符(Fragment Identification) 作为发送的数据报的唯一标识, 其值随机生成, 该值在数据包被分片时复制到每个分片中, 用于标识分片来自同一个数据报
片标记(Fragment Flag) 3位标记, 第一位保留, 第二位标识禁止分片, 此时若传输的数据报超过MTU, 将被丢弃, 返会一个ICMP差错报文, 第三位表示更多分片, 除了数据报的最后一个分片外, 其他分片都要置1
片偏移(Fragment Offset) 相对原始IP数据报开始处的偏移量
生存时间(Time to Live) 用于限制数据包在路由时转发的次数, 当TTL为0时, 数据报将被丢弃, 并向源发送一个ICMP差错报文, 以防止数据报陷入路由循环
协议(Protocol) 用于解释数据内容所使用的协议, 如ICMP是1, TCP是6, UDP是16
头部校验和(Header CheckSum) 接收端对其使用CRC算法来检验IPv4头部数据的正确性, 这里仅仅针对头部数据, 数据部分的校验有上层来保证
原地址(Source Address) 数据包发送方的地址
目标地址(Destination Address) 既可以使数据包接收方的IP地址, 也可以使发送给多台主机的特殊地址
可选项(Option) 一些其他项(详情查阅)

IP分片

当IP数据报的长度超过帧的MTU时, 它将被分片传输, 分片可能发生在发送端, 也可能发生在中转路由器上, 而且在传输过程中可能被多次分片, 只有在最终的目标机器上, 这些分片才会被内核中的IP模块重新组装.
IP头部中的片标识, 片标记, 片偏移为分片和重组提供了足够的信息, 一个IP数据报的每个分片都具有自己的IP头部, 它们具有相同的标识符, 但具有不同的片偏移, 并且除了最后一个分片外,其他分片都将置位更多分片标识, 此外, 每个分片的IP头部的总长度字段将被设置为该分片的长度.

TCP详解

字段 描述
端口号(Port Number) 告知主机该报文段来自哪里(源端口)以及传送给那个上层协议或应用程序(目的端口)
序列号(Sequence Number) 一次TCP通信(从TCP连接建立到断开)过程中某一个方向上的字节流和每个字节的编号
确认号(Acknowledgement Number) 发送方希望下一个字节的序列号
数据偏移(Data Offset) 表示已4字节位单位的TCP头部大小, 因此TCP头部最长为60字节
保留位 -
控制位(Control Bits) 有关头部的元数据URG/ACK/PSH/RST/SYN/FIN(详情查阅)
接收窗口(Receive Window) 对于传入的数据, 剩余缓冲空间的最大容量, 用于流量控制
紧急指针(Urgent Pointer) 表示TCP段数据的第一个字节和紧急数据的第一个字节之间的距离, 需要URG标志有效.
可选项(Options) (详情查阅)

三次握手和四次挥手

  • 建立连接时发生三次握手
    第一次握手: 客户端向服务端发送位码SYN表示希望建立连接, 此时客户端陷入SYN_SEND状态, 并等待服务端的回应, 服务器通过SYN得知客户端希望建立连接.
    第二次握手: 服务端确认联机, 向客户端发送新的SYN并回应客户端的SYN+1为ACK, 随后进入SYN_RECV状态
    第三次握手: 客户端收到ACK并验证是否为发送的SYN+1, 若是则客户端知道请求连接已经通过, 随即状态变为ESTABLISHED, 并向服务端返回信息ACK为服务端发送而来的SYN+1, 服务器收到信息并检查是否为发送的SYN+1, 若是则服务端知道连接已经建立, 随即将自身状态设置为ESTABLISHED, 完成TCP三次握手, 双方都建立连接了.
  • 断开连接时发生的四次挥手

相关概念

  • 半关闭状态: TCP连接时全双工的, 所以它允许两个方向的数据传输被独立关闭. 当通信的一方可以发送结束报文给对方, 告诉他本端已经完成了数据的发送, 但允许继续接受来自对方的数据, 直到对方也发送结束报文段以关闭连接, 此时的状态即为半关闭状态.
  • 半打开状态: 建立连接的双方由于服务器异常崩溃, 客户端还维持着连接, 服务器重启后, 之前维护的状态已然消失, 这种情况被称之为半打开状态.
  • 连接超时: 由于一些客观原因造成简历TCP连接超时, 对TCP来说会选择多次重连, 无果后, 会通知应用程序连接超时.
  • 超时重传: TCP服务能够重传超过时间范围未收到的确认TCP报文段, TCP模块将维护一个重传定时器, 该定时器在TCP报文段第一次被发送时启动, 如果超时时间内未收到接收方的应答, 将重传TCP报文并重置定时器.
  • 拥塞控制: TCP其中一个任务是, 提高网络利用率, 降低丢包率, 并保证网络资源对每条数据流的公平性, 这就是拥塞控制. 其解决方案设计到几种算法: 慢启动, 拥塞避免, 快速重传和快速恢复(需要详细理解).

Linux下的网络编程

大端字节序为网络字节序
小端字节序为主机字节序
发送端总是把要发送的字节转换成大端字节序数据后在发送

UNIX/Linux的一个哲学是: 所有东西都是文件. socket也不例外, 它是可读 可写 可控制 可关闭的文件描述符

服务器模型

  • C/S模型: 适合资源集中的场合, 实现简单. 缺点是服务器是通讯的中心, 当访问量过大时, 客户端的响应将会变慢.
    服务器启动后, 首先创建一个或多个监听socket, 并调用bind函数将其绑定到服务器感兴趣的端口上, 然后调用listen函数等待客户连接, 服务器运行稳定后, 客户端就可以调用connect函数向服务器发起连接, 由于客户连接请求是随机到达的异步事件, 所以服务器需要某种I/O模型来监听这一事件.
  • P2P模型: 每台机器在消耗服务的同时也给别人提供服务, 这样资源能够充分自由的共享, 例如云计算机群, 缺点是当用户之间传输的请求过多时, 网络的负载将加重.

I/O处理单元 – [请求队列] – 逻辑单元 – [请求队列] – 网络存储单元
模块|单个服务器程序|服务器机群
-|-|-
I/O处理单元|处理客户连接, 读写网络数据|作为接入服务器, 实现负载均衡
逻辑单元|业务进程或线程|逻辑服务器
网络存储单元|本地数据库 文件或缓存|数据库服务器
请求队列|各单元之间的通信方式|各服务器之间的永久TCP连接

I/O模型: 非阻塞I/O和阻塞I/O
非阻塞I/O需要和其他I/O通知机制一起使用比如I/O复用和SIGIO信号

I/O复用是最常使用的I/O通知机制, 应用程序通过I/O复用函数向内核注册一组事件, 内核通过I/O复用函数把其中就绪的时间通知给其他应用程序. linux上最常使用的I/O复用函数是select poll epoll_wait, I/O复用函数本身是阻塞的, 他们能提高程序效率的原因在于他们具有同时监听多个I/O事件的能力.

SIGIO信号

高效的事件处理模式: Reactor Proactor, 同步I/O模型通常用于实现Reactor模式, 异步I/O模型则用于实现Proactor模式

Reactor: 它要求主线程(I/O处理单元)只负责监听文件描述上是否有事件发生, 有的话就立即将该事件通知工作线程(逻辑单元), 如此之外主线程不做任何其他实质性的工作. 读写数据, 接受新的连接, 以及处理客户请求均在工作线程中完成.
使用同步I/O模型(以epoll_wait为例)实现的Reactor模式的工作流程:

  1. 主线程往epoll内核时间表中注册socket上的读就绪事件.
  2. 主线程调用epoll_wait等待socket上有数据可读
  3. 当socket上有数据可读时, epoll_wait通知主线程, 主线程则将socket可读时间放入请求队列
  4. 睡眠在请求队列上的某个工作线程被唤醒, 它从socket读取数据, 并处理客户请求, 然后往epoll内核时间表中注册该socket上的写就绪时间
  5. 主线程调用epoll_wait等到socket可写
  6. 当socket可写时, epoll_wait通知主线程, 主线程将socket可写时间放入请求队列
  7. 睡眠在请求队列上的某个工作线程被唤醒, 它往socket上写入服务器处理客户端请求的结果
    Proactor: 与Reactor模式不同, Proactor模式将所有I/O操作都交给主线程和内核处理, 工作线程仅仅负责业务逻辑.
    使用异步I/O模型(以aio_read和aio_write为例)实现的Proactor模式的工作流程:
  8. 主线程调用aio_read函数想内核注册socket上的读完成事件, 并告诉内核用户读缓冲区的位置, 以及读操作完成时如何通知应用程序
  9. 主线程继续处理其他逻辑
  10. 当socket上的数据被读入用户缓冲区后, 内核将向应用程序发送一个信号, 以通知应用程序已经可用
  11. 应用程序预先定义好的信号处理函数选择一个工作线程来处理客户请求. 工作线程处理客户请求之后, 调用aio_write函数向内核注册socket上的写完成事件, 并告诉内核用户写缓冲区的位置, 以及写操作完成时如何通知应用程序
  12. 主线程继续处理其他逻辑
  13. 当用户缓冲区的数据写入socket之后, 内核将向应用程序发送一个信号, 以通知应用程序数据以及发送完毕
  14. 应用程序预先定义好信号处理函数选择一个工作线程来善后处理, 比如决定是否关闭socket

使用同步I/O模拟Proactor…

并发编程的目的是让程序”同时”执行多个任务, 如果程序时计算密集型的, 并发编程并没有优势, 反而由于任务的切换使效率降低, 但如果程序是I/O密集型的, 比如经常读写文件, 访问数据库等, 情况就不同了. 由于I/O操作的速度远没有CPU的计算速度快, 所以让长须阻塞于I/O操作将浪费大量的CPU时间.
并发编程: 多线程 多进程
并发模式: 半同步/半异步模式, 领导者/追随者模式
(在I/O模型中的”同步”和”异步”是区分内核向应用程序通知是何种I/O时间(就绪事件还是完成时间), 以及该由谁来完成I/O读写(是应用程序还是内核))
在并发模式中, “同步”是指程序完全按照代码序列的顺序执行, “异步”指的是程序的执行需要由系统事件来驱动, 常见的系统事件包括中断 信号等
半同步/半异步模式中, 同步线程用于执行客户逻辑, 异步线程用于处理I/O事件

I/O复用

  • select系统调用
    select系统调用的用途: 在一段指定时间内, 监听用户感兴趣的文件描述符上的可读可写和异常等事件.
    1
    2
    3
    ```
    * poll系统调用
    和select类似, 也是在指定时间内轮训一定数量的文件描述符, 以测试其中是否有就绪者.

#include <poll.h>
int poll(struct pollfd* fds, nfds_f nfds, int timeout)

struct pollfd{
ind fd; // 文件描述符
short events; // 注册的事件
short revents; // 实际发生的事件, 由内核填充
}

1
2
3
4
5
6
* epoll系列系统调用
epoll是linux特有的I/O复用函数, 在实现和使用上与select poll有很大差异.
epoll使用一组函数来完成任务, 而不是单个函数
epoll把用户关心的文件描述符上的事件放在内核的一个事件表里, 从而无须像select和poll那样每次调用都要重复传入文件描述符或事件集
epoll需要使用一个额外的文件描述符, 来唯一表示内核中的这个时间表
该文件描述符使用epoll_create函数来创建

#include <sys/epoll.h>
int epoll_create(int size) // size参数现在并不起作用, 只是给内核一个提示, 告诉它时间表需要多大
该函数返回的文件描述符将作为其他epoll系统调用的第一个参数, 以指定要访问的内核时间表

操作epoll的内核事件表:
int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event)
op: 指定操作类型(EPOLL_CTL_ADD EPOLL_CTL_MOD EPOLL_CTL_DEL)
fd: 是要操作的文件描述符
struct epoll_event{
__uint32_t events; // epoll事件 和poll支持的事件类型基本一致
epoll_data_t data; // 用户数据
}

typedef union epoll_data{
void* ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;

epoll_wait 在一段超时时间内等待一组文件描述符上的时间
int epoll_wait(int epfd, sturct epoll_event* events, int maxevents, int timeout);
epoll_wait函数如果检测到事件, 就将所有就绪的事件从内核事件表中赋值到它的第二个参数events指定的数组中, 这个数组只用于输出epoll_wait检测到的就需事件, 而不像select和poll的数组参数那样既用于传入用户注册的时间, 又用于输出内核检测到的就绪事件, 这就极大地提高了应用程序索引就绪文件描述符的效率
`
select和poll采用的都是轮询的方式, 即每次调用都要扫描整个注册文件描述符集合, 并将其中就绪文件描述符返回给用户程序
epoll_wait则采用的是毁掉的方式, 内核检测到就绪的文件描述符时, 将触发回调函数, 回调函数将该文件描述符上对应的事件插入内核就绪时间队列, 内核最后在适当的实际将该就绪事件队列中的内容拷贝到用户控件.

但是当活动链接较多时, epoll_wait的效率未必比select和poll高, 因为此时回调函数被触发的过于频繁, 所以epoll_wait适用于连接数量多, 但活动链接较少的情况
系统调用|select|poll|epoll
-|-|-|-
事件集合|用户通过3个参数分别传入感兴趣的可写 可读以及异常等事件, 内核通过对这些参数的在线修改来反馈其中的就绪时间, 这使得用户每次调用select都要重置这3个参数|统一处理所有事件类型, 因此只需要一个事件集参数, 用户通过pollfd.events传入感兴趣的时间, 内核通过修改pollfd.revents反馈其中就绪的事件|内核通过一个事件表直接管理用户感兴趣的所有事件, 因此每次调用epoll_wait时, 无须繁复传入用户感兴趣的时间, epoll_wait系统高调用的参数events仅用来反馈就绪的事件
应用程序索引就绪文件描述符的时间复杂度|O(n)|O(n)|O(1)
最大支持文件描述符数|一般有最大限制|65535|65535
工作模式|LT|LT|支持ET高效模式
内核实现和工作效率|采用轮询方式来检测就绪时间, 算法时间复杂度为O(n)|采用轮询方式来检测就绪时间, 算法时间复杂度为O(n)|采用毁掉方式来检测就绪时间, 算法时间复杂度为O(1)

基于Reactor模式的I/O框架库组件:
句柄(Handle): 一个事件源(即I/O事件, 信号和定时时间, 为I/O框架的主要处理对象)通常和一个句柄绑定在一起, 句柄的作用是, 当内核检测到就绪事件时, 它将通过句柄来通知应用程序这一件事, 在linux下, I/O事件对应的句柄是文件描述符, 信号时间对应的句柄就是信号值
事件多路分发器(EventDemultiplexer): 事件是随机的, 异步的 事件循环 等待事件-I/O复用技术 内部为select poll epoll_wait
事件处理器(EventHandler): 事件源与句柄绑定 , 当事件发生时时间处理器方能正常工作
具体的时间处理器(ConcreteEventHandler): 继承自事件处理器
Reactor: 核心, 执行事件循环, 注册事件, 移除事件

0%