Socket--UDP 构建简单聊天室

UDP 是无连接、不可靠、基于数据报 的传输协议,相比 TCP,它不需要建立连接、速度更快。

一、函数补充

sendto

sendtoLinux 下 C 语言 用于UDP 套接字 发送数据的核心系统调用,专门用于无连接的 UDP 通信(必须指定目标地址)。

也就是:

复制代码
#include <sys/socket.h>

ssize_t sendto(
    int sockfd,                // 套接字文件描述符
    const void *buf,          // 要发送的数据缓冲区
    size_t len,               // 数据长度(字节)
    int flags,                // 标志位,一般填 0
    const struct sockaddr *dest_addr,  // 目标地址(IP+端口)
    socklen_t addrlen         // 目标地址结构体长度
);

参数:

参数 说明
sockfd UDP 创建的 socket 文件描述符
buf 要发送的数据指针(char 数组、字符串等)
len 发送数据的字节数
flags 控制选项,UDP 正常发送直接填 0
dest_addr 目标地址结构体(必须指定:对方 IP + 端口)
addrlen 地址结构体的大小,一般用 sizeof()

返回值:

成功:返回实际发送的字节数,失败:返回 -1,并设置 errno 错误码

**注意事项:**UDP 必须填写目标地址,Linux 用 sockaddr_in 存储 IP 和端口

sendto 核心特点

  1. 无连接:每次发送都必须指定目标地址
  2. 数据报模式:一次 sendto = 一个独立 UDP 包
  3. 不保证到达 :函数返回成功只代表数据进入发送缓冲区,不代表对方收到
  4. 不粘包 :一次 sendto 对应对方一次 recvfrom
  5. 客户端 / 服务端都能用:UDP 没有严格区分

recvfrom

recvfrom 是 Linux 下用于从 UDP 套接字(SOCK_DGRAM)接收数据报 的核心系统调用,它不仅能读取数据,还能同时获取数据发送方的 IP 地址和端口号,这是 UDP 无连接通信的关键特性。

复制代码
#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
    • 已创建并绑定(bind)的 UDP 套接字文件描述符
  • buf
    • 指向接收数据缓冲区的指针,用于存放接收到的内容。
  • len
    • 缓冲区 buf最大容量(字节),决定单次最多接收的数据大小。
  • flags
    • 控制接收行为的标志位,常用值:
      • 0默认阻塞模式,无数据时进程会挂起等待。
      • MSG_DONTWAIT非阻塞模式 ,无数据立即返回 -1errno 设为 EAGAIN
      • MSG_PEEK窥探数据,读取后不清除内核缓冲区,下次仍可读到。
      • MSG_WAITALL:等待直到接收满 len 字节(UDP 很少用)。
  • src_addr
    • 输出参数 :指向 struct sockaddr 结构体指针,接收完成后自动填充发送方的 IP 和端口
    • 实际使用时,通常传入 struct sockaddr_in(IPv4)或 struct sockaddr_in6(IPv6),再强制类型转换。
    • 不需要源地址可设为 NULL,但老机器上可能会出现bug
  • addrlen
    • 输入输出参数
      • 传入:src_addr 结构体的初始大小
      • 返回:内核写入的实际地址长度
    • 必须初始化为有效地址,不能为 NULL

返回值

  • 成功 :返回实际接收到的字节数ssize_t 有符号整型)。
  • 失败 :返回 -1,全局变量 errno 存储错误码。
  • 注意(UDP 特有)UDP 无连接,永远不会返回 0(返回 0 属异常 / 网络错误)。

关键注意事项

  1. UDP 数据报边界
    • 每次 recvfrom 严格接收一个完整数据报
    • 数据报 > len截断 ,多余字节丢弃,errno 设为 MSG_TRUNC
  2. 阻塞 vs 非阻塞
    • 默认阻塞:无数据时进程休眠等待
    • 非阻塞(MSG_DONTWAIT):无数据立即返回 -1 / EAGAIN
  3. 地址结构体
    • IPv4 用 struct sockaddr_in,IPv6 用 struct sockaddr_in6
    • 必须初始化 addr_len,否则地址获取失败。

关于sockaddr结构体的介绍请见上一篇。

二、代码实现

在正式敲代码前,我们先考虑一下聊天室的大概逻辑:

首先,有多个用户进行聊天,那么就需要将所有用户管理起来;其次,每个用户都会有自己的IP加Port地址,故每个用户的地址也需要管理起来。以及等等......

地址管理封装:InetAddr

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

// 网络地址封装
class InetAddr
{
public:
    InetAddr(const struct sockaddr_in &address)
        : _address(address), _len(sizeof(address))
    {
        _ip = inet_ntoa(address.sin_addr);
        _port = ntohs(address.sin_port);
    }

    InetAddr(uint16_t port, const std::string &ip = "0.0.0.0")
        : _ip(ip), _port(port)
    {
        // 使用sockaddr_in结构体前先清空,别忘了
        bzero(&_address, sizeof(_address));
        _address.sin_family = AF_INET;
        _address.sin_port = htons(_port);                  // 主机向网络的时候记得转化端口号
        _address.sin_addr.s_addr = inet_addr(_ip.c_str()); // ip将点字符串方式转化为四字节模式

        _len = sizeof(_address);
    }

    InetAddr() {}

    struct sockaddr_in *GetNetAddress()
    {
        return &_address;
    }

    socklen_t Len()
    {
        return _len;
    }
    std::string ToString()
    {
        return "[" + _ip + ":" + std::to_string(_port) + "]";
    }

    ~InetAddr()
    {
    }

    bool operator==(const InetAddr &addr)
    {
        return (this->_ip == addr._ip) && (this->_port == addr._port);
    }

private:
    struct sockaddr_in _address;
    std::string _ip;
    uint16_t _port;
    socklen_t _len;
};

用户信息管理:UserManager

复制代码
#pragma once

#include <iostream>
#include <string>
#include <vector>
#include "InetAddr.hpp"
#include "Logger.hpp"

class UserManager
{
public:
    UserManager() {}

    void AddUser(const InetAddr &addr)
    {
        if (SearchUser(addr))
            return;
        _users.push_back(addr);
    }

    void DelUser(const InetAddr &addr)
    {
        for (auto its = _users.begin(); its < _users.end(); its++)
        {
            if (*its == addr)
            {
                _users.erase(its);
                break; //迭代器这里记得break
            }
        }
    }

    bool SearchUser(const InetAddr &addr)
    {
        // e是容器中的元素本身
        for (auto &e : _users)
        {
            if (e == addr)
                return true;
        }
        return false;
    }

    void ModUser(const InetAddr &addr)
    {
        DelUser(addr);
        AddUser(addr);
    }

    std::vector<InetAddr> &Users()
    {
        return _users;
    }

    ~UserManager()
    {
    }

private:
    std::vector<InetAddr> _users;
};

路由器,消息转发:Route

复制代码
// 路由器,消息转发
#pragma once

#include <iostream>
#include <memory>
#include <string>
#include <sys/socket.h>
#include <netinet/in.h>
#include "Mutex.hpp"
#include "UserManager.hpp"
#include "InetAddr.hpp"

class Route
{
public:
    Route() : _uma(std::make_unique<UserManager>())
    {
    }

    void CheckUser(const InetAddr &addr)
    {
        LockGuard lockguard(_lock);
        _uma->AddUser(addr);
    }

    void OfflineUser(const InetAddr &addr)
    {
        LockGuard lockguard(_lock);
        _uma->DelUser(addr);
    }

    void Broadcast(int sockfd, std::string message)
    {
        LockGuard lockguard(_lock);
        auto &users = _uma->Users();
        for (auto &user : users)
        {
            sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)user.GetNetAddress(), user.Len());
        }
    }

    ~Route()
    {
    }

private:
    std::unique_ptr<UserManager> _uma;
    Mutex _lock;
};

服务端:UdpServer

复制代码
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstdlib>
#include <string>
#include <strings.h>
#include <functional>
#include "InetAddr.hpp"
#include "Logger.hpp"

using namespace NS_LOG_MODULE;
const static int default_fd = -1;

// 回调函数
using handler_addr_t = std::function<void(const InetAddr &)>;           // 管理地址
using handler_msg_t = std::function<void(int sokcfd, std::string msg)>; // 管理信息

enum
{
    SUCCESS,
    SOCKET_ERROR,
    USAGE_ERROR,
    BIND_ERR
};

class UdpServer
{
public:
    //  UdpServer(std::string &ip, uint16_t port)
    UdpServer(uint16_t port)
        : _socketfd(default_fd), _port(port)
    {
    }
    ~UdpServer()
    {
        close(_socketfd);
    }

    void Init()
    {
        // socket函数创建网络通信的接口 / 通道,返回值是文件描述符
        // AF_INET:指定使用 IPv4 协议进行网络通信
        // SOCK_DGRAM:指定为 UDP 数据报套接字,无连接、不可靠传输
        // 0:让系统自动匹配 UDP 协议,无需手动指定
        _socketfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_socketfd < 0)
        {
            LOG(LogLevel::FATAL) << "create socket error";
            exit(SOCKET_ERROR);
        }
        LOG(LogLevel::INFO) << "socket success, fd:" << _socketfd;

        // 第二步,填充网络信息
        // 注意了,最好了解一下三种sockaddr结构体的组成
        // struct sockaddr_in local;
        // // 使用sockaddr_in结构体前先清空
        // bzero(&local, sizeof(local));
        // local.sin_family = AF_INET;
        // local.sin_port = htons(_port); // 主机向网络的时候记得转化端口号
        // // local.sin_addr.s_addr = inet_addr(_ip.c_str()); // ip将点字符串方式转化为四字节模式
        // local.sin_addr.s_addr = INADDR_ANY; // 绑定该机器上的任意IP地址!!

        //将上面的地址处理交给封装好的对象处理
        InetAddr local(_port);

        // 第三步,bin socket信息
        int n = bind(_socketfd, (struct sockaddr *)local.GetNetAddress(), local.Len());
        if (n < 0)
        {
            perror("bind failed reason: ");
            LOG(LogLevel::FATAL) << "bind socket error";
            exit(BIND_ERR);
        }
        LOG(LogLevel::INFO) << "bind socket success" << ", port: " << _port;
    }

    void RegisterService(handler_addr_t handler_addr, handler_msg_t handler_msg)
    {
        _handler_addr = handler_addr;
        _handler_msg = handler_msg;
    }

    void Start()
    {
        char inbuffer[1024];
        while (true)
        {
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            // 1. 用户发来的数据
            // 2. 用户的socket信息
            ssize_t n = recvfrom(_socketfd, inbuffer, sizeof(inbuffer) - 1, 0,
                                 (struct sockaddr *)&peer, &len);
            if (n > 0)
            {
                // 服务端
                inbuffer[n] = 0;
                // 1. 检测新用户
                InetAddr clientaddress(peer);
                std::string tips = clientaddress.ToString();
                std::string message = tips + inbuffer;
                LOG(LogLevel::DEBUG) << message;
                _handler_addr(clientaddress);
                // 2. 转发消息

                _handler_msg(_socketfd, message);
            }
            else
            {
                LOG(LogLevel::ERROR) << "recvfrom error";
            }
        }
    }

private:
    int _socketfd;
    //  std::string _ip; // ip有两种形式(点分式和四字节ip),这里选点分式
    uint16_t _port; // uint16_t = 无符号 16 位整数,专门用来存端口号等网络数据。
    handler_addr_t _handler_addr;
    handler_msg_t _handler_msg;
};

客户端函数入口:ChatClient

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

using namespace NS_THREAD_MODULE;

int sockfd = 0;
std::string server_ip;
uint16_t server_port = 0;
std::string nickname;

static void Usage(const std::string &proc)
{
    std::cout << "Usage:\n\t";
    std::cout << proc << " server_ip server_port" << std::endl;
}
static void Online(InetAddr &serveraddr)
{
    std::cout << "Please Set Your Nick Name# ";
    std::getline(std::cin, nickname);
    std::string online_message = nickname + " online!";
    ssize_t n = sendto(sockfd, online_message.c_str(), online_message.size(), 0,
                       (struct sockaddr *)serveraddr.GetNetAddress(), serveraddr.Len());
    (void)n;
}

void RecvMessage()
{
    while (true)
    {
        // recvfrom
        char inbuffer[1024] = {0};
        struct sockaddr_in temp;
        socklen_t len = sizeof(temp);
        ssize_t m = recvfrom(sockfd, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr *)&temp, &len);
        if (m > 0)
        {
            inbuffer[m] = 0;
            std::cerr << inbuffer << std::endl; // 2
        }
    }
}

void SendMessage()
{
    InetAddr serveraddr(server_port, server_ip);
    Online(serveraddr);

    while (true)
    {
        std::string message;
        // 1. 获取用户输入
        std::cout << "Please Enter# "; // 1
        std::getline(std::cin, message);

        message = nickname + "# " + message;

        // 2. clinet 发送数据给 server,首次发送即自动bind
        ssize_t n = sendto(sockfd, message.c_str(), message.size(), 0,
                           (struct sockaddr *)serveraddr.GetNetAddress(), serveraddr.Len());
        (void)n;
    }
}

// 我怎么知道server对方的IP和端口啊, 类似IP+Port 是被内置到client的!!!
// ./client_udp server_ip server_port
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }
    server_ip = argv[1];
    server_port = std::stoi(argv[2]);

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

    Thread recver(RecvMessage);
    Thread sender(SendMessage);

    recver.Start();
    sender.Start();

    recver.Join();
    sender.Join();
    return 0;
}

服务端函数入口:ChatServer

复制代码
#include "ThreadPool.hpp" // 执行者,执行处理动作的人
#include "Route.hpp"      // 任务
#include "UdpServer.hpp"  // 获取事件
#include <memory>

static void Usage(const std::string &process)
{
    std::cerr << "Usage:\n\t";
    std::cerr << process << " local_port" << std::endl;
}

using namespace NS_THREAD_POOL_MODULE;

using task_t = std::function<void()>;

int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
        exit(USAGE_ERROR);
    }
    ENABLE_CONSOLE_LOG_STRATEGY();
    uint16_t server_port = std::stoi(argv[1]);

    // 线程池模块
    auto thread_pool = ThreadPool<task_t>::Instance();

    // 路由模块
    Route r;

    // 网络模块
    UdpServer usvr(server_port);
    usvr.Init();

    usvr.RegisterService(
        [&r](const InetAddr &addr)
        {
            r.CheckUser(addr);
        },
        [&r, thread_pool](int sockfd, std::string msg)
        {
            auto t = std::bind(&Route::Broadcast, &r, sockfd, msg);
             thread_pool->Enqueue([&r, &sockfd, &msg](){
                 r.Broadcast(sockfd, msg);
             });
        });

    usvr.Start();
    return 0;
}

除了上面给出的函数文件外,还额外包含了自个创建的线程、线程池、日志、锁的封装文件。这些基本都能直接使用库中的函数,日志则可以找其它大佬的开源,懒的话只当练习可以直接去掉。

结果预览:

相关推荐
A小辣椒3 小时前
TShark:Wireshark CLI 功能
linux
A小辣椒7 小时前
TShark:基础知识
linux
AlfredZhao9 小时前
OCI 明明分配了 200G 系统盘,为什么 df 只看到 30G?
linux·oci
AlfredZhao1 天前
vi 删除指定范围的行,不用再反复按 dd
linux·vi
用户9718356334661 天前
银河麒麟 KY10 申威(SW64) 安装 nginx-1.16.1-2.p01.ky10.sw_64.rpm 详细步骤
linux
猪脚踏浪1 天前
linux 拷贝文件或目录到指定的位置
linux
摇滚侠2 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
bush42 天前
嵌入式linux学习记录十四、术语
linux·嵌入式
载数而行5202 天前
Linux 11 动态监控指令top
linux
网络研究院2 天前
2026年网络安全
网络·安全·法律·法规·趋势·发展