计算机网络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. 效果展示

相关推荐
Mr.简锋14 分钟前
c++设计模式demo
c++
follycat30 分钟前
2024强网杯Proxy
网络·学习·网络安全·go
dickredone37 分钟前
使用CentOS宝塔面板docker搭建EasyTier内网穿透服务
linux·网络·docker·内网穿透·宝塔面板·组网·easytier
职创未来官方39 分钟前
大话C++:第26篇 类模板
c++·面向对象·模板·模板类·嵌入式物联网·隐式实例化·显示实例化
188_djh40 分钟前
# Python基础到实战一飞冲天(一)--linux基础(一)
linux·开发语言·python·ubuntu·centos·os·operationsystem
_flierx42 分钟前
【C++】RBTree——红黑树
开发语言·c++
sf_www1 小时前
flink 内存配置(五):网络缓存调优
大数据·网络·flink
winds~1 小时前
ubuntu中安装matplotcpp绘图
linux·运维·ubuntu
敲敲er1 小时前
[项目] C++基于多设计模式下的同步&异步日志系统
c++·设计模式
ZachOn1y1 小时前
计算机网络:网络层 —— 软件定义网络 SDN
网络·计算机网络·sdn·软件定义网络·openflow协议