Linux笔记---UDP套接字编程

发布于:2025-09-08 ⋅ 阅读:(17) ⋅ 点赞:(0)

1. 核心接口

1.1 创建套接字:socket()

用于创建一个套接字(通信端点),返回套接字描述符(类似文件描述符)。

#include <sys/socket.h>

int socket(int domain, int type, int protocol);
  • 参数
    • domain:地址族(协议族),如AF_INET(IPv4)、AF_INET6(IPv6)。
    • type:套接字类型,如SOCK_STREAM(TCP,面向连接)、SOCK_DGRAM(UDP,无连接)。
    • protocol:具体协议(通常为 0,由系统根据前两个参数自动选择,如 TCP 用IPPROTO_TCP,UDP 用IPPROTO_UDP)。
  • 返回值:成功返回套接字描述符(非负整数),失败返回-1(需检查errno)。

1.2 绑定地址和端口:bind()

将套接字与特定的 IP 地址和端口号绑定(主要用于服务器端,指定监听的地址)。

#include <sys/socket.h>

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • 参数
    • sockfd:socket()返回的套接字描述符。
    • addr:指向sockaddr(或sockaddr_in)的指针,包含要绑定的 IP 和端口。
    • addrlen:addr结构的长度(如sizeof(struct sockaddr_in))。
  • 返回值:成功返回0,失败返回-1。
  • 注意
    • 端口号需用htons()转换为网络字节序(大端)。
    • IP 地址通常设为INADDR_ANY(表示监听所有本地网卡的 IP,值为0.0.0.0);若绑定特定网卡的IP,则需要使用htonl()转换;若IP为点分十进制字符串,则需要inet_addr()进行转换(自动转换为网络字节序)。

1.3 UDP发送:sendto()

向指定地址的 UDP 套接字发送数据报(无需提前连接)。

#include <sys/socket.h>

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
               const struct sockaddr *dest_addr, socklen_t addrlen);
  • 参数:sockfd(通信套接字)、buf(数据缓冲区)、len(数据长度)、flags(通常为 0)、dest_addr(目标地址)、addrlen(目标地址长度)。
  • 返回值:成功返回发送的字节数,失败返回-1。

1.4 UDP 接收:recvfrom()

接收 UDP 数据报,并获取发送方地址(通过src_addr输出)。

#include <sys/socket.h>

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                 struct sockaddr *src_addr, socklen_t *addrlen);
  • 参数:sockfd(通信套接字)、buf(接收缓冲区)、len(缓冲区大小)、flags(通常为 0)、src_addr(来源地址)、addrlen(来源地址的长度)。
  • 返回值:成功返回接收的字节数(0表示对方关闭连接),失败返回-1。

1.5 关闭套接字:close() 或 shutdown()

  • close():关闭套接字,释放资源,终止所有读写操作:
    #include <unistd.h>
    int close(int fd);  // fd为套接字描述符
  • shutdown():更灵活地关闭连接(可单独关闭读或写):
    #include <sys/socket.h>
    int shutdown(int sockfd, int how);
    • how:SHUT_RD(关闭读)、SHUT_WR(关闭写)、SHUT_RDWR(关闭读写,同close())。

1.6 辅助函数(地址转换)

  • inet_addr():将点分十进制 IP(如"192.168.1.1")转换为网络字节序的 32 位整数
    #include <arpa/inet.h>
    in_addr_t inet_addr(const char *cp);
  • inet_ntoa:将网络字节序的 32 位整数转换为点分十进制 IP 字符串:
    char *inet_ntoa(struct in_addr in);
    注意,该函数返回的字符串存放在一个静态全局空间,每次调用会覆盖掉上一次的结果,是一个线程不安全的函数。
  • 字节序转换:htons()(主机→网络,16 位)、htonl()(主机→网络,32 位)、ntohs()(网络→主机,16 位)、ntohl()(网络→主机,32 位)。

2. UDP客户/服务器通信流程

2.1 服务器端

服务器端的通用流程(日志可以忽略):

// (1)面向数据报, UDP套接字
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0)
{
    LOG(LogLevel::FATAL) << "socket: 套接字创建失败! ";
    exit(errno);
}

// (2)初始化需要绑定的网络信息
struct sockaddr_in addr;
bzero(&addr, sizeof(addr));
addr.sin_addr.s_addr = INADDR_ANY;
addr.sin_family = AF_INET;
addr.sin_port = htons(_port);

int n = bind(_sockfd, (struct sockaddr *)&addr, sizeof(addr));
if (n != 0)
{
    LOG(LogLevel::FATAL) << "bind: 网络信息绑定失败! ";
    exit(errno);
}
LOG(LogLevel::INFO) << "UDPServer: UDP套接字(sockfd=" << _sockfd << ")创建成功";

//(3)循环获取客户端发来的消息,处理并回复
// 下面的代码将客户端发来的消息原封不动地返回给客户端
while (isrunning)
{
    // 等待客户端发送信息
    struct sockaddr_in client_addr;
    socklen_t client_addr_len = sizeof(client_addr);
    ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&client_addr, &client_addr_len);
    if (n > 0)
    {
        buffer[n] = 0;
        std::string client_ip = inet_ntoa(client_addr.sin_addr);
        in_port_t client_port = ntohs(client_addr.sin_port);
        // 对客户端传来的信息进行处理
        _func(buffer, client_ip, client_port);
        sendto(_sockfd, buffer, strlen(buffer), 0, (struct sockaddr *)&client_addr, client_addr_len);
    }
}

// (4)关闭套接字
close(_sockfd);

封装UDP服务端:

#pragma once
#include <iostream>
#include <functional>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstring>
#include "Log.hpp"
#define BUFFER_SIZE 1024
#define DEFAULT_PROT 8888
#define EXITSIGNAL "exit"

using func_t = std::function<std::string(const std::string &, const std::string &, in_port_t)>;
std::string default_func(const std::string &message, const std::string &ip, in_port_t port)
{
    std::cout << "Client[" << ip << ":" << port << "] Massage# " << message << std::endl;
    return message;
}

using namespace LogModule;

class UDPServer
{
public:
    UDPServer(in_port_t port = DEFAULT_PROT, func_t func = default_func)
        : isrunning(false), _port(port), _func(func)
    {
        // 面向数据报, UDP套接字
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockfd < 0)
        {
            LOG(LogLevel::FATAL) << "socket: 套接字创建失败! ";
            exit(errno);
        }
        // 初始化需要绑定的网络信息
        struct sockaddr_in addr;
        bzero(&addr, sizeof(addr));
        addr.sin_addr.s_addr = INADDR_ANY;
        addr.sin_family = AF_INET;
        addr.sin_port = htons(_port);

        int n = bind(_sockfd, (struct sockaddr *)&addr, sizeof(addr));
        if (n != 0)
        {
            LOG(LogLevel::FATAL) << "bind: 网络信息绑定失败! ";
            exit(errno);
        }
        LOG(LogLevel::INFO) << "UDPServer: UDP套接字(sockfd=" << _sockfd << ")创建成功";
    }
    ~UDPServer()
    {
        close(_sockfd);
    }
    UDPServer(const UDPServer&) = delete;
    UDPServer& operator=(const UDPServer&) = delete;
    void Start()
    {
        isrunning = true;
        char buffer[BUFFER_SIZE];
        while (isrunning)
        {
            // 等待客户端发送信息
            struct sockaddr_in client_addr;
            socklen_t client_addr_len = sizeof(client_addr);
            ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&client_addr, &client_addr_len);
            if (n > 0)
            {
                buffer[n] = 0;
                std::string client_ip = inet_ntoa(client_addr.sin_addr);
                in_port_t client_port = ntohs(client_addr.sin_port);
                std::string ret_message = _func(buffer, client_ip, client_port);
                sendto(_sockfd, ret_message.c_str(), ret_message.size(), 0, (struct sockaddr *)&client_addr, client_addr_len);
            }
        }
    }

private:
    int _sockfd;
    in_port_t _port;
    bool isrunning;
    func_t _func;
};

2.2 服务端ip与端口号

首先我们在命令行输入[ifconfig],来获取本地网卡的ip地址:

注意,eth0处的ip地址是局域网地址,lo处的地址127.0.0.1 是 IPv4 协议中的环回地址(Loopback Address),专门用于本地主机(当前计算机)内部的网络通信,其核心特点是:数据发送到这个地址后,会直接被本机的网络协议栈接收,不会通过物理网卡发送到外部网络。

除此之外,如果你使用的是一台服务器,它应该还配备了公网ip。

因为我们的服务器端绑定的地址是INADDR_ANY,所以对于客户端来说:

  1. 位于服务器所在的机器上:三个ip地址皆可访问到服务端。
  2. 位于同一局域网的不同机器上:可以使用局域网ip和公网ip。
  3. 位于不同网域需跨网通信:只能使用公网ip。

如果我们绑定的是特定地址,那么就只能通过这个特定地址来访问服务端,可用的范围依然遵循上面的规则。

至于端口号,我们只需要在可用范围[1024 - 65535]内任选即可,但是注意不要和其他进程冲突。

2.3 客户机端

// (1)面向数据报,UDP套接字
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0)
{
    LOG(LogLevel::FATAL) << "socket: 套接字创建失败! ";
    exit(errno);
}
LOG(LogLevel::INFO) << "UDPClient: UDP套接字(sockfd=" << _sockfd << ")创建成功";

// (2)初始化服务器端的地址信息
struct sockaddr_in _server_addr;
socklen_t _server_addr_len = sizeof(_server_addr);
_server_addr.sin_addr.s_addr = inet_addr(ip.c_str());
_server_addr.sin_family = AF_INET;
_server_addr.sin_port = htons(port);

// (3)循环发送信息并接收回复
std::string message;
char buffer[BUFFER_SIZE];
while (true)
{
    std::cout << "Send to Server# ";
    std::getline(std::cin, message);
    sendto(_sockfd, message.c_str(), strlen(message.c_str()), 0, (struct sockaddr *)&_server_addr, _server_addr_len);
    ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&_server_addr, &_server_addr_len);
    if (n > 0)
    {
        buffer[n] = 0;
        std::cout << "Server Message# " << buffer << std::endl;
    }
}

// (4)关闭套接字
close(_sockfd);

封装UDP客户端:

#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstring>
#include "Log.hpp"
#define BUFFER_SIZE 1024
#define DEFAULT_PROT 8888

using namespace LogModule;

class UDPClient
{
public:
    UDPClient(const std::string &ip, in_port_t port)
        : _server_addr_len(sizeof(_server_addr))
    {
        _server_addr.sin_addr.s_addr = inet_addr(ip.c_str());
        _server_addr.sin_family = AF_INET;
        _server_addr.sin_port = htons(port);

        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockfd < 0)
        {
            LOG(LogLevel::FATAL) << "socket: 套接字创建失败! ";
            exit(errno);
        }
        LOG(LogLevel::INFO) << "UDPClient: UDP套接字(sockfd=" << _sockfd << ")创建成功";
    }
    ~UDPClient()
    {
        close(_sockfd);
    }
    UDPClient(const UDPClient&) = delete;
    UDPClient& operator=(const UDPClient&) = delete;
    void Start()
    {
        std::string message;
        char buffer[BUFFER_SIZE];
        while (true)
        {
            std::cout << "Send to Server# ";
            std::getline(std::cin, message);
            sendto(_sockfd, message.c_str(), strlen(message.c_str()), 0, (struct sockaddr *)&_server_addr, _server_addr_len);
            ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&_server_addr, &_server_addr_len);
            if (n > 0)
            {
                buffer[n] = 0;
                std::cout << "Server Message# " << buffer << std::endl;
            }
        }
    }

private:
    int _sockfd;
    struct sockaddr_in _server_addr;
    socklen_t _server_addr_len;
};

2.4 客户端ip与端口号

客户端并不需要显式绑定ip与端口号,在客户端套接字首次发送消息时,操作系统会自动分配端口号并绑定ip地址与端口号。

那为什么服务端需要我们显式绑定呢?这是因为,隐式绑定的ip与端口号是动态的,可能变化的;而服务端需要固定的ip与端口号,方便用户进行访问。

2.5 客户服务器通信演示

// 客户端代码
#include "Client.hpp"

int main(int argc, char* args[])
{
    if(argc != 3)
    {
        std::cerr << "Usage: Server + ip + port" << std::endl;
        exit(errno); 
    }
    in_port_t port = std::stoi(args[2]);
    UDPClient client(args[1], port);
    client.Start();
    return 0;
}


// 服务端代码
#include "Server.hpp"

int main(int argc, char* args[])
{
    if(argc != 2)
    {
        std::cerr << "Usage: Server + port" << std::endl;
        exit(errno); 
    }
    in_port_t port = std::stoi(args[1]);
    UDPServer server(port);
    server.Start();
    return 0;
}

3. C/S音译中字典程序

即,客户端发送想要翻译的英文单词,服务器端进行翻译并返回中文。

客户端的代码不需要变,就和上面的回声程序保持一致即可,我们对服务器端进行如下调整:

3.1 Server.cpp

#include "Server.hpp"
#include "Dictionary.hpp"

int main(int argc, char* args[])
{
    if(argc != 2)
    {
        std::cerr << "Usage: Server + port" << std::endl;
        exit(errno); 
    }

    Dictionary dict;
    dict.Load();

    in_port_t port = std::stoi(args[1]);
    UDPServer server(port, [&dict](const std::string& message, const InetAddr& client){
        return dict.Check(message);
    });
    server.Start();
    return 0;
}

3.2 Server.hpp

#pragma once
#include <iostream>
#include <functional>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstring>
#include "Log.hpp"
#include "InetAddr.hpp"
#define BUFFER_SIZE 1024
#define DEFAULT_PROT 8888
#define EXITSIGNAL "exit"

using func_t = std::function<std::string(const std::string &, const InetAddr&)>;
std::string default_func(const std::string &message, const InetAddr& client)
{
    std::cout << "Client[" << client.Ip() << ":" << client.Port() << "] Massage# " << message << std::endl;
    return message;
}

using namespace LogModule;

class UDPServer
{
public:
    UDPServer(in_port_t port = DEFAULT_PROT, func_t func = default_func)
        : isrunning(false), _port(port), _func(func)
    {
        // 面向数据报, UDP套接字
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockfd < 0)
        {
            LOG(LogLevel::FATAL) << "socket: 套接字创建失败! ";
            exit(errno);
        }
        // 初始化需要绑定的网络信息
        struct sockaddr_in addr;
        bzero(&addr, sizeof(addr));
        addr.sin_addr.s_addr = INADDR_ANY;
        addr.sin_family = AF_INET;
        addr.sin_port = htons(_port);

        int n = bind(_sockfd, (struct sockaddr *)&addr, sizeof(addr));
        if (n != 0)
        {
            LOG(LogLevel::FATAL) << "bind: 网络信息绑定失败! ";
            exit(errno);
        }
        LOG(LogLevel::INFO) << "UDPServer: UDP套接字(sockfd=" << _sockfd << ")创建成功";
    }
    ~UDPServer()
    {
        close(_sockfd);
    }
    UDPServer(const UDPServer&) = delete;
    UDPServer& operator=(const UDPServer&) = delete;
    void Start()
    {
        isrunning = true;
        char buffer[BUFFER_SIZE];
        while (isrunning)
        {
            // 等待客户端发送信息
            struct sockaddr_in client_addr;
            socklen_t client_addr_len = sizeof(client_addr);
            ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&client_addr, &client_addr_len);
            if (n > 0)
            {
                buffer[n] = 0;
                InetAddr client(client_addr);
                std::string ret_message = _func(buffer, client);
                sendto(_sockfd, ret_message.c_str(), ret_message.size(), 0, (struct sockaddr *)&client_addr, client_addr_len);
            }
        }
    }

private:
    int _sockfd;
    in_port_t _port;
    bool isrunning;
    func_t _func;
};

3.3 InetAddr.hpp

#pragma once
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Log.hpp"

using namespace LogModule;

class InetAddr
{
public:
    InetAddr(const struct sockaddr_in& addr)
    {
        _ip = inet_ntoa(addr.sin_addr);
        _port = ntohs(addr.sin_port);
    }
    std::string Ip() const {return _ip;}
    in_port_t Port() const {return _port;}
private:
    std::string _ip;
    in_port_t _port;
};

3.4 Dictionary.hpp

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

const std::string esp = ": ";
const std::string default_path = "./dictionary.txt";
const std::string default_ret = "未知单词";

using namespace LogModule;

class Dictionary
{
public:
    bool Load(const std::string& path = default_path)
    {
        std::ifstream dict_file(path);
        if(!dict_file.is_open())
        {
            LOG(LogLevel::ERROR) << "未能打开字典文件! ";
            return false;
        }
        std::string pair;
        while(std::getline(dict_file, pair))
        {
            auto pos = pair.find(esp);
            if(pos == std::string::npos)
            {
                LOG(LogLevel::WARNING) << "无法解析的对应关系[" << pair << "]";
                continue;
            }
            std::string english = pair.substr(0, pos);
            std::string chinese = pair.substr(pos + esp.size());
            if(english.empty() || chinese.empty())
            {
                LOG(LogLevel::WARNING) << "对应关系[" << pair << "]不完整";
                continue;
            }
            _dict[english] = chinese;
        }
        return true;
    }

    std::string Check(const std::string& word)
    {
        if(_dict.count(word))
            return _dict[word];
        else
            return default_ret;
    }
private:
    std::unordered_map<std::string, std::string> _dict;
};

3.5 dictionary.txt

apple: 苹果
hello: 你好
shishen: 佐助
winter: 冬天
summer: 夏天
hate: 恨
banana: 香蕉
orange: 橘子
god: 神
car: 车
cup: 杯子
key: 钥匙