Linux之Socket 编程 UDP

一、UDP 网络编程

1.1、V1 版本 - echo server

**功能:**简单的回显服务器和客户端代码

**注意:**云服务器不允许直接 bind 公有 IP,我们也不推荐编写服务器的时候,bind 明确的 IP,推荐直接写成 INADDR_ANY。

C++

/* Address to accept any incoming messages. */

#define INADDR_ANY ((in_addr_t) 0x00000000)

在网络编程中,当一个进程需要绑定一个网络端口以进行通信时,可以使用INADDR_ANY 作为 IP 地址参数。这样做意味着该端口可以接受来自任何 IP 地址的连 接请求,无论是本地主机还是远程主机。例如,如果服务器有多个网卡(每个网卡上 有不同的 IP 地址),使用 INADDR_ANY 可以省去确定数据是从服务器上具体哪个网 卡/IP 地址上面获取的。

接口介绍:

// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)

int****socket(int domain, int type, int protocol);

  • 参数:
    • domain(协议族/地址族),指定通信的协议族,常见选项:
      • AF_INET → IPv4 网络协议
      • AF_INET6 → IPv6 网络协议
      • AF_UNIX 或 AF_LOCAL → 本地进程间通信(Unix Domain Socket)
    • type(套接字类型),指定数据传输方式:
      • SOCK_STREAM → 面向连接的可靠传输(如TCP,保证数据顺序和可靠性)
      • SOCK_DGRAM → 无连接的不可靠传输(如UDP,速度快但不保证顺序和到达)
      • SOCK_RAW → 原始套接字(直接访问底层协议,如自定义 IP 报文)
    • protocol(具体协议)通常设为 0,表示根据 domain 和 type 自动选择默认协议(如 SOCK_STREAM 默认用 TCP,SOCK_DGRAM 默认用 UDP)。也可显式指定:
      • IPPROTO_TCP → TCP
      • IPPROTO_UDP → UDP
  • 返回值:
    • 成功:返回一个 套接字描述符(非负整数,类似文件描述符)。
    • 失败:返回 -1,并设置 errno(如 EACCES 权限不足、EINVAL 参数无效等)。

// 绑定端口号 (TCP/UDP, 服务器)

int bind(int socket, const struct sockaddr *address, socklen_t address_len);

  • 参数:
    • socket:套接字描述符(由 socket() 创建,即socket方法成功后的返回值)
    • address:指向要绑定的地址结构体(IPv4 用 struct sockaddr_in,IPv6 用 struct sockaddr_in6)
    • address_len:地址结构体的长度(如 sizeof(struct sockaddr_in))
  • 返回值:
    • 成功:返回 0。
    • 失败:返回 -1,并设置 errno。

#include<sys/types.h>

#include<sys/socket.h>

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);

  • 参数:
    • sockfd:套接字描述符(需为 SOCK_DGRAM 或 SOCK_RAW 类型)
    • buf:接收数据的缓冲区指针
    • len:缓冲区的最大容量(字节数)
    • flags:控制接收行为的标志(如 MSG_WAITALL、MSG_DONTWAIT,通常设为 0)
    • src_addr:可选参数,用于存储发送方的地址信息
    • addrlen:输入时指定 src_addr 缓冲区长度,输出时返回实际地址长度
  • 返回值:
    • 成功:返回接收到的字节数(可能小于 len)。
    • 失败:返回 -1,并设置 errno。

ssize_t****sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);

  • 参数:
    • sockfd:套接字描述符(需为 SOCK_DGRAM 或 SOCK_RAW 类型)
    • buf:待发送数据的缓冲区指针
    • len:待发送数据的字节数
    • flags:控制发送行为的标志(如 MSG_DONTWAIT、MSG_NOSIGNAL,通常设为 0)
    • dest_addr:目标地址结构体(如 struct sockaddr_in)
    • addrlen:目标地址结构体的长度(如 sizeof(struct sockaddr_in))
  • 返回值:
    • 成功:返回实际发送的字节数(可能小于 len)。
    • 失败:返回 -1,并设置 errno。
      **#**include <arpa/inet.h>

uint32_t****htonl(uint32_t hostlong);

  • 功能:将32位无符号整数从主机字节序转换为网络字节序(大端序),一般用于IP

  • 参数:hostlong(主机字节序的32位值)

  • 返回值:网络字节序的32位值

uint16_t****htons(uint16_t hostshort);

  • 功能:将16位无符号整数从主机字节序转换为网络字节序(大端序),一般用于端口号

  • 参数:hostshort(主机字节序的16位值)

  • 返回值:网络字节序的16位值

uint32_t ntohl(uint32_t netlong);

  • 功能:将32位无符号整数从网络字节序转换回主机字节序,一般用于IP

  • 参数:netlong(网络字节序的32位值)

  • 返回值:主机字节序的32位值

uint16_t ntohs(uint16_t netshort);

  • 功能:将16位无整数从网络字节序转换回主机字节序,一般用于端口号

  • 参数:netshort(网络字节序的16位值)

  • 返回值:主机字节序的16位值

这些函数主要用于解决不同系统间(大端/小端架构)通过网络通信时的字节序兼容性问题。网络协议规定使用大端序作为标准字节序,因此在发送数据前需要用hton函数转换,接收数据后用ntoh函数转换回来。
#include<arpa/inet.h>

const****char*inet_ntop(int af, const void *src,char *dst, socklen_t size);

  • 功能:将二进制格式的网络地址,即IP(如 IPv4 或 IPv6 地址)转换成可读的字符串格式(点分十进制或十六进制表示)。
  • 参数:
    • af(Address Family):地址族,指定地址类型:
      • AF_INET(IPv4)
      • AF_INET6(IPv6)
    • src:指向二进制格式的 IP 地址(如 struct in_addr 或 struct in6_addr)。
    • dst:存储转换后字符串的缓冲区。该缓冲区由用户自己定义。
    • size:缓冲区 dst 的大小(防止溢出)。
  • 返回值:
    • 成功:返回 dst 的指针(即转换后的字符串)。
    • 失败:返回 NULL,并设置 errno。

#include<sys/socket.h>
**#**include <netinet/in.h>
#include<arpa/inet.h>

in_addr_t****inet_addr(const char *cp);

  • 功能:将点分十进制格式的 IPv4 地址字符串(如 "192.168.1.1")转换为 32 位网络字节序(大端序)的二进制值(in_addr_t 类型)。

  • 参数:cp:点分十进制格式的 IPv4 地址字符串(如 "192.168.1.1")。

  • 返回值:

    • 成功:返回 网络字节序的 32 位无符号整数(in_addr_t)。
    • 失败(如非法格式):返回 INADDR_NONE(通常是 0xFFFFFFFF)。

char*inet_ntoa(struct in_addr in);

  • 功能:将 32 位网络字节序的 IPv4 地址(struct in_addr)转换为 点分十进制字符串(如 "192.168.1.1")。
  • 参数:in:struct in_addr 结构体,包含一个网络字节序的 IPv4 地址。
  • 返回值:
    • 返回 静态分配的字符串指针(存储在静态缓冲区中)。
    • 非线程安全(多次调用可能会覆盖之前的结果)。

示例代码:(日志和锁使用线程同步与互斥博文中示例代码中封装的, 即Log.hpp, Mutex.hpp)

Common.hpp:

cpp 复制代码
#pragma once

#include <iostream>

#define Die(code) do{ \
    exit(code);       \
}while(0)             \

#define CONV(v) (struct sockaddr *)(v)

enum
{
    USAGE_ERR  = 1,
    SOCKET_ERR,
    BIND_ERR
};

InetAddr.hpp:

cpp 复制代码
#pragma once

#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Common.hpp"

class InetAddr
{
private:
    void PortNet2Host()
    {
        _port = ::ntohs(_net_addr.sin_port);
    }

    void IpNet2Host()
    {
        char ipbuffer[64];
        const char *ip = ::inet_ntop(AF_INET, &_net_addr.sin_addr, ipbuffer, sizeof(ipbuffer));
        (void)ip;
    }
public:
    InetAddr()
    {}

    InetAddr(const struct sockaddr_in &addr)
        : _net_addr(addr)
    {
        PortNet2Host();
        IpNet2Host();
    }

    InetAddr(uint16_t port)
        :_port(port),
         _ip("")
    {
        _net_addr.sin_family = AF_INET;
        _net_addr.sin_port = ::htons(_port);
        _net_addr.sin_addr.s_addr = INADDR_ANY;
    }

    struct sockaddr *NetAddr(){ return CONV(&_net_addr); }
    socklen_t NetAddrLen(){ return sizeof(_net_addr); }
    std::string Ip(){ return _ip; }
    uint16_t Port(){ return _port; }

    ~InetAddr()
    {}

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

UdpClientMain.cc:

cpp 复制代码
#include "Common.hpp"
#include <iostream>
#include <cstring>
#include <string>
#include <cstdlib>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

//CS模式
int main(int argc, char *argv[])
{
    if(argc != 3)
    {
        std::cerr << "Usage: " << argv[0] << "serverip serverport" << std::endl;
        Die(USAGE_ERR);
    }

    std::string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]);

    //创建socket
    int sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);
    if(sockfd < 0)
    {
        std::cerr << "socket error" << std::endl;
        Die(SOCKET_ERR);
    }

    //填充server信息
    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());

    //开始通信
    while(true)
    {
        std::cout << "Please Enter# ";
        std::string message;
        std::getline(std::cin, message);
        // client必须也要有自己的ip和端口!但是客户端,不需要自己显示的调用bind!!
        // 客户端首次sendto消息的时候,由OS自动进行bind
        // 1. 为什么client自动随机bind端口号? 一个端口号,只能被一个进程bind,防止用户自己绑定导致端口冲突,进而无法启动应用
        // 2. 为什么server要显示的bind?服务器的端口号,必须稳定!!必须是众所周知且不能改变轻易改变的! 一般显示绑定一个固定的

        int n = ::sendto(sockfd, message.c_str(), message.size(), 0, CONV(&server), sizeof(server));
        (void)n;

        struct sockaddr_in temp;
        socklen_t len = sizeof(temp);
        char buffer[1024];
        n = ::recvfrom(sockfd, buffer, sizeof(buffer), 0, CONV(&temp), &len);
        if(n > 0)
        {
            buffer[n] = 0;
            std::cout << buffer << std::endl;
        }
    }

    return 0;
} 

UdpServer.hpp:

cpp 复制代码
#ifndef __UDP_SERVER_HPP__
#define __UDP_SERVER_HPP__

#include <iostream>
#include <string>
#include <memory>
#include <cstring>
#include <cerrno>
#include <strings.h>

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include "InetAddr.hpp"
#include "Log.hpp"
#include "Common.hpp"

using namespace LogModule;

const static int gsockfd = -1;
// const static std::string gdefaultip = "127.0.0.1"; // 表示本地主机
const static uint16_t gdefaultport = 8080;

class UdpServer
{
public:
    UdpServer(uint16_t port = gdefaultport)
        :_sockfd(gsockfd),
         _addr(port),
         _isrunning(false)
    {}

    void InitServer()
    {
        //创建socket
        _sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);
        if(_sockfd < 0)
        {
            LOG(LogLevel::FATAL) << "socket: " << strerror(errno);
            Die(SOCKET_ERR);
        }
        LOG(LogLevel::INFO) << "socket success, sockfd is : "<< _sockfd;

        // 2. 填充网络信息,并bind绑定
        // 2.1 有没有把socket信息,设置进入内核中??没有,只是填充了结构体!
        // struct sockaddr_in local;
        // bzero(&local, sizeof(local));
        // local.sin_family = AF_INET;
        // local.sin_port = ::htons(_port);                  // 要被发送给对方的,即要发到网络中!
        // //local.sin_addr.s_addr = ::inet_addr(_ip.c_str()); // 1. string ip->4bytes 2. network order //TODO
        // local.sin_addr.s_addr = INADDR_ANY;

        //bind:设置进入内核中
        int n = ::bind(_sockfd, _addr.NetAddr(), _addr.NetAddrLen());
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "bind: " << strerror(errno);
            Die(BIND_ERR);
        }
        LOG(LogLevel::INFO) << "bind success";
    }

    void Start()
    {
        _isrunning = true;
        while (true)
        {
            char inbuffer[1024]; // string
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer); // 必须设定

            ssize_t n = ::recvfrom(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0, CONV(&peer), &len);
            if (n > 0)
            {
                // 1. 消息内容 && 2. 谁发给我的
                // uint16_t clientport = ::ntohs(peer.sin_port);
                // std::string clientip = ::inet_ntoa(peer.sin_addr);

                InetAddr cli(peer);
                inbuffer[n] = 0;

                std::string clientinfo = cli.Ip() + ":" + std::to_string(cli.Port()) + " # " + inbuffer;

                LOG(LogLevel::DEBUG) << clientinfo;

                std::string echo_string = "echo# ";
                echo_string += inbuffer;

                ::sendto(_sockfd, echo_string.c_str(), echo_string.size(), 0, CONV(&peer), sizeof(peer));
            }
        }
        _isrunning = false;
    }

    ~UdpServer()
    {
        if (_sockfd > gsockfd)
            ::close(_sockfd);
    }

private:
    int _sockfd;
    InetAddr _addr;

    // uint16_t _port;//服务器端口号
    // std::string _ip; //服务器对应IP

    bool _isrunning;
};

#endif

UdpServerMain.cc:

cpp 复制代码
#include "UdpServer.hpp"

// ./server_udp localport
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        std::cerr << "Usage: " << argv[0] << " localport" << std::endl;
        Die(USAGE_ERR);
    }
    // std::string ip = argv[1];
    uint16_t port = std::stoi(argv[1]);

    ENABLE_CONSOLE_LOG();

    std::unique_ptr<UdpServer> svr_uptr = std::make_unique<UdpServer>(port);
    svr_uptr->InitServer();
    svr_uptr->Start();

    return 0;
}

注意:

  • 云服务器不允许我们绑定公网IP。
  • 正常情况下,一个服务器可能会接受到不同IP的客户端的信息,但是服务器一旦绑定了某一个IP后就只能接受这一个IP发来的信息,所以正常情况下服务器的代码中不要绑定具体的IP地址。而是像示例代码一样,可以接受任意IP的信息。

1.2、V2 版本 - DictServer

**功能:**实现一个简单的英译汉的网络字典

示例代码:(日志和锁使用线程同步与互斥博文中示例代码中封装的, 即Log.hpp, Mutex.hpp,这里不在展示)

Common.hpp:

cpp 复制代码
#pragma once

#include <iostream>
#include <string>


#define Die(code)   \
    do              \
    {               \
        exit(code); \
    } while (0)

#define CONV(v) (struct sockaddr *)(v)

enum 
{
    USAGE_ERR = 1,
    SOCKET_ERR,
    BIND_ERR
};

// happy: 快乐的
bool SplitString(const std::string &line, std::string *key, std::string *value, const std::string &sep)
{
    auto pos = line.find(sep);
    if(pos == std::string::npos) return false;
    *key = line.substr(0, pos);
    *value = line.substr(pos+sep.size());
    if(key->empty() || value->empty()) return false;
    return true;
}

Dictionary.hpp:

cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <fstream>
#include <unordered_map>
#include "Log.hpp"
#include "Common.hpp"

const std::string gpath = "./";
const std::string gdictname = "dict.txt";
const std::string gsep = ": ";

using namespace LogModule;

class Dictionary
{
private:
    bool LoadDictionary()
    {
        std::string file = _path + _filename;
        std::ifstream in(file.c_str());
        if (!in.is_open())
        {
            LOG(LogLevel::ERROR) << "open file " << file << " error";
            return false;
        }

        std::string line;
        while (std::getline(in, line))
        {
            // happy: 快乐的
            std::string key;
            std::string value;
            if (SplitString(line, &key, &value, gsep))
            { // line -> key, value
                _dictionary.insert(std::make_pair(key, value));
            }
        }

        in.close();
        return true;
    }

public:
    Dictionary(const std::string &path = gpath, const std::string &filename = gdictname)
        : _path(path),
          _filename(filename)
    {
        LoadDictionary();
        Print();
    }
    std::string Translate(const std::string &word)
    {
        auto iter = _dictionary.find(word);
        if(iter == _dictionary.end()) return "None";
        return iter->second;
    }
    void Print()
    {
        for(auto &item : _dictionary)
        {
            std::cout << item.first << ":" << item.second<< std::endl;
        }
    }
    ~Dictionary()
    {
    }

private:
    std::unordered_map<std::string, std::string> _dictionary;
    std::string _path;
    std::string _filename;
};

dict.txt:

cpp 复制代码
apple: 苹果
banana: 香蕉
cat: 猫
dog: 狗
book: 书
pen: 笔
happy: 快乐的
sad: 悲伤的
run: 跑
jump: 跳
teacher: 老师
student: 学生
car: 汽车
bus: 公交车
love: 爱
hate: 恨
hello: 你好
goodbye: 再见
summer: 夏天
winter: 冬天

InetAddr.hpp:

cpp 复制代码
#pragma once

#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Common.hpp"

class InetAddr
{
private:
    void PortNet2Host()
    {
        _port = ::ntohs(_net_addr.sin_port);
    }
    void IpNet2Host()
    {
        char ipbuffer[64];
        const char *ip = ::inet_ntop(AF_INET, &_net_addr.sin_addr, ipbuffer, sizeof(ipbuffer));
        (void)ip;
    }

public:
    InetAddr()
    {
    }
    InetAddr(const struct sockaddr_in &addr) : _net_addr(addr)
    {
        PortNet2Host();
        IpNet2Host();
    }
    InetAddr(uint16_t port) : _port(port), _ip("")
    {
        _net_addr.sin_family = AF_INET;
        _net_addr.sin_port = htons(_port);
        _net_addr.sin_addr.s_addr = INADDR_ANY;
    }
    struct sockaddr *NetAddr() { return CONV(&_net_addr); }
    socklen_t NetAddrLen() { return sizeof(_net_addr); }
    std::string Ip() { return _ip; }
    uint16_t Port() { return _port; }
    ~InetAddr()
    {
    }

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

UdpClientMain.cc:

cpp 复制代码
#include "Common.hpp"
#include <iostream>
#include <cstring>
#include <string>
#include <cstdlib>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>


// CS
// ./client_udp serverip serverport
int main(int argc, char *argv[])
{
    if(argc != 3)
    {
        std::cerr << "Usage: " << argv[0] << " serverip serverport" << std::endl;
        Die(USAGE_ERR);
    }
    std::string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]);

    // 1. 创建socket
    int sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);
    if(sockfd < 0)
    {
        std::cerr << "socket error" << std::endl;
        Die(SOCKET_ERR);
    }

    // 1.1 填充server信息
    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());

    // 2. clientdone
    while(true)
    {
        std::cout << "Please Enter# ";
        std::string message;
        std::getline(std::cin, message);

        int n = ::sendto(sockfd, message.c_str(),message.size(), 0, CONV(&server), sizeof(server));
        (void)n;

        struct sockaddr_in temp;
        socklen_t len = sizeof(temp);
        char buffer[1024];
        n = ::recvfrom(sockfd, buffer, sizeof(buffer)-1, 0, CONV(&temp), &len);
        if(n > 0)
        {
            buffer[n] = 0;
            std::cout << buffer << std::endl;
        }
    }

    return 0;
}

UdpServer.hpp:

cpp 复制代码
#ifndef __UDP_SERVER_HPP__
#define __UDP_SERVER_HPP__

#include <iostream>
#include <string>
#include <memory>
#include <functional>
#include <cstring>
#include <cerrno>
#include <strings.h>

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include "InetAddr.hpp"
#include "Log.hpp"
#include "Common.hpp"

using namespace LogModule;

const static int gsockfd = -1;
// const static std::string gdefaultip = "127.0.0.1"; // 表示本地主机
const static uint16_t gdefaultport = 8080;

using func_t = std::function<std::string(const std::string&)>;

class UdpServer
{
public:
    UdpServer(func_t func, uint16_t port = gdefaultport)
        : _sockfd(gsockfd),
          _addr(port),
          _isrunning(false),
          _func(func)
    {
    }
    // 都是套路
    void InitServer()
    {
        // 1. 创建socket
        _sockfd = ::socket(AF_INET, SOCK_DGRAM, 0); // IP?PORT?网络?本地?
        if (_sockfd < 0)
        {
            LOG(LogLevel::FATAL) << "socket: " << strerror(errno);
            Die(SOCKET_ERR);
        }
        LOG(LogLevel::INFO) << "socket success, sockfd is : " << _sockfd;

        // 2. 填充网络信息,并bind绑定
        // 2.1 有没有把socket信息,设置进入内核中??没有,只是填充了结构体!
        // struct sockaddr_in local;
        // bzero(&local, sizeof(local));
        // local.sin_family = AF_INET;
        // local.sin_port = ::htons(_port);                  // 要被发送给对方的,即要发到网络中!
        // //local.sin_addr.s_addr = ::inet_addr(_ip.c_str()); // 1. string ip->4bytes 2. network order //TODO
        // local.sin_addr.s_addr = INADDR_ANY;

        // 2.2 bind : 设置进入内核中
        int n = ::bind(_sockfd, _addr.NetAddr(), _addr.NetAddrLen());
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "bind: " << strerror(errno);
            Die(BIND_ERR);
        }
        LOG(LogLevel::INFO) << "bind success";
    }
    void Start()
    {
        _isrunning = true;
        while (true)
        {
            char inbuffer[1024]; // string
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer); // 必须设定

            ssize_t n = ::recvfrom(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0, CONV(&peer), &len);
            if (n > 0)
            {
                // 1. 消息内容 && 2. 谁发给我的
                // uint16_t clientport = ::ntohs(peer.sin_port);
                // std::string clientip = ::inet_ntoa(peer.sin_addr);
                // InetAddr cli(peer);
                // inbuffer[n] = 0;
                // std::string clientinfo = cli.Ip() + ":" + std::to_string(cli.Port()) + " # " + inbuffer;
                // LOG(LogLevel::DEBUG) << clientinfo;
                // std::string echo_string = "echo# ";
                // echo_string += inbuffer;
                // 把英文单词转化成为中文
                inbuffer[n] = 0;
                std::string result = _func(inbuffer); // 这个是回调,出去,还会回来!!!!
                ::sendto(_sockfd, result.c_str(), result.size(), 0, CONV(&peer), sizeof(peer));
            }
        }
        _isrunning = false;
    }
    ~UdpServer()
    {
        if(_sockfd > gsockfd)
            ::close(_sockfd);
    }

private:
    int _sockfd;
    InetAddr _addr;
    // uint16_t _port;  // 服务器未来的端口号
    // // std::string _ip; // 服务器所对应的IP
    bool _isrunning; // 服务器运行状态

    // 业务,回调
    func_t _func;
};

#endif

UdpServerMain.cc:

cpp 复制代码
#include "UdpServer.hpp"
#include "Dictionary.hpp"

// ./server_udp localport
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        std::cerr << "Usage: " << argv[0] << " localport" << std::endl;
        Die(USAGE_ERR);
    }
    // std::string ip = argv[1];
    uint16_t port = std::stoi(argv[1]);

    ENABLE_CONSOLE_LOG();
    std::shared_ptr<Dictionary> dict_sptr = std::make_shared<Dictionary>();

    std::unique_ptr<UdpServer> svr_uptr = std::make_unique<UdpServer>([&dict_sptr](const std::string &word){
        std::cout << "|" << word << "|" << std::endl;
        return dict_sptr->Translate(word);
    }, port);

    // std::unique_ptr<UdpServer> svr_uptr = std::make_unique<UdpServer>(std::bind(&Dictionary::Translate,\
    //             dict_sptr.get(), std::placeholders::_1), port);

    svr_uptr->InitServer();
    svr_uptr->Start();

    return 0;
}

1.3、V3 版本 - 简单聊天室

UDP 协议支持全双工,一个 sockfd,既可以读取,又可以写入,对于客户端和服务端同样如此,多线程客户端,同时读取和写入。

示例代码:(Cond.hpp,Log.hpp,Mutex.hpp,Thread.hpp,ThreadPool.hpp在以前的文章中都有封装过,这里就不重复展示了)

Common.hpp:

cpp 复制代码
#pragma once

#include <iostream>


#define Die(code)   \
    do              \
    {               \
        exit(code); \
    } while (0)

#define CONV(v) (struct sockaddr *)(v)

enum 
{
    USAGE_ERR = 1,
    SOCKET_ERR,
    BIND_ERR
};

InetAddr.hpp:

cpp 复制代码
#pragma once

#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Common.hpp"

class InetAddr
{
private:
    void PortNet2Host()
    {
        _port = ::ntohs(_net_addr.sin_port);
    }
    void IpNet2Host()
    {
        char ipbuffer[64];
        const char *ip = ::inet_ntop(AF_INET, &_net_addr.sin_addr, ipbuffer, sizeof(ipbuffer));
        (void)ip;
        _ip = ipbuffer;
    }

public:
    InetAddr()
    {
    }
    InetAddr(const struct sockaddr_in &addr) : _net_addr(addr)
    {
        PortNet2Host();
        IpNet2Host();
    }
    bool operator == (const InetAddr &addr)
    {
        return _ip == addr._ip && _port == addr._port; //debug
        //return _ip == addr._ip; //debug
    }
    InetAddr(uint16_t port) : _port(port), _ip("")
    {
        _net_addr.sin_family = AF_INET;
        _net_addr.sin_port = htons(_port);
        _net_addr.sin_addr.s_addr = INADDR_ANY;
    }
    struct sockaddr *NetAddr() { return CONV(&_net_addr); }
    socklen_t NetAddrLen() { return sizeof(_net_addr); }
    std::string Ip() { return _ip; }
    uint16_t Port() { return _port; }

    std::string Addr()
    {
        return Ip() + ":" + std::to_string(Port());
    }
    ~InetAddr()
    {
    }

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

UdpClientMain.cc:

cpp 复制代码
#include "Common.hpp"
#include <iostream>
#include <cstring>
#include <string>
#include <cstdlib>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
#include <signal.h>

int sockfd = -1;
struct sockaddr_in server;

void ClientQuit(int signo)
{
    (void)signo;
    const std::string quit = "QUIT";
    int n = ::sendto(sockfd, quit.c_str(), quit.size(), 0, CONV(&server), sizeof(server));
    exit(0);
}

void *Recver(void *args)
{
    while (true)
    {
        (void)args;
        struct sockaddr_in temp;
        socklen_t len = sizeof(temp);
        char buffer[1024];
        int n = ::recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, CONV(&temp), &len);
        if (n > 0)
        {
            buffer[n] = 0;
            std::cerr << buffer << std::endl; // 代码没问题,重定向也没问题,管道读写同时打开,才会继续向后运行
            // fprintf(stderr, "%s\n", buffer);
            // fflush(stderr);
        }
    }
}

// CS
// ./client_udp serverip serverport
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        std::cerr << "Usage: " << argv[0] << " serverip serverport" << std::endl;
        Die(USAGE_ERR);
    }
    signal(2, ClientQuit);
    std::string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]);

    // 1. 创建socket
    sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0)
    {
        std::cerr << "socket error" << std::endl;
        Die(SOCKET_ERR);
    }

    // 1.1 填充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());

    pthread_t tid;
    pthread_create(&tid, nullptr, Recver, nullptr);

    // 1.2 启动的时候,给服务器推送消息即可
    const std::string online = " ... 来了哈!";
    int n = ::sendto(sockfd, online.c_str(), online.size(), 0, CONV(&server), sizeof(server));

    // 2. clientdone
    while (true)
    {
        std::cout << "Please Enter# ";
        std::string message;
        std::getline(std::cin, message);

        int n = ::sendto(sockfd, message.c_str(), message.size(), 0, CONV(&server), sizeof(server));
        (void)n;
    }

    return 0;
}

UdpServer.hpp:

cpp 复制代码
#ifndef __UDP_SERVER_HPP__
#define __UDP_SERVER_HPP__

#include <iostream>
#include <string>
#include <memory>
#include <cstring>
#include <cerrno>
#include <strings.h>

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <functional>

#include "InetAddr.hpp"
#include "Log.hpp"
#include "Common.hpp"
#include "ThreadPool.hpp"

using namespace LogModule;
using namespace ThreadPoolModule;

const static int gsockfd = -1;
// const static std::string gdefaultip = "127.0.0.1"; // 表示本地主机
const static uint16_t gdefaultport = 8080;

using adduser_t = std::function<void(InetAddr &id)>;
using remove_t = std::function<void(InetAddr &id)>;

using task_t = std::function<void()>;
using route_t = std::function<void(int sockfd, const std::string &message)>;

class nocopy
{
public:
    nocopy(){}
    nocopy(const nocopy &) = delete;
    const nocopy& operator = (const nocopy &) = delete;
    ~nocopy(){}
};

class UdpServer : public nocopy
{
public:
    UdpServer(uint16_t port = gdefaultport)
        : _sockfd(gsockfd),
          _addr(port),
          _isrunning(false)
    {
    }
    // 都是套路
    void InitServer()
    {
        // 1. 创建socket
        _sockfd = ::socket(AF_INET, SOCK_DGRAM, 0); // IP?PORT?网络?本地?
        if (_sockfd < 0)
        {
            LOG(LogLevel::FATAL) << "socket: " << strerror(errno);
            Die(SOCKET_ERR);
        }
        LOG(LogLevel::INFO) << "socket success, sockfd is : " << _sockfd;

        // //local.sin_addr.s_addr = ::inet_addr(_ip.c_str()); // 1. string ip->4bytes 2. network order //TODO
        // local.sin_addr.s_addr = INADDR_ANY;

        // 2.2 bind : 设置进入内核中
        int n = ::bind(_sockfd, _addr.NetAddr(), _addr.NetAddrLen());
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "bind: " << strerror(errno);
            Die(BIND_ERR);
        }
        LOG(LogLevel::INFO) << "bind success";
    }
    void RegisterService(adduser_t adduser, route_t route, remove_t removeuser)
    {
        _adduser = adduser;
        _route = route;
        _removeuser = removeuser;
    }
    void Start()
    {
        _isrunning = true;
        while (true)
        {
            char inbuffer[1024]; // string
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer); // 必须设定

            ssize_t n = ::recvfrom(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0, CONV(&peer), &len);
            if (n > 0)
            {
                // 1. 消息内容 && 2. 谁发给我的
                // uint16_t clientport = ::ntohs(peer.sin_port);
                // std::string clientip = ::inet_ntoa(peer.sin_addr);

                InetAddr cli(peer);
                inbuffer[n] = 0;
                std::string message;
                if (strcmp(inbuffer, "QUIT") == 0)
                {
                    // 移除观察者
                    _removeuser(cli);
                    message = cli.Addr() + "# " + "我走了,你们聊!";
                }
                else
                {
                    // 2. 新增用户
                    _adduser(cli);
                    message = cli.Addr() + "# " + inbuffer;
                }

                // 3. 构建转发任务,推送给线程池,让线程池进行转发
                task_t task = std::bind(UdpServer::_route, _sockfd, message);
                ThreadPool<task_t>::getInstance()->Equeue(task);

                // std::string clientinfo = cli.Ip() + ":" + std::to_string(cli.Port()) + " # " + inbuffer;

                // LOG(LogLevel::DEBUG) << clientinfo;

                // std::string echo_string = "echo# ";
                // echo_string += inbuffer;

                // ::sendto(_sockfd, echo_string.c_str(), echo_string.size(), 0, CONV(&peer), sizeof(peer));
            }
        }
        _isrunning = false;
    }
    ~UdpServer()
    {
        if (_sockfd > gsockfd)
            ::close(_sockfd);
    }

private:
    int _sockfd;
    InetAddr _addr;
    bool _isrunning; // 服务器运行状态

    // 新增用户
    adduser_t _adduser;
    // 移除用户
    remove_t _removeuser;
    // 数据转发
    route_t _route;
};

#endif

UdpServerMain.cc:

cpp 复制代码
#include "UdpServer.hpp"
#include "User.hpp"

// ./server_udp localport
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        std::cerr << "Usage: " << argv[0] << " localport" << std::endl;
        Die(USAGE_ERR);
    }
    // std::string ip = argv[1];
    uint16_t port = std::stoi(argv[1]);

    ENABLE_CONSOLE_LOG();

    // 用户管理模块
    std::shared_ptr<UserManager> um = std::make_shared<UserManager>();
    // 网络模块
    std::unique_ptr<UdpServer> svr_uptr = std::make_unique<UdpServer>(port);
    svr_uptr->RegisterService([&um](InetAddr &id){ um->AddUser(id); },
                              [&um](int sockfd, const std::string &message){ um->Router(sockfd, message);},
                              [&um](InetAddr &id){ um->DelUser(id);}
                              );
    svr_uptr->InitServer();

    svr_uptr->Start();

    return 0;
}

User.hpp:

cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <list>
#include <memory>
#include <algorithm>
#include <sys/types.h>
#include <sys/socket.h>

#include "InetAddr.hpp"
#include "Log.hpp"
#include "Mutex.hpp"

using namespace LogModule;
using namespace LockModule;

class UserInterface
{
public:
    virtual ~UserInterface() = default;
    virtual void SendTo(int sockfd, const std::string &message) = 0;
    virtual bool operator==(const InetAddr &u) = 0;
    virtual std::string Id() = 0;
};

class User : public UserInterface
{
public:
    User(const InetAddr &id) : _id(id)
    {
    }
    void SendTo(int sockfd, const std::string &message) override
    {
        LOG(LogLevel::DEBUG) << "send message to " << _id.Addr() << " info: " << message;
        int n = ::sendto(sockfd, message.c_str(), message.size(), 0, _id.NetAddr(), _id.NetAddrLen());
        (void)n;
    }
    bool operator==(const InetAddr &u) override
    {
        return _id == u;
    }
    std::string Id() override
    {
        return _id.Addr();
    }
    ~User()
    {
    }

private:
    InetAddr _id;
};

// 对用户消息进行路由
//  UserManager 把所有的用户先管理起来!
//  观察者模式!observer
class UserManager
{
public:
    UserManager()
    {
    }
    void AddUser(InetAddr &id)
    {
        LockGuard lockguard(_mutex);
        for (auto &user : _online_user)
        {
            if (*user == id)
            {
                LOG(LogLevel::INFO) << id.Addr() << "用户已经存在";
                return;
            }
        }
        LOG(LogLevel::INFO) << " 新增该用户: " <<  id.Addr();
        _online_user.push_back(std::make_shared<User>(id));
        PrintUser();
    }
    void DelUser(InetAddr &id)
    {
        // v1
        auto pos = std::remove_if(_online_user.begin(), _online_user.end(), [&id](std::shared_ptr<UserInterface> &user){
            return *user == id;
        });
        _online_user.erase(pos, _online_user.end());
        PrintUser();

        //v2
        // for(auto user : _online_user)
        // {
        //     if(*user == id)
        //     {
        //         _online_user.erase(user);
        //         break; // 迭代器失效的问题
        //     }
        // }
    }
    void Router(int sockfd, const std::string &message)
    {
        LockGuard lockguard(_mutex);
        for (auto &user : _online_user)
        {
            user->SendTo(sockfd, message);
        }
    }
    void PrintUser()
    {
        for(auto user : _online_user)
        {
            LOG(LogLevel::DEBUG) <<"在线用户-> "<<  user->Id();
        }
    }
    ~UserManager()
    {
    }

private:
    std::list<std::shared_ptr<UserInterface>> _online_user;
    Mutex _mutex;
};

1.4、补充内容

1.4.1、地址转换函数

本节只介绍基于 IPv4 的 socket 网络编程,sockaddr_in 中的成员 sin_addr(struct in_addr 类型) 表示 32 位 的 IP 地址。但是我们通常用点分十进制的字符串表示 IP 地址,以下函数可以在字符串表示和 in_addr 表示之间转换。字符串转 in_addr 的函数:

in_addr 转字符串的函数:

其中 inet_pton 和 inet_ntop 不仅可以转换 IPv4 的 in_addr,还可以转换 IPv6 的in6_addr,因此函数接口是 void *addrptr。

1.4.2、inet_ntoa

inet_ntoa 这个函数返回了一个 char*, 很显然是这个函数自己在内部为我们申请了一块内存来保存 ip 的结果. 那么是否需要调用者手动释放呢?

man 手册上说, inet_ntoa 函数, 是把这个返回结果放到了静态存储区. 这个时候不需要我们手动进行释放。但是这也导致了它是非线程安全的。因为 inet_ntoa 把结果放到自己内部的一个静态存储区, 这样第二次调用时的结果会覆盖掉上一次的结果。

如果有多个线程调用 inet_ntoa, 是否会出现异常情况呢?

在 APUE 中, 明确提出 inet_ntoa 不是线程安全的函数。但是在 centos7 上测试, 并没有出现问题, 可能内部的实现加了互斥锁。在多线程环境下, 推荐使用 inet_ntop, 这个函数由调用者提供一个缓冲区保存结果, 可以规避线程安全问题。

二、网络命令

2.1、Ping 命令

**功能:**ping(Packet Internet Groper)是一种基础的 网络诊断工具,用于测试主机之间的 连通性 和 网络延迟。它通过发送 ICMP Echo Request 数据包到目标主机,并等待对方返回 ICMP Echo Reply 来检测网络是否通畅。

**语法:**ping 目标IP或域名

使用:

ping 8.8.8.8 # 测试与 Google DNS 的连通性

ping www.baidu.com # 测试与百度的连通性

**注意:**ping命名一旦发送,默认会一直检测,不会停下。

选项:

  • -c 次数 指定发送的请求次数 ping -c 4 8.8.8.8 只检测四次。
  • -i 秒数 设置发送间隔(默认 1 秒) ping -i 0.5 8.8.8.8

2.2、netstat

netstat 是一个用来查看网络状态的重要工具。

**语法:**netstat 选项

**功能:**查看网络状态

常用选项:

  • n 拒绝显示别名,能显示数字的全部转化成数字
  • l 仅列出有在 Listen (监听) 的服務状态
  • p 显示建立相关链接的程序名,即展示相关进程信息
  • t (tcp)仅显示 tcp 相关选项
  • u (udp)仅显示 udp 相关选项
  • a (all)显示所有选项,默认不显示 LISTEN 相关

使用:

常规使用:netstat -nuap

// 每个 1s 执行一次 netstat -nltp

watch -n 1 netstat -nltp

2.3、pidof

在查看服务器的进程 id 时非常方便。

**语法:**pidof 进程名

**功能:**通过进程名,查看进程 id

三、验证 UDP - windows 作为 client 访问 Linux

**注意:**一定要开放云服务器对应的端口号,在你的阿里云或者腾讯云或者华为云的网站后台中开放。

UdpClient.cc:(Windows端)

cpp 复制代码
#include <iostream>
#include <cstdio>
#include <thread>
#include <string>
#include <cstdlib>
#include <WinSock2.h>
#include <Windows.h>

#pragma warning(disable : 4996)

#pragma comment(lib, "ws2_32.lib")

std::string serverip = "";  // 填写你的云服务器ip
uint16_t serverport = 8888; // 填写你的云服务开放的端口号

int main()
{
    WSADATA wsd;
    WSAStartup(MAKEWORD(2, 2), &wsd);

    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 sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd == SOCKET_ERROR)
    {
        std::cout << "socker error" << std::endl;
        return 1;
    }
    std::string message;
    char buffer[1024];
    while (true)
    {
        std::cout << "Please Enter@ ";
        std::getline(std::cin, message);
        if(message.empty()) continue;
        sendto(sockfd, message.c_str(), (int)message.size(), 0, (struct sockaddr *)&server, sizeof(server));
        struct sockaddr_in temp;
        int len = sizeof(temp);
        int s = recvfrom(sockfd, buffer, 1023, 0, (struct sockaddr *)&temp, &len);
        if (s > 0)
        {
            buffer[s] = 0;
            std::cout << buffer << std::endl;
        }
    }

    closesocket(sockfd);
    WSACleanup();
    return 0;
}

UdpServer.cc:(Linux端)

cpp 复制代码
#include <iostream>
#include <string>
#include <memory>
#include <cerrno>
#include <cstring>
#include <unistd.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

const static uint16_t defaultport = 8888;
const static int defaultfd = -1;
const static int defaultsize = 1024;

enum
{
    Usage_Err = 1,
    Socket_Err,
    Bind_Err
};

class UdpServer
{
public:
    UdpServer(uint16_t port = defaultport)
        : _port(port), _sockfd(defaultfd)
    {
    }
    void Init()
    {
        // 1. 创建socket,就是创建了文件细节
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockfd < 0)
        {
            exit(Socket_Err);
        }

        // 2. 绑定,指定网络信息
        struct sockaddr_in local;
        bzero(&local, sizeof(local)); // memset
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        local.sin_addr.s_addr = INADDR_ANY; // 1. 4字节IP 2. 变成网络序列

        // 结构体填完,设置到内核中了吗??没有
        int n = ::bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
        if (n != 0)
        {
            exit(Bind_Err);
        }
    }
    void Start()
    {
        // 服务器永远不退出
        char buffer[defaultsize];
        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)
            {
                uint16_t clientport = ntohs(peer.sin_port);
                std::string clientip = inet_ntoa(peer.sin_addr);
                std::string prefix = clientip + ":" + std::to_string(clientport);
                buffer[n] = 0;
                std::cout << prefix << "# " << buffer << std::endl;


                std::string echo = buffer;
                echo += "[udp server echo message]";
                sendto(_sockfd, echo.c_str(), echo.size(), 0, (struct sockaddr *)&peer, len);
            }
        }
    }
    ~UdpServer()
    {
    }

private:
    uint16_t _port;
    int _sockfd;
};

void Usage(std::string proc)
{
    std::cout << "Usage : \n\t" << proc << " local_port\n"
              << std::endl;
}

// ./udp_server 8888
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
        return Usage_Err;
    }

    uint16_t port = std::stoi(argv[1]);
    std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port);
    usvr->Init();
    usvr->Start();

    return 0;
}

代码运行后可以验证 udp tcpclient(Windows)和 tcpserver(Linux)可以通信。这里就不演示了,下面是Windows端代码的一些解释:

C++:

WinSock2.h是 Windows Sockets API(应用程序接口)的头文件,用于在Windows 平台上进行网络编程。它包含了 Windows Sockets 2(Winsock2)所需的数据类型、函数声明和结构定义,使得开发者能够创建和使用套接字 (sockets)进行网络通信。

在编写使用 Winsock2 的程序时,需要在源文件中包含 WinSock2.h 头文件。这 样,编译器就能够识别并理解 Winsock2 中定义的数据类型和函数,从而能够正确地编译和链接网络相关的代码。

此外,与 WinSock2.h 头文件相对应的是 ws2_32.lib 库文件。在链接阶段,需要 将这个库文件链接到程序中,以确保运行时能够找到并调用 Winsock2 API 中实现的函数。

在 WinSock2.h 中定义了一些重要的数据类型和函数,如:

WSADATA:保存初始化 Winsock 库时返回的信息。

SOCKET:表示一个套接字描述符,用于在网络中唯一标识一个套接字。

sockaddr_in:IPv4 地址结构体,用于存储 IP 地址和端口号等信息。

socket():创建一个新的套接字。

bind():将套接字与本地地址绑定。

listen():将套接字设置为监听模式,等待客户端的连接请求。

accept():接受客户端的连接请求,并返回一个新的套接字描述符,用于与客户端 进行通信。
C++:

WSAStartup 函数是 Windows Sockets API 的初始化函数,它用于初始化Winsock 库。该函数在应用程序或 DLL 调用任何 Windows 套接字函数之前必须首 先执行,它扮演着初始化的角色。

以下是 WSAStartup 函数的一些关键点:

它接受两个参数:wVersionRequested 和 lpWSAData。wVersionRequested 用于 指定所请求的 Winsock 版本,通常使用 MAKEWORD(major, minor)宏,其中major 和 minor 分别表示请求的主版本号和次版本号。lpWSAData 是一个指向WSADATA 结构的指针,用于接收初始化信息。

如果函数调用成功,它会返回 0;否则,返回错误代码。

WSAStartup 函数的主要作用是向操作系统说明我们将使用哪个版本的 Winsock

库,从而使得该库文件能与当前的操作系统协同工作。成功调用该函数后,Winsock 库的状态会被初始化,应用程序就可以使用 Winsock 提供的一系列套接字 服务,如地址家族识别、地址转换、名字查询和连接控制等。这些服务使得应用程 序可以与底层的网络协议栈进行交互,实现网络通信。

在调用 WSAStartup 函数后,如果应用程序完成了对请求的 Socket 库的使用,应调用 WSACleanup 函数来解除与 Socket 库的绑定并释放所占用的系统资源。

相关推荐
cccccc语言我来了1 分钟前
Linux (11) 进程控制
linux·运维·数据库
菜菜的院子7 分钟前
WSL2 运行 Ubuntu 26.04 完整 GNOME 桌面(WSLg Wayland)
linux·运维·ubuntu·wsl
David猪大卫9 分钟前
【C++修炼】string类:string类的模拟实现
c语言·开发语言·数据结构·c++·经验分享·笔记·学习
暖核16 分钟前
从 CentOS 到 Ubuntu24.04 运维迁移手册:命令对照表 + 实操 + 避坑
linux·运维·centos
pride.li18 分钟前
第12章 Python 教程-流程控制:循环结构
linux·服务器·python·算法
迷途之人不知返23 分钟前
【基础IO】-5-简单理解三个标准流——stdin、stdout、stderr
linux
牛马鸡niumasi25 分钟前
【Linux】进程退出
linux
北京中科新远科技26 分钟前
高速光链路故障排查实战:从DOM光功率到FEC与BER
运维·服务器·网络
数据库小学妹29 分钟前
MySQL内存占用过高怎么排查?分层定位与OOM根因
数据库·经验分享·mysql·内存·oom
lisw0538 分钟前
计算生物学学习笔记:NHGRI基因组数据科学分析、可视化和信息学实验室空间(AnVIL)
笔记·计算生物学