TCP/IP 协议栈系列(八):HTTPS证书原理


TCP/IP 协议栈系列(七):HTTPS基础知识


TCP/IP 协议栈系列(六):HTTP2.0协议详解


TCP/IP 协议栈系列(五):HTTP1.0与HTTP1.1协议详解


TCP/IP 协议栈系列(四):HTTP协议概述


TCP/IP 协议栈系列(三):TCP协议详解


TCP/IP 协议栈系列(二):协议概述


本章将自底向上来说明 TCP/IP协议栈各层的具体工作流程

传输介质

首先,我们应该都知道计算机之间必须通过一定的传输媒介才能将数据相互传递,例如,光缆,光纤,或者无线电波,不同的传输媒介决定了电信号(0 1)的传输方式,同时也影响了电信号的传输速率、传输带宽。

链路层

我们自然的会去思考:

如何才能将 0 1 的电信号通过传输媒介传输到对方的主机?

很好解决:我们将每个计算机都安装一个能接受数据和发送数据的设备,然后将 0 1 的电信号分组,也就是组成字节的形式发送出去。

为什么要组成字节发送,因为单纯的 0 1 是没有意义的,计算机用 8 个 0 或 1 的二进制位来表示一个字节,字节才是我们发送数据的最小单位

那么这里我们提到的接受数据和发送数据的设备,就是网卡。我们规定,数据包必须是从一块网卡到另一块网卡,而网卡的地址(即我们常说的MAC 地址)就是数据包要发送的地址和接受地址。
MAC 地址就像网卡的身份证一样,必须具有全球唯一性。MAC 地址采用 16 进制表示,共 6 个字节,前 3 个字节是厂商的编号,后三个字节是网卡流水号。例如:5C-0F-6E-13-D1-18

解决了发送设备,接下来解决如何发送,所以就有人设计出来了一种发送数据的规范,也就是以太网协议。

以太网协议规定,一组电信号就是一个数据包,一个数据包也被称为一帧。一个以太网数据包的格式如下:

1
2
3
+--------------+----------------------+--------------+
| head(14 byte)| data(46 ~ 1500 byte) | end (4 byte) |
+--------------+----------------------+--------------+

整个数据包由三部分组成,头部,数据,和尾部,头部占14个字节,包含原MAC地址,目标MAC地址和类型;数据区最短 46个字节,最大 1500 个字节,如果发送的数据大于 1500 个字节,则必须拆开多个数据包来发送。
尾部为 4 个字节,用来存数据帧的校验序列,用来验证整个数据包是否完整。

以太网数据包发送过程:
以太网协议会通过广播的形式将以太网数据包发送给在同一个子网的所有主机,这些主机接受到数据包之后,会取出数据包的头部里的 MAC 地址和自己的 MAC 地址进行比较,如果相等,就会接着处理数据,如果不相等,则会丢弃这个数据包。

总结:链路层的工作就是将 0,1的电信号分组并组装成以太网数据包,然后网卡通过传输媒介以广播的形式将数据包发送给在同一个子网的接收方

注意:以太网协议始终是以广播的形式将数据包发给在同一个子网的主机

网络层

首先再回过头来看一下链路层,为了能让链路层工作,我们必须知道对方主机的 MAC 地址,而且还要知道对方的 MAC 地址是否和自己处于同一网络。

  1. 如果我们使用 MAC 地址来传输数据,那就必须记住每个 MAC 地址,但是去记一串这样长( 5C-0F-6E-13-D1-18 )的地址显然是不友好的
  2. 即使我们能记住 MAC 地址,MAC 地址也只于厂商有关,和网络无关,怎么能知道是不是在一个子网?
  3. 如果不是一个子网,那怎么办,以太网的协议难道不能发生以太网数据包了?

别急,能提出问题,那自然有解决方案,当没有解决办法的时候,那就设计一套新的协议来弥补这些问题。

为了解决以上的问题,我们的前辈们设计了三个协议:IP 协议,ARP 协议,路由协议。同时呢将这三个协议放在了网路层。

IP 协议

为了解决 1 和 2,必须指定了一套新的地址。使得我们能够区分两个主机是否在同一个网络。IP 地址分为 IPV4 和 IPV6 两种,现在普遍还在使用的是 IPV4 地址,IPV4 地址由 4 个字节 32 位组成,每个字节可以用一个十进制的数表示,通常,我们使用 . 隔开每个十进制数来表示 ip 地址,例如:192.168.12.11 。
同时,IPV4 对 IP 地址进行了分类,主要是 A B C D 四类,以 C 类地址 192.168.12.11 为例,其中前 24 位就是网络地址,后 8 位就是主机地址。那么网络地址相同的就是在一个局域网子网内
为了判断 IP 地址中的网络地址,IP 协议还引入了子网掩码,通过子网掩码和 IP 地址按位与运算,就能得到网络地址。

因为在上层的传输层中,开发者会将 IP 地址传入,所以我们只用通过子网掩码进行运算后就能判断两个 IP 是否在一个子网内。

这里简单介绍一下 IP 数据包:

1
2
3
+-----------------+--------------------+
| head(20 byte) | data (65515 byte) |
+----------------=+--------------------+

ARP 协议

我们解决了问题1和问题2,但是随之问题又来

现在设计的 IP 协议解决了在不在一个子网内的问题,但是如果用 IP 协议,以太网协议必须知道目标主机的 MAC 地址才能传输,怎么获取到目前主机的 MAC 地址?

为了解决这个问题,ARP 协议被设计出来,即 IP 地址解析协议,主要作用就是通过 IP 来获取到对应的 MAC 地址。

ARP 协议的具体工作过程:
ARP 会首先发起一个数据包,数据包里面包含了目标主机的 IP 地址,然后发送到链路层再次包装成以太网数据包,最终由以太网广播给自己当前子网的所有主机,主机接受到这个数据包之后,取出数据包的 IP 地址,和自己的 IP 地址进行对比如果相同就返回自己的 MAC 地址,如果不同就丢弃这个数据包。ARP 接受消息来确定目标主机的 MAC 地址。如果查询到了 MAC 地址,ARP 还会将该 IP 的 MAC 地址缓存到本机保留一段时间
等下次再有请求查询,直接先从缓存里取出。这样可以节约资源,提高查询效率。

相反的 RARP 协议是用来解析 MAC 地址为 IP 地址的协议

路由协议

我们发现 ARP 协议通过 IP 获取 MAC 地址依然是局限在子网内,那不在子网的 IP 地址,ARP 不就拿不到 MAC 地址了?这也就是我们开始提出的第三个问题还没有解决。

为了解决这个问题,前辈们又设计出了另一种协议-路由协议,路由协议必须借助路由设备来完成,即路由器或交换机,路由器扮演着交通枢纽的角色,会根据信道的情况,选择合适的路径来转发数据包
因此,刚刚我们的那个问题得到解决,首先是通过 IP 协议来判断两个 IP 是否在同一个子网内,如果在一个子网,那就通过 ARP 协议去获取 MAC 地址,然后再通过以太网协议将数据包广播到子网内;
如果不在一个子网内,以太网会将数据包先转发到本子网的网关进行路由,网关会进行多次转发,并最终将数据包转发到目标 IP 的子网内,然后再通过 ARP 协议获取目标主机的 MAC 地址,最终再通过以太网
协议将数据包发送到目标 MAC 地址。

总结一下:网络层的工作主要是定义网络地址划分网段查询 MAC 地址、对不是同一网段的数据包进行路由转发

传输层

依靠传输层和链路层的工作,数据已经能够正常的从一台主机发送到另一台主机,但是,我们在一台主机中往往不可能只有一个网络程序,所以当又多个网络程序同时工作的时候,我们依然会发现有如下问题

如何在多个网络程序运行的主机间进行数据传输,更简单的来说,就是如何一个主机的某个应用程序发出,然后由对方主机的应用程序接收?

为了解决这个问题,聪明的前辈们又想到了解决办法,为每一个网络程序分配一个不同的数字来表示,发送数据的时候指定发送到某台主机的某个数字的网络程序不就可以了。这个数字就是端口。
端口用 2 个字节来表示,范围是 0 ~ 65535,也就是最大 65535 个端口。一般情况下是足够用了。有了端口,我们来简单介绍下传输层的两种协议。

TCP

我们知道网络层的数据包 IP 数据包都是不保证可靠性的,也就是说将数据发送出去,并不保证数据可达,并且数据发送也不保证有序。所以,为了满足一些对数据可靠性和有序性的应用。前辈们设计了新的协议
TCP 协议。TCP 协议是保证了数据的可靠性,有序性,面向连接的传输协议。如果发现有一个数据包收不到确认,就重新发送数据包。

有了 TCP 协议,我们的应该程序可以将数据有序的,可靠的发送到对方指定端口的网络程序中。TCP 数据包格式

1
2
3
+-----------------+--------------------+
| head(20 byte) | data |
+----------------=+--------------------+

TCP 建立连接需要经过 3 次握手,断开连接需要经过 4 次挥手。后面章节会详细来讲解整个过程,再次不详细讨论

UDP

不一定是所有的场景都必须要求数据的可靠性和有序性,有些应用程序只要求数据能快速高效的发送出去,至于可靠性并不十分关系,那这个时候 TCP 协议似乎不能满足这种需求。

其实 IP 协议的数据包就可以满足我们的新的需求,但是 IP 协议是网络层,不存在端口。而我们在传输层规定了端口,所以干脆就在传输层新设计一个协议 UDP 协议,UDP 协议其实就是在 IP 协议的基础上指定了端口(简单理解)。

UDP 是面向用户的(非连接的)传输层协议,这是因为 UDP 不像 TCP 需要 3 次握手建立连接的机制。UDP 协议相较于 TCP 来说实现比较简单,没有确认机制,数据包一旦发出,不保证数据可达和有序。不过一般情况下 UDP
的数据包也不会有那么差的可靠性,还是能保证一定的可靠性。但是相较于 TCP ,UDP 的发送效率是比较快的。UDP 数据包的格式如下

1
2
3
+-----------------+--------------------+
| head(8 byte) | data (65527 byte) |
+----------------=+--------------------+

应用层

有了上面介绍的三层协议的支持,我们可以满足各种情况下,将我们的数据包发送到指定的端口的网络程序中,但是,传输的数据都是字节流,程序并不能很好的识别,操作性相对比较差。因此,在应用层
规范了各种各样的协议来规范我们的数据格式,同时也使得我们程序开发更为便利。常见的应用层协议有:HTTP、FTP、SMTP 等。针对不同类型的应用程序开发,可以使用不同的协议来开发。

每天在浏览器浏览网页,我们最熟悉的莫过于 HTTP 协议了。后面我会有专门的章节来详细讲解 HTTP 协议,这里不做过多介绍。

总结

有了分层的模型,每一层基于协议又有非常明确的分工,使得计算机之间的数据传输有条不紊的进行。我们来自顶向下回顾一下每一层的数据传输过程:

  • 应用层:应用层将用户输入的数据规范化,并根据应用层协议(如 HTTP 协议)封装成数据消息,传输给传输层
  • 传输层:传输层拿到应用层消息,根据传输层协议,将数据再次包装,并加上传输层协议头,发给网络层
  • 网络层:拿到传输层数据包,根据 IP 协议,将数据再次包装,加上 IP 协议头,发送给链路层
  • 链路层:链路层拿到网络层数据包,再次包装层以太网数据包,加上以太网协议头。通过网卡发送给对方主机

再来自顶向下回顾一下每一层的职责:

  • 应用层:按照应用层协议解析和规范用户数据
  • 传输层:定义端口,确定要发送的目标主机上的应用程序,根据协议的不同,控制数据的传输
  • 网络层:定义 IP 地址;分配网络地址和主机地址;解析 MAC 地址;将不在同一子网的数据包路由转发
  • 链路层:对 0 1进行分组,定义数据帧,确认对方主机 MAC 地址。通过物理媒介传输到对方主机的网卡

本文只是对 TCP/IP 协议栈各个层工作的一个概述,具体每一层的协议,后面会有专门的章节来介绍。

参考文献

TCP/IP 协议栈系列(一):模型简介


本系列将由浅入深通俗的讲解 TCP/IP 协议栈的基本知识,希望能对读者有用。

OSI 七层模型

TCP/IP 协议栈是在借鉴了 OSI 七层模型的基础上提出来的模型,所以我们有必要先来了解一下 OSI 七层模型。

OSI 模型是计算机网路体系结构发展的产物。它的基本内容是开放系统通信功能的分层结构。通俗来讲,就是为了计算机系统的通信功能设计的一套标准的框架,大家都遵循这套标准来进行网络通信的开发,
OSI 七层模型如下:

查看更多

Go 启动过程


通过查阅资料,了解 Go 语言的启动过程。

启动总体顺序

  1. 命令行参数解析
  2. 操作系统相关初始化
  3. 调度器初始化
  4. 创建 main.goroutine
  5. 运行 main 函数

命令行参数初始化

主要是解析命令行参数并保存

操作系统相关初始化

主要是确定操作系统的 CPU 核数,CPU 核数决定默认的了 P 的数量

查看更多

并发服务器的实现方式


并发服务器的实现方式一般有三种:多进程服务器,多线程服务器,多路复用服务器

多进程服务器

实现原理:当父进程 accept 一个请求之后,立即 Fork 出一个子进程去处理请求。而父进程则继续循环等待 accept 接受到新的请求。没有请求的情况下,父进程处于阻塞状态。

  • 创建套接字
  • 绑定(bind)服务器端口
  • 监听(listen)端口
  • 受理(accept)连接请求
  • 给获取到新的请求创建网络套接字传递(fork)给子进程
  • 子进程处理连接
  • 继续(accept)等待新的连接

子进程会复制父进程的所有资源,多个子进程之间相互独立,互不影响。

多进程服务器的优点

  1. 由操作系统进行调度,运行相对稳定健壮
  2. 通过操作系统可以方便的进行监控和管理
  3. 比较好的隔离性,每个进程相互独立,不影响主程序的稳定性。
  4. 充分利用多核 CPU , 实现并行处理

多进程服务器的缺点

  1. 进程的创建和销毁比较消耗资源,每个进程都独立加载完整的应用环境,内存消耗比较大。
  2. CPU 消耗高,高并发下,进程之间频繁的进行调度切换,需要大量的内存操作
  3. 进程数量限制了并发处理数,使得 I/O 的并发处理能力比较低

多线程服务器

通常在一个进程中可以包含若干个线程,当然一个进程中至少有一个线程,不然没有存在的意义。线程可以利用进程所拥有的资源,在引入线程的操作系统中,通常都是把进程作为分配资源的基本单位,而把线程作为独立运行和独立调度的基本单位,由于线程比进程更小,基本上不拥有系统资源,故对它的调度所付出的开销就会小得多,能更高效的提高系统多个程序间并发执行的程度。

多线程服务器的实现:

  • 创建套接字
  • 绑定(bind)服务器端口
  • 监听(listen)端口
  • 受理(accept)连接请求
  • 服务器通过 accept 受理连接请求
  • 每当有新连接时,创建新的线程来处理用户请求
  • 处理完成后,销毁线程
  • 继续(accept)接收新的连接请求

多线程服务器的优点

  1. 对内存消耗小,线程之间共享进程的堆内存和数据,每个线程的栈都比较小,不超过 1M
  2. CPU 上下文切换比较快
  3. 线程的切换开销远低于进程,I/O 的并发能力强

多线程服务器的缺点

  1. 不方便操作系统的管理
  2. 由于线程存在对资源的共享操作,一旦出现死锁和线程阻塞,使得影响整个应用的稳定性

多路复用服务器

多路复用即 I/O 多路复用,是指内核一旦发现进程指定的一个或者多个 I/O 条件准备读取,它就通知该进程。
I/O复用原理:让应用程序可以同时对多个I/O端口进行监控以判断其上的操作是否可以进行,达到时间复用的目的。

select 模型

使用 select 函数时,可以将多个文件描述符集中到一起进行监视:

  • 是否存在套接字接受数据
  • 无需阻塞传输数据的套接字有哪些
  • 哪些套接字发生了异常

利用 select 函数实现 I/O 复用服务器

实现过程描述:

  • 创建套接字
  • 绑定(bind)服务器端口
  • 监听(listen)端口
  • 注册服务端套接字到 fd_set 变量
  • while 循环
    • 调用 select 函数监听 fd_set 里的套接字
    • 监听发生状态变化的(有接受数据的)网络套接字
    • 首先发生变化的是否是验证服务端套接字,如果是,则说明有新的连接请求,accept 新的请求,并将客户端连接的套接字注册到 fd_set 变量中
    • 如果发生变化不是服务端套接字,则说明是客户端连接套接字,则读取客户端数据
    • 读取的数据是 EOF,则证明套接需要关闭套接字,并从 select 注册的套接字中删除该套接字

select 的几大缺点:

  1. 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
  2. 同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
  3. select支持的文件描述符数量太小了,默认是1024

利用 poll 实现 I/O 复用服务器

实现过程和 select 函数大致相同,区别在于 select 使用的结构是集合 fd_set 结构,poll 使用的结构是 pollsd 结构。

利用 epoll 实现 I/O 复用服务器

基于 select 的 I/O 复用服务器,有比较明显的不合理:

  1. 每次调用 select 函数后针对所有文件描述符分循环
  2. 每次调用 select 函数都需要向该函数传递监视对象的信息(fd_set)

每次调用 select 函数时是向操作系统传递监视对象信息,那必然会发生系统调用,需要把 fd 集合从用户态拷贝到内核态。开销太大

Linux 下的 epoll 具有如下优点:

  1. 无需编写以监视状态变化为目的针对所有文件描述符的循环语句
  2. 调用对应于 select 函数的 epoll_wait 函数无需每次都传递监视对象信息

epoll 提供了三个函数:

  • epoll_create:是创建一个 epoll 句柄
  • epoll_ctl:是注册要监听的事件类型
  • epoll_wait:则是等待事件的产生

实现过程描述:

  • 创建套接字
  • 绑定(bind)服务器端口
  • 监听(listen)端口
  • epoll_create 创建 epoll 例程
  • epoll_ctl(add) 注册事件到 epoll 句柄
  • while 循环
    • 调用 epoll_wait 函数监听套接字
    • 监听发生状态变化的(有接受数据的)网络套接字
    • 首先发生变化的是否是验证服务端套接字,如果是,则说明有新的连接请求,accept 新的请求,并调用 epoll_ctl 将客户端连接的套接字注册到 epoll 句柄
    • 如果发生变化不是服务端套接字,则说明是客户端连接套接字,则读取客户端数据
    • 读取的数据是 EOF,则证明套接需要关闭套接字,调用 epoll_ctl(del) 注册事件到 epoll 句柄

epoll 和 selectp/poll 最大的区别:

  • epoll_ctl 函数,每次注册新的事件到 epoll 句柄时,会把所有的 fd 拷贝进内核,而不是在 epoll_wait 的时候重复拷贝。epoll 保证了每一个 fd 在整个过程中只会拷贝一次
  • epoll 的解决方案不像 select 或 poll 一样每次都把需要监听的套接字加入 fd 对应的设备等待队列中,而只在 epoll_ctl 时挂载一遍,并为每个 fd 设置一个回调函数,当设备就绪,唤醒等待队列的等待者时,就会调用这个函数,而这个毁掉函数就会把 fd 加入一个有序链表。
  • epoll_wait 的工作实际上就是在这个就绪的链表中查看有没有就绪的 fd。

总结