【Linux】UDP Socket编程实战(一):Echo Server从零到一

文章目录

    • [UDP Socket编程实战(一):Echo Server从零到一](#UDP Socket编程实战(一):Echo Server从零到一)
    • 一、项目结构与辅助模块
      • [1.1 代码结构](#1.1 代码结构)
      • [1.2 nocopy.hpp:禁止拷贝](#1.2 nocopy.hpp:禁止拷贝)
      • [1.3 Comm.hpp:公共枚举](#1.3 Comm.hpp:公共枚举)
      • [1.4 InetAddr.hpp:地址封装](#1.4 InetAddr.hpp:地址封装)
    • 二、服务器代码拆解
      • [2.1 看UdpServer之前先理思路](#2.1 看UdpServer之前先理思路)
      • [2.2 类定义和成员变量](#2.2 类定义和成员变量)
      • [2.3 Init():创建socket和绑定端口](#2.3 Init():创建socket和绑定端口)
        • [2.3.1 socket() 的三个参数](#2.3.1 socket() 的三个参数)
        • [2.3.2 sockaddr_in 的填充](#2.3.2 sockaddr_in 的填充)
        • [2.3.3 bind() 的强制转换](#2.3.3 bind() 的强制转换)
      • [2.4 Start():收消息和回显](#2.4 Start():收消息和回显)
        • [2.4.1 recvfrom 逐参数解析](#2.4.1 recvfrom 逐参数解析)
        • [2.4.2 sendto 逐参数解析](#2.4.2 sendto 逐参数解析)
        • [2.4.3 buffer[n] = 0 是干嘛的](#2.4.3 buffer[n] = 0 是干嘛的)
    • 三、客户端代码拆解
      • [3.1 客户端的整体流程](#3.1 客户端的整体流程)
      • [3.2 启动和参数解析](#3.2 启动和参数解析)
      • [3.3 创建socket和填充服务器地址](#3.3 创建socket和填充服务器地址)
      • [3.4 发送和接收循环](#3.4 发送和接收循环)
    • 四、客户端为什么不用显式bind
      • [4.1 这个问题很多新手都会问](#4.1 这个问题很多新手都会问)
      • [4.2 为什么要让OS自动分配](#4.2 为什么要让OS自动分配)
      • [4.3 自动bind发生在哪里](#4.3 自动bind发生在哪里)
    • 五、地址转换函数详解
      • [5.1 为什么需要地址转换](#5.1 为什么需要地址转换)
      • [5.2 字符串 → in_addr](#5.2 字符串 → in_addr)
        • [5.2.1 inet_addr(简单版)](#5.2.1 inet_addr(简单版))
        • [5.2.2 inet_pton(严格版)](#5.2.2 inet_pton(严格版))
      • [5.3 in_addr → 字符串](#5.3 in_addr → 字符串)
        • [5.3.1 inet_ntoa(简单版,但有坑)](#5.3.1 inet_ntoa(简单版,但有坑))
        • [5.3.2 inet_ntop(线程安全版)](#5.3.2 inet_ntop(线程安全版))
      • [5.4 四个函数对比总结](#5.4 四个函数对比总结)
    • [六、INADDR_ANY 专题](#六、INADDR_ANY 专题)
      • [6.1 为什么服务器要用 INADDR_ANY](#6.1 为什么服务器要用 INADDR_ANY)
    • 七、本篇总结
      • [7.1 核心要点](#7.1 核心要点)
      • [7.2 容易混淆的点](#7.2 容易混淆的点)

UDP Socket编程实战(一):Echo Server从零到一

💬 开篇:前三篇讲清楚了协议分层、数据传输流程和Socket的基础概念。从这篇开始,我们正式动手写代码。第一个项目是Echo Server------客户端发什么,服务器就原原本本地回来什么。听起来很简单,但这个过程中会涉及socket创建、地址绑定、数据收发、地址转换等核心操作,把这些写得熟练了,后面所有UDP项目都是在此基础上扩展。

👍 点赞、收藏与分享:这篇会逐行拆解Echo Server和Client的代码,还会讲清楚地址转换函数的细节。如果对你有帮助,请点赞收藏!

🚀 循序渐进:从代码结构到逐行分析,从服务器到客户端,从常规用法到易错点,一步步把UDP编程的基础打稳。


一、项目结构与辅助模块

1.1 代码结构

正式写Echo Server之前,先把项目的文件结构理清楚。这个项目用了几个辅助头文件,理解它们的作用,后面看主代码就不会迷。

项目文件列表:

bash 复制代码
.
├── nocopy.hpp      // 禁止拷贝的基类
├── Log.hpp         // 日志模块(已有,不重复)
├── Comm.hpp        // 公共枚举和常量
├── InetAddr.hpp    // 地址封装类
├── UdpServer.hpp   // UDP服务器核心
└── UdpClient.cpp   // UDP客户端(main在这里)

1.2 nocopy.hpp:禁止拷贝

cpp 复制代码
#pragma once
#include <iostream>

class nocopy
{
public:
    nocopy(){}
    nocopy(const nocopy &) = delete;              // 删除拷贝构造
    const nocopy& operator = (const nocopy &) = delete;  // 删除拷贝赋值
    ~nocopy(){}
};

这个类的唯一目的就是禁止拷贝。UdpServer会继承它,这样如果你不小心写了 UdpServer s2 = s1;,编译器直接报错。

为什么要禁止拷贝?服务器对象内部持有一个 _sockfd(文件描述符),如果被拷贝,两个对象会共享同一个fd,关闭一个的时候另一个就崩了。这是网络编程中非常常见的一个坑,用nocopy预防。

1.3 Comm.hpp:公共枚举

cpp 复制代码
#pragma once

enum{
    Usage_Err = 1,
    Socket_Err,
    Bind_Err
};

用枚举定义退出码。socket失败退出码是2,bind失败退出码是3。这样看进程退出码就能马上知道哪一步出问题了,比全都返回-1要方便排查。

1.4 InetAddr.hpp:地址封装

cpp 复制代码
#pragma once
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

class InetAddr
{
public:
    InetAddr(struct sockaddr_in &addr):_addr(addr)
    {
        _port = ntohs(_addr.sin_port);                // 网络序 → 主机序
        _ip = inet_ntoa(_addr.sin_addr);              // in_addr → 点分十进制字符串
    }

    std::string Ip() {return _ip;}
    uint16_t Port() {return _port;};

    std::string PrintDebug()
    {
        std::string info = _ip;
        info += ":";
        info += std::to_string(_port);    // 拼接成 "127.0.0.1:4444" 的格式
        return info;
    }

    ~InetAddr(){}

private:
    std::string _ip;
    uint16_t _port;
    struct sockaddr_in _addr;
};

这个类做的事很简单:把 sockaddr_in 转成人类可以看懂的格式。构造函数里就完成了转换,后面只需要调 Ip()Port() 或者 PrintDebug() 就行。

注意构造函数里的两个转换:

  • ntohs:把端口号从网络字节序转回主机序,这样打印出来的才是正确的端口号
  • inet_ntoa:把 in_addr 结构体转成点分十进制字符串,比如 "192.168.1.100"

关于 inet_ntoa 的线程安全问题,后面在地址转换函数的章节会专门讲。


二、服务器代码拆解

2.1 看UdpServer之前先理思路

UDP服务器的工作流程很直线:创建socket → 绑定端口 → 进入循环收消息 → 把消息原封不动发回去。没有连接、没有握手,就是这么简单。但每一步里的细节都值得仔细看一遍。

2.2 类定义和成员变量

cpp 复制代码
const static uint16_t defaultport = 8888;
const static int defaultfd = -1;
const static int defaultsize = 1024;

class UdpServer : public nocopy   // 继承nocopy,禁止拷贝
{
private:
    uint16_t _port;     // 监听的端口号
    int _sockfd;        // socket文件描述符,-1表示未创建
};

_sockfd 初始化为 -1,这是一个约定:-1表示"还没创建"。后面每次用之前都可以先判断是否为-1来知道状态。

2.3 Init():创建socket和绑定端口

cpp 复制代码
void Init()
{
    // 第一步:创建socket
    _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (_sockfd < 0)
    {
        lg.LogMessage(Fatal, "socket errr, %d : %s\n", errno, strerror(errno));
        exit(Socket_Err);
    }
    lg.LogMessage(Info, "socket success, sockfd: %d\n", _sockfd);

    // 第二步:绑定端口
    struct sockaddr_in local;
    bzero(&local, sizeof(local));           // 清零,养成习惯
    local.sin_family = AF_INET;             // IPv4
    local.sin_port = htons(_port);          // 端口号转网络序
    local.sin_addr.s_addr = INADDR_ANY;     // 监听所有网卡

    int n = ::bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
    if (n != 0)
    {
        lg.LogMessage(Fatal, "bind errr, %d : %s\n", errno, strerror(errno));
        exit(Bind_Err);
    }
}

逐行看:

2.3.1 socket() 的三个参数
cpp 复制代码
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
  • AF_INET:IPv4地址族
  • SOCK_DGRAM:数据报类型,对应UDP。如果是TCP就写 SOCK_STREAM
  • 0:让系统根据前两个参数自动选择协议,对于 AF_INET + SOCK_DGRAM,系统会自动选UDP

返回值是一个文件描述符(fd),本质上就是一个整数。操作系统用这个fd来标识这个网络连接,后面所有收发操作都围绕着这个fd展开。

2.3.2 sockaddr_in 的填充
cpp 复制代码
struct sockaddr_in local;
bzero(&local, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = INADDR_ANY;

这里有几个要注意的点:

bzero 为什么要清零? sockaddr_in 有一个8字节的填充区域 sin_zero,如果不清零,里面是随机数据。虽然正常情况下不影响功能,但养成清零的习惯可以避免一些奇怪的问题。

htons 是干嘛的? 把端口号从主机字节序转为网络字节序。前篇讲过,网络传输统一用大端。如果你的机器是小端(x86),不转的话端口号就错了。

INADDR_ANY 是什么意思? 值是0,表示监听所有网卡上的所有IP地址。如果服务器有多个网卡(比如既有内网又有外网),用 INADDR_ANY 就不用去纠结要监听哪个IP,所有的都收。

2.3.3 bind() 的强制转换
cpp 复制代码
int n = ::bind(_sockfd, (struct sockaddr *)&local, sizeof(local));

bind 的第二个参数类型是 struct sockaddr *(通用地址),但我们填的是 sockaddr_in(IPv4地址)。所以要强制转换。这个转换在前篇解释过,是C语言实现多态的方式。

注意前面的 ::bind,加了全局作用域限定符。这是为了明确调用系统调用的 bind,而不是类自身可能有的同名函数。养成这个习惯,避免名字冲突。

2.4 Start():收消息和回显

cpp 复制代码
void Start()
{
    char buffer[defaultsize];           // 接收缓冲区,1024字节
    for (;;)                            // 永远循环,服务器不退出
    {
        struct sockaddr_in peer;        // 存储发送方的地址
        socklen_t len = sizeof(peer);   // 地址长度

        // 接收数据
        ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer) - 1,
                             0, (struct sockaddr *)&peer, &len);
        if (n > 0)
        {
            InetAddr addr(peer);        // 把地址封装起来
            buffer[n] = 0;              // 手动加'\0',变成C字符串
            std::cout << "[" << addr.PrintDebug() << "]# " << buffer << std::endl;

            // 把数据原封不动发回去
            sendto(_sockfd, buffer, strlen(buffer), 0,
                   (struct sockaddr *)&peer, len);
        }
    }
}
2.4.1 recvfrom 逐参数解析
cpp 复制代码
ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer) - 1,
                     0, (struct sockaddr *)&peer, &len);
参数 含义
_sockfd 从哪个socket收
buffer 收进哪个缓冲区
sizeof(buffer) - 1 最多收多少字节。留1个字节给'\0'
0 flags,一般写0
&peer 收到数据后,把发送方的地址存到这里
&len 传入时是地址结构体的大小,返回时是实际填充的大小

recvfrom 最重要的特点就是会把发送方的地址记录下来。这正好是UDP Echo Server需要的:收到消息,知道是谁发的,然后回复给他。

len 为什么不能乱写? 如果你写的值比 sizeof(sockaddr_in) 小,内核写地址的时候会截断或者报错。写 sizeof(peer) 就对了,别图省事。

2.4.2 sendto 逐参数解析
cpp 复制代码
sendto(_sockfd, buffer, strlen(buffer), 0,
       (struct sockaddr *)&peer, len);
参数 含义
_sockfd 用哪个socket发
buffer 发什么数据
strlen(buffer) 发多少字节
0 flags,一般写0
&peer 发给谁(之前recvfrom记录的地址)
len 目的地址的长度

注意这里用的 strlen(buffer) 而不是 sizeof(buffer)。因为 sizeof 是缓冲区的大小(1024),而实际数据长度是 n(或者说是 strlen 的结果)。发的时候只发实际有内容的部分。

2.4.3 buffer[n] = 0 是干嘛的

recvfrom 不会自动在收到的数据后面加'\0'。如果你想把 buffer 当C字符串用(比如打印、传给 strlen),必须手动加一个'\0'。这是C语言处理字符串时的经典细节,别忘了。


三、客户端代码拆解

3.1 客户端的整体流程

客户端比服务器简单一点:创建socket → 填充服务器地址 → 循环发消息、收回复。注意客户端不需要 bind,这个问题后面单独讲。

3.2 启动和参数解析

cpp 复制代码
// 用法:./udp_client server_ip server_port
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        return 1;
    }
    std::string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]);
    // ...
}

客户端从命令行参数里拿到服务器的IP和端口。这样就不用硬编码,方便测试不同环境。

3.3 创建socket和填充服务器地址

cpp 复制代码
// 1. 创建socket
int sock = socket(AF_INET, SOCK_DGRAM, 0);
if (sock < 0)
{
    std::cerr << "socket error: " << strerror(errno) << std::endl;
    return 2;
}

// 2. 填充服务器地址信息
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(serverport);
server.sin_addr.s_addr = inet_addr(serverip.c_str());

和服务器一样,socket创建完全相同。区别在于后面填的不是本地地址,而是服务器的地址。这个地址结构体会一直用到发送数据的时候。

inet_addr 把点分十进制字符串转成 in_addr 结构体,同时也完成了字节序转换。关于它的详细用法,后面地址转换函数的部分会讲。

3.4 发送和接收循环

cpp 复制代码
while (true)
{
    std::string inbuffer;
    std::cout << "Please Enter# ";
    std::getline(std::cin, inbuffer);       // 读取用户输入

    // 发送给服务器
    ssize_t n = sendto(sock, inbuffer.c_str(), inbuffer.size(),
                       0, (struct sockaddr*)&server, sizeof(server));
    if(n > 0)
    {
        char buffer[1024];
        struct sockaddr_in temp;
        socklen_t len = sizeof(temp);

        // 收服务器的回复
        ssize_t m = recvfrom(sock, buffer, sizeof(buffer)-1,
                             0, (struct sockaddr*)&temp, &len);
        if(m > 0)
        {
            buffer[m] = 0;
            std::cout << "server echo# " << buffer << std::endl;
        }
        else break;
    }
    else break;
}
close(sock);

流程很清晰:输入 → 发送 → 等待回复 → 打印回复 → 下一轮。这里用了 getline 而不是 cin >> 读输入,好处是能处理带空格的字符串。

注意 recvfrom 的第五个参数传了 &temp 而不是 &server。因为这里只是收数据,不需要用到发送方地址,所以用一个临时变量就行。


四、客户端为什么不用显式bind

4.1 这个问题很多新手都会问

服务器需要 bind 端口,这能理解------客户端要知道服务器在哪里。但客户端自己的端口呢?服务器回复的时候也需要知道客户端的地址和端口,那客户端的端口是怎么确定的?

答案是:客户端在第一次调用 sendto 的时候,操作系统会自动为它分配一个端口并绑定

4.2 为什么要让OS自动分配

两个原因:

端口号要唯一。 同一台机器上可能同时跑几十个客户端程序,如果每个都自己写死一个端口号,很容易冲突。让OS从动态端口范围(49152-65535)里随机分配,就不会冲突。

客户端数量可以非常多。 服务器端口必须固定,因为所有客户端都要知道去哪里连接。但客户端的端口只有服务器需要知道(用来回复),它用哪个端口客户端自己不在乎。所以让OS帮忙分配就行。

4.3 自动bind发生在哪里

具体来说,当客户端调用 sendto 的时候:

  1. 内核看到这个socket还没有绑定本地地址
  2. 自动从动态端口池里选一个空闲端口,把这个端口绑定到socket上
  3. 然后才真正发送数据

从这一刻开始,服务器收到数据后,recvfrom 记录的就是客户端自动绑定的那个端口,回复的时候就用这个端口。

记住:客户端一定会bind,只是不需要你显式写出来。


五、地址转换函数详解

5.1 为什么需要地址转换

人看的IP地址是字符串:"192.168.1.100"。但内核处理的是32位整数(in_addr 结构体)。网络传输用的是网络字节序(大端)。三种表示之间需要来回转换,这就是地址转换函数的作用。

5.2 字符串 → in_addr

有两个函数可以做这件事:

5.2.1 inet_addr(简单版)
cpp 复制代码
in_addr_t inet_addr(const char *cp);

输入点分十进制字符串,直接返回网络字节序的32位整数。使用很简单:

cpp 复制代码
server.sin_addr.s_addr = inet_addr("192.168.1.100");

一步到位,字符串转换和字节序转换都帮你做了。但它有个问题:错误时返回 INADDR_NONE(也就是0xFFFFFFFF),而 255.255.255.255 的合法值也是这个数,分不清楚是错误还是合法地址。一般场景不受影响,但严格的代码应该用下面这个。

5.2.2 inet_pton(严格版)
cpp 复制代码
int inet_pton(int af, const char *src, void *dst);
  • af:地址族,AF_INET(IPv4)或 AF_INET6(IPv6)
  • src:输入字符串
  • dst:输出的 in_addr 结构体指针
  • 返回值:成功返回1,失败返回0或-1
cpp 复制代码
struct in_addr addr;
int ret = inet_pton(AF_INET, "192.168.1.100", &addr);
if (ret != 1) {
    // 转换失败,处理错误
}
server.sin_addr = addr;

inet_pton 的优势是:支持IPv4和IPv6,返回值能明确区分成功和失败。多线程环境推荐用这个。

5.3 in_addr → 字符串

5.3.1 inet_ntoa(简单版,但有坑)
cpp 复制代码
char *inet_ntoa(struct in_addr in);

in_addr 转成点分十进制字符串,返回一个 char*

cpp 复制代码
char *ip = inet_ntoa(peer.sin_addr);   // "192.168.1.100"

看起来很方便,但这里有个隐蔽的问题:返回的 char* 指向函数内部的静态存储区。意味着:

  • 不需要你手动 free
  • 但如果你调用两次 inet_ntoa,第二次的结果会覆盖第一次
cpp 复制代码
char *ip1 = inet_ntoa(addr1.sin_addr);  // "192.168.1.1"
char *ip2 = inet_ntoa(addr2.sin_addr);  // "10.0.0.1"
printf("%s\n", ip1);  // 打印的是 "10.0.0.1"!!ip1被覆盖了

这是一个经典的坑。如果要保留结果,必须马上复制出来:

cpp 复制代码
char *ip = inet_ntoa(peer.sin_addr);
std::string saved_ip(ip);   // 立刻复制,后面用 saved_ip

多线程环境下千万不要用 inet_ntoa 多个线程同时调用,结果会互相覆盖,数据乱得不行。

5.3.2 inet_ntop(线程安全版)
cpp 复制代码
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
  • af:地址族
  • src:输入的 in_addr 结构体指针
  • dst:你自己提供的输出缓冲区
  • size:缓冲区大小
cpp 复制代码
char ip[INET_ADDRSTRLEN];   // INET_ADDRSTRLEN = 16,IPv4地址字符串的最大长度
inet_ntop(AF_INET, &peer.sin_addr, ip, sizeof(ip));

结果写到你自己的缓冲区里,不存在静态存储区覆盖的问题。多线程环境优先选这个。

5.4 四个函数对比总结

函数 方向 支持IPv6 线程安全 推荐场景
inet_addr 字符串 → in_addr 简单场景,单线程
inet_pton 字符串 → in_addr 严格场景,多线程
inet_ntoa in_addr → 字符串 简单场景,单线程
inet_ntop in_addr → 字符串 严格场景,多线程

记住一个规律:ptonntop 是新版本,支持IPv6和线程安全;addrntoa 是旧版本,简单但有限制。 如果对场景没特别要求,用新版本养成习惯。


六、INADDR_ANY 专题

6.1 为什么服务器要用 INADDR_ANY

很多新手第一次写服务器,会把bind的IP写成自己机器的IP地址,比如 "192.168.1.100"。这样确实能用,但在生产环境有问题。

服务器通常有多个网卡:内网卡、外网卡、回环网卡(127.0.0.1)。如果你只bind一个IP,那么从其他网卡进来的请求就收不到。

INADDR_ANY 的值是0,意思是"监听所有网卡上的所有IP"。用它bind之后,无论客户端从哪个网卡连过来,服务器都能收到。

cpp 复制代码
local.sin_addr.s_addr = INADDR_ANY;   // 推荐
// local.sin_addr.s_addr = inet_addr("192.168.1.100");  // 不推荐

还有一个实际问题:云服务器通常不允许你直接bind公有IP 。因为云平台的公有IP经过了NAT映射,你bind的应该是内网IP或者 INADDR_ANY。所以养成用 INADDR_ANY 的习惯,省得在本地能用但放到云上就不行。


七、本篇总结

7.1 核心要点

服务器流程

  • socket() 创建fd → bind() 绑定端口 → recvfrom() 收数据 → sendto() 回复
  • INADDR_ANY 监听所有网卡,这是生产环境的标准写法
  • 服务器永远循环,不主动退出

客户端流程

  • socket() 创建fd → 填充服务器地址 → sendto() 发数据 → recvfrom() 收回复
  • 客户端不需要显式bind,第一次sendto时OS自动分配端口
  • 客户端的端口是动态分配的,每次启动可能不同

地址转换

  • inet_addr / inet_pton:字符串 → in_addr
  • inet_ntoa / inet_ntop:in_addr → 字符串
  • 多线程环境用 inet_ptoninet_ntop
  • inet_ntoa 的静态存储区坑:结果要马上复制,不能保存指针

辅助类

  • nocopy 禁止拷贝,防止fd被复制导致双重关闭
  • InetAddr 封装地址转换,打印调试用

7.2 容易混淆的点

  1. sizeof(buffer) - 1 而不是 sizeof(buffer):留一个字节给'\0'。如果不留,收满的时候加'\0'会越界。

  2. len 必须初始化为 sizeof(peer)recvfrom 用它来知道你给了多大的地址缓冲区。如果写错了,轻则报错,重则写越界。

  3. sendto 用的是 strlen(buffer) 不是 sizeof(buffer):只发实际数据的长度,不是整个缓冲区。

  4. 客户端的 bind 是隐式的:不是没有bind,是OS帮你做了。服务器回复的时候用的就是这个自动绑定的端口。

  5. inet_ntoa 的坑是静态存储区,不是内存泄漏:不需要free,但结果会被下一次调用覆盖。多线程更危险。

  6. ::bind 前面的 :::全局作用域限定符,确保调用的是系统的bind函数,而不是可能同名的类方法。


💬 总结:这一篇把Echo Server的每一行代码都拆解清楚了。从socket创建到数据收发,从地址填充到转换函数,这些是UDP编程的基础操作。下一篇我们会在此基础上引入回调机制,把Echo Server改成一个网络字典服务器,同时讲清楚封装版UdpSocket的设计思路。
👍 点赞、收藏与分享:如果这篇帮你理清了UDP编程的基本操作,请点赞收藏!下一篇会有更多实战代码,敬请期待!

相关推荐
HellowAmy1 小时前
我的C++规范 - 线程池
开发语言·c++·代码规范
czy87874751 小时前
const 在 C/C++ 中的全面用法(C/C++ 差异+核心场景+实战示例)
c语言·开发语言·c++
十五年专注C++开发1 小时前
MinHook:Windows 平台下轻量级、高性能的钩子库
c++·windows·钩子技术·minhook
嵌入小生0071 小时前
Shell | 命令、编程及Linux操作系统的基本概念
linux·运维·服务器
咖丨喱2 小时前
IP校验和算法解析与实现
网络·tcp/ip·算法
那就回到过去2 小时前
交换机特性
网络·hcip·ensp·交换机
一只小小的芙厨2 小时前
寒假集训笔记·树上背包
c++·笔记·算法·动态规划
以卿a3 小时前
C++(继承)
开发语言·c++·算法
-Try hard-3 小时前
Linuv软件编程 | Shell命令
linux·运维·服务器