跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客GitHub 精选镜像工具UI配色美学隐私政策关于联系
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
C

Linux 网络编程实战:TCP/IP 协议栈与 UDP 通信

综述由AI生成介绍 Linux 网络编程基础,涵盖 TCP/IP 协议分层模型、报文打包与解包过程、网络字节序及 UDP 通信接口。通过服务端与客户端的代码实现,演示了套接字创建、地址绑定、数据发送与接收的核心流程,帮助理解网络通信原理并掌握 Socket 编程实践。

疯疯癫癫发布于 2026/2/4更新于 2026/5/248.4K 浏览
Linux 网络编程实战:TCP/IP 协议栈与 UDP 通信

Linux 网络编程实战:TCP/IP 协议栈与 UDP 通信

【一】初识协议

(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 协议栈主要由操作系统来实现,因此二者存在如下关系:协议栈其实是在操作系统里面。

Linux 协议栈关系

【三】通信的过程

(1)报文打包

学习通信过程之前,我们需要先知道报文:报文=报头(每层的识别标志)+ 有效载荷(数据内容)。

以'你好'为例:消息从发出到网卡,需要经过四大层,每层将上一层打包的数据增加报头,就形成了该层的报文。

例如:

报文打包示例

(2)外设传输

MAC 地址:每个网卡自出厂开始设置有唯一的编号,这是全球内的唯一标识。

局域网:小区域的计算机网络范围。

以太网:一种网络通信技术规定,任何时刻,只允许一台机器向网络中发送数据。

完成了上面的四层,此时需要经过网卡来完成信息传递:当数据被打包完成后会经过网卡发送到局域网:数据不是准确发送的,它需要一个个访问。

外设传输示意图

(3)报文解包

如果对方不是目标 MAC,途中被访问到的网卡也会自动摒弃,继续寻找。

如果对方是目标 MAC,就被对方网卡接收,开始逐层解包。

逆向开始逐层解包:

【四】长距离通信

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)创建套接字

原型:

#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)绑定地址和端口

原型:

#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
  • 第三个参数:addr 指向的结构体的大小

返回值:

  • 成功: 返回 0
  • 失败: 返回 -1,并设置 errno

作用:将一个套接字与特定的 IP 地址和端口号绑定。

(3)发送数据

原型:

#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)接收数据

原型:

#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)创建套接字

//创建套接字
int fd = socket(AF_INET, SOCK_DGRAM, 0);
if(fd == -1) {
    std::cerr << "错误码:" << errno << ",错误原因:" << strerror(errno) << std::endl;
    exit(1);
} else {
    std::cout << "socket is successful!" << std::endl;
}

(2)绑定 IP 和端口

IP 设置:由于部分设备有两个网卡,所以如果我们绑定了其中一个,就接收不到另外的一个了,因此这里我们实验就采用 INADDR_ANY,允许所有人访问。

端口设置:同时端口需要自己使用 1024 以上的端口(避免冲突和权限问题)。

//绑定 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(bd == -1) {
    std::cerr << "错误码:" << errno << ",错误原因:" << strerror(errno) << std::endl;
    exit(1);
} else {
    std::cout << "bind is successful!" << std::endl;
}

(3)接收数据

接收数据我们就设置为循环,因为服务器都是随时打开供客户访问的:

//接收数据
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, &sz);
    if(re == -1) // 根据 -1 来判断是错误还是没有数据
    {
        if (errno == EAGAIN || errno == EWOULDBLOCK) {
            //没有数据,稍等片刻再试
            printf("暂时没有数据,等待...\n");
            sleep(1);
        } else {
            //发生其他错误,处理错误
            std::cerr << "错误码:" << errno << ",错误原因:" << strerror(errno) << std::endl;
            exit(1);
        }
    } else {
        buffer[re] = 0;
        std::cout << "读取到数据:" << buffer << std::endl;
    }
}

(4)自定义选择回发数据

你可以在接收到对方发送的内容之后,直接返回信息给对方,自行选择即可:

服务端回发示意

(5)效果展示

此时接收到数据,就卡在'recvfrom()'那里了,很正常:

服务端效果

【八】客户端实现

(1)main 传参

客户端需要向服务端发送,所以客户端需要知道对方的 IP 和端口,这里就需要给 main 传参:

客户端传参

(2)创建套接字

和服务端实现一样:

//创建套接字
int cd = socket(AF_INET, SOCK_DGRAM, 0);
if(cd == -1) {
    std::cerr << "错误码:" << errno << ",错误原因:" << strerror(errno) << std::endl;
    exit(1);
} else {
    std::cout << "socket is successful!" << std::endl;
}

(3)不需要 bind()

bind 无非就是解决 IP 和端口的绑定问题,下面我们看为什么一般不需要绑定:

下面的 IP 分配和端口设置都为 0 其实是操作系统隐藏式的 bind(OS 自动选择 bind)。

  • IP:向对方发送信息,我们只需要知道对方的地址即可,一般选择 0(网络分配)
  • 端口:一般选择 0,由操作系统自己分配,若自己绑定,可能造成当前端口正在被使用,bind 失败

(4)发送数据

//发送数据
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) {
    std::cerr << "错误码:" << errno << ",错误原因:" << strerror(errno) << std::endl;
    exit(1);
}

(5)自定义选择接收数据

你可以在接收到对方回发的内容之后,接收打印查看:

客户端接收

(6)效果展示

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

通信效果

目录

  1. Linux 网络编程实战:TCP/IP 协议栈与 UDP 通信
  2. 【一】初识协议
  3. (1)协议简介
  4. (2)协议分层
  5. (3)OSI 七层模型
  6. (4)TCP/IP 分层模型
  7. 【二】Linux 与协议关系
  8. 【三】通信的过程
  9. (1)报文打包
  10. (2)外设传输
  11. (3)报文解包
  12. 【四】长距离通信
  13. 【五】网络字节序
  14. 【六】UDP 网络协议通信接口
  15. (1)创建套接字
  16. (2)绑定地址和端口
  17. (3)发送数据
  18. (4)接收数据
  19. 【七】服务端实现
  20. (1)创建套接字
  21. (2)绑定 IP 和端口
  22. (3)接收数据
  23. (4)自定义选择回发数据
  24. (5)效果展示
  25. 【八】客户端实现
  26. (1)main 传参
  27. (2)创建套接字
  28. (3)不需要 bind()
  29. (4)发送数据
  30. (5)自定义选择接收数据
  31. (6)效果展示
  • 💰 8折买阿里云服务器限时8折了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 🤖 一键搭建Deepseek满血版了解详情
  • 一键打造专属AI 智能体了解详情
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • Docker 容器化 Whisper:镜像选型与模型预加载实战
  • Ollama 集成 Llama 3.2 Vision 与视觉 RAG 系统实战
  • Spring AOP 注解实现详解
  • Python 七大主流就业方向及核心技能解析
  • 宜搭低代码高级认证:待办列表自定义页面实战指南
  • Git Worktree 原理与实战:解析 Cursor 2.0 多 Agent 并行模式
  • DeOldify 图像上色:黑白漫画转赛博朋克/水墨/油画风格
  • 安卓 Termux 部署 AstrBot 与 NapCat 搭建 QQ 机器人
  • Agent 开发的三重境界:从 API 调用到自主智能
  • AI 时代的内容创作:从代码到认知的范式转移
  • mdev 与 udev:嵌入式及桌面 Linux 设备管理对比
  • Python 遍历目录
  • 手写 C++ TCP 服务器:自定义协议与粘包处理实战
  • Verilog 实现半加器:FPGA 硬件入门实战
  • 鸿蒙游戏开发:AI 驱动的智能 NPC 实现体验
  • OpenClaw 本地部署飞书机器人配置指南
  • 前端函数防抖详解:原理、手写与 Lodash 实战
  • 使用双指针解决链表问题
  • 医疗 AI 可信系统全栈实现:向量索引与贝叶斯网络(下)
  • 金仓 KingbaseES 融合架构实践:从多库并存到一库多能

相关免费在线工具

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online

  • Markdown转HTML

    将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online

  • HTML转Markdown

    将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online

  • JSON 压缩

    通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online

  • JSON美化和格式化

    将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online