计算机网络socket编程(1)_UDP网络编程实现echo server

个人主页:C++忠实粉丝
欢迎 点赞👍 收藏✨ 留言✉ 加关注💓本文由 C++忠实粉丝 原创

计算机网络socket编程(1)_UDP网络编程实现echo server

收录于专栏【计算机网络
本专栏旨在分享学习计算机网络的一点学习笔记,欢迎大家在评论区交流讨论💌

目录

[功能介绍 :](#功能介绍 :)

[1. nocopy.hpp](#1. nocopy.hpp)

[2. InetAddr.hpp](#2. InetAddr.hpp)

[3. UdpServer.hpp](#3. UdpServer.hpp)

[4. UdpServerMain.cc](#4. UdpServerMain.cc)

[5. UdpClientMain.cc](#5. UdpClientMain.cc)

[6. 效果展示](#6. 效果展示)


功能介绍 :

简单的回显服务器和客服端代码, 能接受多个客服端的代码.

1. nocopy.hpp

cpp 复制代码
#pragma once

class nocopy
{
public:
    nocopy(){}
    ~nocopy(){}
    nocopy(const nocopy&) = delete;
    const nocopy& operator=(const nocopy&) = delete;
};

定义一个 nocopy 的类, 通过C++11 delete关键字的特性, 阻止该类的拷贝构造和拷贝赋值.

比如下面的例子都是不可以的 :

cpp 复制代码
nocopy obj1;
nocopy obj2 = obj1;  // 错误:拷贝构造函数被删除
cpp 复制代码
nocopy obj1;
nocopy obj2;
obj2 = obj1;  // 错误:拷贝赋值运算符被删除

为什么需要禁止对象的拷贝呢?

这样的设计常见于以下情况 :

  1. 资源管理类 : 例如, 涉及底层资源 (如文件句柄, 网络连接等) 的类通常不希望被复制, 因为这些资源可能会导致资源管理上的混乱或错误~

  2. 不想允许拷贝的类: 有些类可能设计上不希望被拷贝, 例如类的状态可能是唯一的, 拷贝它没有意义~~

我们这里显然是第一种~~

2. InetAddr.hpp

cpp 复制代码
#pragma once

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

class InetAddr
{
private:
    void ToHost(const struct sockaddr_in &addr)
    {
        _port = ntohs(addr.sin_port);
        _ip = inet_ntoa(addr.sin_addr);
    }

public:
    InetAddr(const struct sockaddr_in &addr):_addr(addr)
    {
        ToHost(addr);
    }
    std::string Ip()
    {
        return _ip;
    }
    uint16_t Port()
    {
        return _port;
    }
    ~InetAddr()
    {
    }

private:
    std::string _ip;
    uint16_t _port;
    struct sockaddr_in _addr;
};

InetAddr 类它封装了网络地址 (IP 端口), 以及提供了访问这些信息的方法, 该类通过了 struct sockaddr_in 来存储 IP 地址和端口, 并提供了转换和获取信息的功能

成员变量 :

_ip: 存储 IP 地址, 使用 std::string 类型, 因为 IP 地址通常表示一个点分十进制的字符串

_port: 存储端口号, 使用 uint16_t 类型, 端口号是一个16位的无符号整数

_addr: 存储原始的 struct sockaddr_in 结构体, 它包含了 IP 地址和端口信息

ToHost():

**ToHost() :**将 struct sockaddr_in 中的网络字节数据转换为主机字节序, 并提取 IP 和端口.

**ntohs(addr.sin_port) :**将网络字节序的端口号转换为主机字节序

**inet_ntoa(addr.sin_addr) :**将网络字节序的 IP 地址转换为点分十进制的字符串的形式

构造函数 :

构造函数 : 接受一个 struct sockaddr_in 类型的参数, 表示网络地址

构造函数初始化 _addr 成员, 存储传入的地址

然后调用 ToHost() 方法来从该地址中提取 IP 和端口, 并进行转换

Ip() && Port()

Ip() : 返回存储的 IP 地址, 类型为 std::string

Port() : 返回存储的端口号, 类型为 uint16_t

析构函数 :

由于没有动态分配资源, 所以没有必要进行额外的清理工作~

3. UdpServer.hpp

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

#include "nocopy.hpp"
#include "Log.hpp"
#include "InetAddr.hpp"

using namespace log_ns;

static const int gsockfd = -1;
static const uint16_t glocalport = 8888;

enum
{
    SOCKET_ERROR = 1,
    BIND_ERROR
};

// UdpServer user("192.1.1.1", 8899);
// 一般服务器主要是用来进行网络数据读取和写入的。IO的
// 服务器IO逻辑 和 业务逻辑 解耦
class UdpServer : public nocopy
{
public:
    UdpServer(uint16_t localport = glocalport)
        : _sockfd(gsockfd),
          _localport(localport),
          _isrunning(false)
    {
    }
    void InitServer()
    {
        // 1. 创建socket文件
        _sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockfd < 0)
        {
            LOG(FATAL, "socket error\n");
            exit(SOCKET_ERROR);
        }
        LOG(DEBUG, "socket create success, _sockfd: %d\n", _sockfd); // 3

        // 2. bind
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(_localport);
        // local.sin_addr.s_addr = inet_addr(_localip.c_str()); // 1. 需要4字节IP 2. 需要网络序列的IP -- 暂时
        local.sin_addr.s_addr = INADDR_ANY; // 服务器端,进行任意IP地址绑定

        int n = ::bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
        if (n < 0)
        {
            LOG(FATAL, "bind error\n");
            exit(BIND_ERROR);
        }
        LOG(DEBUG, "socket bind success\n");
    }
    void Start()
    {
        _isrunning = true;
        char inbuffer[1024];
        while (_isrunning)
        {
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            ssize_t n = recvfrom(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr *)&peer, &len);
            if (n > 0)
            {
                InetAddr addr(peer);
                inbuffer[n] = 0;
                std::cout << "[" << addr.Ip() << ":" << addr.Port() << "]# " << inbuffer << std::endl;


                std::string echo_string = "[udp_server echo] #";
                echo_string += inbuffer;
                sendto(_sockfd, echo_string.c_str(), echo_string.size(), 0, (struct sockaddr *)&peer, len);
            }
            else
            {
                std::cout << "recvfrom ,  error"  << std::endl;
            }
        }
    }
    ~UdpServer()
    {
        if(_sockfd > gsockfd) ::close(_sockfd);
    }

private:
    int _sockfd;
    uint16_t _localport;
    // std::string _localip; // TODO:后面专门要处理一下这个IP
    bool _isrunning;
};

这段代码定义了一个 UdpServer 类, 用于创建和管理一个 UDP 服务器, 它能够接受客户端的消息并发送相应~

1. 常量和枚举

**gsockfd :**设置为 -1, 用作初始的 sockfd, 代表一个无效的套接字描述符

**glocalport :**服务器的默认接口, 默认值为 8888

SOCKET_ERRORBIND_ERROR: 自定义的错误代码, 分别代表套接字创建失败和绑定失败.

2. 成员变量

_sockfd: 存储套接字文件描述符

_localport: 服务器绑定的本地端口号

_isrunning: 一个布尔值, 指示服务器是否正在运行

3. 构造函数

UdpServer 类的构造函数的初始化了以下成员变量 :

**_sockfd :**默认初始化为 gsockfd (-1), 表示无效的套接字

_localport : 使用传入的端口号 localport 或默认端口 glocalport (8888)

isrunning : 初始化为 false , 表示服务器的初始状态是未运行.

4. InitServer()

创建套接字 : 使用 socket() 函数创建一个 UDP 套接字, AF_INET 表示使用 IPv4 协议, SOCK_DGRAM 表示使用 UDP.

  1. 如果创建失败, 记录日志并退出

  2. 成功后, 输出日志, 显示套接字描述符 _sockfd.

绑定套接字 : 使用 bind() 函数将套接字与本地地址 (IP 和端口) 绑定

local.sin_family = AF_INET : 设置为 IPv4 地址族

**local.sin_port = htons(_localport) :**将端口号转换为网络字节序

**local.sin_addr.s_addr = INADDR_ANY :**服务器绑定到任意 IP 地址

**band()**调用套接字与本地地址绑定, 如果绑定失败, 记录错误日志并退出

5. Start()

Start() 方法用于接收数据并发送响应

**_isrunning = true :**启动服务器, 进入接收数据的循环

**recvfrom() :**接收 UDP 数据包, 数据存放在 inbuffer 中, peer 存储发送方的地址信息

如果接收成功 (n > 0), 则:

  1. 创建一个 InetAddr 对象, 解析发送对方的 IP 地址和端口

  2. 输出接收到的消息及其来源 IP 和端口

  3. 生成一个响应消息 ("[udp_server echo]#" + 接收的消息), 然后使用 sendto() 将其发送回客户端

如果 recvfrom() 失败, 打印错误消息.

6. 析构函数

在 UdpServer 对象销毁时, 关闭套接字 _sockfd, 如果 _sockfd 大于 gsockfd (即有效套接字), 则调用 close() 关闭套接字

4. UdpServerMain.cc

cpp 复制代码
#include "UdpServer.hpp"

#include <memory>

// ./udp_server local-port
// ./udp_server 8888
int main(int argc, char *argv[])
{
    if(argc != 2)
    {
        std::cerr << "Usage: " << argv[0] << " local-port" << std::endl;
        exit(0);
    }
    uint16_t port = std::stoi(argv[1]);

    EnableScreen();  
    std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port); //C++14的标准
    usvr->InitServer();
    usvr->Start();
    return 0;
}

std::stoi(argv[1]) : 将命令行参数中的端口号字符串 (argv[1]) 转换为 uint6_t 类型 (无符号16位整数), std::stoi 是 C++ 标准库函数, 用于将字符串转换为整数, 如果 argv[1] 不是一个有效的数组, std::stoi 将抛出异常

std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port) :

这行代码使用了 C++14 引入的 std::make_unique, 它用来创建一个 std::unique_ptr, 智能指针类型, 用于管理动态分配的 UdpServer 对象.

std::make_unique<UdpServer>(port) : 动态创建一个 UdpServer 对象, 并将端口号 port 传递给它的构造函数.

std::unique_ptr<UdpServer> usvr : usvr 是一个智能指针, 指向创建的 Udpserver 对象, unique_ptr 确保在 usvr 离开作用域时自动销毁对象, 避免内存泄露.

UdpServer 类接收一个端口号作为构造参数, 表示该服务器将在指定的端口上运行

然后便直接 初始化服务器启动服务

5. UdpClientMain.cc

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

// 客户端在未来一定要知道服务器的IP地址和端口号
// ./udp_client server-ip server-port
// ./udp_client 127.0.0.1 8888
int main(int argc, char *argv[])
{
    if(argc != 3)
    {
        std::cerr << "Usage: " << argv[0] << " server-ip server-port" << std::endl;
        exit(0);
    }

    std::string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]);

    int sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);
    if(sockfd < 0)
    {
        std::cerr << "create socket error" << std::endl;
        exit(1);
    }

    // client的端口号,一般不让用户自己设定,而是让client OS随机选择?怎么选择,什么时候选择呢?
    // client 需要 bind它自己的IP和端口, 但是client 不需要 "显示" bind它自己的IP和端口, 
    // client 在首次向服务器发送数据的时候,OS会自动给client bind它自己的IP和端口

    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(serverport);
    server.sin_addr.s_addr = inet_addr(serverip.c_str());
    while(1)
    {
        std::string line;
        std::cout << "Please Enter# ";
        std::getline(std::cin, line);

        // std::cout << "line message is@ " << line << std::endl;

        int n = sendto(sockfd, line.c_str(), line.size(), 0, (struct sockaddr*)&server, sizeof(server)); // 你要发送消息,你得知道你要发给谁啊!
        if(n > 0)
        {
            struct sockaddr_in temp;
            socklen_t len = sizeof(temp);
            char buffer[1024];
            int m = recvfrom(sockfd, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&temp, &len);
            if(m > 0)
            {
                buffer[m] = 0;
                std::cout << buffer << std::endl;
            }
            else
            {
                std::cout << "recvfrom error" << std::endl;
                break;
            }
        }
        else
        {
            std::cout << "sendto error" << std::endl;
            break;
        }
    }

    ::close(sockfd);
    return 0;
}
  1. 从命令行获取目标服务器的 IP 地址和端口号。

  2. 创建 UDP 套接字并配置服务器的地址。

  3. 进入循环,等待用户输入数据。

  4. 将用户输入的数据通过 sendto 发送给服务器。

  5. 接收服务器的响应数据并输出到控制台。

  6. 出现错误时终止通信并关闭套接字。

6. 效果展示

相关推荐
典学长编程27 分钟前
Linux操作系统从入门到精通!第二天(命令行)
linux·运维·chrome
wuk99841 分钟前
基于MATLAB编制的锂离子电池伪二维模型
linux·windows·github
彭祥.1 小时前
Jetson边缘计算主板:Ubuntu 环境配置 CUDA 与 cudNN 推理环境 + OpenCV 与 C++ 进行目标分类
c++·opencv·分类
会飞的鱼先生2 小时前
Node.js-http模块
网络协议·http·node.js
lzb_kkk2 小时前
【C++】C++四种类型转换操作符详解
开发语言·c++·windows·1024程序员节
snoopyfly~3 小时前
Ubuntu 24.04 LTS 服务器配置:安装 JDK、Nginx、Redis。
java·服务器·ubuntu
独行soc3 小时前
#渗透测试#批量漏洞挖掘#HSC Mailinspector 任意文件读取漏洞(CVE-2024-34470)
linux·科技·安全·网络安全·面试·渗透测试
BD_Marathon4 小时前
Ubuntu下Tomcat的配置
linux·ubuntu·tomcat
胖大和尚4 小时前
clang 编译器怎么查看在编译过程中做了哪些优化
c++·clang
饥饿的半导体4 小时前
Linux快速入门
linux·运维