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 源码

查看源码点我 ^ _ ^


相关推荐
_叶小格_2 小时前
cp、scp、rsync命令详解
linux·运维·服务器·ubuntu·centos·运维开发
发光小北2 小时前
MS_F155_AM (TW)/MS_F155_VM (TW)特点与功能介绍
网络
B2_Proxy2 小时前
破解TikTok运营困境:静态住宅IP与封号限流深度解析
网络·网络协议·tcp/ip
wheeldown2 小时前
【Linux网络编程】 Linux TCP网络编程:客户端开发+守护进程实战
linux·网络·tcp/ip
永不复还2 小时前
linux 使用Xcb监听键盘鼠标输入
linux·x11·xcb
mango_mangojuice2 小时前
Linux学习笔记 1.19
linux·服务器·数据库·笔记·学习
i建模3 小时前
linux断点续传下载文件
linux·运维·服务器
Aaron15883 小时前
通信灵敏度计算与雷达灵敏度计算对比分析
网络·人工智能·深度学习·算法·fpga开发·信息与通信·信号处理
执笔论英雄3 小时前
【RL]分离部署与共置模式详解
服务器·网络·windows