Linux--Socket 编程 UDP(简单的回显服务器和客户端代码)

目录

0.上篇文章

1.V1 版本 - echo server

1.1认识接口

1.2实现 V1 版本 - echo server(细节)

1.3添加的日志系统(代码)

 1.4 解析网络地址

 1.5 禁止拷贝逻辑(基类)

1.6 服务端逻辑 (代码)

1.7客户端逻辑(代码)

1.8 用例测试 


0.上篇文章

Linux--Socket编程预备-CSDN博客

1.V1 版本 - echo server


1.1认识接口

1.创建socket

        它允许不同计算机或同一计算机上的不同进程之间进行数据交换。Socket编程基于客户端-服务器模型,其中服务器监听来自客户端的连接请求,并在连接建立后交换数据。

  1. socket():创建一个新的socket。
    • 原型:int socket(int domain, int type, int protocol);
    • 参数:
      • domain:指定socket使用的协议族,如AF_INET(IPv4)或AF_INET6(IPv6)。
      • type:指定socket的类型,如SOCK_STREAM(TCP)或SOCK_DGRAM(UDP)。
      • 此处我们先对 TCP(Transmission Control Protocol 传输控制协议)有一个直观的认识;
        后面我们再详细讨论 TCP 的一些细节问题.
        • 传输层协议
        • 有连接
        • 可靠传输(可靠性高)
        • 面向字节流
      • 此处我们也是对 UDP(User Datagram Protocol 用户数据报协议)有一个直观的认识; 后
        面再详细讨论.
        • 传输层协议
        • 无连接
        • 不可靠传输(但操作简单)
        • 面向数据报
      • protocol:通常设置为0,表示选择给定domain和type的默认协议。

        AF_UNIX表示本地协议族,是进行本地通信的;AF_INET表示网络协议族,是进行网络通信的。

返回值概述

  • 成功时socket()函数成功执行时,会返回一个非负整数,这个整数被称为socket的文件描述符。文件描述符是一个非负整数,用于标识打开的文件、管道、socket等I/O资源。在后续的网络通信中,会使用这个文件描述符来引用和操作这个socket。
  • 失败时:如果socket()函数执行失败,它会返回-1,并设置全局变量errno以指示错误的具体原因。errno是一个由系统定义的全局变量,用于在函数调用失败时提供错误代码。

2.bind

        络服务的 bind(绑定):在网络编程中,bind 是一个系统调用,用于将套接字(socket)与一个特定的 IP 地址和端口号关联起来。这是服务器程序启动时的常见步骤,它告诉操作系统这个套接字将监听来自特定 IP 地址和端口号的连接请求。

参数说明:

1. sockfd

  • 类型int
  • 描述:这是由socket函数返回的套接字文件描述符(socket file descriptor)。它代表了创建的套接字,是后续网络通信操作的基础。

2. addr

  • 类型const struct sockaddr *
  • 描述:这是一个指向sockaddr结构体的指针,但实际上更常用的是其派生结构体如sockaddr_in(用于IPv4)或sockaddr_in6(用于IPv6)。这个结构体包含了要绑定的IP地址和端口号信息。
    • 对于sockaddr_in结构体,它至少包含以下成员:
      • sin_family:地址族,对于IPv4,此值应为AF_INET
      • sin_port:端口号,在网络字节序中。
      • sin_addr:IP地址,也是以网络字节序表示。
    • 需要注意的是,由于bind函数要求的是sockaddr类型的指针,因此在使用sockaddr_insockaddr_in6时,需要将其地址强制转换为sockaddr*类型。

3. addrlen

  • 类型socklen_t
  • 描述:这个参数指定了addr参数所指向的地址结构体的长度(以字节为单位)。由于sockaddr是一个通用结构体,其大小可能因不同的地址族而异(尽管sockaddr本身的大小是固定的,但使用其派生结构体时,实际长度会更大),因此需要通过这个参数来明确告诉bind函数应该如何处理addr参数。

3.recvfrom收消息

        这个函数允许程序从指定的套接字(socket)接收数据,并且能够获取发送数据的源地址信息。

参数说明

  • sockfd:要接收数据的套接字文件描述符。
  • buf:指向数据缓冲区的指针,用于存储接收到的数据。
  • len:缓冲区 buf 的长度,即最大可接收的数据量。
  • flags:调用标志,通常设置为 0,但在某些情况下可以指定特殊的行为(如 MSG_DONTWAIT)。
  • src_addrsrc_addr 是一个指向 sockaddr 结构(或其特定类型,如 sockaddr_in 用于 IPv4)的指针。当 recvfrom 函数被调用时,如果成功接收到数据,发送方的地址信息(包括 IP 地址和端口号)将被填充到这个结构中。这样,接收方就可以知道数据是从哪里来的,并可能基于此信息进行后续操作,比如回复发送方。
  • addrlenaddrlen 是一个指向 socklen_t 类型变量的指针。在调用 recvfrom 之前,这个变量应该被设置为 src_addr 指向的缓冲区的大小(即 sockaddr_in 或其他 sockaddr 派生类型的大小)。recvfrom 函数在成功执行后,会通过这个指针返回实际存储在 src_addr 中的地址结构的大小。这允许调用者知道有多少字节的 src_addr 被实际使用,尽管在大多数情况下,对于特定的 sockaddr 类型(如 sockaddr_in),这个大小是固定的。

返回值

  • 成功时,recvfrom 返回接收到的字节数。
  • 如果连接被对方正常关闭,返回 0。
  • 如果发生错误,返回 -1,并设置 errno 以指示错误的原因。

4.sento发消息

        用于在网络编程中发送数据,特别是与UDP(用户数据报协议)通信时。

参数说明:

  • sockfd:套接字文件描述符,表示要发送数据的套接字。
  • buf:指向要发送数据的缓冲区的指针。
  • len:要发送数据的字节数。
  • flags:发送数据的标志位,通常设置为0。
  • dest_addr:指向目的地址结构体的指针,包括目的IP地址和端口号等信息。
  • addrlen:目的地址结构体的长度。

特点:

  1. 无连接发送sendto支持无连接的发送方式,发送方不需要与接收方建立连接即可发送数据。这使得UDP协议在实时通信、视频流传输等场景中具有优势。
  2. 灵活性sendto允许发送方指定目标地址和端口号,这使得它可以向不同的接收方发送数据,而不需要为每个接收方建立单独的连接。
  3. 可靠性:虽然UDP协议本身不提供可靠性保证(如TCP的确认和重传机制),但sendto函数可以通过分片发送数据的方式,在网络环境不稳定或拥塞的情况下,尽量保证数据的完整性和准确性。

返回值:

  • 成功时sendto函数成功执行时,其返回值是成功发送的数据的字节数。这意味着如果应用程序请求发送的数据量为N字节,并且这N字节全部被成功发送,那么sendto将返回N。
  • 失败时:如果发送操作失败,sendto函数将返回-1。此时,可以通过检查全局变量errno来获取具体的错误原因。

1.2实现 V1 版本 - echo server(细节)

        该服务器就是创建好对应的套接字,和网络信息进行绑定,服务器不断地接收和发送数据。因为我们对应的服务器和客户端都在一台机器上,127.0.0.1 是一个特殊的IP地址,被称为回环地址,当你在计算机上访问 127.0.0.1 时,实际上是在与本机上的某个服务或应用程序进行通信,而不是通过网络与其他计算机通信。

        当然如果你是使用的云服务器,你当然可以使用你服务器的ip。但云服务器上,服务端不能直接(也强烈不建议)bind自己的公网ip!因为该ip是虚拟出来的,该机器上也没有这个ip。下面这个才是你机器上的ip(内网ip)

        但是bind内网ip的话,就不会往服务器上收消息了,外部无法直接访问内网。在云服务器上,ip地址一般设置为0,那如何理解呢?这可以让服务器bind任意ip。

        一般现在的服务器只有一张网卡,绑定了一个公网ip。但如果你的服务器上有很多的ip地址,一个ip1,一个ip2,(如内网ip,回环ip)而我们上层的端口号为:8888。如果你的服务器bind ip1和8888,未来你的服务器收到各种报文都是发给8888的。有ip1的也有ip2的,但服务器只会接收ip1的报文,但如果服务器bind的ip为0,就意味着不管发送的ip是谁,只要是发给端口号为:8888的,服务器都会接收!!!(在做本地测试的时候,你也可以使用公网ip向你的服务器发消息,能不能成功,就看你的云服务器是怎么设定的了)

        因此,我们在server端就不需要ip了,bind端口号为8888就行了  


1.3添加的日志系统(代码)

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

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


 1.4 解析网络地址

此功能实现较为简单,请看注释:

#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);//这个端口号是随机bind的
        _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;//保存一下网络序列
};


 1.5 禁止拷贝逻辑(基类)

 禁止对象之间的拷贝,而非智能指针的,因为有很多网络相关的,这样可以避免出错:

nocopy.hpp

#pragma once
//禁止拷贝
class nocopy
{
public:
    nocopy(){}
    ~nocopy(){}
    nocopy(const nocopy&) = delete;
    const nocopy& operator=(const nocopy&) = delete;
};


1.6 服务端逻辑 (代码)

UdpServer.hpp:

以下是 UdpServer 类的主要功能和组成部分:

  1. 构造函数:接收一个可选的本地端口号参数,默认为 8888。构造函数初始化了文件描述符 _sockfd 为 -1(表示无效的文件描述符),设置了本地端口号 _localport,并将服务器运行状态 _isrunning 设置为 false

  2. InitServer 方法:用于初始化服务器。这个方法首先创建一个UDP套接字,如果创建失败,则记录一条致命错误日志并退出程序。然后,它将套接字绑定到一个本地地址和端口上。如果绑定失败,同样记录一条致命错误日志并退出程序。

  3. Start 方法:用于启动服务器。这个方法将 _isrunning 设置为 true,然后进入一个循环,不断接收来自客户端的数据,处理数据,并向客户端发送响应。如果接收数据失败,它会打印一条错误消息。

  4. 析构函数:在 UdpServer 类的实例被销毁时调用。如果文件描述符 _sockfd 是有效的(即大于 -1),则关闭该套接字。

整个类使用了日志记录功能来记录关键事件,如套接字创建和绑定成功或失败。这有助于调试和监控服务器的运行状态。

        因为实现比较简单,结合逻辑和注释,理解起来较为容易:

#pragma once

#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),//初始化为-1
          _localport(localport),
          _isrunning(false)
    {
    }
    void InitServer()//初始化服务器
    {
        // 1. 创建socket文件
        //AF_INET表示的是网络套接(IPv4),SOCK_DGRAM表示为UDP协议 
        _sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);//系统调用,创建套接字
        if (_sockfd < 0)
        {
            LOG(FATAL, "socket error\n");//提示创建失败,致命错误,日志
            exit(SOCKET_ERROR);
        }
        // 把此次套接字所对应的文件fd也写进日志
        LOG(DEBUG, "socket create success, _sockfd: %d\n", _sockfd); // 把此次套接字所对应的文件fd也写进日志


        // 2. bind
        struct sockaddr_in local;//用于表示Internet地址,特别是IPv4地址和端口号。
        memset(&local, 0, sizeof(local));//先清空再使用
        local.sin_family = AF_INET;//表示你的套接字将使用IPv4协议。
        local.sin_port = htons(_localport);//端口号,htons主机序列转为网络序列
        // 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));//填充进入bind
        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);
            //读取数据
           // (struct sockaddr *)&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;//文件描述符
    //socket()函数成功执行时,会返回一个非负整数,这个整数被称为socket的文件描述符。
    uint16_t _localport;//端口号
    // std::string _localip; //不需要了
    bool _isrunning;//服务器是否在运行
};

UdpServerMain.cc:

        服务端启动程序:

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


1.7客户端逻辑(代码)

UdpClientMain.cc:

  1. 参数检查:程序首先检查用户是否提供了正确的参数数量(即服务器的IP地址和端口号)。如果参数数量不正确,程序将打印出正确的使用方式并退出。

  2. 解析参数:程序从命令行参数中解析出服务器的IP地址和端口号,并将它们存储在相应的变量中。

  3. 创建套接字:程序调用socket函数创建一个UDP套接字。如果套接字创建失败,程序将打印出错误信息并退出。

  4. 填充服务器信息:程序创建一个sockaddr_in结构体来存储服务器的地址信息,包括IP地址和端口号。这些信息将用于向服务器发送数据。

  5. 主循环:程序进入一个无限循环,等待用户输入。用户输入的字符串将被发送到服务器。

    • 发送数据:程序使用sendto函数将用户输入的字符串发送到服务器。如果发送失败,程序将打印出错误信息并退出循环。

    • 接收数据:发送数据后,程序使用recvfrom函数等待并接收来自服务器的响应。如果接收到数据,程序将打印出服务器的响应。如果接收失败,程序将打印出错误信息并退出循环。

  6. 关闭套接字:当程序退出循环时(通常是因为用户输入了某些特定的命令或发生了错误),它将关闭套接字并退出。

#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[])//获取服务端的ip和端口号
{
    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);//要把主机序列转为网络序列
    // inet_addr把字符串形式的ip转为4字节
    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;
        //向服务端发消息 ,并且自动的将客户端的ip和端口号进行绑定
        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.8 用例测试 

如果你的云服务器支持通过公网ip访问,当然是可以通过网络跨机器进行交互的。那么就可以通过公网ip找到你的主机,通过端口号找到你对应的进程了。

 

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值