

**前引:**Linux 是服务器领域的主流系统,其网络能力直接决定服务的稳定性与通信效率。想要真正掌握 Linux 网络,光懂理论不够 ------ 从 TCP/IP 协议的分层逻辑,到 Socket 编程的系统调用,再到实际场景中的数据封装与解包,实战是打通知识的关键。本系列将带着你从协议原理落地到代码实践,手把手搞定 Linux 网络通信的核心技能!
目录
【一】初识协议
(1)协议简介
协议即为双方共同遵守的规定------统一约定
理解:你需要和双方共同遵守一套协议,才能和对方交流,比如发电报,你需要对方有和你 一样的翻译本,才能知道对方是在传递什么信息,只要遵守网络标准,就能实现通信
因此网络也有自己的一套协议,它由权威、最有价值、被所有人认可的机构或者组织一起定制出的网络"约定"!------网络协议
(2)协议分层
如果所有协议全部挤在一起,那么不好维护,因此就出现了协议分层:比如一个信息的传递需要经过发送人------>打包------>传输------>解包------>对方,将协议分层实质是解耦的过程,方便后面维护!因此网络协议是层状结构的!(解耦合、维护性高)那是什么样的分层结构?
(3)OSI七层模型
你可以理解为OSI七层模型是最初的完整的网络通信协议,什么应用层、表示层、用户层.....特别的麻烦复,所以利用它的理论逻辑形成了更加实用现实精简的分层结构------TCP/IP分层模型
(4)TCP/IP分层模型
Linux 网络编程完全遵循 TCP/IP 分层逻辑,每层各司其职,自上而下层层封装数据,接收方自下而上层层解包。常用 "五层模型"(清晰易懂),而我们仅了解其中的物理层(不涉及编程)即可:
- 应用层:直接对接用户进程(如浏览器、QQ),负责处理具体业务逻辑。
- 核心协议:HTTP(网页)、FTP(文件传输)、SMTP(邮件)、Telnet(远程登录)。
- Linux 中实现:由用户编写的代码实现(比如写 HTTP 客户端 / 服务器)。
- 传输层:负责两台主机之间的 "端到端数据传输",解决 "数据发给哪个进程" 的问题。
- 核心协议:TCP(可靠、有连接)、UDP(不可靠、无连接)。
- 关键标识:端口号(16 位整数,如 HTTP 默认 80 端口),用来标识主机上的进程。
- Linux 中实现:由操作系统内核实现,通过系统调用(如 socket)供用户调用。
- 网络层:负责 "地址管理" 和 "路由选择",解决 "数据发给哪个主机" 的问题。
- 核心协议:IP(IPv4 为主)、ICMP(网络差错控制)。
- 关键标识:IP 地址(32 位,点分十进制如 192.168.0.1),用来标识网络中的主机。
- Linux 中实现:内核内置,路由器就是基于这一层工作。
- 数据链路层:负责 "相邻设备间的数据帧传输",解决 "局域网内发给哪个设备" 的问题。
- 核心协议:以太网协议、无线 LAN 协议。
- 关键标识:MAC 地址(48 位,如 08:00:27:03:fb:19),网卡出厂固化的唯一标识。
- Linux 中实现:由网卡驱动程序实现,交换机工作在这一层。
- 物理层 :负责 "光 / 电信号传输",是数据的物理载体。
- 核心载体:网线(双绞线)、光纤、电磁波(WiFi)。
- Linux 中实现:完全由硬件(网卡、集线器)实现,编程中基本不用直接操作。
【二】Linux与协议关系
在上面我们已经知道了,网络协议被划分成了TCP/IP协议栈:应用、传输、网络、数据链路层
而TCP/IP协议栈主要由操作系统来实现,因此二者存在如下关系:协议栈其实是在操作系统里面

【三】通信的过程
(1)报文打包
学习通信过程之前,我们需要先知道报文:报文=报头(每层的识别标志)+有效载荷(数据内容)
以"你好"为例:
消息从发出到网卡:需要经过四大层,每层将上一层打包的数据增加报头,就形成了该层的报文
例如:

(2)外设传输
MAC地址:每个网卡自出厂开始设置有唯一的编号,这是全球内的
局域网:小区域的计算机网络范围
以太网:一种网络通信技术规定,任何时刻,只允许⼀台机器向⽹络中发送数据
完成了上面的四层,此时需要经过网卡来完成信息传递:
当数据被打包完成后会经过网卡发送到局域网:数据不是准确发送的,它需要一个个访问

(3)报文解包
如果对方不是目标MCP,途中被访问到的网卡也会自动摒弃,继续寻找
如果对方是目标MCP,就被对方网卡接收,开始逐层解包
逆向开始逐层解包:
【四】长距离通信
IP地址:每个设备在全球的唯一通信地址(例如:192.168.1.101,传输中不会变)
MAC地址:MAC在远距离传输的时候会变换为途中的中间设备的网卡MAC地址
端口号:用于区分一台计算机上不同的应用程序(比如抖音500,快手120)
路由器:连接不同网络的设备
当数据在网卡中准备发送时,应该是如何样子:
(1)此时数据包进入局域网,局域网看到数据包需要远距离通信后,再交给自己的路由器(2)此时路由器传给途中的其它设备,通过解析拿到IP地址(被每个路由器解析到网络层)
(3)再按照内部的目标路径向下一个路由器传递
(注意:MAC在传输的过程中,会随着接触到的网卡逐渐改变,但是自己IP和目标IP不会)
(4)当路由器根据对应的网卡找到目标IP后,再开始发送解包的过程,根据端口号找到应用

【五】网络字节序
内存中的多字节数据相对于内存地址有⼤端和⼩端之分,磁盘⽂件中的多字节数据相对于⽂件中的偏移地址也有⼤端⼩端之分,就可能因为大小端的问题导致读取数据不一致的问题,为了统一数据读取方式,网络中规定在应用层传入的数据必须是大端
如果主机是⼩端字节序,这些函数将参数做相应的⼤⼩端转换然后返回如果主机是⼤端字节序,这些函数不做转换,将参数原封不动地返回
【六】UDP网络协议通信接口
在网络协议中,分为了TCP和UDP两套协议,UDP是面向非连接的,下面我们学习UDP协议
如果你听见"网络套接字",其实就是 "IP 地址 + 端口号 + 协议" 的总称,即网络通信相关
既然网络协议是在操作系统里面,那我们自己要实现通信就必须使用系统调用系列接口:socket
(1)创建套接字
原型:
cpp
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
参数:
**第一个参数:**通信域
AF_INET: IPv4 互联网协议AF_INET6: IPv6 互联网协议AF_UNIX或AF_LOCAL: 本地通信(进程间通信)
**第二个参数:**套接字类型
SOCK_STREAM: 流式套接字,提供可靠、面向连接的通信(对应 TCP)SOCK_DGRAM: 数据报套接字,提供不可靠、无连接的通信(对应 UDP)SOCK_RAW: 原始套接字,允许直接访问底层协议(如 IP、ICMP)
第三个参数: 指定具体协议。通常设为 0,表示根据 domain 和 type 自动选择默认协议。
返回值:
- 成功: 返回一个非负整数,即套接字文件描述符(这说明也是基于文件实现通信!)
- 失败 : 返回
-1,并设置errno
作用:创建一个用于网络通信的端点
(2)绑定地址和端口
原型:
cpp
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数:
第一个参数: socket() 函数返回的套接字文件描述符
第二个参数: struct sockaddr 类型的指针,该结构体包含了要绑定的 IP 地址和端口号等信息
(
- 对于 IPv4,通常使用
struct sockaddr_in结构体,并强制转换为struct sockaddr * - 对于 IPv6,使用
struct sockaddr_in6
)
cppstruct sockaddr_in { sa_family_t sin_family; /* 协议族,必须是 AF_INET in_port_t sin_port; /* 端口号(大端)(网络字节序) (注意:端口号可以设置为0,表示需要OS自己分配) struct in_addr sin_addr; /* IPv4 地址(网络字节序) (注意:IPV4地址需要 inet_addr(char*) 进行转换为二进制整数) unsigned char sin_zero[8];/* 填充字段,必须设为 0 (注意:可以用memset(&local_send.sin_zero, 0, sizeof(local_send.sin_zero))) }; // IPv4 地址结构体 struct in_addr { in_addr_t s_addr; /* 32位 IPv4 地址(网络字节序) };
第三个参数: addr 指向的结构体的大小
返回值:
- 成功 : 返回
0 - 失败 : 返回
-1,并设置errno
作用:将一个套接字与特定的 IP 地址和端口号绑定
(3)发送数据
原型:
cpp
#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);
参数:
**第一个参数:**通信使用的套接字文件描述符
**第二个参数:**发送数据的字符指针
**第三个参数:**发送的长度
第四个参数: 发送标志(通常设为 0,表示默认行为)
**第五个参数:**指向目标接收方地址结构体的指针(包含对方 IP 和端口)
第六个参数: dest_addr 结构体的大小
返回值:
- 成功:返回实际发送的字节数
- 失败 :返回
-1,并设置errno
作用:给指定的IP和端口发送数据
(4)接收数据
原型:
cpp
#include <sys/socket.h>
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
参数:
**第一个参数:**套接字文件描述符,需要和发送方一致
**第二个参数:**存储数据
**第三个参数:**接收数据的最大容量
第四个参数: 接收标志(通常设为 0,默认行为)
第五个参数:(用于存储 "寄件人" 的 IP 和端口)
第六个参数: socklen_t 类型的变量地址(变量等于存储"寄件人"结构体大小),例如:

返回值:
- 成功:返回实际接收的字节数
- 失败 :返回
-1,并设置errno
作用:接收对方发送的数据
【七】服务端实现
作为服务端,它的IP和端口是固定的,所以我们需要:创建套接字->绑定->(自选)发送 || 接收
(1)创建套接字
cpp
//创建套接字
int fd = socket(AF_INET,SOCK_DGRAM,0);
if(fd==-1)
{
log_message(LOG_LEVEL_ERROR,__FILE__,__LINE__,"错误码:%d,错误原因:%s",errno,strerror(errno));
exit(1);
}
else
{
log_message(LOG_LEVEL_ERROR,__FILE__,__LINE__,"socket is successful!");
}
(2)绑定IP和端口
IP设置:由于部分设备有两个网卡,所以如果我们绑定了其中一个,就接收不到另外的一个 了,因此这里我们实验就采用 INADDR_ANY ,允许所有人访问
端口设置:同时端口需要自己使用1024以上的端口(避免冲突和权限问题)
cpp
//绑定IP和端口
struct sockaddr_in local;
local.sin_family=AF_INET;
local.sin_addr.s_addr=INADDR_ANY;
local.sin_port=htons(8000);
memset(&local.sin_zero, 0, sizeof(local.sin_zero));
int bd = bind(fd,(struct sockaddr *)&local,sizeof(local));
if(fd==-1)
{
log_message(LOG_LEVEL_ERROR,__FILE__,__LINE__,"错误码:%d,错误原因:%s",errno,strerror(errno));
exit(LOG_LEVEL_BIND);
}
else
{
log_message(LOG_LEVEL_ERROR,__FILE__,__LINE__,"bind is successful!");
}
(3)接收数据
接收数据我们就设置为循环,因为服务器都是随时打开供客户访问的:
cpp
//接收数据
char buffer[1024]={'\0'};
struct sockaddr_in local;
socklen_t sz=sizeof(local);
while(1)
{
ssize_t re= recvfrom(fd,buffer,sizeof(buffer) - 1,0,(struct sockaddr*)&local_t,&sz);
if(re==-1)//根据-1来判断是错误还是没有数据
{
if (errno == EAGAIN || errno == EWOULDBLOCK)
{
//没有数据,稍等片刻再试
printf("暂时没有数据,等待...\\n");
sleep(1);
}
else
{
//发生其他错误,处理错误
log_message(LOG_LEVEL_ERROR,__FILE__,__LINE__,"错误码:%d,错误原因:%s",errno,strerror(errno));
exit( LOG_LEVEL_RECVFROM);
}
}
else
{
buffer[re] = 0;
std::cout<<"读取到数据:"<<buffer<<std::endl;
}
}
(4)自定义选择回发数据
你可以在接收到对方发送的内容之后,直接返回信息给对方,自行选择即可:

(5)效果展示:
此时接收到数据,就卡在"recvfrom()"那里了,很正常:

【八】客户端实现
(1)main传参
客户端需要向服务端发送,所以客户端需要知道对方的IP和端口,这里就需要给main传参:

(2)创建套接字
和服务端实现一样:
cpp
//创建套接字
int cd = socket(AF_INET,SOCK_DGRAM,0);
if(cd==-1)
{
log_message(LOG_LEVEL_ERROR,__FILE__,__LINE__,"错误码:%d,错误原因:%s",errno,strerror(errno));
exit(LOG_LEVEL_SOCKET);
}
else
{
log_message(LOG_LEVEL_ERROR,__FILE__,__LINE__,"socket is successful!");
}
(3)不需要bind()
bind无非就是解决IP和端口的绑定问题,下面我们看为什么一般不需要绑定:
下面的IP分配和端口设置都为0其实是操作系统隐藏式的bind(OS自动选择bind)
IP:向对方发送信息,我们只需要知道对方的地址即可,一般选择0(网络分配)
端口:一般选择0,由操作系统自己分配,若自己绑定,可能造成当前端口正在被使用,bind失败
(4)发送数据
cpp
//发送数据
const char* ptr="Hello,你吃了吗!";
struct sockaddr_in local;
local.sin_family=AF_INET;
local.sin_addr.s_addr=inet_addr(argv[1]);
local.sin_port=htons(atoi(argv[2]));
ssize_t ac =sendto(cd,ptr,strlen(ptr),0,(const sockaddr*)&local,sizeof(local));
if(ac==-1)
{
log_message(LOG_LEVEL_ERROR,__FILE__,__LINE__,"错误码:%d,错误原因:%s",errno,strerror(errno));
exit(LOG_LEVEL_SENDTO);
}
(5)自定义选择接收数据
你可以在接收到对方回发的内容之后,接收打印查看:

(6)效果展示
现在我们同时运行客户端和服务端,同时打开云服务端口(或者直接采用别的任意IP)开始通信:



