ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Linux 学习笔记(三十九):网络基石——TCP/IP 模型与 UDP 实战

Linux 学习笔记(三十九):网络基石——TCP/IP 模型与 UDP 实战

📌 核心结论(TL;DR):

  1. 网络通信本质:是跨主机的进程间通信。靠IP + Port(即 Socket 套接字)唯一标识网络中的进程。

  2. 网络字节序:网络规定使用大端字节序。发送前必须用htons/htonl转换。

  3. MAC vs IP:IP 是终极目标(跨网络不变),MAC 是下一跳目标(局域网内一直变)。

  4. UDP 特性:无连接、不可靠、面向数据报。

  5. UDP 开发核心:服务器必须bind(且 IP 选INADDR_ANY),客户端不用显式bind。

一、 网络基础概念

计算机网络的核心目的是数据共享。为了实现不同设备、不同操作系统之间的无障碍通信,采用了TCP/IP 四层(或五层)模型(应用层、传输层、网络层、链路层)。数据在发送时自上而下层层封装(加报头),接收时自下而上层层解包(去报头)。

二、 核心身份标识:IP、Port 与 MAC

  • IP 地址:IPv4 是 32 位整数。标识互联网中唯一的主机,路径选择的重要依据。

  • 端口号(Port):16 位整数。标识主机上的唯一进程。

  • MAC 地址:48 位。标识局域网内唯一的网卡。注意:跨网络传输时,IP 地址基本不变,但 MAC 地址每经过一个路由器就会改变。

三、 UDP 协议与核心 API

UDP(User Datagram Protocol)不保证数据一定到达,但速度快。

  • socket():创建套接字。

  • bind():绑定端口(仅服务器端)。

  • sendto():发送数据。

  • recvfrom():接收数据。

四、 代码实战:UDP 回显服务器

struct sockaddr_in server_addr; server_addr.sin_family = AF_INET; server_addr.sin_port = htons(8888); // 关键:INADDR_ANY 表示绑定所有本机网卡IP server_addr.sin_addr.s_addr = INADDR_ANY; bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr));

五、 踩坑记录:云服务器端口不通


现象:本地测试正常,但用别的电脑连不上云服务器的 UDP 端口。
原因:云厂商(如阿里云、腾讯云)有安全组防火墙机制。
解决:登录云控制台,在安全组入站规则中,放行对应的 UDP 端口。

自我实战操作

cd ~/linux_study mkdir network_test cd network_test
实战 1:UDP 服务端(UdpServer)
vim udp_server.c

(输入以下 C 语言代码,最纯粹的底层逻辑)

#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #define PORT 8888 #define BUF_SIZE 1024 int main() { // 1. 创建 UDP 套接字 int server_fd = socket(AF_INET, SOCK_DGRAM, 0); if (server_fd < 0) { perror("socket"); return 1; } // 2. 绑定 IP 和 端口 struct sockaddr_in server_addr; memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_port = htons(PORT); // 端口转网络字节序 server_addr.sin_addr.s_addr = INADDR_ANY; // 监听任意IP(超级重要) if (bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) { perror("bind"); return 1; } printf("UDP 服务器已启动,监听端口 %d...\n", PORT); // 3. 循环接收和发送 char buffer[BUF_SIZE]; struct sockaddr_in client_addr; socklen_t client_len = sizeof(client_addr); while (1) { // 接收客户端发来的数据 int n = recvfrom(server_fd, buffer, BUF_SIZE - 1, 0, (struct sockaddr*)&client_addr, &client_len); if (n > 0) { buffer[n] = '\0'; printf("收到来自 %s:%d 的消息: %s\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), buffer); // Echo (原样发回) sendto(server_fd, buffer, n, 0, (struct sockaddr*)&client_addr, client_len); } } close(server_fd); return 0; }
实战 2:UDP 客户端(UdpClient)
vim udp_client.c

(输入以下代码)

#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #define SERVER_IP "127.0.0.1" // 本地测试 #define PORT 8888 #define BUF_SIZE 1024 int main() { // 1. 创建 UDP 套接字 (不需要显式 bind,系统会自动分配随机端口) int client_fd = socket(AF_INET, SOCK_DGRAM, 0); if (client_fd < 0) { perror("socket"); return 1; } // 2. 配置服务器地址信息 struct sockaddr_in server_addr; memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_port = htons(PORT); inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr); // 字符串IP转二进制 char buffer[BUF_SIZE]; while (1) { printf("请输入要发送的消息 (输入 quit 退出): "); fgets(buffer, BUF_SIZE, stdin); buffer[strcspn(buffer, "\n")] = 0; // 去掉换行符 if (strcmp(buffer, "quit") == 0) break; // 发送数据 sendto(client_fd, buffer, strlen(buffer), 0, (struct sockaddr*)&server_addr, sizeof(server_addr)); // 接收回显数据 struct sockaddr_in temp_addr; socklen_t temp_len = sizeof(temp_addr); int n = recvfrom(client_fd, buffer, BUF_SIZE - 1, 0, (struct sockaddr*)&temp_addr, &temp_len); if (n > 0) { buffer[n] = '\0'; printf("收到服务器回显: %s\n", buffer); } } close(client_fd); return 0; }
实战 3:编译测试与网络排错
# 编译 gcc udp_server.c -o udp_server gcc udp_client.c -o udp_client

测试流程:

  1. 开终端 A(服务端):./udp_server

  2. 开终端 B(客户端):./udp_client

  3. 在终端 B 输入hello,看看终端 A 和 B 分别打印了什么。

  4. 排错加餐:如果你把服务端跑在云服务器上,客户端在自己的电脑上(Windows),你必须在云服务器后台的安全组里,开放8888的UDP 端口,否则数据会被防火墙拦截!

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进