【Linux网络#3】:Socket编程应用层UDP

1.前言

UDP(User Datagram Protocol)是面向无连接的不可靠数据报传输协议,其特点如下:

  • 低开销 :无需建立连接(三次握手),直接发送数据
  • 实时性强 :适合音视频传输等对实时性要求高的场景
  • 数据独立 :每个数据报携带完整地址信息
  • 无流量控制 :可能导致数据丢失,但效率更高

典型应用场景:

  • DNS查询
  • DHCP协议
  • 多媒体流传输
  • 在线游戏数据同步

2.代码框架设计

项目结构

udp_project/
├── include/
│   ├── UdpServer.hpp     // 服务器核心类
│   ├── Common.hpp        // 公共定义
│   └── Log.hpp           // 日志系统
├── src/
│   ├── UdpServerMain.cc  // 服务器入口
│   └── UdpClientMain.cc  // 客户端入口
└── Makefile              // 编译脚本

3.基本实现 - EchoServer

UdpServer.hpp 核心实现

class UdpServer
{
public:
    UdpServer(const std::string &ip = gdefaultip, uint16_t port = gdefaultport)
        : _sockfd(gsockfd), _ip(ip), _port(port), _isrunning(false) {}

    void InitServer()
    {
        // 创建套接字
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockfd < 0) {
            LOG(FATAL) << "Socket creation failed: " << strerror(errno);
            Die(SOCKET_ERR);
        }
        LOG(INFO) << "Socket created successfully, FD: " << _sockfd;

        // 绑定地址
        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());

        if (bind(_sockfd, CONV(&local), sizeof(local)) < 0) {
            LOG(FATAL) << "Binding failed: " << strerror(errno);
            Die(BIND_ERR);
        }
        LOG(INFO) << "Binding successful";
    }

    void Start()
    {
        _isrunning = true;
        while (true) {
            char inbuffer[1024];
            sockaddr_in peer;
            socklen_t len = sizeof(peer);
            
            // 接收数据
            ssize_t n = recvfrom(_sockfd, inbuffer, sizeof(inbuffer)-1, 0, 
                                CONV(&peer), &len);
            if (n > 0) {
                inbuffer[n] = '\0';
                LOG(DEBUG) << "Client says: " << inbuffer;
                
                // 构建响应
                std::string response = "Echo from server: ";
                response += inbuffer;
                
                // 发送响应
                sendto(_sockfd, response.c_str(), response.size(), 0,
                      CONV(&peer), sizeof(peer));
            }
        }
    }

private:
    int _sockfd;
    uint16_t _port;
    std::string _ip;
    bool _isrunning;
};

代码片段分析

InitServer() 方法详解
void InitServer()
{
    // 1. 创建套接字
    _sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);
    if(_sockfd < 0) {
        // 错误处理:使用日志系统记录错误信息
        LOG(LogLevel::FATAL) << "socket: " << strerror(errno);
        Die(SOCKET_ERR);
    }
    
    // 2. 填充地址结构
    struct sockaddr_in local;
    bzero(&local, sizeof(local)); // 清空结构体
    local.sin_family = AF_INET;  // IPv4协议
    local.sin_port = htons(_port); // 主机字节序转网络字节序
    local.sin_addr.s_addr = inet_addr(_ip.c_str()); // IP地址转换
    
    // 3. 绑定套接字
    int n = ::bind(_sockfd, CONV(&local), sizeof(local));
    if(n < 0) {
        LOG(LogLevel::FATAL) << "bind: " << strerror(errno);
        Die(BIND_ERR);
    }
}

关键点解析:

  1. socket()函数

    • AF_INET:IPv4协议族
    • SOCK_DGRAM:数据报套接字
    • protocol=0:自动选择UDP协议

    sockaddr_in结构体

struct sockaddr_in {
    sa_family_t    sin_family; // 地址族
    in_port_t      sin_port;   // 端口号
    struct in_addr sin_addr;   // IPv4地址
    char           sin_zero[8]; // 填充字段
};
  1. 地址转换函数

    • htons():主机字节序转网络字节序(16位)
    • htonl():32位版本
    • inet_addr():将点分IP转为32位网络序整数
Start() 方法详解
void Start()
{
    _isrunning = true;
    while(true) {
        char inbuffer[1024];
        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) {
            inbuffer[n] = '\0'; // 添加字符串结束符
            
            // 构建响应数据
            std::string echo_string = "echo# ";
            echo_string += inbuffer;
            
            // 发送响应
            sendto(_sockfd, echo_string.c_str(), echo_string.size(), 0,
                  CONV(&peer), sizeof(peer));
        }
    }
}
核心API解析:

1.recvfrom()

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:地址结构长度

2.sendto() :

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
              const struct sockaddr *dest_addr, socklen_t addrlen);
  • dest_addr:目标地址
  • addrlen:地址长度

3.UdpClientMain.cc 客户端实现

#include "UdpClient.hpp"
#include <iostream>
#include <cstring>
#include <string>
#include <cstdlib>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Common.hpp"

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]);

    // 创建套接字
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if(sockfd < 0) {
        std::cerr << "Socket creation failed: " << strerror(errno) << std::endl;
        Die(SOCKET_ERR);
    }

    // 填充服务器地址
    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 << "Client> ";
        std::string message;
        std::getline(std::cin, message);

        // 发送请求
        sendto(sockfd, message.c_str(), message.size(), 0, 
              CONV(&server), sizeof(server));

        // 接收响应
        char buffer[64];
        sockaddr_in tmp;
        socklen_t len = sizeof(tmp);
        ssize_t n = recvfrom(sockfd, buffer, sizeof(buffer)-1, 0, 
                            CONV(&tmp), &len);
        
        if(n > 0) {
            buffer[n] = '\0';
            std::cout << "Server> " << buffer << std::endl;
        }
    }

    return 0;
}

注意事项

  1. 客户端绑定机制

    • 不需要显式调用bind(),操作系统会自动分配端口
    • 首次调用sendto()时,OS会自动完成绑定
  2. 端口选择原则

    • 0-1023:系统端口(需root权限)
    • 1024-49151:注册端口
    • 49152-65535:动态/私有端口
    • 建议使用8080/8081等未被占用的端口

地址转换技巧

// 字符串IP转网络序整数
uint32_t ip = inet_addr("192.168.1.1");

// 网络序整数转字符串
char ip_str[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &ip, ip_str, INET_ADDRSTRLEN);

4.UdpServer.hpp 修改 - 打印客户端信息

void Start()
{
    _isrunning = true;
    while (true) {
        char inbuffer[1024];
        sockaddr_in peer;
        socklen_t len = sizeof(peer);
        
        ssize_t n = recvfrom(_sockfd, inbuffer, sizeof(inbuffer)-1, 0, 
                            CONV(&peer), &len);
        
        if (n > 0) {
            inbuffer[n] = '\0';
            
            // 获取客户端信息
            std::string client_ip = inet_ntoa(peer.sin_addr);
            uint16_t client_port = ntohs(peer.sin_port);
            
            LOG(DEBUG) << "Client[" << client_ip << ":" << client_port 
                       << "] says: " << inbuffer;
            
            // 构建响应
            std::string response = "Echo from server: ";
            response += inbuffer;
            
            sendto(_sockfd, response.c_str(), response.size(), 0,
                  CONV(&peer), sizeof(peer));
        }
    }
}

5.通信验证(Linux-Linux)

启动服务器

# 编译服务器
g++ -o server UdpServerMain.cc -Iinclude

# 运行服务器
./server 127.0.0.1 8080

启动客户端

# 编译客户端
g++ -o client UdpClientMain.cc -Iinclude

# 运行客户端
./client 127.0.0.1 8080

交互示例

服务器端日志

[INFO] Socket created successfully, FD: 3
[INFO] Binding successful
[DEBUG] Client[127.0.0.1:34567] says: Hello Server
[DEBUG] Client[127.0.0.1:34567] says: UDP Test

客户端交互:

Client> Hello Server
Server> Echo from server: Hello Server

Client> UDP Test
Server> Echo from server: UDP Test

6.核心API总结

7.错误处理机制

错误码定义(Common.hpp)

enum ErrorType {
    USAGE_ERR = 1,
    SOCKET_ERR,
    BIND_ERR,
    // ...其他错误码
};

日志系统(Log.hpp)

namespace LogModule {
    enum LogLevel {
        DEBUG = 0,
        INFO,
        WARNING,
        ERROR,
        FATAL
    };
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

北海有初拥

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值