Linux之Socket 编程 UDP

发布于:2025-07-04 ⋅ 阅读:(15) ⋅ 点赞:(0)

一、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:

#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:

#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:

#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:

#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:

#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:

#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:

#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:

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

InetAddr.hpp:

#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:

#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:

#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:

#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:

#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:

#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:

#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:

#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:

#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:

#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端)

#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端)

#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 库的绑定并释放所占用的系统资源。