UDP网络编程:从入门到精通

文章目录

  • [1. Echo server](#1. Echo server)
    • [1.1 功能介绍](#1.1 功能介绍)
    • [1.2 Socket 系统调用](#1.2 Socket 系统调用)
    • [1.3 demo v1](#1.3 demo v1)
      • [1.3.1 源码](#1.3.1 源码)
      • [1.3.2 实验结果](#1.3.2 实验结果)
      • [1.3.3 流程详解](#1.3.3 流程详解)
    • [1.4 demo v2](#1.4 demo v2)
  • [2. DictServer](#2. DictServer)
    • [2.1 功能介绍](#2.1 功能介绍)
    • [2.2 源码](#2.2 源码)
    • [2.3 实验结果](#2.3 实验结果)
  • [3. 简单聊天室](#3. 简单聊天室)
    • [3.1 源码](#3.1 源码)

本文主要是关于UDP网络通信编程的三个demo,通过这三个demo完全掌握udp网络通信。

1. Echo server

1.1 功能介绍

功能:简单的回显服务器和客戶端代码,客户端向服务器发送消息,服务器接受消息之后向客户端回显。

解释:写三个文件,将来分别生成两个可执行程序,客户端和服务端。

客户端向服务器发送消息,服务端接受消息,然后回显给客户端输入的内容。

1.2 Socket 系统调用

网络通信就要打开网络文件,打开方式: socket套接字

Q:啥是套接字?套接字是干啥用的?

A:

套接字 = ip + port

Socket 是操作系统分配的特殊文件描述符(文件指针),应用程序通过读写这个"文件",

操作系统会把数据交给网卡,送到目标主机的「IP + 端口」对应的 Socket(文件描述符)指向的文件中;

接收方的进程也通过自己的Socket(文件描述符)读取数据。

Socket 套接字的系统调用是操作系统提供的网络编程接口(POSIX 标准),主要用于实现进程间的网络通信(跨主机/本机)。

1.2.1 socket():创建套接字文件描述符

功能

创建一个套接字(本质是一个文件描述符),是所有网络编程的第一步,用于后续的网络通信。

函数原型
c 复制代码
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
参数解析
参数 说明
domain 地址族(协议族):表明通信方式 - AF_INET:IPv4 网络协议(最常用) - AF_INET6:IPv6 - AF_UNIX:本地进程通信
type 套接字类型: - SOCK_DGRAM:UDP(无连接、不可靠、面向数据报) - SOCK_STREAM:TCP(有连接、可靠、面向字节流)
protocol 具体协议: - 0:根据 domain+type 自动选择(UDP/TCP 填 0 即可) - IPPROTO_UDP/IPPROTO_TCP:显式指定
返回值
  • 成功:返回一个非负整数(套接字文件描述符,类似文件的 fd);
  • 失败:返回 -1,并设置 errno(可通过 perror() 打印错误)。
示例(UDP 场景)
cpp 复制代码
// 创建 IPv4 + UDP 套接字
int sock_fd = socket(AF_INET, SOCK_DGRAM, 0);
if (sock_fd < 0) {
    perror("socket create failed"); // 打印错误信息
    return -1;
}

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

功能

将创建的套接字与本地 IP 地址 + 端口号绑定,让操作系统知道该套接字要监听哪个端口的网络数据。

  • UDP 服务器必须绑定端口(否则客户端无法定位服务器);
  • UDP 客户端一般不绑定(系统自动分配临时端口)。
函数原型
c 复制代码
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数解析
参数 说明
sockfd socket() 返回的套接字文件描述符
addr 指向 sockaddr 结构体的指针(实际用 sockaddr_in(IPv4)强转)
addrlen addr 结构体的长度(sizeof(struct sockaddr_in)
关键结构体:sockaddr_in(IPv4 地址)
cpp 复制代码
#include <netinet/in.h>
struct sockaddr_in {
    sa_family_t    sin_family; // 协议族,固定为 AF_INET,表示用哪个协议
    in_port_t      sin_port;   // 端口号(需转网络字节序:htons())
    struct in_addr sin_addr;   // IP 地址(in_addr 仅包含一个 uint32_t 成员 s_addr)
};

// 常用 IP 地址设置:
// - INADDR_ANY:绑定所有本机网卡(0.0.0.0),接收任意网卡的数据包
// - inet_addr("192.168.1.100"):绑定指定 IP
返回值
  • 成功:返回 0;
  • 失败:返回 -1,并设置 errno(常见错误:端口被占用 → EADDRINUSE)。
示例(UDP 服务器绑定)
cpp 复制代码
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr)); // 初始化结构体
server_addr.sin_family = AF_INET;            // IPv4
server_addr.sin_addr.s_addr = INADDR_ANY;    // 监听所有网卡
server_addr.sin_port = htons(8080);          // 端口转网络字节序(主机→网络)

// 绑定
if (bind(sock_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
    perror("bind failed");
    close(sock_fd);
    return -1;
}

1.2.3 recvfrom():UDP 接收数据

功能

从 UDP 套接字接收数据,并获取发送方的 IP 地址和端口(UDP 无连接,每次接收都要知道对方地址才能回复)。

函数原型
c 复制代码
#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 接收数据的缓冲区(char 数组)
len 缓冲区长度(注意留 1 字节给 '\0',避免字符串越界)
flags 接收标志:0(阻塞接收,默认);MSG_DONTWAIT(非阻塞)
src_addr 输出参数:存储发送方(客户端)的地址结构体(sockaddr_in)
addrlen 输入输出参数:传入 sizeof(sockaddr_in),返回实际地址长度
返回值
  • 成功:返回接收到的字节数;
  • 失败:返回 -1(如中断 → EINTR,可重试);
  • 0:无实际意义(UDP 无连接,不会返回 0)。
示例
cpp 复制代码
char buffer[1024] = {0};
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);

// 阻塞接收数据
ssize_t recv_len = recvfrom(sock_fd, buffer, sizeof(buffer)-1, 0,
                           (struct sockaddr*)&client_addr, &client_len);
if (recv_len < 0) {
    perror("recvfrom failed");
    continue;
}
buffer[recv_len] = '\0'; // 确保字符串结束
// 转换客户端 IP 为字符串(网络字节序→点分十进制)
char client_ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, INET_ADDRSTRLEN);
std::cout << "收到 " << client_ip << ":" << ntohs(client_addr.sin_port) 
          << " 的数据:" << buffer << std::endl;

1.2.4 sendto():UDP 发送数据

功能

向指定的 IP + 端口发送 UDP 数据(UDP 无连接,每次发送都要指定目标地址)。

函数原型
c 复制代码
#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 要发送的数据长度(strlen(buf)
flags 发送标志:0(默认)
dest_addr 目标地址结构体(客户端/服务器的 sockaddr_in)
addrlen 目标地址结构体长度(sizeof(sockaddr_in)
返回值
  • 成功:返回发送的字节数;
  • 失败:返回 -1,并设置 errno
示例(回复客户端)
cpp 复制代码
const char* reply = "收到你的消息!";
// 用 recvfrom 获取的 client_addr 作为目标地址
ssize_t send_len = sendto(sock_fd, reply, strlen(reply), 0,
                         (struct sockaddr*)&client_addr, client_len);
if (send_len < 0) {
    perror("sendto failed");
}

1.2.5 close():关闭套接字

功能

关闭套接字文件描述符,释放系统资源(如端口、文件描述符)。

  • 必须调用,否则会导致文件描述符泄漏
  • unique_ptr 管理的对象析构时调用 close() 是最佳实践(如你之前的 UdpServer 析构函数)。
函数原型
c 复制代码
#include <unistd.h>
int close(int fd);
参数/返回值
  • fd:套接字文件描述符;
  • 成功返回 0,失败返回 -1。
示例
cpp 复制代码
~UdpServer() {
    if (sock_fd_ != -1) {
        close(sock_fd_); // 析构时自动关闭
        sock_fd_ = -1;   // 标记为已关闭,避免重复关闭
    }
}

1.2.6 辅助函数(网络字节序转换)

Socket 通信要求端口/IP 必须是网络字节序(大端序),而主机字节序可能是小端(x86 架构),因此需要转换:

函数 功能 适用场景
htons() 主机字节序 → 网络字节序 端口号转换(16 位)
ntohs() 网络字节序 → 主机字节序 解析收到的端口号
htonl() 主机字节序 → 网络字节序 IP 地址转换(32 位)
ntohl() 网络字节序 → 主机字节序 解析收到的 IP 地址
inet_pton() 点分十进制 → 网络字节序 字符串 IP 转二进制
inet_ntop() 网络字节序 → 点分十进制 二进制 IP 转字符串
示例
cpp 复制代码
// 端口转换:主机 8080 → 网络字节序
uint16_t port = htons(8080);
// IP 转换:字符串 "127.0.0.1" → 网络字节序
struct in_addr addr;
inet_pton(AF_INET, "127.0.0.1", &addr);
// 反向转换:网络字节序 IP → 字符串
char ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &addr, ip, sizeof(ip)); // ip = "127.0.0.1"

1.3 demo v1

1.3.1 源码

Server.hpp

cpp 复制代码
#pragma once
#include <iostream>
#include <string>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Log.hpp"

using namespace LogModule;
const int defaultfd = -1;
class UdpServer
{

public:
    UdpServer(const std::string &ip, uint16_t port)
        : _sockfd(defaultfd),
          _ip(ip),
          _port(port)
    {
    }

    void Init()
    {
        // 1. 创建套接字
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockfd < 0)
        {
            LOG(LogLevel ::FATAL) << "socket error !";
            exit(1);
        }
        LOG(LogLevel::INFO) << "socket success, sockfd: " << _sockfd;

        // 2. 绑定 socket信息,主要是ip和端口号
        // 2.1 先填充结构体,供bind使用
        struct sockaddr_in local;
        bzero(&local, sizeof(local)); // 先清零

        local.sin_family = AF_INET; // 填协议家族

        local.sin_port = htons(_port); // 填端口号,需要将本地主机序列转化为网络序列

        // 填IP,需要先把IP字符串转成四字节格式(32位无符号整数),
        // 然后四字节再转成网络序列,还好库已经提供实现转化的接口
        local.sin_addr.s_addr = inet_addr(_ip.c_str());

        // 2.2 绑定,注意第二个参数,为了更通用所以传基类,所以强转
        int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));

        // 2.3 判断是否绑定成功
        if (n < 0)
        {
            LOG(LogLevel ::FATAL) << "bind error!";
            exit(2);
        }
        // 绑定成功
        LOG(LogLevel ::INFO) << "bind success, sockfd: " << _sockfd;
    }

    void Start()
    {
        _isrunning = true;
        while (_isrunning)
        {
            // 1. 读取,收消息
            char buffer[1024];
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);

            size_t s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);

            if (s > 0)
            {
                buffer[s] = 0;
                LOG(LogLevel ::DEBUG) << "buffer: " << buffer;
                // 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; // 本地服务器用的是点分十进制风格,字符串
    bool _isrunning;
};

Client.cc

cpp 复制代码
#include <iostream>
#include <sys/socket.h>
#include <sys/types.h>
#include <string>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>

// 使用格式: ./upclient server_ip server_port
int main(int argc, char *argv[])
{
    // 预备工作
    if (argc != 3)
    {
        std::cerr << "Usage: " << argv[0] << " server_ip server_port" << std::endl;
        return 1;
    }

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

    // 访问服务器
    // 1. 创建套接字
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0)
    {
        std::cerr << "socket error" << std ::endl;
        return 2;
    }

    // 2. 2.1绑定。意义是将本地的ip,port和上面的文件关联。
    // client不需要显示绑定,首次发送消息,OS会自动给client进行绑定,
    // 操作系统知道客户端IP,端口号随机分配 。
    // 不显示绑定的原因:为了避免client端口冲突。
    // client的端口号是几不重要,只要是唯一的就行。
    // 2.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());

    while (true)
    {
        std::string input;
        std::cout << "Please inpute: ";
        std::getline(std::cin, input);
        int n = sendto(sockfd, input.c_str(), input.size(), 0, (struct sockaddr *)&server, sizeof(server));
        (void)n;

        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);
        if (m > 0)
        {
            buffer[m] = 0;
            std::cout << buffer << std::endl;
        }
    }

    return 0;
}

Server.cc

cpp 复制代码
#include <iostream>
#include <memory>
#include "UdpServer.hpp"

// 使用格式 ./udpserver ip port
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        std::cerr << "Usage: " << argv[0] << " ip port" << std::endl;
        return 1;
    }

    std::string ip = argv[1];
    uint16_t port = std::stoi(argv[2]);
    Enable_Console_Log_Strategy();

    std::unique_ptr<UdpServer> user = std::make_unique<UdpServer>(ip, port);
    user->Init();
    user->Start();

    return 0;
}

1.3.2 实验结果

1.3.3 流程详解

先运行UdpServer.cc,运行格式必须为

复制代码
./udpserver ip port

进行参数判断,第一个参数是程序名字,

第二个参数是ip地址,

第三个参数是端口号。

如果不是该格式运行,提示运行错误。

往下,创建ip字符串(因为是点分十进制存储,所以是字符串类型),赋值参数

创建port,看到类型是uint16_t,我们转到定义查看

发现实际上就是一个短整型。赋值要转为int

然后,使用智能指针创建服务器对象,user。

我们转到UdpServer.hpp

先看一下成员变量,分别是_sockfd(套接字文件描述符)。

有socket套接字的文件描述符,服务端的IP和端口,还有服务端是否运行的标志位。

构造函数

有默认的_sockfd,需要输入ip 和 端口号。

创建user对象的时候已经输入参数,这个参数是调用程序的时候传的

至此对象创建完毕,调用对象的init方法。

首先创建套接字,指定通信方式,说明通信类型,自动选择协议。

创建成功返回文件描述符。失败打印错误信息。

下一步:绑定socket信息,绑定的目的是:

Socket 本身是操作系统内核里的一个 "通信端点" 结构体,bind()是你(应用程序)告诉操作系统:

"请把「本机 IP + 端口」分配给这个 Socket,以后所有发往这个 IP + 端口的数据包,都交给我这个进程处理;我从这个 Socket 发出去的数据包,也用这个 IP + 端口作为源地址。"

简单说:bind()是应用程序向操作系统 "认领" IP + 端口,而非 socket "主动知道"。

socket相当于一个窗口,或者文件指针:而这个文件被一个特定进程写的,所以要绑定IP和port

在绑定之前要先填写结构体

填写结构体之前先清零,用的bzero函数。然后填写家族协议,IP,端口号。

对于端口号,需要将本地主机序列转化为网络序列

填IP,需要先把IP字符串转成四字节格式(32位无符号整数),然后四字节再转成网络序列,还好库已经提供实现转化的接口

填写完结构体,进行绑定,提供参数,进程绑定的套接字(窗口/文件),服务端的信息(IP, port),信息长度。

最后检查是否绑定成功。

随后调用Start函数。运行服务端(是一个死循环)。

使用recvfrom函数接受来自指定socket窗口的数据。

填写参数:文件描述符(指明要从哪个窗口(文件)拿数据),buffer(拿到数据之后放在哪里),第三个参数(缓冲区大小),第四个参数:接受数据的方式(阻塞?非阻塞?),第五个参数:类型sockaddr_in实际要强转成sockaddr,是一个输出型参数,用于带回发送者的信息(IP,port),第六个参数是结构体的大小

recvfrom的peer是输出型参数,用来记录 "数据是谁发的"(服务器的 IP + 端口)
!!!学到这里完全明白套接字的意义了,看作是窗口就太棒了!

关于sockaddr_in

实际上就是

ok,已经从网卡中拿到数据传给了对应的socket窗口,又通过socket传给了对应的进程。

现在要实现服务器给客户端回显。

判断是否接受数据成功,将缓冲区的最后一个数据的下一个数据赋值0。

拼接字符串,目的是格式化输出。

最后使用sendto函数,发送给对应的主机进程。

参数是:要发的socket窗口,要发的内容,发送内容的大小,发送的对象信息(通过recvfrom带回来的)

Q:recvfrom为什么能带回来?为啥他就知道要从网卡中拿哪个数据?

A:因为有sockfd啊,已经给指明窗口了!!这里设计确实妙。

ok,这里服务端就设计完了。

下面看用户端:

首先指定客户端进程调用的格式,必须传ip和对应端口号。

创建套接字

先填写服务器结构体信息,这里使用memset清零。填进来服务器 对应的IP和port。

这里不用和socket进行显示绑定。

原因:client不需要显示绑定。首次客户端向服务端发送消息时,OS会自动给client进行绑定,操作系统知道客户端IP,对于端口号要采用随机分配 。

不显示绑定的原因:为了避免client端口冲突。

那为什么服务器要绑定?

核心原因

  1. 服务端:是「被动被访问方」,必须绑定固定端口作为"唯一寻址标识",让所有客户端能精准定位到该进程;若不绑定,客户端无固定目标,无法建立通信。
  2. 客户端:是「主动访问方」,发起通信时操作系统会自动分配临时端口,服务端可通过数据包中的"客户端源端口"回复数据,无需固定端口。

嗷!懂了,对于服务器来说,客户端的信息本来就是知道的,因为是客户端主动发起的通信。

emmm。。。咋能问出这问题,服务端不绑定的话客户端就不知道访问谁了呀。

完事儿,向服务器发送信息:

同样是一个死循环,从cin流中获取数据放在input中。

调用sendto函数,将信息发送给特定服务器。

然后再从对应socket窗口获取数据。再多说一下peer参数,他是一个输出型参数,在这里就存放的是服务器的信息,放在这里其实没啥用,起一个占位的作用,因为我们客户端已经知道服务器信息了。

终于弄懂了...


1.4 demo v2

优化代码,首先搞一个问题

Q:服务器需不需要手动绑定IP?

A:"服务器端不建议手动绑定特定IP"

核心原因是限制了服务器的访问范围,降低了灵活性和兼容性 ------手动绑一个具体IP(比如192.168.1.100),会导致服务器只能接收发往这个IP的请求,而绑定INADDR_ANY(0.0.0.0)能接收本机所有网卡的请求,这是服务器的最佳实践。

  1. 多网卡场景兼容 :服务器可能有多个网卡(有线/无线/内网/外网IP),绑特定IP只会响应该网卡的请求,其他网卡的请求会被丢弃;绑INADDR_ANY能接收所有网卡的请求。
  2. IP变动适配 :服务器IP可能因网络配置(如DHCP、云服务器弹性IP)变化,绑特定IP会导致IP变了之后服务不可用,INADDR_ANY无需修改代码。
  3. 部署简化:无需提前知道服务器的具体IP,代码可直接跨环境部署(开发/测试/生产),不用因IP不同改配置。

嗷 懂了 服务器可能有多个IP 如果绑特定的 就会丢弃其他IP收到的信息

完整代码:
sever.hpp

cpp 复制代码
#pragma once
#include <iostream>
#include <string>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Log.hpp"

using namespace LogModule;
const int defaultfd = -1;
class UdpServer
{

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

    void Init()
    {
        // 1. 创建套接字
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockfd < 0)
        {
            LOG(LogLevel ::FATAL) << "socket error !";
            exit(1);
        }
        LOG(LogLevel::INFO) << "socket success, sockfd: " << _sockfd;

        // 2. 绑定 socket信息,主要是ip和端口号
        // 2.1 先填充结构体,供bind使用
        struct sockaddr_in local;
        bzero(&local, sizeof(local)); // 先清零

        local.sin_family = AF_INET; // 填协议家族

        local.sin_port = htons(_port); // 填端口号,需要将本地主机序列转化为网络序列

        // 填IP,需要先把IP字符串转成四字节格式(32位无符号整数),
        // 然后四字节再转成网络序列,还好库已经提供实现转化的接口
        // local.sin_addr.s_addr = inet_addr(_ip.c_str());
        local.sin_addr.s_addr = INADDR_ANY;

        // 2.2 绑定,注意第二个参数,为了更通用所以传基类,所以强转
        int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));

        // 2.3 判断是否绑定成功
        if (n < 0)
        {
            LOG(LogLevel ::FATAL) << "bind error!";
            exit(2);
        }
        // 绑定成功
        LOG(LogLevel ::INFO) << "bind success, sockfd: " << _sockfd;
    }

    void Start()
    {
        _isrunning = true;
        while (_isrunning)
        {
            // 1. 读取,收消息
            char buffer[1024];
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);

            size_t s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);

            if (s > 0)
            {
                int peer_port = ntohs(peer.sin_port);
                std::string peer_ip = inet_ntoa(peer.sin_addr);

                buffer[s] = 0;
                LOG(LogLevel ::DEBUG) << "[" << peer_ip << ":" << peer_port << "]# " << buffer;
                // 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; // 本地服务器用的是点分十进制风格,字符串
    bool _isrunning;
};

server.cc

cpp 复制代码
#include <iostream>
#include <memory>
#include "UdpServer.hpp"

// 使用格式 ./udpserver ip port
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        std::cerr << "Usage: " << argv[0] << " port" << std::endl;
        return 1;
    }

    // std::string ip = argv[1];
    uint16_t port = std::stoi(argv[1]);
    Enable_Console_Log_Strategy();

    std::unique_ptr<UdpServer> user = std::make_unique<UdpServer>(port);
    user->Init();
    user->Start();

    return 0;
}

client.cc

cpp 复制代码
#include <iostream>
#include <sys/socket.h>
#include <sys/types.h>
#include <string>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>

// 使用格式: ./upclient server_ip server_port
int main(int argc, char *argv[])
{
    // 预备工作
    if (argc != 3)
    {
        std::cerr << "Usage: " << argv[0] << " server_ip server_port" << std::endl;
        return 1;
    }

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

    // 访问服务器
    // 1. 创建套接字
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0)
    {
        std::cerr << "socket error" << std ::endl;
        return 2;
    }

    // 2. 2.1绑定。意义是将本地的ip,port和上面的文件关联。
    // client不需要显示绑定,首次发送消息,OS会自动给client进行绑定,
    // 操作系统知道客户端IP,端口号随机分配 。
    // 不显示绑定的原因:为了避免client端口冲突。
    // client的端口号是几不重要,只要是唯一的就行。
    // 2.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());

    while (true)
    {
        std::string input;
        std::cout << "Please inpute: ";
        std::getline(std::cin, input);
        int n = sendto(sockfd, input.c_str(), input.size(), 0, (struct sockaddr *)&server, sizeof(server));
        (void)n;

        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);
        if (m > 0)
        {
            buffer[m] = 0;
            std::cout << buffer << std::endl;
        }
    }

    return 0;
}

2. DictServer

2.1 功能介绍

实现一个简单的英译汉的网络字典

2.2 源码

dict.hpp

cpp 复制代码
#pragma once
#include <iostream>
#include <fstream>
#include <string>
#include <unordered_map>
#include "Log.hpp"
#include "InetAddr.hpp"

const std::string defaultdict = "./dictionary.txt";
const std::string sep = ": ";

using namespace LogModule;

class Dict
{
public:
    Dict(const std::string &path = defaultdict) : _dict_path(path)
    {
    }
    bool LoadDict()
    {
        std::ifstream in(_dict_path);
        if (!in.is_open())
        {
            LOG(LogLevel::DEBUG) << "打开字典: " << _dict_path << " 错误";
            return false;
        }
        std::string line;
        while (std::getline(in, line))
        {
            auto pos = line.find(sep);
            if (pos == std::string::npos)
            {
                LOG(LogLevel::WARNING) << "解析: " << line << " 失败";
                continue;
            }
            std::string english = line.substr(0, pos);
            std::string chinese = line.substr(pos + sep.size());
            if (english.empty() || chinese.empty())
            {
                LOG(LogLevel::WARNING) << "没有有效内容: " << line;
                continue;
            }

            _dict.insert(std::make_pair(english, chinese));
            LOG(LogLevel::DEBUG) << "加载: " << line;
        }

        in.close();
        return true;
    }
    std::string Translate(const std::string &word, InetAddr &client)
    {
        auto iter = _dict.find(word);
        if (iter == _dict.end())
        {
            LOG(LogLevel::DEBUG) << "进入到了翻译模块, [" << client.Ip() << " : " << client.Port() << "]# " << word << "->None";
            return "None";
        }
        LOG(LogLevel::DEBUG) << "进入到了翻译模块, [" << client.Ip() << " : " << client.Port() << "]# " << word << "->" << iter->second;
        return iter->second;
    }
    ~Dict()
    {
    }

private:
    std::string _dict_path; 
    std::unordered_map<std::string, std::string> _dict;
};

server.hpp

cpp 复制代码
#pragma once
#include <iostream>
#include <string>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <functional>
#include "Log.hpp"
#include "InetAddr.hpp"

using func_t = std::function<std::string(const std::string&, InetAddr&)>;
using namespace LogModule;
const int defaultfd = -1;

class UdpServer
{

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

    void Init()
    {
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockfd < 0)
        {
            LOG(LogLevel ::FATAL) << "socket error !";
            exit(1);
        }
        LOG(LogLevel::INFO) << "socket success, sockfd: " << _sockfd;

        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)
        {
            LOG(LogLevel ::FATAL) << "bind error!";
            exit(2);
        }
        LOG(LogLevel ::INFO) << "bind success, sockfd: " << _sockfd;
    }

    void Start()
    {
        _isrunning = true;
        while (_isrunning)
        {
            char buffer[1024];
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);

            size_t s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);

            if (s > 0)
            {
                InetAddr client(peer);
                buffer[s] = 0;
                std::string result = _func(buffer, client);
                sendto(_sockfd, result.c_str(), result.size(), 0, (struct sockaddr *)&peer, len);
            }
        }
    }

    ~UdpServer()
    {
    }

private:
    int _sockfd;
    uint16_t _port;
    bool _isrunning;
    func_t _func;
};

server.cc

cpp 复制代码
#include <iostream>
#include <memory>
#include "UdpServer.hpp"
#include "Dict.hpp"

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

    uint16_t port = std::stoi(argv[1]);
    Enable_Console_Log_Strategy();

    Dict dict;
    dict.LoadDict();

    std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port, [&dict](const std::string &word, InetAddr &cli) -> std::string
                                                                  { return dict.Translate(word, cli); });
    usvr->Init();
    usvr->Start();

    return 0;
}

client.cc

cpp 复制代码
#include <iostream>
#include <sys/socket.h>
#include <sys/types.h>
#include <string>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>

int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        std::cerr << "Usage: " << argv[0] << " server_ip server_port" << std::endl;
        return 1;
    }

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

    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0)
    {
        std::cerr << "socket error" << std ::endl;
        return 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());

    while (true)
    {
        std::string input;
        std::cout << "Please inpute: ";
        std::getline(std::cin, input);
        int n = sendto(sockfd, input.c_str(), input.size(), 0, (struct sockaddr *)&server, sizeof(server));
        (void)n;

        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);
        if (m > 0)
        {
            buffer[m] = 0;
            std::cout << buffer << std::endl;
        }
    }

    return 0;
}

2.3 实验结果

3. 简单聊天室

3.1 源码

查看源码点我 ^ _ ^


相关推荐
网安情报局38 分钟前
除了 CDN,DDoS 攻击还有哪些更有效的防护方式?
网络
Promise微笑1 小时前
2026年国产替代油介损测试仪:油介损全场景解决方案与技术演进
大数据·网络·人工智能
蜡台1 小时前
Python包管理工具pip完全指南-----2
linux·windows·python
Ujimatsu2 小时前
虚拟机安装Debian 13.x及其常用软件(2026.4)
linux·运维·ubuntu
千百元2 小时前
zookeeper启不来了
linux·zookeeper·debian
AnalogElectronic4 小时前
linux 测试网络和端口是否连通的命令详解
linux·网络·php
Edward111111114 小时前
4月28日防火墙问题
linux·运维·服务器
Rust研习社5 小时前
使用 Axum 构建高性能异步 Web 服务
开发语言·前端·网络·后端·http·rust
灰子学技术5 小时前
Envoy HTTP 流量层面的 Metric 指标分析
网络·网络协议·http
上海云盾-小余5 小时前
海外恶意 UDP 攻击溯源:分层封禁策略与业务兼容平衡方案
网络·网络协议·udp