Linux网络基础概念

目录

引言

一、网络协议

1.1硬件协议

1.2软件协议

[1.3 网络分层与协议划分](#1.3 网络分层与协议划分)

[1.3.1 TCP/IP五层(或四层)模型](#1.3.1 TCP/IP五层(或四层)模型)

[1.3.2 OSI七层模型](#1.3.2 OSI七层模型)

1.3.3协议和操作系统之间的关系

二、网络传输基本流程

2.1局域网通信

2.2跨网络通信

三、Socket编程预备

3.1认识端口号

3.2拓展认识

3.3传输层的典型代表

3.3.1初步认识TCP协议

3.3.2初步认识UDP协议

3.4网络字节序

3.5socket通用结构体


引言

前面我们聊了 Linux 系统的进程管理、内存调度与文件系统,而 Linux 之所以能成为服务器领域的绝对霸主,离不开它强大而完善的网络能力。从网卡驱动到内核协议栈,从 Socket 接口到系统调用,Linux 的每一处设计都在为高效、稳定的网络通信服务。而支撑这一切的底层基础,不只是一两个网络协议,而是一整套完整的计算机网络体系。接下来我们从网络协议开始,逐步了解整个计算机网络体系。

一、网络协议

计算机之间的传输媒介是光信号和电信号。通过 "频率" 和 "强弱" 来表示 0 和 1 这样的信息。要想传递各种不同的信息,就需要约定好双方的数据格式。

定好协议,但是你用频率表示 01, 我用强弱表示 01, 就好比我用中文,你用英语一样,虽然大家可能遵守的一套通信规则,但是语言不同,即是订好了基本的协议,也是无法正常通信的。

所以,完善的协议,需要更多更细致的规定,并让参与的人都要遵守。计算机生产厂商有很多;计算机操作系统,也有很多;计算机网络硬件设备,还是有很多。

那应该如何让这些不同厂商之间生产的计算机能够相互顺畅的通信?

计算机网络中,通信双方必须共同遵守的一组规则、标准和约定 ,用来规定数据怎么传输、格式是什么、出错如何处理。大家都遵循这样的约定就能使得计算机之间能够相互顺畅地通信。而这种约定就是网络协议

而协议又分为硬件级别的协议和软件级别的协议

1.1硬件协议

硬件协议,也可以理解为物理层与链路层的通信约定,是整个网络通信的 "物理地基"。它解决的核心问题,就是如何在网线、光纤、无线电这些物理介质上,可靠地传递 0 和 1 的信号。

1.2软件协议

软件协议,是运行在操作系统和应用程序中的逻辑通信规则,也是我们平时说的 "网络协议栈" 的主体部分。它解决的核心问题,是如何在不同设备之间,把数据从源主机准确、高效地送到目标主机,并被正确理解和处理。

1.3 网络分层与协议划分

协议本身也是软件,为了更好地被模块化和解耦合,被设计成了层状结构。

网络通信的核心目的,是解决人的实际需求,而 "让数据从 A 跨网路传到 B" 只是实现这一目的的手段。在这个过程中,我们会遇到一系列需要解决的问题:首先,数据需要在相邻设备之间可靠传递;其次,必须解决目标主机的定位和传输路径的选择问题;同时,还要应对数据丢失等异常情况;最后,也是最关键的一步,接收方需要明确数据的用途,并正确处理这些数据,完成请求或应答,最终实现通信的完整闭环。

而为了解决这些问题,网络通信协议-TCP/IP协议就应运而生。

1.3.1 TCP/IP五层(或四层)模型

TCP/IP 是一组协议的代名词,它还包括许多协议,组成了 TCP/IP 协议栈。

TCP/IP 通讯协议采用了 5 层的层级结构,每一层都呼叫它的下一层所提供的网络来完成自己的需求。

  • 物理层: 负责光 / 电信号的传递方式。比如现在以太网通用的网线 (双绞线)、早期以太网采用的同轴电缆 (现在主要用于有线电视)、光纤,现在的 wifi 无线网使用电磁波等都属于物理层的概念。物理层的能力决定了最大传输速率、传输距离、抗干扰性等。集线器 (Hub) 工作在物理层。

  • 数据链路层: 负责设备之间的数据帧的传送和识别。例如网卡设备的驱动、帧同步 (就是说从网线上检测到什么信号算作新帧的开始)、冲突检测 (如果检测到冲突就自动重发)、数据差错校验等工作。有以太网、令牌环网,无线 LAN 等标准。交换机 (Switch) 工作在数据链路层。

  • 网络层: 负责地址管理和路由选择。例如在 IP 协议中,通过 IP 地址来标识一台主机,并通过路由表的方式规划出两台主机之间的数据传输的线路 (路由)。路由器 (Router) 工作在网络层。

  • 传输层: 负责两台主机之间的数据传输。如传输控制协议 (TCP),能够确保数据可靠的从源主机发送到目标主机。

  • 应用层: 负责应用程序间沟通,如简单电子邮件传输(SMTP)、文件传输协议(FTP)、网络远程访问协议(Telnet)等。我们的网络编程主要就是针对应用层。

TCP/IP协议通过分层架构,把数据传递、寻址路由、差错处理、应用交互等复杂问题,拆解到不同层级中分别解决:从底层的相邻设备传输,到跨网络的路径选择,再到数据丢失的重传保障,最终到应用层的数据处理,每一层都对应解决通信流程中的一个关键问题,让不同厂商、不同系统的设备,都能遵循同一套规则顺畅通信。

1.3.2 OSI七层模型

既然 TCP/IP 这么好用,为什么还有一个 OSI 模型?

OSI(Open Systems Interconnection,开放式系统互联)模型,是国际标准化组织(ISO)提出的理论参考模型,它把网络通信更细致地划分为 7 层,分别是:物理层、数据链路层、网络层、传输层、会话层、表示层、应用层。

和 TCP/IP 五层模型相比,OSI 多了会话层和表示层,把应用层的部分功能拆了出来:

会话层:负责建立、管理和终止两台主机之间的会话连接

表示层:负责数据格式转换、加密解密、压缩解压,让不同系统能读懂对方的数据

但在实际应用中,TCP/IP 协议栈并没有严格按照 OSI 的七层划分来实现,而是把会话层、表示层的功能合并到了应用层中,形成了我们现在主流使用的五层(或四层)模型。

1.3.3协议和操作系统之间的关系

网络协议栈的实现,通常会被直接集成在操作系统内核中。无论是 Linux、Windows 还是 macOS,操作系统都需要提供一套完整的 TCP/IP 协议栈,来支撑本机的网络通信能力。

但这并不意味着不同系统的协议栈可以随意设计。恰恰相反,所有操作系统的网络协议栈,都必须遵循同一套标准规范。比如,无论你用的是 Linux 还是 Windows,IP 地址的格式、TCP 的三次握手流程、以太网帧的结构都必须完全一致。正是因为这种 "标准统一",才实现了跨平台的互联互通。

用户在用户层通过指令、程序发起网络请求,经由 Shell 和系统库,通过系统调用进入 Linux 内核;内核中集成了完整的 TCP/IP 协议栈,负责处理传输层、网络层、数据链路层的协议逻辑,再通过网卡驱动程序,将数据传递给底层的网卡硬件,最终完成数据的发送或接收。

所以说网络协议是属于操作系统的一部分。

协议本质上在内核中就是一组约定好的结构体 :TCP、UDP、IP 等协议的头部格式、连接状态、收发队列、序号与窗口等,都用 C 语言 struct 来定义和管理。因为协议栈是分层的,所以,每层都有双方都有协议,同层之间,互相可以认识对方的协议。而这些结构体变量,就是实际传输的协议报头

数据在网络中传输时,同样是以协议定义的结构体形式进行封装与传递的。正是因为所有系统都遵循同一套网络协议标准,数据在跨设备传输时,双方才能准确识别并解析这些结构体中的字段,完成互联互通。

在 TCP/IP 协议栈里,数据会跟着分层走,每一层都加上自己的协议头。应用层的请求与应答,到了传输层加上 TCP/UDP 头就是数据段,再加上 IP 头就成了网络层的数据报,最后在数据链路层加上 MAC 头和校验,就成了数据帧。

二、网络传输基本流程

2.1局域网通信

在同一个局域网上的主机,是可以直接进行通信的。要实现这种局域网内的精准传输,每台主机都必须有一个唯一的标识,来确保数据能准确送达目标设备,不会发错对象。这个唯一标识,就是 MAC地址(物理地址)

MAC地址是烧录在网卡硬件中的48比特位的全球唯一地址,相当于设备在局域网里的 "身份证号",它在主机出厂时就已经设定完成,每台主机有且仅有一个。在Linux中我们可以使用ip addr指令来查看当前主机的MAC地址:

link/ether 52:54:00:ab:2e:8f 就是这台主机的真实物理网卡 MAC 地址。

以太网的本质是一个共享介质的碰撞域 。在早期总线型以太网中,所有主机共享同一条传输介质,当多台主机在同一局域网内同时发送数据时,数字信号在传输过程中会发生波形叠加,导致数据失真、传输失败。为了解决这个问题,以太网要求所有主机都必须具备碰撞检测能力, 并通过碰撞避免算法来避免和处理冲突。

而局域网通信,就是基于碰撞检测和碰撞避免的不断重试的过程!所以,局域网和以太网的关系是:以太网是一种用于实现局域网通信的主流通信标准,而局域网是以太网最典型的应用场景!

我们可以用操作系统的视角来理解以太网通信:以太网的本质是一种共享传输介质的临界资源,同一时间内,只能有一台主机占用它发送数据,否则就会发生信号碰撞,导致传输失败。在这个类比里,主机就像是多个并发的线程,它们的发送过程就是需要互斥执行的临界区代码。为了保证对以太网这个临界资源的访问是原子的,就必须引入一套机制来避免冲突,而碰撞检测和碰撞避免算法就是用来保证这一过程的原子性的。

当然,局域网中除了以太网这种通信标准,还有一种通信标准,叫做令牌环网。

令牌环网是早期一种通过令牌传递机制实现无冲突访问的局域网通信标准,它依靠节点间循环传递的 "令牌" 来分配发送权,避免了以太网的碰撞问题。

而从操作系统的角度来看,令牌环网中的 "令牌" 本质上就是一把互斥锁:只有成功 "申请" 到这把锁,也就是拿到空闲令牌的节点,才被允许进入临界区并访问共享传输介质、发送数据,传输完成后再解锁,释放拿到的令牌,供其他节点竞争获取,从而从根本上保证了对共享资源访问的互斥性与原子性。

令牌环网现如今已经逐渐淡出人们的视野,是因为其成本高、扩展性差等缺陷,最终被不断迭代的以太网所淘汰。

上图展示了同一局域网内两台主机通信时,数据从发送方应用层到接收方应用层,经过传输层、网络层、数据链路层的逐层封装与解封装过程。每一层的封装都要添加对应层的报头,而每一层解封装时,则要把对应层的报头去掉后再交给上一层。正是这种对数据的层层封装与层层解封,实现了局域网内的端到端通信。

我们把每一层封装添加的报头叫做协议报头 ,每一层除了当前层的协议报头之外的内容我们把它称为有效载荷 。封装和解包的过程,从上到下、再从下到上,本质上就是一种入栈与出栈 的过程:发送方每经过一层,就把对应层的协议头部 "压入栈顶",层层包裹原始数据;而接收方则从栈顶开始,每上一层就把当前层的头部 "弹出栈",逐层剥离,最终还原出原始数据。所以我们把TCP/IP层状协议我们把它称之为**"协议栈"。**

2.2跨网络通信

在跨网络通信中必不可少的就是IP协议以及IP地址,要理解跨网络通信的原理,我们就得先从认识IP协议以及IP地址。

IP协议 是整个互联网通信的基础,定义了网络中主机寻址、数据转发和路由的核心规则。目前主流的IP协议有两个版本:IPv4和IPv6。凡是提到 IP 协议,没有特殊说明的,默认都指IPv4。

在IPv4体系下,IP 地址就是IP协议用来标识网络中不同主机的唯一地址,它本质上是一个 4 字节(32 位)的整数,我们平时用的 "点分十进制"(比如 192.168.0.1),只是为了方便人类阅读而设计的表示方式,每个分段对应一个字节,取值范围为 0-255。

在Linux中我们可以输入ifconfig指令来查看我们当前主机的IP地址:

inet表示的就是当前主机的IP地址。

在Windows命令行中我们可以输入ipconfig指令来获取当前主机的IP地址:

所以IP 地址的作用就是标识公网中的唯一一台主机,它能为我们解决主机定位和路径选择的问题。

我们前面有提到过一台主机有自己全球唯一的MAC地址,既然已经有了MAC地址来标识这台主机,为什么还需要有IP地址呢?

之所以要同时用 IP 地址和 MAC 地址,是因为它们的分工不同,解决问题的层次不一样。IP 地址管的是 "最终要去哪",负责为网络通信提供长远目标 ,是跨网络路由选择 的依据;而 MAC 地址管的是 "下一跳给谁",解决的是局域网内转发的问题!

跨网段的主机的数据传输,数据从一台计算机到另一台计算机传输过程中要经过一个或多个路由器。 下面是一张跨网络通信的示意图:

跨网段的主机通信时,数据会经过路由器转发。就像这张图里的 FTP 客户端和服务器,它们分别在以太网和令牌环网两种不同的网络里:应用层的 FTP、传输层的 TCP 全程保持逻辑通信,IP 层的源 IP 和目的 IP 也始终不变,保证了最终目标。但链路层的 MAC 地址和帧格式会在每一跳发生变化,客户端先把数据发给路由器的以太网口,路由器再换成令牌环帧格式,转发给服务器的网卡,这样就完成了跨不同网络的端到端传输。

用户 A 给用户 B 发数据时,会在网络层给数据打上源 IP(192.168.2.2)和目的 IP(172.168.2.2),判断出目的 IP 和自己不在同一网段后,就把数据发给默认网关,也就是 IP 为 192.168.2.1 的路由器。这一步里,数据会先在 A 所在的 192.168.2.0 局域网里传输,数据帧使用主机 A 的源 MAC 地址、路由器内网网卡的 MAC 地址,通过交换机等设备,最终送到路由器的内网接口上。路由器收到数据后,读取目的 IP 172.168.2.2,查路由表确认目标网段在另一侧接口,重新封装新的数据帧,修改源 MAC 和目标 MAC,再把数据转发到用户 B 所在的 172.168.2.0 局域网里,最后交给用户 B,完成数据传输。整个过程中IP地址都不变 ,而MAC地址会逐跳发生改变

网络通信最早的时候,先实现的是局域网通信。但不同团队研发的局域网标准不一样,为了实现跨网络通信,必须有一个统一的标准。可直接修改底层局域网的标准难度太大、阻力也太大,所以人们就想,能不能用软件的方式,在上面封装一层来统一标准,于是就有了 IP 协议。路由器会配合 IP 协议完成数据转发,同时完成链路层的重新封装,以此统一不同局域网的通信标准,最终实现多个网络之间互通。

所以 IP 地址的另一个核心作用就是统一互联网的通信标准!路由器是实现IP通信的底层最重要的硬件!

三、Socket编程预备

IP是用来在网络中标识主机的唯一性的,不过数据传输到主机不是最终目的,只是个手段,真正的目的是要把数据交给主机里对应的进程。当数据报文到达主机后,会逐层解封装,再通过分用机制向上传递,最终送达应用层,交给对应的应用进程处理。例如聊天、下载、浏览网页,这些操作都要靠 QQ、迅雷、浏览器这些进程来完成,进程就相当于人在系统里的代表,数据交给进程,人才能真正拿到信息。可一台主机上同时跑着很多进程,数据到了之后,怎么知道该发给哪个进程呢?所以就需要新的标识,在主机内部唯一标识进程。这个标识就是端口号(port)

3.1认识端口号

端口号是传输层协议的内容,它是一个 2 字节、16 位的整数,作用就是标识主机里的进程,告诉操作系统数据该交给哪个进程处理。IP 地址负责找到主机,而 IP 地址加上端口号,就能唯一确定网络上某一台主机里的某一个进程。一个端口号只能被一个进程占用,这样就能保证端口到进程的唯一性,反过来,一个进程可以同时使用多个端口号。

IP 地址负责在全网找到主机,端口号负责在主机里找到进程,二者配合,才能把数据精准送到目标进程。数据本质上是从一个进程被传输到另一个进程,所以说,网络通信的本质就是不同主机之间的进程间通信!

很多人会问,系统里的 PID 不就能唯一标识进程了吗?为什么网络通信里还要用端口号?

其实很简单,这是为了系统和网络的解耦。PID 是操作系统内部的概念,不同系统的 PID 规则不一样,而且进程重启后 PID 也会变,没法在网络上稳定标识一个服务;并且并不是每一个进程都需要进行网络通信,有些进程是不需要进行网络通信的。

而端口号是专门为网络设计的统一标识,它和操作系统无关,只要约定了端口号,网络上的任何设备都能直接找到对应的服务进程,这样就把系统内部的进程管理和网络通信分离开了,实现了解耦工作。

端口号范围划分

0 - 1023:知名端口号,HTTP、FTP、SSH 等这些广为使用的应用层协议,他们的端口号都是固定的。

1024 - 65535:操作系统动态分配的端口号。客户端程序的端口号,就是由操作系统从这个范围分配的。

理解源端口号和目的端口号

传输层协议(TCP 和 UDP)的数据段中有两个端口号,分别叫做源端口号和目的端口号。就是在描述 "数据是谁发的,要发给谁";

理解 socket

综上所述,IP地址用来标识互联网中唯一的一台主机,port用来标识该主机上唯一的一个网络进程。而IP+Port就能表示互联网中唯一的一个进程。我们把IP+Port叫做套接字Socket。

所以,通信的时候,本质是两个互联网进程代表人来进行通信,{srcIp, srcPort, dstIp, dstPort} 这样的四元组就能标识互联网中唯二的两个进程。

本地不同进程交换数据,采用的是System V通信方式,适用于同一操作系统内部。当进程需要跨主机、通过网络传输数据时,就会使用Posix规范下的Socket通信。

3.2拓展认识

操作系统是如何把收到的数据转发给特定端口的进程的呢?

操作系统收到数据后,会根据数据报里的目的端口号,去一张以端口号为键的哈希表里查找,找到对应的进程 PCB。因为进程在启动并绑定端口时,系统就已经把它的 PCB 指针填进了这张表的对应位置,这样就能快速定位到目标进程。

找到进程之后,如何把网络数据交给进程呢?

在 Linux 中,为了高效管理网络报文,内核会为每个报文设计专门的结构体,再通过链表把这些报文串联起来管理。每个报文结构体都自带缓冲区,用来存放具体的数据内容。

找到进程之后,操作系统会通过进程的内核接收缓冲区,把网络数据交付给它。因为在 Linux 下一切皆文件,网卡也被抽象成了文件,它有自己的struct file结构体和文件缓冲区,进程要操作它也需要通过文件描述符fd。

网卡收到数据后,先由内核协议栈逐层解封装,最终把数据放到对应端口的内核接收队列里;进程再通过调用recv/read这类系统调用,主动从内核缓冲区读取数据;最后数据从内核空间拷贝到进程的用户空间,进程就拿到了完整的网络数据。

3.3传输层的典型代表

我们在了解了系统,也了解了网络协议栈之后,我们知道,传输层是属于内核的。由于网络是属于操作系统的,操作系统不信任用户。所以我们要通过网络协议栈进行通信,必定要调用传输层提供的系统调用,来进行网络通信。

3.3.1初步认识TCP协议

TCP协议是面向连接的可靠传输协议,通信前需要建立连接。TCP协议是面向字节流的。

3.3.2初步认识UDP协议

UDP是无连接的不可靠传输协议,发送信息前无需建立连接。UDP协议是面向数据报的。

3.4网络字节序

我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分,磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分,网络数据流同样有大端小端之分。那么如何定义网络数据流的地址呢?

发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出,接收主机则把从网络上收到的字节依次按内存地址从低到高的顺序保存,因此网络数据流的地址规定为先发出的数据是低地址、后发出的数据是高地址 。也就是说网络中传输的数据必须是大端数据序列!TCP/IP协议规定网络数据流应采用大端字节序(即低地址存放高字节),无论主机本身是大端机还是小端机,都须遵循此网络字节序来发送或接收数据;若当前发送主机是小端机,就需要先将数据转换为大端字节序,否则直接发送即可。

为了使得网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换:

cpp 复制代码
#include <arpa/inet.h>

uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);

h 表示 host(主机),n 表示 network(网络),l 表示 32 位长整数,s 表示 16 位短整数。例如 htonl 表示将 32 位的长整数从主机字节序转换为网络字节序,常用来将 IP 地址转换后准备发送。如果主机是小端字节序,这些函数会将参数做相应的大小端转换后返回;如果主机是大端字节序,则不做转换,将参数原封不动地返回。

3.5socket通用结构体

3.5.1socket常见API

cpp 复制代码
// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
 int socket(int domain, int type, int protocol);

// 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr *address, socklen_t address_len);

// 开始监听socket (TCP, 服务器)
int listen(int socket, int backlog);

// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address, socklen_t* address_len);

// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

3.5.2Struct sockaddr结构

Socket通信的设计者为了让自己开发的技术能被更广泛地使用,为其设计了多种通信类型,使得Socket既能支持网络通信,也能支持本地通信。

为了实现不同的通信功能,Socket通信的设计者设计了三套套接字,分别是INET socket、Unix 域 socket和Raw socket。其中INET socket是用来网络通信的(跨主机或本机自环);Unix 域 socket是用来进行本地通信的(同一台机器内的进程间通信);Raw socket是用来底层网络操作的(比如构造自定义 IP 包、开发 ping、抓包、路由协议等),常用于设计网络工具。

但如果把三种套接字(INET、Unix 域、Raw)完全独立成三套系统调用,用户就需要学习三组不同的函数接口,这样做会大大提高用户的学习成本,不利于技术的传播。

为了解决这个问题,Socket 的设计者引入了一层统一的抽象层(即 BSD Socket 接口),对上提供完全相同的函数名和返回值(如 socket()、bind()、sendto() 等),从而让用户只用一套 API 就能操作不同类型的套接字。

然而,不同类型的套接字所需的参数差异很大:INET 需要 IP 地址和端口,Unix 域需要路径名,Raw 需要协议类型和网络接口索引。为了实现"统一参数传递",设计者采用了结构体封装 + 类型擦除的方法:

1.为每种套接字定义专用的参数结构体:

cpp 复制代码
struct sockaddr_in(INET,包含 sin_family、sin_port、sin_addr)

struct sockaddr_un(Unix 域,包含 sun_family、sun_path)

struct sockaddr_ll(Raw 常用,包含链路层地址等)

2.再定义一个通用的结构体 struct sockaddr,它只有两个字段:一部分字段用于标识16位地址类型,另一部分字段用于存储14字节的地址数据。

3.在传参时统一使用通用结构体指针,例如 bind(sockfd, (struct sockaddr*)&addr_in, sizeof(addr_in))。内核收到struct sockaddr* 后,会先读取该结构体中的 16位地址类型字段,来判断实际传入的具体地址结构是 sockaddr_in、sockaddr_un 还是 sockaddr_ll;然后再根据地址类型以及传入的 addrlen 长度,将 14字节的地址数据字段按照对应的结构体布局进行解析(例如对于 INET,这14字节中会包含端口和 IP 地址;对于 Unix 域,则包含路径字符串)。

这样设计既实现了函数接口的统一,又保留了不同类型参数的自描述能力,用户只需要学会一套系统调用,就能操作所有 Socket 类型,同时底层还能灵活扩展新的地址族(如 AF_BLUETOOTH、AF_VSOCK 等)。

在了解完上述结构体对象后,我们会发现,Socket通信这种"统一结构体 + 强制类型转换"的模式不正是 C 语言中模拟面向对象多态的经典手法吗?struct sockaddr扮演的就是基类的角色,而struct sockaddr_in、struct sockaddr_un和struct sockaddr_ll则是派生出的子类。通过让基类指针指向不同的子类,便实现了多态。