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;
}

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

结果预览:

相关推荐
TechWayfarer2 小时前
电竞玩家的IP分布:中韩对抗赛的观众地域画像分析
网络·网络协议·tcp/ip
SPC的存折2 小时前
分布式(加一键部署脚本)LNMP-Redis-Discuz5.0部署指南-小白详细版
linux·运维·服务器·数据库·redis·分布式·缓存
Cx330❀2 小时前
线程进阶实战:资源划分与线程控制核心指南
java·大数据·linux·运维·服务器·开发语言·搜索引擎
Hello_Embed2 小时前
嵌入式上位机开发入门(二十):写文件功能的 RTU/TCP 双协议适配
网络·笔记·单片机·网络协议·tcp/ip·嵌入式
铅笔小新z2 小时前
【Linux】进程控制(上)
linux·运维·服务器
AI周红伟2 小时前
Hermes Agent 工具-周红伟
linux·网络·人工智能·腾讯云·openclaw
大卡片2 小时前
linux和IO常见面试题
linux·运维·服务器
zzzyyy5382 小时前
Linux程序地址空间
linux·运维·服务器
RisunJan2 小时前
Linux命令-newusers(用于批处理的方式一次创建多个命令)
linux·运维·服务器