Linux网络编程:Socket UDP

文章目录

  • [Linux Socket UDP 初学者实战踩坑笔记(C++)](#Linux Socket UDP 初学者实战踩坑笔记(C++))
    • 前言
    • [1. 套接字(socket)到底是什么?](#1. 套接字(socket)到底是什么?)
    • [2. UDP核心API介绍](#2. UDP核心API介绍)
      • [bind 重点(初学者最容易懵)](#bind 重点(初学者最容易懵))
      • recvfrom
      • [地址结构体 `sockaddr_in` & `in_addr sin_addr`](#地址结构体 sockaddr_in & in_addr sin_addr)
      • [回环地址 127.0.0.1](#回环地址 127.0.0.1)
    • [3. 完整UDP 服务端代码(C++)](#3. 完整UDP 服务端代码(C++))
    • [4. 完整UDP客户端代码(C++)](#4. 完整UDP客户端代码(C++))
    • [5. 阻塞到底是什么?(重点,很多人混淆缓冲区)](#5. 阻塞到底是什么?(重点,很多人混淆缓冲区))
    • [6. 云服务器大坑!我们实操踩坑合集【本文核心】](#6. 云服务器大坑!我们实操踩坑合集【本文核心】)
    • [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个函数:

  1. socket():创建套接字,拿到 sockfd(文件描述符)
  2. bind()绑定本地IP+端口(服务端必用,客户端一般不用)
  3. recvfrom():UDP接收函数,可以拿到发送方IP+端口(UDP专属)
  4. 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.4127.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(关闭)≠网络通

云服务器有两层防火墙:

  1. 操作系统防火墙:ufw(本机,可以关闭)
  2. 云厂商安全组/轻量云防火墙 :在虚拟机外面!默认全部入站流量拦截
    👉 想外网访问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. 测试顺序(推荐一步步测,不要直接公网)

  1. 回环测试:同一台服务器,./udpserver 8080./udpclient 127.0.0.1 8080
    ✅ 能收发 → 代码没问题。不能收发 → 代码bug。
  2. 同机器没问题,再内网测试(同VPC)
  3. 最后公网测试,开放安全组UDP端口

9. 总结

  1. UDP无连接,recvfrom + sendto;客户端一般不用bind,操作系统隐式分配端口。
  2. bind是绑定本地端口,服务端监听端口用;不要硬绑定云服务器公网IP,用INADDR_ANY。
  3. 云服务器有安全组,这是初学者Socket最容易踩的大坑,本机防火墙关闭不等于网络通。
  4. 排错优先用tcpdump抓包,判断数据包有没有抵达服务器。
  5. 阻塞是recvfrom等数据的行为,和缓冲区不是一回事。
相关推荐
Quanqiucard1 小时前
监控用物联网卡如何优化管理,降低安防项目通信成本
网络·物联网
爱吃香菜的初学者1 小时前
九.Linux——文件操作
linux
园长的牧歌1 小时前
Ubuntu 打开应用在导航栏没有图标是个齿轮
linux·ubuntu
曦夜日长2 小时前
Linux系统篇,进程概念(四):内核链表的深度理解、进程优先级的底层理解
linux·运维·服务器·php
‎ദ്ദിᵔ.˛.ᵔ₎2 小时前
Linux 库制作与原理
linux
Wang's Blog2 小时前
Java 接入Redis: Redis下载与源码编译安装
java·服务器·redis
liulilittle2 小时前
Linux AI 开发环境搭建实录
linux·ai·llm·agent·hyper-v·dev·opencode
Ivanqhz2 小时前
矩阵引擎的数据流模式与 BM1684X 架构
java·服务器·网络·深度学习·神经网络
.YM.Z2 小时前
C++——【继承】的底层逻辑与【多态】实现机制
c++·多态·继承