[网络] socket编程--udp_echo_server

发布于:2025-03-17 ⋅ 阅读:(11) ⋅ 点赞:(0)

好的, 今天我们开始写一个简单的网络代码. 整体代码难度不大, 不过我打算其中穿插着说一点基础知识, 整体可能就比较多了.

1. udp_echo_server是个什么东西?

  首先, 我们先来说一下udp_echo_server这个程序要做什么? 这个是我们开始学习网络的第一个编码实践, 所以说是很简单, 整体的功能是有两个端, 一端是客户端, 一端是服务端, 客户端每次向服务端发送数据, 服务端接受到后吧这个数据又发回客户端, 实现一个客户端数据回显的效果.
  然后这个程序使用的网络协议是UDP协议, 整体很简单.

2. 初步文件构建.

我们先来简单预测一下有哪些文件?
我打算:

  • UdpServer.hpp 服务端头文件
  • UdpServerMain.cc 服务端主函数
  • UdpClientMain.cc 客户端主函数
    在这里插入图片描述
      不过因为我们可能中间需要多次编译, 所以我们先写一个makefile脚本文件吧:
    在这里插入图片描述
      哎, 那有人可能会有疑问, 你这个用户端你咋不编译啊? 这是因为我们一上来先写服务端, 用户端等写完了我们再用make编译, 因为现在还没写嘛…

好的, 下面我们开始编码吧.

3. nocopy服务器防止被拷贝

  一般来说, 服务器程序是不会用到拷贝的, 因此为了防止其他人误写拷贝或者错误使用服务端的类, 我们写一个nocopy, 我们的服务端类可以继承这个类, 从而间接实现服务端不会被拷贝的一个效果.

  我花两分钟就编完了, 我把这个类的拷贝构造赋值运算符重载函数直接delete掉了:
在这里插入图片描述
那下面我们让UdpServer.hpp中的服务端继承一下:
在这里插入图片描述
好的, 这样的话我们的UdpServer类就不能拷贝了…

  之后, 我们来构思一下我们的服务端主函数要实现什么功能?
  作为一个服务器, 应该先构建出一个服务类对象, 然后这个类对象需要先初始化, 然后启动起来进行服务. 在这里插入图片描述
  好的, 我们围绕这个实现功能, 我们来写一写服务端.

4. 创建套接字

  一个程序要跨网络通信, 需要套接字, 即socket, 那我们来给我们的服务端写一个套接字. 套接字socket = ip + port, 实际上套接字就是一个整数, 一个文件描述符, 我们在网络编程的时候万物皆文件, 然后把这个文件和ip以及端口号绑定起来, 表示一个网络当中具有唯一性的进程, 这叫做套接字的作用.
在这里插入图片描述

  这样就完成了吗? 显然没有啊兄弟, 这样你只是在栈上定义出来了一个类型, 一个空间而已, 系统中没有真正的文件的兄弟, 想要有真正的文件, 我们需要调用接口socket. 来, 让我们看看对应的接口介绍:
在这里插入图片描述

int socket(int domain, int type, int protocol);

  • domain: 域, 用来指定该socket是网络通信还是本地通信.
    • AF_UNIX Local communication unix(7)
    • AF_INET IPv4 Internet protocols ip(7)
  • type: 套接字类型
    • SOCK_DGRAM Supports datagrams (connectionless, unreliable messages of a fixed maximum length).
  • protocol 协议类型
    • 因为我们前面指明了AF_INETSOCK_DGRAM, 因此可以写0为默认.
  • 返回值:
    • success 返回文件描述符
    • 失败, -1, 错误码被设置

好的, 所以我们写这样:
在这里插入图片描述
好的, 显然我们的exit是红色的, 所以我们把头文件给他包上.
在这里插入图片描述
  好的, 我们再来考虑一下如果socket创建失败了? 那我们是不是需要终止这个进程呀? 因为文件命名符都要不过来了, 后面肯定是没办法继续的嘛. 如果socket创建成功了, 那么咱们就打印一个std::cout << "DEBUG :socket create success, _sockfd: " << _sockfd << std::endl;用来当作debug信息. 我们预期这个sockfd是3, 为什么? 因为0是标准输入, 1是标准输出, 2是标准错误, 所以下一个系统肯定给咱们的程序分配一个3给到sockfd.
在这里插入图片描述

基于这个预测, 我们来测试一下.
在这里插入图片描述
  看来是符合我们预期的, 那我们继续? 我们创建好了一个sockfd文件描述符, 但是我们的套接字已经弄好了吗?
  显然没有啊兄弟, 现在只是创建了这么一个文件和文件描述符而已, 咱们现在需要把这个sockfd关联上ip地址和prot.
所以, 下一步是bind操作.

5. sockfd bind ip & port

接下来我们继续写我们的bind操作哈. 我们先来看一下bind接口所需要的参数是啥?
在这里插入图片描述


int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

  • sockfd: 套接字描述符
  • sockaddr: 绑定的ip + port信息
  • addrlen: 绑定的ip + port结构体的大小.
  • 返回值是如果失败返回-1, 错误码被设置, 成功返回0.

  所以, 我们可以get到, 我们绑定ip + port, 就需要用一个struct sockaddr_in类型的结构体给他套进去.
  请注意, 这个地方用的是struct sockaddr_in而非struct sockaddr, 这是一个类似于多态的接口, struct sockaddr*是一个通用指针, 然后struct sockaddr_in是专门用来处理网络通信的ip & port等信息的结构体.

我们来认识一下struct sockaddr_in这个结构体:
在这里插入图片描述在这里插入图片描述

  不过好像有个小问题啊? 咋有个"不允许使用不完整的类型"的报错呢? 这是因为没有包头文件, 我们查找一下对应的头文件.
在这里插入图片描述
在这里插入图片描述在这里插入图片描述

  上面这个地方需要额外注意, 就是我们在填充sockadd_in类型的时候, 我们这个类型中的数据将来是要发到网络当中的, 所以需要把主机字节序转换成网络字节序. 说白了就是大小端问题.
  其中, 对于ip地址, 需要把ip地址从字符串形式转换成4字节的形式, 然后再转成网络字节序, 对于port, 需要转网络字节序.
自己做这个工作很麻烦, 还需要判断大小端, 然后再进行转换, 我们有对应的库函数去做这个事情, 转ip: inet_addr()以及 转port: htons().
下面是两个接口的截图:
在这里插入图片描述
在这里插入图片描述
  但是代码中local.sin_family = AF_INET;是啥玩意呢? 是一个标志位, 用来标识struct sockaddr_in是将来用来网络通信的还是本地进程通信的. AF_INET标识是网络通信.

  那到这里是绑定成功了吗? 没有啊兄弟, 我们只是填充好了一个在用户栈上的一个变量而已, 下面开始绑定, 把sockfd 与 对应的ip和port绑定起来.
在这里插入图片描述
在这里插入图片描述
  咱们来解释一下为啥用ip -> "127.0.0.1"来进行测试, 这个是因为这个ip比较特殊, 是一个本地回环, 就是在本主机内部通讯的ip地址, 用这个ip如果通过了, 则证明我们的软件内部是没问题的, 再上线网络, 如果这个都过不了, 则问题一定在软件内部.
然后, 我们运行测试一下:
在这里插入图片描述
  看来没啥大问题哈, 我们看来是绑定成功了.
  所以呢, 我们创建套接字然后绑定完了ip和port, 这才算是为网络通信做了铺垫, 下面我们得开始写我们的服务逻辑了.

6. start 开始服务

  首先搞明白一个问题, 就是服务器是个啥玩意? 这个东西是一个24小时不断运行的死循环代码. 所以我们的进程一旦开始, 就不会主动退出.
所以我们的Start()逻辑应该是这样的:
在这里插入图片描述

  写到这里, 我们需要想我们的服务器要干嘛的? 接受数据包, 然后把这个数据包扔回客户端的!
  要接受数据包, 需要用接口 -> recvfrom(), 这个地方咋不是read()呢? 因为UDP协议是面向数据报的, 而非字节流的.
在这里插入图片描述
我们来简单介绍一下这个recvfrom()接口.

  • sockfd: sockfd.
  • buf: 缓冲区
  • len: 缓冲区大小
  • flags: 接受标识符, (0是阻塞接受)
  • src_addr: 输入输出参数, 用来保存接收到的数据报的客户端信息.
  • addrlen: 输入输出参数, 用来保存接收到的数据报的客户端信息的大小.

在这里插入图片描述
  我们拿到了数据呢? 之后我们是不是要在服务器端打印一下, 看看是不是真的拿到了, 然后再构造一个返回给客户端的数据? 如果n <= 0, 咱们直接std::cout << "recvfrom , error" << std::endl; // debug 信息.即可.
在这里插入图片描述
好的, 我们大体逻辑写好了吧? 然后把析构写一下: 要记得关掉文件描述符.
在这里插入图片描述
  我们现在可以测试吗? 不能啊兄弟, 没有客户端怎么测试呢? 我们先快速写一个客户端代码出来:

7. 编码客户端

我们的客户端也需要跨网络发数据吧? 所以, 也是第一步弄一个sockfd.
在这里插入图片描述
然后呢, 我们要发给服务器, 所以我们需要提前"内置"好服务器的套接字吧?
在这里插入图片描述
  这个地方需要注意, 我们的客户端是不需要绑定端口号的ip地址的, 为啥呢? 为啥服务端需要绑定端口号和ip地址, 而我们的客户端不需要绑定端口号和ip地址?
  兄弟, 你想想看啊, 服务器在公司手里, 服务端是运行在服务器上的, 一般一个主机上是只运行那个服务的, 所以端口号基本不会改变嘛, 但是客户端呢? 运行在用户的手机上吧? 用户开多少个应用你知道吗? 你不知道, 那有没有各种应用的端口号如果让程序员指定会不会冲突呢? 肯定会啊, 所以说客户端让用户的OS分配就好了, 程序员是不需要明确写的.
  那, 有人可能会有疑问, 那么服务器怎么找到用户的用户端呢? 首先, 是客户端先给服务器发消息, 第二就是服务器拿到消息之后, 就会拿到这次客户端的具体ip地址和port.

在这里插入图片描述

  所以, 我们的客户端应该不断发消息, 所以写一个循环嘛, 然后发成功了, 就阻塞等待接受消息, 如果收到了, 咱们就打印一遍(回显), 如果接受失败, 就break出去. 如果一上来就没发成功, 我们也是break, 然后打个提示信息, 好找错误.

我们测试一下吧?
  不过, 我们先把makefile在写一下, 因为现在是一个客户端和一个服务端同时编译了.
在这里插入图片描述
输入: netstat -aupn
在这里插入图片描述
在这里插入图片描述
看来好像是能跑了~

但是, 还是稍微存在一点细节的, 下面来进行细说:

8. 服务端ip应该绑0号pi.

  实际上, 我们的服务端是由很多ip地址的, 有内网, 有公网, 我们尝试用客户端以内网的形式发送信息给服务端.

我们看一下这台机器的内网ip:
在linux机器上输入: ifconfig
在这里插入图片描述
在这里插入图片描述
我们启动一下看一下结果:
在这里插入图片描述
  这是为啥呢? 因为我们的服务端绑定的不是内网啊, 服务端有很多个ip, 但是只能通过绑定的ip给他发信息, 是不太好的, 我们希望服务端的任何ip的数据包都可以被拿到. 所以:
  我们给服务端绑定ip地址为0号ip(0.0.0.0). 看来我们需要修改一下代码:
在这里插入图片描述

在这里插入图片描述

9. 加入日志

  为了输入一些debug和error信息比较方便, 我们可以写一个日志加进去, 之前是写好的了, 所以我们直接CV到代码里就好了.
在这里插入图片描述
在这里插入图片描述
我们再来看一下效果:
在这里插入图片描述

  然后我们还可以把那个ip + port封装起来, 可以让他自动转为主机序列, 而不用在外面转换.

10. 封装ip + port类

// InetAddr.hpp

#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;
};

我们把封装好的这个ip + port弄到服务端代码中去:
在这里插入图片描述
在这里插入图片描述
  好的, 终于差不多了, 当然也有一些细节是没有处理好的, 或者说想要处理好细节, 还需要很多代码进行补充, 这次只是一个练习, 也就不求甚解了~

11. reference code

UdpServer.hpp

#pragma once
#include "nocopy.hpp"
#include <sys/socket.h> // 主要 socket 函数和类型
#include <stdlib.h>
#include <iostream>
#include <string.h>
#include <netinet/in.h>
#include <string>
#include <arpa/inet.h>
#include <unistd.h>
#include "LockGuard.hpp"
#include "Log.hpp"
#include "InetAddr.hpp"
using namespace log_ns;

static const int gsockfd = -1;
static const int glocalport = 8899;
enum
{
    SOCKET_ERROR = 1,
    BIND_ERROR
};

class UdpServer : public nocopy
{
public:
    UdpServer(std::string localip) : _sockfd(gsockfd), _localport(glocalport), /*_localip(localip),*/ _isrunning(false)
    {
    }
    void InitServer()
    {
        // 创建sockfd文件
        _sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockfd < 0)
        {
            // std::cout << "FATAL: socket error" << std::endl;
            LOG(FATAL, "socket error\n");
            exit(SOCKET_ERROR);
        }
        // std::cout << "DEBUG :socket create success, _sockfd: " << _sockfd << std::endl; // 3"
        LOG(DEBUG, "socket create success, _sockfd: %d\n", _sockfd); // 3

        // bind ip & port
        struct sockaddr_in local;
        memset(&local, 0, sizeof(sockaddr_in));
        local.sin_family = AF_INET;
        /*local.sin_addr.s_addr = inet_addr(_localip.c_str());*/
        local.sin_addr.s_addr = INADDR_ANY; // 服务器端,进行任意IP地址绑定, INADDR_ANY -> 0, 表示绑定任意端口号
        local.sin_port = htons(_localport);
        int n = bind(_sockfd, (sockaddr *)&local, sizeof(local));
        if (n < 0)
        {
            // std::cout << "FATAL, bind error" << std::endl;
            LOG(FATAL, "bind error\n");
            exit(BIND_ERROR);
        }
        // std::cout << "DEBUG, socket bind success" << std::endl;
        LOG(DEBUG, "socket bind success\n");
    }
    void Start()
    {
        _isrunning = true;
        char inbuffer[1024];
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        while (_isrunning)
        {
            int n = recvfrom(_sockfd, inbuffer, sizeof(inbuffer), 0, (sockaddr *)&peer, &len);
            if (n > 0)
            {
                // 转换成主机序列
                InetAddr addr(peer);
                // uint16_t _port = ntohs(peer.sin_port);
                // std::string _ip = inet_ntoa(peer.sin_addr);
                inbuffer[n] = 0;
                // std::cout << "[" << _ip << ":" << _port << "]# " << inbuffer << std::endl; // 打印一下消息
                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; // debug 信息.
            }
        }
    }
    ~UdpServer()
    {
        if (_sockfd > gsockfd)
            ::close(_sockfd);
    }

private:
    int _sockfd;
    uint16_t _localport;
    // std::string _localip;
    bool _isrunning;
};

UdpServerMain.cc

#include"UdpServer.hpp"
#include<memory>

int main()
{
    std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>("127.0.0.1"); //C++14的标准
    usvr->InitServer();
    usvr->Start();

    return 0;
}

UdpClientMain.cc

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

int main()
{
    int sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0)
    {
        std::cerr << "create socket error" << std::endl;
        exit(1);
    }
    // 客户端的socket文件会自动绑定ip地址和端口号, 我们无需关心

    // 你要发给谁?
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(8899);
    server.sin_addr.s_addr = inet_addr("10.0.16.4");

    while(1)
    {
        // 让用户输入信息
        std::string line;
        std::cout << "Please Enter# ";
        std:getline(std::cin, line);
        
        // 发消息
        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; // debug
                break;
            }
        }
        else // 没有发送出去
        {
            std::cout << "sendto error" << std::endl;
            break;
        }
    }
    ::close(sockfd);

    return 0;
}

nocopy.hpp

#pragma once

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

Log.hpp

#pragma once

#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <ctime>
#include <cstdarg>
#include <fstream>
#include <cstring>
#include <pthread.h>
#include "LockGuard.hpp"

namespace log_ns
{

    enum
    {
        DEBUG = 1,
        INFO,
        WARNING,
        ERROR,
        FATAL
    };

    std::string LevelToString(int level)
    {
        switch (level)
        {
        case DEBUG:
            return "DEBUG";
        case INFO:
            return "INFO";
        case WARNING:
            return "WARNING";
        case ERROR:
            return "ERROR";
        case FATAL:
            return "FATAL";
        default:
            return "UNKNOWN";
        }
    }

    std::string GetCurrTime()
    {
        time_t now = time(nullptr);
        struct tm *curr_time = localtime(&now);
        char buffer[128];
        snprintf(buffer, sizeof(buffer), "%d-%02d-%02d %02d:%02d:%02d",
                 curr_time->tm_year + 1900,
                 curr_time->tm_mon + 1,
                 curr_time->tm_mday,
                 curr_time->tm_hour,
                 curr_time->tm_min,
                 curr_time->tm_sec);
        return buffer;
    }

    class logmessage
    {
    public:
        std::string _level;
        pid_t _id;
        std::string _filename;
        int _filenumber;
        std::string _curr_time;
        std::string _message_info;
    };

#define SCREEN_TYPE 1
#define FILE_TYPE 2

    const std::string glogfile = "./log.txt";
    pthread_mutex_t glock = PTHREAD_MUTEX_INITIALIZER;

    // log.logMessage("", 12, INFO, "this is a %d message ,%f, %s hellwrodl", x, , , );
    class Log
    {
    public:
        Log(const std::string &logfile = glogfile) : _logfile(logfile), _type(SCREEN_TYPE)
        {
        }
        void Enable(int type)
        {
            _type = type;
        }
        void FlushLogToScreen(const logmessage &lg)
        {
            printf("[%s][%d][%s][%d][%s] %s",
                   lg._level.c_str(),
                   lg._id,
                   lg._filename.c_str(),
                   lg._filenumber,
                   lg._curr_time.c_str(),
                   lg._message_info.c_str());
        }
        void FlushLogToFile(const logmessage &lg)
        {
            std::ofstream out(_logfile, std::ios::app);
            if (!out.is_open())
                return;
            char logtxt[2048];
            snprintf(logtxt, sizeof(logtxt), "[%s][%d][%s][%d][%s] %s",
                     lg._level.c_str(),
                     lg._id,
                     lg._filename.c_str(),
                     lg._filenumber,
                     lg._curr_time.c_str(),
                     lg._message_info.c_str());
            out.write(logtxt, strlen(logtxt));
            out.close();
        }
        void FlushLog(const logmessage &lg)
        {
            // 加过滤逻辑 --- TODO

            LockGuard lockguard(&glock);
            switch (_type)
            {
            case SCREEN_TYPE:
                FlushLogToScreen(lg);
                break;
            case FILE_TYPE:
                FlushLogToFile(lg);
                break;
            }
        }
        void logMessage(std::string filename, int filenumber, int level, const char *format, ...)
        {
            logmessage lg;

            lg._level = LevelToString(level);
            lg._id = getpid();
            lg._filename = filename;
            lg._filenumber = filenumber;
            lg._curr_time = GetCurrTime();

            va_list ap;
            va_start(ap, format);
            char log_info[1024];
            vsnprintf(log_info, sizeof(log_info), format, ap);
            va_end(ap);
            lg._message_info = log_info;

            // 打印出来日志
            FlushLog(lg);
        }
        ~Log()
        {
        }

    private:
        int _type;
        std::string _logfile;
    };

    Log lg;

#define LOG(Level, Format, ...)                                        \
    do                                                                 \
    {                                                                  \
        lg.logMessage(__FILE__, __LINE__, Level, Format, ##__VA_ARGS__); \
    } while (0)
#define EnableScreen()          \
    do                          \
    {                           \
        lg.Enable(SCREEN_TYPE); \
    } while (0)
#define EnableFILE()          \
    do                        \
    {                         \
        lg.Enable(FILE_TYPE); \
    } while (0)
};

LockGuard.hpp

#pragma once

#include <pthread.h>

class LockGuard
{
public:
    LockGuard(pthread_mutex_t *mutex):_mutex(mutex)
    {
        pthread_mutex_lock(_mutex);
    }
    ~LockGuard()
    {
        pthread_mutex_unlock(_mutex);
    }
private:
    pthread_mutex_t *_mutex;
};

InetAddr.hpp

#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;
};

makefile

.PHONT: all
all: udpserver udpclient

udpserver: UdpServerMain.cc
	g++ -o $@ $^ -std=c++14
udpclient: UdpClientMain.cc
	g++ -o $@ $^ -std=c++14
.PHONY:clean
clean:
	rm -rf udpserver udpclient

网站公告

今日签到

点亮在社区的每一天
去签到