文章目录
- [Linux Socket UDP 初学者实战踩坑笔记(C++)](#Linux Socket UDP 初学者实战踩坑笔记(C++))
-
- 前言
- [1. 套接字(socket)到底是什么?](#1. 套接字(socket)到底是什么?)
- [2. UDP核心API介绍](#2. UDP核心API介绍)
- [3. 完整UDP 服务端代码(C++)](#3. 完整UDP 服务端代码(C++))
- [4. 完整UDP客户端代码(C++)](#4. 完整UDP客户端代码(C++))
- [5. 阻塞到底是什么?(重点,很多人混淆缓冲区)](#5. 阻塞到底是什么?(重点,很多人混淆缓冲区))
- [6. 云服务器大坑!我们实操踩坑合集【本文核心】](#6. 云服务器大坑!我们实操踩坑合集【本文核心】)
-
- 坑1:云服务器公网EIP不能直接bind
- 坑2:云服务器安全组(最重要!)
- [坑3:抓包排查工具 tcpdump](#坑3:抓包排查工具 tcpdump)
- 坑4:内网网段判断
- 坑5:IP区分
- [7. 回调函数(UDP框架拓展)](#7. 回调函数(UDP框架拓展))
- [8. 测试顺序(推荐一步步测,不要直接公网)](#8. 测试顺序(推荐一步步测,不要直接公网))
- [9. 总结](#9. 总结)
Linux Socket UDP 初学者实战踩坑笔记(C++)
前言
很多同学初学Socket,卡在一堆名词:套接字、bind、recvfrom、安全组、内网公网。
网上教程大多只给代码,不讲云服务器真实踩坑。本文带你从零写UDP回显服务,重点讲我们实操遇到的所有坑:为什么代码看着没问题,发包就是收不到。
UDP特点:无连接、不可靠,只管发,不确认对方有没有收到。像寄明信片,发出去就不管了。
1. 套接字(socket)到底是什么?
套接字不是网线、不是电信号、不是无线电波。
套接字是操作系统内核给应用程序开的通信"文件句柄"。
- 类比:你去快递站开一个收件窗口,这个窗口就是socket。
- 数据传输底层:有线是网线里的电压信号;手机/WiFi是无线电波。应用层代码完全不用管介质,全部交给内核协议栈。
一句话:socket = 应用程序 和 操作系统网络协议栈 的接口。
2. UDP核心API介绍
UDP是数据报,核心4个函数:
socket():创建套接字,拿到 sockfd(文件描述符)bind():绑定本地IP+端口(服务端必用,客户端一般不用)recvfrom():UDP接收函数,可以拿到发送方IP+端口(UDP专属)sendto():UDP发送数据,指定目标IP端口
bind 重点(初学者最容易懵)
bind作用:在内核登记,当前这个socket占用本机某个IP+端口,监听别人发过来的包
- ✅ 服务器:必须bind固定端口(例如8080),别人才知道往哪个端口发数据。
- ✅ 客户端:默认不用手动bind 。第一次调用sendto发包的时候,操作系统会隐式bind,随机分配一个临时端口。>
误区纠正:bind只是告诉内核"我要占这个端口",不会主动把自己IP端口发给任何人;只有发送数据包,对方才能拿到你的地址。
端口知识
- 服务端:固定端口,例如ssh默认22。
- 客户端:默认随机端口,由操作系统分配。>
不是"一个服务器一个端口,一个客户端一个端口"。服务器只监听一个固定端口,可以接收成千上万不同客户端发来的UDP数据包。
recvfrom
UDP接收专用函数。和recv不一样:recvfrom会额外拿到发送端sockaddr_in地址结构体,知道是谁发过来的消息。
UDP没有连接,每次收到包,都要记录对方地址,想回复的时候用sendto把数据发回这个地址。
地址结构体 sockaddr_in & in_addr sin_addr
struct sockaddr_in server_addr;
server_addr.sin_addr; // in_addr类型,存放IP的二进制形式
sin_addr:保存二进制IP,网络字节序。- 我们写代码输入的
10.0.0.4、127.0.0.1是字符串点分十进制 ,不能直接赋值给sin_addr。
需要函数做转换:inet_addr/inet_pton:>
作用:把人类可读的字符串IP("10.0.0.4"),转为内核能识别的二进制IP。反过来
inet_ntop把二进制IP转回字符串打印日志。
回环地址 127.0.0.1
127.0.0.1 是本机回环地址 ,所有电脑都自带。
数据包不经过网卡 ,直接在内核内部闭环。
✅ 本地测试首选:服务端客户端都在同一台机器,用127.0.0.1,完全不会被云服务器安全组拦截。
三层IP区分:
- 127.0.0.1:本机内部,只能自己访问自己
- 内网IP:云服务器网卡IP,同VPC内机器互通(例:10.0.0.4、172.18.x.x)
- 公网EIP:外网访问的IP,云服务器的公网IP不在本机网卡上,在云厂商网关,代码bind公网IP会失败!
3. 完整UDP 服务端代码(C++)
#include <iostream>
#include <string>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
// ./udpserver 8080
int main(int argc, char *argv[])
{
if(argc != 2)
{
std::cerr << "Usage: " << argv[0] << " port" << std::endl;
return 1;
}
uint16_t port = std::stoi(argv[1]);
// 1. 创建UDP套接字 SOCK_DGRAM代表UDP
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sockfd < 0)
{
perror("socket create fail");
return -1;
}
std::cout << "socket success, sockfd : " << sockfd << std::endl;
// 2. bind绑定本机端口
struct sockaddr_in local_addr;
memset(&local_addr, 0, sizeof(local_addr));
local_addr.sin_family = AF_INET;
local_addr.sin_port = htons(port);
// 重点!云服务器服务端监听写INADDR_ANY,不要硬写公网IP
local_addr.sin_addr.s_addr = htonl(INADDR_ANY);
int ret = bind(sockfd, (struct sockaddr*)&local_addr, sizeof(local_addr));
if(ret < 0)
{
perror("bind fail");
close(sockfd);
return -1;
}
std::cout << "bind success" << std::endl;
char buf[128];
while(true)
{
struct sockaddr_in client_addr;
socklen_t addr_len = sizeof(client_addr);
memset(buf, 0, sizeof(buf));
// recvfrom接收UDP数据,拿到客户端地址
ssize_t n = recvfrom(sockfd, buf, sizeof(buf)-1, 0,
(struct sockaddr*)&client_addr, &addr_len);
if(n > 0)
{
std::cout << "收到消息:" << buf << std::endl;
// 打印客户端IP
std::string client_ip = inet_ntoa(client_addr.sin_addr);
std::cout << "来自客户端 " << client_ip << std::endl;
// 回显,把消息发回客户端
sendto(sockfd, buf, n, 0, (struct sockaddr*)&client_addr, addr_len);
}
}
close(sockfd);
return 0;
}
4. 完整UDP客户端代码(C++)
#include <iostream>
#include <string>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
// ./udpclient server_ip server_port
int main(int argc, char *argv[])
{
if (argc != 3)
{
std::cerr << "Usage: " << argv[0] << " server_ip server_port" << std::endl;
return 1;
}
std::string server_ip = argv[1];
uint16_t server_port = std::stoi(argv[2]);
// 创建UDP套接字,客户端不需要bind!
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sockfd < 0)
{
perror("socket");
return -1;
}
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(server_port);
inet_pton(AF_INET, server_ip.c_str(), &serv_addr.sin_addr);
std::string msg;
while(true)
{
std::cout << "Please Enter# ";
std::cin >> msg;
// sendto发包,触发隐式bind,内核分配随机端口
sendto(sockfd, msg.c_str(), msg.size(), 0,
(struct sockaddr*)&serv_addr, sizeof(serv_addr));
}
close(sockfd);
return 0;
}
5. 阻塞到底是什么?(重点,很多人混淆缓冲区)
❌ 误区:阻塞 = 缓冲区没释放
✅ 正确理解:
recvfrom默认是阻塞调用 。调用之后,线程卡在这个函数,一直等数据包到达。
- 缓冲区有数据 → recvfrom立刻拿走数据,函数返回。
- 缓冲区空 → 程序原地卡住(阻塞),等待内核收到数据包。
缓冲区 和 阻塞是两个独立概念
- 缓冲区:内核里存放收到数据的一块内存。
- 阻塞:函数调用的行为(等还是不等)。
6. 云服务器大坑!我们实操踩坑合集【本文核心】
本地虚拟机跑代码正常,放到云服务器,代码编译无错,但是数据包发不过来,90%是下面问题。
坑1:云服务器公网EIP不能直接bind
云服务器公网IP在云厂商网关 ,不在虚拟机本机网卡。
代码不能bind(公网IP),监听直接写 INADDR_ANY(监听本机所有网卡)。
坑2:云服务器安全组(最重要!)
Ubuntu本机ufw防火墙inactive(关闭)≠网络通
云服务器有两层防火墙:
- 操作系统防火墙:ufw(本机,可以关闭)
- 云厂商安全组/轻量云防火墙 :在虚拟机外面!默认全部入站流量拦截
👉 想外网访问UDP 8080:需要在云控制台安全组放行UDP协议 8080端口入站
课堂网课场景解释:老师和学生都用云服务器,同一VPC内网互通 ,安全组只放行内网网段。
如果两台云服务器属于不同VPC,内网IP(10.0.0.x 和172.18.x)不能直接内网通信!只能走公网IP通信。
坑3:抓包排查工具 tcpdump
# 在服务端执行,监听eth0网卡,抓取udp 8080数据包
sudo tcpdump -i eth0 udp port 8080
- 抓包没有任何输出:数据包根本没抵达服务器,大概率安全组拦截。
- 抓包能看到数据包:网络层面没问题,代码逻辑问题。
坑4:内网网段判断
例如服务端内网:10.0.0.4/22,客户端内网172.18.17.240
网段前缀不一样,不属于同一个内网VPC,内网无法直连,只能用公网IP通信。
坑5:IP区分
hostname -i:查看本机网卡IP- 127.0.0.1:回环,本机自测优先用
7. 回调函数(UDP框架拓展)
在C++封装UDP服务的时候,经常用到回调。
using func_t = std::function<std::string(const std::string&)>;
func_t:定义回调函数类型,接收string,返回string。- 不是我们手动调用回调!当服务端收到数据,UDP框架内部自动执行这个回调。
- 作用:解耦,网络接收逻辑 和 业务处理逻辑分开。网络模块只管收包,业务逻辑交给回调。
8. 测试顺序(推荐一步步测,不要直接公网)
- 回环测试:同一台服务器,
./udpserver 8080,./udpclient 127.0.0.1 8080
✅ 能收发 → 代码没问题。不能收发 → 代码bug。 - 同机器没问题,再内网测试(同VPC)
- 最后公网测试,开放安全组UDP端口
9. 总结
- UDP无连接,
recvfrom + sendto;客户端一般不用bind,操作系统隐式分配端口。 - bind是绑定本地端口,服务端监听端口用;不要硬绑定云服务器公网IP,用INADDR_ANY。
- 云服务器有安全组,这是初学者Socket最容易踩的大坑,本机防火墙关闭不等于网络通。
- 排错优先用
tcpdump抓包,判断数据包有没有抵达服务器。 - 阻塞是recvfrom等数据的行为,和缓冲区不是一回事。