深入Linux网络栈:套接字接口工作机制与端到端通信开发
Linux系列
文章目录
- Linux系列
- 前言
- 一、网络通信(跨网络)
- 1.1 令牌环网
- 1.2 跨网络传输、IP地址和Mac地址
- 二、网络套节字
- 2.1 认识端口号
- 2.2 常用的SOCKET编程接口
- 2.3 sockaddr结构
前言
上篇文章中我们介绍了网络通信下相关概念(上一篇:Linux网络基础全面解析:从协议分层到局域网通信原理)本篇内容将衔接上篇,继续补充介绍。
一、网络通信(跨网络)
1.1 令牌环网
补充:令牌环网与采用碰撞检测、碰撞避免机制的以太网不同,二者虽同属数据链路层,但底层协议有别(这也是网络分层的优点,即使改变某一层的协议,也不会对其他层产生影响)。
在令牌环网中,同一局域网内任一时刻仅一台主机可发送消息。其通过 “令牌”(唯一标识数字) 实现控制:主机需先获取令牌,才能发送数据;数据传输完毕后释放令牌,使其在环网中循环(同互斥锁的原理)。
要点总结:
- 核心特性:局域网内仅一个令牌,获令牌主机才能通信,避免冲突
- 分层优势:作为数据链路层协议,替换以太网后,上层网络不受影响
那么在跨网络通信时,主机之间是如何定位的呢?不同设备可能采用不同的网络(如:以太网、令牌环网、网线LAN),通信是是如何规避差异,实现统一的呢?接下来我们通过示例回答这个问题。
1.2 跨网络传输、IP地址和Mac地址
在上篇文章中,我们通过模拟实验,详细探究了同一局域网内两台主机基于 Mac 地址实现通信定位的具体机制。这一过程如同在同一座城市中,通过独特的门牌号码精准找到目标地点。那么在网络传输时,两台主机之间是如何定位的呢?接下来,我们将视角拓展到更广阔的网络空间,学习主机在跨网络通信时的完整流程,以及在数据传输时各类协议如何发挥 “交通规则” 与 “导航系统” 的关键作用。
当主机A向主机B发起请求时,数据会逐层穿越协议栈:在网络层封装源IP(主机A)和目的IP(主机B),用于标识传输终点;在数据链路层添加源Mac(主机A)和目的Mac(路由器接口),指明数据的下一跳路径。
数据进入以太网后,会被所有主机捕获,但因Mac地址不符而丢弃,直至路由器识别出目的Mac为自身,对数据包解包获取目的IP。路由器通过查询IP路由表确定下一跳地址,再依据令牌环网协议重新封装报头,将目的Mac更新为下一跳主机的Mac地址。
数据进入令牌环网后继续传输,直至主机B捕获数据包,比对Mac地址确认目标身份,最终解包提取用户请求,在此过程中:
- 网络层:IP地址负责全网寻址(导航作用);
- 数据链路层:Mac地址控制链路跳转;
IP地址一般不会改变,他协助我们做路径规划,而Mac地址才出局域网后会被丢弃进入新的局域网后会根据协议包装为新的源Mac地址和目的Mac。
PS: IP 地址:是主机在公网上的唯一标识(用于跨网络通信时的全局寻址)。 Mac地址:是固化在网络设备(如网卡)硬件中的唯一标识符,用于在数据链路层标识设备,确保数据在局域网内准确传输。
所以在跨网络通讯时,我们的数据帧会不断地进入局域网、跳出局域网:
下面我们再进一步理解网络通讯,先思考一个问题:网络通信的主体究竟是主机本身,还是主机上运行的程序?就像手机上的微信如果不启动,绝不会自行收发信息;抖音不打开,也无法从服务器获取资源。实际上,当我们启动抖音等软件时,操作系统会将其创建为一个进程,主机接收的视频资源本质上是由这些进程进行接收与处理。因此,网络通信的本质并非主机间的直接交互,而是不同主机上进程之间的通讯,网络通信的本质就是进程间的交互。
二、网络套节字
2.1 认识端口号
此前我们已经解决了,同一局域网中主机之间数据的定位以及跨网络传输主机的定位,下面我们又面临着一个新的问题, 网络通信时,数据从源主机到达另目标主机后,面对目标主机中运行着的大量进程,传输层该如何将数据准确的交付给对应的进程呢?
这时候我们就需要,一个能够在主机上唯一标识某一进程的“名字”—-端口号。
当用户向服务器发送请求时,服务器端口号会被封装到报头中。服务器接收数据帧后,在传输层解包获取该请求对应的端口号,通过哈希映射找到对应进程的指针,即可将数据交付给进程。
下面我们来回答几个简单的问题:
1、传输层哈希表中存储的内容是什么?
每个进程都有对应的task_struct
结构体,在哈希表中存储的就是对应进程的task_struct
类型的指针。
2、客户端如何得知需要将请求发送到进程对应的端口号呢?
端口号内置在程序中了,而且用户还可以通过协议标准确定目标进程的端口号。
3、服务端如何得知,自己要回应的端口号呢?
当客户端向服务端发送请求时,客户端会将自己进程的端口号一并打包。
4、每个进程都拥有一个在当前主机上,唯一标识的PID,为什么不直接使用PID而非要单独定义端口号呢?
进程PID是OS系统上的概念而端口号是网络层的概念,这样定义可以将他们解耦(降低耦合度)。
现在我们就可以使用IP地址,在公网上对一台主机进行定位,使用用端口号对主机上的进程进行定位。那么我们就可以使用:网络套接字=IP地址+端口号,实现在公网上对一个进程进行唯一定位。
2.2 常用的SOCKET编程接口
下接口我们会在下篇文章详细介绍:
// 创建 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);
2.3 sockaddr结构
在网络编程中,sockaddr
结构体用于表示套接字的地址信息。由于不同协议(如 IPv4、IPv6)的地址格式不同,实际使用中通常通过以下三种变体结构来处理不同类型的地址:
在网络编程中,sockaddr
结构体用于表示套接字的地址信息。由于不同协议(如 IPv4、IPv6、Unix 域套接字)的地址格式不同,实际使用中通常通过以下三种变体结构来处理不同类型的地址:
-
通用地址结构:
struct sockaddr
用于类型兼容性,所有具体地址结构需强制转换为此类型传递给套接字函数,我们在编写网络通信代码时,一般都会将使用的套接字类型转化为该类型。 -
IPv4 地址结构:
struct sockaddr_in
用于 IPv4 协议的地址表示。 -
Unix 域套接字结构:
struct sockaddr_un
用于本地进程间通信(Unix Domain Socket)。
由于网络通信涉及多种不同的地址格式,所有网络套接字类型也是多样的,但是网络编程中有许多函数,如 bind(绑定套接字地址)、connect(建立连接)、recvfrom(接收数据报并获取源地址)、sendto(向指定地址发送数据报) 等,不可能每种类型都单独提供接口,为了降低设计、学习成本在进行接口设计时,采用了统一的方式来处理地址参数。
可以看到不同类型的套接字会在,该类型前16位标识出来,这样我们就可以采用将他们统一转化为相同类型,并在函数内部提取前16位进行判断,根据不同类型执行不同语句,这样就实现了接口的统一化。
本篇就分享到这里,接下来的文章可能会更新服务端、客户端的实现。