UDP网络编程

1.Socket编译预备

1.2.IP

理解源 IP 地址和目的 IP 地址:  IP 在网络中,用来标识主机的唯一性

1.2.端口

认识端口号:
端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来 处理;

所以:IP 地址 + 端口号能够标识网络上的某一台主机的某一个进程

但是一个端口号只能被一个进程占用
端口号的划分规则:
1.0 - 1023: 知名端口号, HTTP, FTP, SSH 等这些广为使用的应用层协议, 他们的
端口号都是固定的.
2.  1024- 65535: 操作系统动态分配的端口号. 客户端程序的端口号, 就是由操作
系统从这个范围分配的
理解 "端口号" 和 "进程 ID"的关系
我们之前在学习系统编程的时候, 学习了 pid 表示唯一一个进程; 此处我们的端口号也 是唯一表示一个进程. 那么这两者之间是怎样的关系?
1.一个进程可以绑定多个端口号; 但是一个端口号不能被多个进程绑定;
2.进程 ID 属于系统概念,技术上也具有唯一性,确实可以用来标识唯一的一个进 程,但是这样做,会让系统进程管理和网络强耦合,实际设计的时候,并没有选择这 样做。

1.3.理解socket

1.综上,IP 地址用来标识互联网中唯一的一台主机,port 用来标识该主机上唯一的 一个网络进程
2.IP+Port 就能表示互联网中唯一的一个进程
3.所以,通信的时候,本质是两个互联网进程代表人来进行通信,{srcIp, srcPort,dstIp,dstPort}这样的 4 元组就能标识互联网中唯二的两个进程。
4.所以,网络通信的本质,也是进程间通信
5.我们把 ip+port 叫做套接字 socke

1.4.传输层的两个代表协议

TCP:

传输层协议
有连接
可靠传输
面向字节流

UDP:

传输层协议
无连接
不可靠传输
面向数据报

1.5.网络字节序

内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的 多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之 分. 那么如何定义网络数据流的地址呢?

1.发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出

2.接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从 低到高的顺序保存;

3.因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高 地址

4.TCP/IP 协议规定,网络数据流应采用大端字节序,即低地址高字节

5.不管这台主机是大端机还是小端机, 都会按照这个 TCP/IP 规定的网络字节序来 发送/接收数据;

6.如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即 可;

1.6.socket编程接口

1.6.1.socket常见API

// 创建 socket 文件描述符(TCP/UDP,客户端 + 服务器)
int socket(int domain, int type, int protocol);

// 绑定端口号(TCP/UDP,服务器)
int bind(int socket, const struct sockaddr *address,
         socklen_t address_len);

// 开始监听 socket(TCP,服务器)
int listen(int socket, int backlog);

// 接收请求(TCP,服务器)
int accept(int socket, struct sockaddr* address,
           socklen_t* address_len);

// 建立连接(TCP,客户端)
int connect(int sockfd, const struct sockaddr *addr,
            socklen_t addrlen);

1.6.2.sokaddr结构

基类:sockaddr 结构体

在 POSIX 标准中,sockaddr结构体定义如下:

struct sockaddr {
    sa_family_t sa_family;  // 地址族,如AF_INET、AF_INET6等
    char        sa_data[14]; // 用于存放地址相关的数据,具体含义依赖于地址族
};

派生类:sockaddr_in(网络之间的通信):结构体

#include <netinet/in.h>
struct sockaddr_in {
    sa_family_t    sin_family;  // 地址族,通常为AF_INET
    in_port_t      sin_port;    // 端口号,网络字节序
    struct in_addr sin_addr;    // IPv4地址
    unsigned char  sin_zero[8]; // 填充字段,使sockaddr_in与sockaddr长度相同
};
struct in_addr {
    in_addr_t s_addr;  // 32位的IPv4地址,网络字节序
};

派生类:sockaddr_un(本地之间的通信):结构体

#include <sys/un.h>
struct sockaddr_un {
    sa_family_t sun_family;  // 地址族,通常为AF_UNIX
    char        sun_path[108];// 本地套接字文件路径
};

2.UDP网络编程

2.1.V1版本 -- echo server

2.1.1.UdpServer.hpp

这个UdpServer类实现了一个简单的 UDP 服务器,主要功能包括:

  • 创建 UDP 套接字
  • 绑定 IP 地址和端口
  • 接收客户端消息
  • 解析客户端信息
  • 向客户端发送响应

头文件与常量定义

#pragma once

#include <iostream>
#include <string>
#include <functional>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

const int defaultfd = -1;

  • 头文件说明

    • iostream:提供输入输出功能
    • string:支持 C++ 字符串操作
    • strings.h:提供bzero等函数
    • 后四个头文件:提供套接字编程所需的结构和函数
  • 常量定义

    • defaultfd = -1:定义套接字的默认无效值

类声明与成员变量

class UdpServer
{
private:
    int _sockfd;    // 套接字描述符
    uint16_t _port; // 服务器端口
    bool _isrunning; // 是否运行

  • 成员变量说明
    • _sockfd:存储套接字文件描述符,-1 表示未初始化
    • _port:服务器监听的端口号
    • _isrunning:服务器运行状态标志

构造函数与析构函数

public:
    UdpServer(uint16_t port)
        : _sockfd(defaultfd),
          _port(port), 
          _isrunning(false)
    {
    }

    ~UdpServer()
    {
    }

  • 构造函数

    • 接收端口号作为参数
    • 使用初始化列表初始化成员变量
    • 将套接字设为默认无效值,运行状态设为 false

初始化方法 (Init)

void Init()
{
    // 1. 创建套接字
    _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (_sockfd < 0)
    {
        exit(1);
    }

    // 2. 绑定socket信息
    struct sockaddr_in local;
    bzero(&local, sizeof(local));
    local.sin_family = AF_INET;
    local.sin_port = htons(_port);
    local.sin_addr.s_addr = INADDR_ANY;

    int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
    if (n < 0)
    {
        exit(2);
    }
}

关键步骤解析:

  1. 创建套接字

    • socket(AF_INET, SOCK_DGRAM, 0)
      • AF_INET:使用 IPv4 协议
      • SOCK_DGRAM:创建 UDP 套接字
      • 返回值为套接字文件描述符,<0 表示失败
  2. 绑定地址信息

    • sockaddr_in结构体初始化:

      • sin_family:协议族 (AF_INET)
      • sin_port:端口号 (使用htons转换为网络字节序)
      • sin_addr.s_addrINADDR_ANY表示绑定所有本地 IP
    • bind函数:

      • 将套接字与指定的 IP 和端口绑定
      • 服务器必须显式绑定,因为客户端需要知道服务器地址

启动方法 (Start)

void Start()
{
    _isrunning = true;
    while (_isrunning)
    {
        char buffer[1024];
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        
        // 1. 收消息
        ssize_t s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
        if (s > 0)
        {
            // 1.1 解析客户端信息
            int peer_port = ntohs(peer.sin_port);
            std::string peer_ip = inet_ntoa(peer.sin_addr);

            buffer[s] = 0; // 字符串结尾
            
            std::cout << "receive from " << peer_ip << ":" << peer_port << " data: " << buffer << std::endl;
            
            // 2. 发消息,回应客户端
            std::string echo_string = "server echo@ ";
            echo_string += buffer;
            sendto(_sockfd, echo_string.c_str(), echo_string.size(), 0, (struct sockaddr *)&peer, len);
        }
    }
}

核心功能解析:

  1. 消息接收循环

    • 使用while (_isrunning)保持服务器持续运行
    • recvfrom函数接收客户端消息:
      • 参数包括:套接字、缓冲区、缓冲区大小、标志位、客户端地址、地址长度
      • 返回值为接收的字节数,>0 表示成功
  2. 客户端信息解析

    • ntohs:将端口号从网络字节序转换为主机字节序
    • inet_ntoa:将二进制 IP 地址转换为点分十进制字符串
    • buffer[s] = 0:确保接收的字节数组以 null 结尾,形成合法字符串
  3. 消息响应

    • 构造响应字符串,格式为 "server echo@ [客户端消息]"
    • 使用sendto向客户端发送响应:
      • 参数与recvfrom类似,使用接收到的客户端地址

完整代码:

#pragma once

#include <iostream>
#include <string>
#include <functional>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

const int defaultfd = -1;

class UdpServer
{
public:
    UdpServer(uint16_t port)
        : _sockfd(defaultfd),
          _port(port), // 服务器端口
          _isrunning(false)
    {
    }
    void Init()
    {
        // 1. 创建套接字
        // AF_INET: IPv4协议
        // SOCK_DGRAM: UDP协议
        // 0: 协议无关
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockfd < 0)
        {

            exit(1);
        }

        // 2. 绑定socket信息,ip和端口,
        // 2.1 填充sockaddr_in结构体
        struct sockaddr_in local;
        bzero(&local, sizeof(local));       // 清空
        local.sin_family = AF_INET;         // IPv4协议
        local.sin_port = htons(_port);      // 端口号,网络序列
        local.sin_addr.s_addr = INADDR_ANY; // 自动获取本机IP地址

        // 那么为什么服务器端要显式的bind呢?IP和端口必须是众所周知且不能轻易改变的!
        int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
        if (n < 0)
        {
            exit(2);
        }
    }
    void Start()
    {
        // 服务器是一直运行的,不停的接收消息
        _isrunning = true;
        while (_isrunning)
        {
            char buffer[1024];
            struct sockaddr_in peer; // 客户端信息
            socklen_t len = sizeof(peer);
            // 1. 收消息
            ssize_t s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
            if (s > 0)
            {
                // 1.1 解析客户端信息
                int peer_port = ntohs(peer.sin_port);           // 从网络中拿到的!网络序列
                std::string peer_ip = inet_ntoa(peer.sin_addr); // 4字节网络风格的IP -> 点分十进制的字符串风格的IP

                buffer[s] = 0; // 字符串结尾
                std::cout << "receive from " << peer_ip << ":" << peer_port << " data: " << buffer << std::endl;
                // 2. 发消息,回应客户端
                std::string echo_string = "server echo@ ";
                echo_string += buffer;
                sendto(_sockfd, echo_string.c_str(), echo_string.size(), 0, (struct sockaddr *)&peer, len);
            }
        }
    }

    ~UdpServer()
    {
    }

private:
    int _sockfd;    // 套接字描述符
    uint16_t _port; // 服务器端口
    // std::string _ip; // 用的是字符串风格,点分十进制, "192.168.1.1"
    bool _isrunning; // 是否运行
};

注意:绑定服务器IP

INADDR_ANY 是服务器编程中的一个重要概念,它提供了以下优势

  • 灵活性:支持通过任意本地 IP 接收连接
  • 简化配置:无需手动指定具体 IP 地址
  • 适应性:适用于多网卡和动态 IP 环境

2.1.2.UdpServer.cc

完整代码:

#include "UdpServer.hpp"
#include <iostream>
#include <string>
#include <unique_ptr>

using namespace std;

int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        cout << "Usage: " << argv[0] << " port" << endl;
        return 1;
    }

    uint16_t _port = stoi(argv[1]);

    unique_ptr<UdpServer> server = make_unique<UdpServer>(_port);

    server->Init();
    server->Start();
    return 0;
}

2.1.3.UdpClient.cc

1. 头文件与命名空间

#include <iostream>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>

using namespace std;

  • 头文件
    • iostream:用于输入输出流(如coutgetline
    • sys/socket.h:提供套接字编程的基本函数和数据结构
    • arpa/inet.h:包含 IP 地址转换函数(如inet_addr
    • string.h:提供字符串处理函数(如memset
  • 命名空间:使用std命名空间以简化标准库的使用

2. 主函数与参数解析

int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        perror("Usage:./ a.out ip port");
        return -1;
    }

    string server_ip = argv[1];
    uint16_t server_port = stoi(argv[2]);
    ...
}

  • 参数检查:程序需要两个参数(服务器 IP 和端口)
  • 参数解析
    • argv[1]:服务器 IP 地址(如 "127.0.0.1")
    • argv[2]:服务器端口号(如 "8080")

3. 创建 UDP 套接字

int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0)
{
    perror("create socket error");
    return 2;
}

  • 套接字创建
    • AF_INET:使用 IPv4 协议
    • SOCK_DGRAM:使用 UDP 协议(无连接、不可靠、数据报)
    • 0:默认协议
  • 错误处理:如果套接字创建失败,输出错误信息并退出

4. 配置服务器地址

struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(server_port);
server.sin_addr.s_addr = inet_addr(server_ip.c_str());

  • 地址结构初始化
    • sockaddr_in:IPv4 地址结构
    • memset:将结构清零
    • sin_family:协议族(AF_INET)
    • sin_port:端口号(使用htons转换为网络字节序)
    • sin_addr.s_addr:IP 地址(使用inet_addr转换为二进制格式)

5. 消息循环

while (true)
{
    // 3.1.输入消息
    string input;
    cout << " Please input message: ";
    getline(cin, input);

    // 3.2.发送消息
    int n = sendto(sockfd, intput.c_str(), input.size(), 0, (struct sockaddr *)server, sizeof(server));

    // 3.3.接收响应
    char buffer[1024];
    struct sockaddr_in peer;
    socklen_t len = sizeof(peer);
    int m = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)peer, &len);
}

  • 发送消息
    • sendto:向指定地址发送数据
    • 参数:套接字、数据指针、数据长度、标志位、目标地址、地址长度
  • 接收响应
    • recvfrom:接收来自任意地址的数据
    • 参数:套接字、缓冲区、缓冲区大小、标志位、发送方地址(输出参数)、地址长度(输入 / 输出参数)

问题:client要不要bind?

需要bind. 

client要不要显式的bind?

不要

首次发送消息,OS会自动给client进行bind,OS知道IP,端口号采用随机端口号的方式

为什么?

一个端口号,只能被一个进程bind,为了避免client端口冲突 client端的端口号是几,不重要,只要是唯一的就行!

完整代码:

#include <iostream>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>

using namespace std;

// ./ a.out 127.0.0.1 8080
int main(int argc, char *argv[])
{

    if (argc != 3)
    {
        perror("Usage:./ a.out ip port");
        return -1;
    }

    string server_ip = argv[1];
    uint16_t server_port = stoi(argv[2]);

    // 1.创建套接字
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0)
    {
        perror("create socket error");
        return 2;
    }

    // 问题:client要不要bind?需要bind.
    //   答:client要不要显式的bind?不要!!首次发送消息,OS会自动给client进行bind,OS知道IP,端口号采用随机端口号的方式
    //   为什么?一个端口号,只能被一个进程bind,为了避免client端口冲突
    //   client端的端口号是几,不重要,只要是唯一的就行!

    // 2.填写服务器信息
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(server_port);
    server.sin_addr.s_addr = inet_addr(server_ip.c_str());

    // 3.发送数据
    while (true)
    {

        // 3.1.输入消息
        string input;
        cout << " Please input message: ";
        getline(cin, input);

        int n = sendto(sockfd, intput.c_str(), input.size(), 0, (struct sockaddr *)server, sizeof(server));

        // 3.2.接收服务器的响应
        char buffer[1024];
        struct sockaddr_in peer; // 接受是哪个服务器的响应
        socklen_t len = sizeof(peer);
        int m = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)peer, &len);
    }
}

2.2.V2版本  -- Translate

这个翻译版本其实就是基于V1版本做出一些·改进

有如下的改进:

1.UdpServer.hpp

核心改变其实就是服务器将收到的数据传给回调函数,再将回调函数返回来的返回值,返回给用户,其余代码都是没有改变的

给类型起别名:

增加一个回调函数成员变量:

在构造函数的时候进行初始化:

2.UdpServer.cc

加载字典+lambda表达式定义回调函数

3.Dict

新增一个类用于翻译工作

#pragma once
#include <unordered_map>
#include <string>
#include <iostream>
#include <fstream>
using namespace std;

const string defaultdict = "./dictionary.txt"; // 默认字典文件路径
const string sep = ": ";                       // 字典文件中每行的分隔符(中文和英文都用:分隔)

class Dict
{

public:
    Dict(const string &path = defaultdict)
        : _dict_path(path)
    {
    }

    // 1.首先加载字典文件
    bool LoadDict()
    {

        // 1.1 打开文件
        ifstream in(_dict_path);
        if (!in.is_open())
        {
            cout << "Failed to open dictionary file." << endl;
            return false;
        }

        // 1.2 读取文件内容
        string line;
        while (getline(in, line))
        {

            // 1.3找到分隔符位置
            auto pos = line.find(sep);
            if (pos == string::npos) // 找不到分隔符
            {
                continue;
            }

            string english = line.substr(0, pos);           // 英文单词
            string chinese = line.substr(pos + sep.size()); // 中文释义

            // 1.4 加入字典
            if (english.empty() || chinese.empty())
            {
                continue;
            }

            _dict[english] = chinese;
        }

        in.close();
        return true;
    }

    string Translate(const string &word)
    {
        auto it = _dict.find(word); // find:返回的的迭代器
        if (it == _dict.end())
        {
            return "not found";
        }

        return it->second;
    }

    ~Dict()
    {
    }

private:
    string _dict_path;                   // dictionary path
    unordered_map<string, string> _dict; // dictionary
};

  

2.3.V3版本   群聊窗口(多线程实现)

这个版本依旧是基于第一版实现的

所作出的改变有下面几方面:

1.新增一个类,用于网络地址和主机地址转化的类:

1. 类的定义与成员变量

class InetAddr{
public:
    // 构造函数等成员函数...
private:
    struct sockaddr_in _addr;// 网络地址结构体
    string _ip;// 主机地址字符串
    uint16_t _port;// 端口号
};

  • struct sockaddr_in _addrsockaddr_in 是 Linux 下用于表示 IPv4 网络地址的结构体,包含了地址族(一般为 AF_INET 表示 IPv4)、端口号(网络字节序)、IP 地址(网络字节序的二进制形式)等信息,这里用来存储原始的网络地址数据。
  • string _ip:用于存储转换后的、方便人类阅读的点分十进制形式的 IP 地址字符串,比如 192.168.1.100 。
  • uint16_t _port:存储转换后的主机字节序的端口号,端口号在网络传输中使用网络字节序,这里转换成主机字节序方便程序内部使用和展示 。

2. 构造函数

InetAddr(struct sockaddr_in addr):_addr(addr)
{
    _port=ntohs(_addr.sin_port);  // 网络字节序转主机字节序
    _ip=inet_ntoa(_addr.sin_addr);// 将网络字节序的IP地址转换为字符串
}

  • 这是一个带参数的构造函数,参数是 struct sockaddr_in 类型的网络地址结构体。
  • 初始化列表 _addr(addr) 用于初始化成员变量 _addr,将传入的网络地址结构体赋值给类内部的 _addr
  • 函数体中:
    • _port = ntohs(_addr.sin_port);ntohs 函数(network to host short 的缩写 )的作用是将网络字节序的 16 位整数(这里是端口号,因为端口号一般用 16 位表示 )转换成主机字节序。因为在网络传输中,数据是以网络字节序(大端序)进行传递的,而不同主机的主机字节序可能不同(比如有的是小端序),所以需要转换后才能在本地程序中正确使用和展示端口号。
    • _ip = inet_ntoa(_addr.sin_addr);inet_ntoa 函数(internet address to ASCII 的缩写 )用于将网络字节序的 IPv4 地址(struct in_addr 类型,这里是 _addr.sin_addr )转换成点分十进制形式的字符串,方便人们阅读和使用,结果赋值给 _ip 成员变量存储起来。

3. 成员函数

(1)Port 函数

uint16_t Port(){
    return _port;
}
  • 这是一个获取端口号的成员函数,返回已经转换为主机字节序的端口号 _port,供外部调用者获取端口信息,比如在需要打印端口或者基于端口做一些判断逻辑时使用。

(2)IP 函数

string IP(){
    return _ip;
}

  • 用于获取转换后的点分十进制形式的 IP 地址字符串 _ip,方便外部获取 IP 地址信息,像展示客户端或服务器的 IP 地址时就可以调用这个函数。

(3)NetAddr 函数

const struct sockaddr_in& NetAddr(){
    return _addr;
}

  • 返回 _addr 的常量引用,_addr 是存储原始网络地址结构体(sockaddr_in 类型 )的成员变量。这样做的目的是在一些需要使用原始网络地址结构体的场景(比如调用 sendtorecvfrom 等套接字函数时,需要传入 sockaddr_in 类型的地址参数 ),可以直接获取到该结构体,同时用 const 和引用保证了原始数据不会被意外修改,并且避免了拷贝开销。

(4)operator== 重载函数

bool operator==(const InetAddr& other){
    return other._addr.sin_port==_addr.sin_port&&other._addr.sin_addr.s_addr==_addr.sin_addr.s_addr;
}

  • 重载了 == 运算符,用于比较两个 InetAddr 对象是否代表同一个网络地址。比较的依据是它们的端口号(sin_port )和 IP 地址(sin_addr.s_addr ,这里比较的是网络字节序的 IP 地址二进制值 )是否都相等。在一些场景中,比如判断某个客户端是否已经在在线用户列表中(像你之前提到的 Route 类里的用户管理逻辑 ),就可以使用这个运算符来比较两个 InetAddr 对象是否相同。

(5)StringAddr 函数

string StringAddr(){
    return _ip+":"+std::to_string(_port);
}

  • 用于将 IP 地址和端口号拼接成一个字符串,格式为 IP:端口 ,比如 192.168.1.100:8080 ,方便在需要展示完整网络地址(IP + 端口)的场景下使用,像打印客户端的地址信息、记录日志等。

4. 类的作用与使用场景

这个 InetAddr 类主要是为了在网络编程中更方便地处理和使用网络地址信息。在基于套接字的编程(比如 UDP 服务器 / 客户端、TCP 服务器 / 客户端 )中,经常需要获取、转换、比较网络地址(IP 和端口 ),通过这个类的封装,可以把相关的操作集中起来,让代码更简洁、清晰,也提高了代码的复用性。例如在你之前的 Route 类中,就可以使用 InetAddr 来管理客户端的地址信息,方便进行在线用户的判断、消息广播时的地址使用等操作 。

总之,InetAddr 类通过对 sockaddr_in 结构体的封装和一系列辅助函数的提供,简化了网络地址在程序中的处理流程,是网络编程中常用的一种封装方式,让开发者可以更方便地操作 IP 地址和端口相关的数据 。

#include <iostream>
#include <string>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
// 网络地址和主机地址之间进行转换的类

using namespace std;


class InetAddr{

    public:

    InetAddr(struct sockaddr_in addr):_addr(addr)
    {
        _port=ntohs(_addr.sin_port);  // 网络字节序转主机字节序
        _ip=inet_ntoa(_addr.sin_addr);// 将网络字节序的IP地址转换为字符串

    }

    uint16_t Port(){
        return _port;
    }

    string IP(){
        return _ip;
    }

    const struct sockaddr_in& NetAddr(){
        return _addr;
    }
    bool operator==(const InetAddr& other){
        return other._addr.sin_port==_addr.sin_port&&other._addr.sin_addr.s_addr==_addr.sin_addr.s_addr;
    }

    string StringAddr(){
        return _ip+":"+std::to_string(_port);
    }

    


    private:
    struct sockaddr_in _addr;// 网络地址结构体
    string _ip;// 主机地址字符串
    uint16_t _port;// 端口号

};

UdpServer.hpp

依旧是将收到的信息给个回调函数,但是不需要服务器自己去给用户发送消息,这个工作交给回调函数中的Route(路由类来实现)

新增的第二个类,用于给开机用户群发消息:Route(路由)

1. 类设计与核心功能

Route 类负责:

  • 维护在线用户列表(_online_users)。
  • 处理客户端消息,自动识别新用户并添加到列表。
  • 向所有在线用户广播消息(包括发送者)。
  • 处理用户退出请求(当消息为 "QUIT" 时)。

2. 关键成员变量

private:
    vector<InetAddr> _online_users; // 在线用户列表
  • _online_users:存储所有当前在线用户的地址信息(InetAddr 类型)。
  • 每个 InetAddr 对象代表一个客户端,包含 IP 地址和端口号。

3. 私有辅助方法

(1)用户存在检查

bool IsExist(const netAddr &peer)
{
    for (auto &user : _online_users)
    {
        if (user == peer)
        {
            return true;
        }
    }
    return false;
}
  • 遍历 _online_users,检查指定用户是否已在线。
  • 通过 InetAddr 类的 == 运算符进行比较。

(2)添加用户

void AddUser(const InetAddr &peer)
{
    _online_users.push_back(peer);
}
  • 将新用户添加到在线列表末尾。

(3)删除用户

for (auto it = _online_users.begin(); it != _online_users.end(); ++it) {
    if (*it == peer) {
        _online_users.erase(it);
        return;
    }
}

4. 核心方法:消息路由处理

void MessageRoute(int socket, const string &message, const InetAddr &perr)
{
    // 首先判断这个客户端是否在队列中,不在则加入队列
    if (!IsEXist(peer))
    {
        AddUser(peer);
    }

    string SendMessage = peer.StringAddr() + "# " + message;

    // 向在线用户广播消息
    for (quto &user : _online_users)
    {
        sendto(socket, SendMessage.c_str(), SendMessage.size(), 0, 
               (const struct sockaddr *)&(user.NetAddr()), sizeof(user.NetAddr()));
    }

    // 这个用户一定时在线的
    if (message == "QUIT")
    {
        DELETE(peer);
    }
}

功能流程

  1. 用户管理

    • 检查发送者是否在线,若不在则添加到 _online_users
    • 这意味着首次发送消息即视为用户登录
  2. 消息组装

    • 将发送者地址(peer.StringAddr())和原始消息拼接,格式为 地址# 消息内容
    • 例如:192.168.1.100:5000# Hello, world!
  3. 广播消息

    • 存在问题:循环变量 quto 是拼写错误,应为 auto
    • 使用 sendto 函数向所有在线用户(包括发送者自身)发送组装后的消息。
    • user.NetAddr() 返回 sockaddr_in 类型的地址结构。
  4. 退出处理

    • 若收到 "QUIT" 消息,调用 DELETE 方法将发送者从在线列表移除。

完整代码实现:

#pragma once
#include <iostream>
#include <string>
#include <vector>
#include "InetAddr.hpp"

using namespace std;

class Route
{

private:
    bool IsExist(const netAddr &peer)
    {
        for (auto &user : _online_users)
        {
            if (user == peer)
            {
                return true;
            }
        }
        return false;
    }

    void AddUser(const InetAddr &peer)
    {
        _online_users.push_back(peer);
    }

    void DELETE(const InetAddr &peer)
    {
        for (auto &user : _online_users)
        {
            if (user == peer)
            {
                _online_users.erase(user);
                return;
            }
        }
    }

public:
    Route()
    {
    }

    void MessageRoute(int socket, const string &message, const InetAddr &perr)
    {
        // 首先判断这个客户端是否在队列中,不在则加入队列'
        if (!IsEXist(peer))
        {
            AddUser(peer);
        }

        string SendMessage = peer.StringAddr() + "# " + message;

        // 向在线用户广播消息
        for (quto &user : _online_users)
        {
            sendto(socket, SendMessage.c_str(), SendMessage.size(), 0, (const struct sockaddr *)&(user.NetAddr()), sizeof(user.NetAddr()));
        }

        // 这个用户一定时在线的
        if (message == "QUIT")
        {
            DELETE(peer);
        }
    }

    ~Route()
    {
    }

private:
    // 首次给我发消息,就等同于登入
    vector<InetAddr> _online_users; // 在线用户列表
};

UdpClient.cc

服务器也要采用多线程的形式,我们创建的是群聊。不然会进行阻塞,导致无法接收到服务器的响应

发送线程:
 

void send()
{

    // 1.填写服务器信息
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(server_port);
    server.sin_addr.s_addr = inet_addr(server_ip.c_str());

    // 2.给服务器发送数据,表示我们要建立连接
    string onlin = "inline";
    sendto(sockfd, online.c_str(), online.size(), 0, (struct sockaddr *)&server, sizeof(server));

    while (true)
    {

        // 2.正式发送数据
        string input;
        cout << " Please input message: ";
        getline(cin, input);

        // 3.发送数据
        int n = sendto(sockfd, input.c_str(), input.size(), 0, (struct sockaddr *)server, sizeof(server));
        void(n);
        if (input == "QUIT")
        {
            break;
        }
    }

接受线程:

void receive()
{

    // 接收服务器的响应

    char buffer[1024];       // 接收缓冲区
    struct sockaddr_in peer; // 接受是哪个服务器的响应

    socklen_t len = sizeof(peer);

    while (true)
    {
        int m = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)peer, &len);

        if (m > 0)
        {
            buffer[m] = 0;
            cout << "Server response: " << buffer << endl;
        }
    }
}

UdpServer.cc

核心代码就是定义回调函数,将多线程和群发功能进行融合

1.定义任务类型

2.创建 Route 类实例 route,通过单例模式获取线程池实例 tp

1.创建 UdpServer:使用 std::make_unique 创建 UdpServer 实例,绑定到指定端口 _por

  • 回调函数

    • 当 UdpServer 收到客户端消息时,触发 lambda 回调。
    • lambda 捕获 Route 实例 route(用于处理业务逻辑)和线程池 tp(用于提交任务)。
    • 参数说明:
      • sockfd:UDP 套接字描述符(用于发送响应)。
      • Message:客户端发送的消息内容。
      • peer:客户端的地址信息(InetAddr 类型,包含 IP 和端口)。
  • 任务封装

    • 使用 std::bind 将 Route::MessageRoute 绑定到 route 的实例,并传递参数 sockfdMessagepeer
    • 生成的 task_t 是一个可调用对象,表示 “执行 route.MessageRoute(sockfd, Message, peer)”。
  • 任务提交tp->Enqueue(task) 将任务提交给线程池,由线程池异步执行。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值