UDP套接字

UDP套接字基础 基本介绍 UDP是一种无连接不可靠的协议,客户端无需与服务器连接起来便可以直接向服务器发送数据,对于不需要发送大量数据的客户端比如DNS等,是比较方便。但是方便也意味着容易出问题,比如可靠性、安全性等,比如另一台PC就可以伪装成服务器向客户端发送错误的信息,我对这方面还不了解,所以就不讨论了。 虽然说UDP是一种无连接的协议,但也是可以通过connect函数进行“伪连接”的。所谓“伪连接”就是 阅读详情

目录

一、echo_UdpServer

二、Dict_UdpServer

三、chat_UdpServer

四、补充内容

地址转换函数

网络命令

ping

netstat

pidof

Windows和Linux进行udp通信


一、echo_UdpServer

使用UDP协议进行网络通信,用代码实现此功能

将服务端server布置在云服务器上,用一个头文件封装服务端的接口,用server.cc和client.cc模拟网络通信

先将之前写的日志包进来,这样通信实现中的错误就可以打印;日志的实现需要锁,因此也要将锁的实现包进来

首先在server中要进行套接字的申请,即要先实现Init,在这个函数中申请一个套接字,相当于文件描述符并且这个描述符在通信中要被使用,因此将其设置为成员属性_sockfd,申请的实际上是一个网络文件,要使用接口

// 这是需要的头文件

 #include <sys/types.h>        
 #include <sys/socket.h>

// 这里是接口,domain是域,这里是网络通信因此设置为AF_INET(AF表示协议家族,INET表示在网络中);type表示通信的数据类型,UDP面向数据报,因此设置为SOCK_DGRAM;protocol表示使用的协议类型,可以设置为0,因为由前两个参数已经可以确认这就是UDP协议

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


RETURN VALUE
       On  success,  a file descriptor for the new socket is returned.  On error, -1 is returned, and
       errno is set appropriately.

 

当然可能申请失败,此时利用日志打印错误 LOG(LogLevel::FATAL) << "socket error!" 并且exit(1);申请成功就打印成功日志以及_sockfd

接下来绑定自己的端口号以及ip,使对方可以确认通信进程

需要使用接口

// 头文件

#include <sys/types.h>     
#include <sys/socket.h>

// 接口,参数解释:第一个就是申请的描述符,第二个就是网络基础中阐述的结构体(继承多态),第三个是这个结构体的大小

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

由于这个结构体是一个输入型参数,我们需要自己定义,并且ip和端口号就是绑定在这里面的,也就是ip和端口号都在里面设置,因为是网络通信,定义的结构体类型应该是struct sockaddr_in;最后一个参数是结构体的大小

介绍这个结构体(下面块引用)->源码定义

因为ip和端口号要在通信中传给对方因此也要设置为成员属性

RETURN VALUE
       On success, zero is returned.  On error, -1 is returned, and errno is set appropriately.

 

// sockaddr_in介绍

这是一个结构体,第一个参数是一个宏定义,将sa_prefix定义为sa_prefix&&family,&&在宏定义中的作用就是将前后两者连接,那么就是sa_prefix_family,在结构体中就是sin_family,类型是协议家族sa_family_t,使用udp的时候初始化为AF_INET

第二个参数是端口号,类型实际上就是无符号短整型

第三个参数是ip地址,这个结构体中只有一个32位长整型类型变量,也就是ip

最后一个参数是填充字段,不用管

填充 sockaddr_in local 结构体,首先要将结构体清空,可以使用memset,这里介绍一个新的方法:bzero

       #include <strings.h>

       void bzero(void *s, size_t n);
// 用0清空s中的数据,n表示s的大小

这部分写代码

注意成员变脸端口号传入网络要转换成网络序列,使用函数htons;成员变量ip地址是用字符串接收的,所以传入网络的时候要转换,首先转为整数,之后转为网络序列,使用一个接口可以一步做到in_addr_t inet_addr(const char *cp)

将ip和端口号设置为成员属性,并且要求构造函数传入,在server.cc启动的时候初始化服务端的ip和端口号

接下来就是Start的编写了

实际上所有的软件都是自死循环,因为其服务器不会停止,那么这里的start启动服务器就让其一直死循环,要设置一个属性_isrunning,初始化为false,Start内部置为true,并且以这个变量为判断条件循环

接下来server要接受来自客户端的消息,需要认识接口recvfrom

       #include <sys/types.h>
       #include <sys/socket.h>

       ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                        struct sockaddr *src_addr, socklen_t *addrlen);
// 第一个参数就是自己的套接字描述符;

// 第二个参数是缓冲区,和文件一样,读取数据要先放入缓冲区;

// 第三个参数是缓冲区的大小;

// 第四个参数是读取方式,设置为0表示阻塞式IO

// 第五个参数是一个输出型参数,先定义一个struct sockaddr_in peer,这个函数接收来自客户端的数据,客户端的那个绑定ip以及端口需要的结构体会被传过来,并且记录在src_addr中,我们用peer(表示远端)来接受,这样server就知道了数据的来源

// 第六个参数是输出输入型参数,表示实际读到的数据大小,需要定义一个len(输入体现在len = sizeof(peer)即要先初始化)传入之后接收,len的值会改变为对方结构体的大小

RETURN VALUE
       These calls return the number of bytes received, or -1 if an error occurred.  In the event  of an error, errno is set to indicate the error.
// 接收成功返回读到的信息是多少字节,否则-1

若是返回值大于0,那么说明接收成功,server先打印接收到哪一个客户端以及其发送的信息(先不考虑来自哪一个客户端),server之后要返回信息表示成功接收到了,这里使用接口sendto

       #include <sys/types.h>
       #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);
// 第一个参数为套接字标识符

// 第二个是发送信息存的缓冲区

// 第三个是缓冲区大小

// 第四个表示发送方式,设置为0表示阻塞式IO

// 第五个是结构体,通过此结构体就能知道目的进程的ip以及端口号,这也是为什么接收信息时要记录下对方的结构体

// 从这里也能知道udp的sockfd是一个即可以写又可以读的,是全双工的

// 第六个参数是结构体大小

RETURN VALUE
       On  success, these calls return the number of bytes sent.  On error, -1 is returned, and errno is set appropriately.

代码

打印接收到的信息的时候是日志的DEBUG类型

发送的消息要在buffer原有的内容前面加上一个server echo# 表示是服务器的响应

接下来是server.cc的代码

首先./server的时候要传入ip以及端口号,那么就要使用命令行参数的形式,因此要先判断命令行参数的个数是不是对的,若是不对应该打印要如何输入并且退出,强制你重新./server

之后记录下ip和端口号,再将日志的打印模式设置为向显示器打印,然后用智能指针定义一个UdpServer类,之后调用方法Init以及Start

server的已经基本完成,现在写client.cc,这个没有封装;

首先也需要传递server的ip以及端口号,因为client发送消息要知道server的进程是哪一个;之后申请自己的套接字,之后需要绑定吗?

答案是不需要绑定,这是因为操作系统会自动为客户端的 UDP Socket 分配临时的源 IP 和端口,手动绑定反而会增加不必要的复杂度,甚至引发端口冲突问题。

之后获取server的ip以及端口号,并且初始化结构体

之后开始通信,也是一个死循环。client先发消息,之后才接受消息,接收消息的结构体是一个输出型参数,可以用peer来接收,这个结构体里面就是server的结构体

开始测试

先启动server连接公网ip,发现连接不了;

启动server连接内网ip或者127.0.0.1,可以绑定,之后启动client使用相同的ip、port,可以进行通信

127.0.0.1是一个本地环回测试的ip,要求cs在同一台机器上,c发出的消息不会传递到网络,而是传到OS,之后转一圈到s,目的是测试代码,因为还没有提交到网络,若是测试有误则说明代码有问题,而不是网络

使用ifconfig可以查看内网ip

启动server连接内网ip或者127.0.0.1,启动client连接127.0.0.1或者内网ip发现无法通信,这是因为:使用netstat -anup(a表示所有,n表示转为数字,u表示udp,p表示进程)查看,发现server绑定了内网或者127.0.0.1而client发送的目的却不是这个ip,所以才无法通信

总结:server无法绑定公网ip,绑定内网或者本地换回并且client也一样可以通信,不一样无法通信;

基于这个总结,那么应该如何跨网络通信呢?实际上server不建议手动绑定特定的ip,否则server只能和固定的主机通信,若是现在有很多主机都向server的同一端口发消息,而server只绑定了这个端口的一个客户端,那么就只能和这个客户端通信,其他的不行。一般将server绑定的ip设置为INADDR_ANY(宏替换为0)表示绑定任意地址ip,这样就可以和多个client通信

此时netstat -anup查看,发现server进程的ip为0.0.0.0,那么就可以接收任何ip发来的消息了

对应地,修改server.hpp的代码,去掉ip,只绑定INADDR_ANY,同样地server.cc的代码也要改变,这是修改部分的代码

// server.hpp
#pragma once

#include <iostream>
#include "Log.hpp"
#include "mutexEncapsulation.hpp"
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <arpa/inet.h>
#include <string>

using namespace LogModule;

const int defaultsockfd = -1;

class UdpServer
{
public:
    UdpServer(uint16_t port)
        : _sockfd(defaultsockfd),
          _port(port),
          _isrunning(false)
    {
    }
    void Init()
    {
        // 申请套接字
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockfd == -1)
        {
            logger(LogLevel::FATAL) << "socket error!";
            exit(1);
        }
        logger(LogLevel::INFO) << "socket success, _sockfd : " << _sockfd;

        // 绑定ip、端口
        struct sockaddr_in addr;
        bzero(&addr, sizeof(addr));
        addr.sin_family = AF_INET;
        addr.sin_port = htons(_port);
        addr.sin_addr.s_addr = INADDR_ANY;
        int n = bind(_sockfd, (struct sockaddr *)&addr, sizeof(addr));
        if (n == -1)
        {
            logger(LogLevel::FATAL) << "bind error!";
            exit(2);
        }
        logger(LogLevel::INFO) << "bind success, _sockfd : " << _sockfd;
    }
    void Start()
    {
        _isrunning = true;
        while (_isrunning)
        {
            char buffer[1024];
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            ssize_t s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, 
                (struct sockaddr*)&peer, &len);
            if (s > 0)
            {
                buffer[s] = 0;
                std::cout << "buffer : " << buffer << std::endl;
                std::string echo_string = "server echo# ";
                echo_string += buffer;
                ssize_t n = sendto(_sockfd, echo_string.c_str(), 
                echo_string.size(), 0, (struct sockaddr*)&peer, len);
            }
        }
    }
    ~UdpServer()
    {
    }

private:
    int _sockfd;
    uint16_t _port;
    bool _isrunning;
};
// server.cc
#include "server.hpp"
#include <memory>

// ./server port
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_ConsoleLog_Strategy();

    std::unique_ptr<UdpServer> usevr = std::make_unique<UdpServer>(port);
    usevr->Init();
    usevr->Start();
    return 0;
}

重新做上述实验,server只需要绑定端口,client用公网、内网、本地环回都可以进行通信,测试如下

这是在一台主机上的测试,跨网络可以在另一台主机上跑client程序,告诉client server所在机器的ip以及server代码进程绑定的端口同样可以通信

server想要知道是哪个client发送的消息,可以在server.cc中收到消息之后,打印client的ip(使用inet_ntoa(struct in_addr in)接口将4字节网络风格的IP转换为点分十进制的字符串风格的IP)和端口(要转换网络序列),因为peer在recvfrom中已经知道了客户端的结构体,这是代码修改

运行代码试试

可以看出先客户端口是自动分配的,就是为了避免冲突


二、Dict_UdpServer

v2版本是实现一个翻译功能的通信,也就是客户端发送消息给服务端,服务端拿着这个字符串去字典里面找到对应的翻译之后再将这个翻译的消息发送给客户端

首先要实现一个字典类Dict,这个类包方法DictLoad(加载字典将翻译的映射关系填入一个哈希表,这个字典是一个文件)、Translate(返回对应单词的意思)。那么就要设置一个属性unordered_map<string, string> trans

之后在server.cc里面实例化一个Dick的对象,将这个对象的translate功能通过lambda表达式传递给server.hpp中的UdpServer类,通过function类型属性包装这个方法(实现了Dict和UdpServer两个类之间的关联,这是低耦合的),在server收到消息之后就将英文单词传给这个func成员变量去调用translate并且将结果传给客户端,client.cc打印这个翻译结果

这是代码

词典信息

apple: 苹果
banana: 香蕉
orange: 橙子
grape: 葡萄
pear: 梨
peach  : 桃子
lemon: 柠檬
mango  : 芒果


strawberry: 草莓
watermelon: 西瓜
pineapple: 菠萝
cherry: 樱桃
kiwi: 猕猴桃
coconut: 椰子
blueberry: 蓝莓

Dict类

// 这个头文件封装Dict类

#pragma once
#include <iostream>
#include <unordered_map>
#include <string>
#include <fstream>
#include "Log.hpp"

using namespace LogModule;

std::string dict_path = "./Dictionary";
const std::string sep = ": "; // 字典中每一对翻译的中英字符串分隔符
class Dict
{
public:
    Dict(std::string &path = dict_path) : _path(path)
    {
    }
    bool DictLoad()
    {
        // 根据路径打开文件
        std::ifstream in(_path);
        if (!in.is_open())
        {
            logger(LogLevel::ERROR) << "打开字典:" << _path << " 错误";
            return false;
        }
        // 读取&&分割文件->getline逐行读取
        std::string line;
        while (std::getline(in, line))
        {
            int pos = line.find(sep);
            // 解析失败不应该退出而是跳过这行接续接续,因此日志等级为WARNING
            if (pos == std::string::npos)
            {
                logger(LogLevel::WARNING) << "解析 " << line << " 失败";
                continue;
            }
            std::string english = line.substr(0, pos);
            std::string chinese = line.substr(pos + sep.size());
            // 解析失败
            if (english.empty() || chinese.empty())
            {
                logger(LogLevel::WARNING) << "没有有效内容:" << line;
                continue;
            }
            // 解析成功,记录对应关系到_trans哈希表
            _trans.insert({english, chinese});
            logger(LogLevel::INFO) << "加载:" << line;
        }
        return true;
    }
    std::string Translate(std::string &word)
    {
        // 若是没有这个对应关系,直接返回
        if (_trans.count(word) == 0)
        {
            logger(LogLevel::DEBUG) << "翻译失败,词典中找不到意思";
            return "None";
        }
        logger(LogLevel::INFO) << "翻译成功,意思是:" << _trans[word];
        return _trans[word];
    }
    ~Dict()
    {
    }

private:
    std::string _path;
    std::unordered_map<std::string, std::string> _trans;
};

server.hpp

#pragma once

#include <iostream>
#include "Log.hpp"
#include "mutexEncapsulation.hpp"
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <arpa/inet.h>
#include <string>
#include <functional>

using namespace LogModule;

const int defaultsockfd = -1;

// 声明类型
typedef std::function<std::string(std::string&)> func_t;

class UdpServer
{
public:
    UdpServer(uint16_t port, func_t func)
        : _sockfd(defaultsockfd),
          _port(port),
          _isrunning(false),
          _func(func)
    {
    }
    void Init()
    {
        // 申请套接字
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockfd == -1)
        {
            logger(LogLevel::FATAL) << "socket error!";
            exit(1);
        }
        logger(LogLevel::INFO) << "socket success, _sockfd : " << _sockfd;

        // 绑定ip、端口
        struct sockaddr_in addr;
        bzero(&addr, sizeof(addr));
        addr.sin_family = AF_INET;
        addr.sin_port = htons(_port);
        addr.sin_addr.s_addr = INADDR_ANY;
        int n = bind(_sockfd, (struct sockaddr *)&addr, sizeof(addr));
        if (n == -1)
        {
            logger(LogLevel::FATAL) << "bind error!";
            exit(2);
        }
        logger(LogLevel::INFO) << "bind success, _sockfd : " << _sockfd;
    }
    void Start()
    {
        _isrunning = true;
        while (_isrunning)
        {
            char buffer[1024];
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            ssize_t s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0,
                                 (struct sockaddr *)&peer, &len);
            if (s > 0)
            {
                buffer[s] = 0;
                // uint16_t client_port = ntohs(peer.sin_port);
                // std::string client_ip = inet_ntoa(peer.sin_addr);
                // std::cout << "[ " << client_ip << " | " << client_port << " ]# " << buffer << std::endl;
                // std::string echo_string = "server echo# ";
                // echo_string += buffer;
                std::string word = buffer;
                std::string result = _func(word);
                ssize_t n = 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变动

测试结果

可以设置一个InetAddr类来获取请求翻译的客户端的ip以及端口号。这个类里面通过传递的struct sockaddr_in 结构体获取ip、port并且返回。那么server.cc以及server.hpp、Dict.hpp中部分内容就需要改变了。Dict.hpp在翻译函数中顺便打印客户端信息,需要多设置一个参数,server.cc的智能指针参数也要改变以及函数体也要改变,server.hpp调用_func的时候多传一个参数

无非就是在lambda表达式传参的时候多一个参数类型,这个参数类型是一个封装的类,server.hpp中的func_t也要多一个参数,这个参数类型的参数的获取是在server.hpp中客户端传来消息中的结构体,这个类的方法在原本的translate中调用,之后调用_func,translate随之被调用

这是代码以及测试结果

InetAddr.hpp

#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string>

class InetAddr
{
public:
    InetAddr(struct sockaddr_in& client):_client(client)
    {
        _ip = inet_ntoa(_client.sin_addr);
        _port = ntohs(_client.sin_port);
    }
    uint16_t Port()
    {
        return _port;
    }
    std::string Ip()
    {
        return _ip;
    }
    ~InetAddr()
    {

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

Dict.hpp中变化

server.cc中

server.hpp

#pragma once

#include <iostream>
#include "Log.hpp"
#include "mutexEncapsulation.hpp"
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <arpa/inet.h>
#include <string>
#include <functional>
#include "InetAddr.hpp"

using namespace LogModule;

const int defaultsockfd = -1;

// 声明类型
typedef std::function<std::string(std::string&, InetAddr&)> func_t;

class UdpServer
{
public:
    UdpServer(uint16_t port, func_t func)
        : _sockfd(defaultsockfd),
          _port(port),
          _isrunning(false),
          _func(func)
    {
    }
    void Init()
    {
        // 申请套接字
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockfd == -1)
        {
            logger(LogLevel::FATAL) << "socket error!";
            exit(1);
        }
        logger(LogLevel::INFO) << "socket success, _sockfd : " << _sockfd;

        // 绑定ip、端口
        struct sockaddr_in addr;
        bzero(&addr, sizeof(addr));
        addr.sin_family = AF_INET;
        addr.sin_port = htons(_port);
        addr.sin_addr.s_addr = INADDR_ANY;
        int n = bind(_sockfd, (struct sockaddr *)&addr, sizeof(addr));
        if (n == -1)
        {
            logger(LogLevel::FATAL) << "bind error!";
            exit(2);
        }
        logger(LogLevel::INFO) << "bind success, _sockfd : " << _sockfd;
    }
    void Start()
    {
        _isrunning = true;
        while (_isrunning)
        {
            char buffer[1024];
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            ssize_t s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0,
                                 (struct sockaddr *)&peer, &len);
            if (s > 0)
            {
                buffer[s] = 0;
                InetAddr inetaddr(peer);
                std::string word = buffer;
                std::string result = _func(word, inetaddr);
                ssize_t n = sendto(_sockfd, result.c_str(),
                                   result.size(), 0, (struct sockaddr *)&peer, len);
            }
        }
    }
    ~UdpServer()
    {
    }

private:
    int _sockfd;
    uint16_t _port;
    bool _isrunning;
    func_t _func;
};


三、chat_UdpServer

v3版本

这是一个聊天的实现,客户端发送消息之后所有的客户端包括自己都应该看见消息的内容。封装一个Route路由类,server收到消息之后将此消息发送到路由通过路由转发给所有的客户端。基本思路和Dict版本一样,lambda传递的函数是Route类里面的MessageRoute方法,Route有一个数组保存所有的客户端InetAddr,首次发送消息就会保存在vector里面,MessageRoute回遍历这个数组并且转发。

MessageRoute在server.hpp中收到消息的时候调用,功能是转发,那就需要使用到sendto接口。所以需要有三个参数:sockfd,message,InetAddr& peer 通过sockfd将message发送给peer

为了方便查看是此时操作的是哪一个在线用户,可以在InetAddr类中疯传一个函数,这个函数将这个用户的ip和port通过字符串的形式返回,我们在MessageRoute类中就可以看到是哪个用户

路由消息向所有在线用户转发,因此需要知道每一个用户的struct sockaddr_in 结构体,在InetAddr中封装一个函数,返回类型就是此结构

但是测试的时候多个客户端必须要先发送消息才能看到其他人发送的消息,这是因为client每次在读的时候都先要写,即sendto,而这个接口设置的是阻塞式的,不先发消息就会一直卡在这里。但是想要实现的是即使一个人不发消息,也应该看见其他人的消息,这也符合多人聊天的规则

所以应该这样做:将发消息和读消息设置为单独的一个线程,而client.cc是主线程,主线程负责创建套接字。这样代码跑起来就会第一时间看到其他人的消息

每次新用户进来应该打印消息并且让所有人看到;用户退出也应该要让所有人看到

这是代码

Route.hpp

#pragma once

#include <vector>
#include "Log.hpp"
#include "InetAddr.hpp"

using namespace LogModule;
 
class Route
{
public:
    Route()
    {
    }
    bool IsExist(InetAddr &peer)
    {
        for (auto &user : _online_user)
        {
            if (peer == user) // 需要在InetAddr里面重载==
                return true;
        }
        return false;
    }
    void AddUser(InetAddr &peer)
    {
        logger(LogLevel::INFO) << "新增一个在线用户:" << peer.StringAddr();
        _online_user.push_back(peer);
    }
    void DeleteUser(InetAddr &peer)
    {
        for (auto iter = _online_user.begin(); iter != _online_user.end(); iter++)
        {
            if (*iter == peer)
            {
                logger(LogLevel::INFO) << "删除在线用户成功:" << peer.StringAddr();
                _online_user.erase(iter);
                break;
            }
        }
    }
    void MessageRoute(int sockfd, std::string &message, InetAddr &peer)
    {
        // 不存在这个user就需要添加
        if (!IsExist(peer))
        {
            // 添加的函数也要封装
            AddUser(peer);
        }

        std::string send_message = peer.StringAddr() + message;
        // 开始路由消息,向所有在线用户转发
        for (auto &user : _online_user)
        {
            ssize_t n = sendto(sockfd, send_message.c_str(),
                               send_message.size(), 0, (const struct sockaddr *)&user.SockAddrIn(), sizeof(user.SockAddrIn()));
        }

        // 若message == "Quit"就表示退出,那么需要在_online_user中删除掉这个用户,封装一个DeleteUser函数
        if (message == "Quit")
        {
            logger(LogLevel::INFO) << "删除一个在线用户";
            DeleteUser(peer);
        }
    }
    ~Route()
    {
    }

private:
    std::vector<InetAddr> _online_user;
};

server.hpp

#pragma once

#include <iostream>
#include "Log.hpp"
#include "mutexEncapsulation.hpp"
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <arpa/inet.h>
#include <string>
#include <functional>
#include "InetAddr.hpp"

using namespace LogModule;

const int defaultsockfd = -1;

// 声明类型
typedef std::function<void(int, std::string&, InetAddr&)> func_t;

class UdpServer
{
public:
    UdpServer(uint16_t port, func_t func)
        : _sockfd(defaultsockfd),
          _port(port),
          _isrunning(false),
          _func(func)
    {
    }
    void Init()
    {
        // 申请套接字
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockfd == -1)
        {
            logger(LogLevel::FATAL) << "socket error!";
            exit(1);
        }
        logger(LogLevel::INFO) << "socket success, _sockfd : " << _sockfd;

        // 绑定ip、端口
        struct sockaddr_in addr;
        bzero(&addr, sizeof(addr));
        addr.sin_family = AF_INET;
        addr.sin_port = htons(_port);
        addr.sin_addr.s_addr = INADDR_ANY;
        int n = bind(_sockfd, (struct sockaddr *)&addr, sizeof(addr));
        if (n == -1)
        {
            logger(LogLevel::FATAL) << "bind error!";
            exit(2);
        }
        logger(LogLevel::INFO) << "bind success, _sockfd : " << _sockfd;
    }
    void Start()
    {
        _isrunning = true;
        while (_isrunning)
        {
            char buffer[1024];
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            ssize_t s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0,
                                 (struct sockaddr *)&peer, &len);
            if (s > 0)
            {
                buffer[s] = 0;
                InetAddr inetaddr(peer);
                std::string message = buffer;
                _func(_sockfd, message, inetaddr);
            }
        }
    }
    ~UdpServer()
    {
    }

private:
    int _sockfd;
    uint16_t _port;
    bool _isrunning;
    func_t _func;
};

server.cc

#include "server.hpp"
#include <memory>
#include "Route.hpp"

// ./server port
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_ConsoleLog_Strategy();

    Route route;
    std::unique_ptr<UdpServer> usevr = std::make_unique<UdpServer>(port, 
        [&route](int sockfd, std::string& message, InetAddr& cli){
            route.MessageRoute(sockfd, message, cli);
    });
    usevr->Init();
    usevr->Start();
    return 0;
}

client.cc

#include <iostream>
#include "Log.hpp"
#include "mutexEncapsulation.hpp"
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <arpa/inet.h>
#include <string>
#include <unistd.h>
#include "thread.hpp"

using namespace LogModule;
using namespace ThreadModule;

// 定义成全局无需传参
int sockfd;
std::string ip;
uint16_t port;
pthread_t id;

void Send()
{
    struct sockaddr_in serv;
    memset(&serv, 0, sizeof(serv));
    serv.sin_family = AF_INET;
    serv.sin_addr.s_addr = inet_addr(ip.c_str());
    serv.sin_port = htons(port);

    // 第一次发送消息就要告诉所有人新用户来了
    std::string input = "inline";
    sendto(sockfd, input.c_str(), input.size(), 0,
           (struct sockaddr *)&serv, sizeof(serv));
    sleep(1);
    // 发消息
    while (true)
    {
        std::string input;
        std::cout << "please input# ";
        std::getline(std::cin, input);
        sendto(sockfd, input.c_str(), input.size(), 0,
               (struct sockaddr *)&serv, sizeof(serv));
        // 当输入是Quit的时候,recev线程也要退出,这里手动退出
        if (input == "Quit")
        {
            pthread_cancel(id);
            break;
        }
    }
}

void Recev()
{
    while (true)
    {
        // 收消息
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        char buffer[1024];
        int s = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0,
                         (struct sockaddr *)&peer, &len);
        if (s > 0)
        {
            // 输出server的响应消息
            buffer[s] = 0;
            std::cerr << buffer << std::endl;
            std::cerr.flush();
        }
    }
}

// ./client serverip serverport
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        std::cerr << "Usage" << argv[0] << "ip port" << std::endl;
        return 1;
    }
    ip = argv[1];
    port = std::stoi(argv[2]);

    // 申请套接字
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd == -1)
    {
        logger(LogLevel::FATAL) << "socket error!";
        return 2;
    }
    // 不需要绑定

    // 创建两个线程,一个send一个recv
    Thread send(Send);
    Thread recev(Recev);

    send.Start();
    recev.Start();

    id = recev.Id();

    send.Join();
    recev.Join();
}

但是消息的打印会揉在一起,就是输出和输入混杂,为了方便聊天,要将输出和输入分别显示到不同终端,这里需要使用到重定向操作

将client的输出打印到cerr标准错误,输入输出独立于两个不同终端

用echo ‘num’>/dev/pts确定终端的编号

那么将client的收到消息的输出输出到1,而0则是用来input的;使用指令./client 127.0.0.1 8080 2>/dev/pts/1 (表示将client运行起来的标准错误打印到1终端,而程序的标准错误打印只有recev函数,也就是收到消息的处理)

测试

这样一来,多人聊天的时候自己界面的输出和输入就分开了,并且可以看到来自于不同用户的输入

消息的转发应该设计成多线程的,那么就要引入线程池,每次要广播消息,就像这个任务入线程池,让开辟出来的线程去进行消息的转发,也就是调用MessageRoute。整体上来看,client发送消息到server,server将广播的任务入线程池,线程池调用MessageRoute将消息广播传回给所有的client

但是这个代码还是存在一定问题,当多个client一起聊天的时候,很多消息会进入Route,有的client可能是第一次,那么就要push,有的可能Quit,就要delete,有的可能会将消息广播,这样一来广播的client就可能不是实时的,所以应该要给MessageRoute这个方法加锁,让client不会出现不一致的问题

这是代码变化


四、补充内容

地址转换函数

字符串转ip地址(in_addr)

in_addr转字符串

其中inet_pton和inet_ntop不仅可以转换IPv4的in_addr,还可以转换IPv6的in6_addr,因此函数接⼝是
void *addrptr。
关于inet_ntoa inet_ntoa这个函数返回了⼀个char*, 很显然是这个函数⾃⼰在内部为我们申请了⼀块内存来保存ip的结果. 那么是否需要调⽤者⼿动释放呢?
man⼿册上说, inet_ntoa函数, 是把这个返回结果放到了静态存储区. 这个时候不需要我们⼿动进⾏释放.那么问题来了, 如果我们调⽤多次这个函数, 会有什么样的效果呢? 参⻅如下代码:

因为inet_ntoa把结果放到⾃⼰内部的⼀个静态存储区, 这样第⼆次调⽤时的结果会覆盖掉上⼀次的结果
思考: 如果有多个线程调⽤ inet_ntoa, 是否会出现异常情况呢?
•
在APUE中, 明确提出inet_ntoa不是线程安全的函数;
在多线程环境下, 推荐使⽤inet_ntop, 这个函数由调⽤者提供⼀个缓冲区保存结果, 可以规避线程
安全问题;
推荐之后使用inet_ntop 和inet_pton函数实现两者转换

网络命令

ping

验证网络连通性

netstat

查看网络状态的重要工具

n 拒绝显⽰别名,能显⽰数字的全部转化成数字
•
l 仅列出有在 Listen (监听) 的服務状态
•
p 显⽰建⽴相关链接的程序名
•
t (tcp)仅显⽰tcp相关选项
•
u (udp)仅显⽰udp相关选项
•
a (all)显⽰所有选项,默认不显⽰LISTEN相关

配合使用的指令 watch -n 1 netstat -nltp 表示每一秒自动刷新查看一次网络状态,往后查看的时候就不需要一直输入netstat指令了

pidof

查看进程的pid,方便对进程进行操作,比如配合kill-9杀死进程,

Windows和Linux进行udp通信

不同系统是肯定可以通信的,所以必然会使用的相同的网络协议,那么Windows和Linux下的网络通信的接口绝大部分相似

#include <iostream>
#include <cstdio>
#include <thread>
#include <string>
#include <cstdlib>
#include <WinSock2.h>
#include <Windows.h>
#pragma warning(disable : 4996)
#pragma comment(lib, "ws2_32.lib")
std::string serverip = "8.129.16.214"; // 填写你的云服务器ip
uint16_t serverport = 8080; // 填写你的云服务开放的端⼝号
int main()
{
	WSADATA wsd;
	WSAStartup(MAKEWORD(2, 2), &wsd);
	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());
	SOCKET sockfd = socket(AF_INET, SOCK_DGRAM, 0);
	if (sockfd == SOCKET_ERROR)
	{
		std::cout << "socker error" << std::endl;
		return 1;
	}
	std::string message;
	char buffer[1024];
	while (true)
	{
		std::cout << "Please Enter@ ";
		std::getline(std::cin, message);
		if (message.empty()) continue;
		sendto(sockfd, message.c_str(), (int)message.size(), 0, (struct
			sockaddr*)&server, sizeof(server));
		struct sockaddr_in temp;
		int len = sizeof(temp);
		int s = recvfrom(sockfd, buffer, 1023, 0, (struct sockaddr*)&temp,
			&len);
		if (s > 0)
		{
			buffer[s] = 0;
			std::cout << buffer << std::endl;
		}
	}
	closesocket(sockfd);
	WSACleanup();
	return 0;
}

WinSock2.h 是 Windows Sockets API (应⽤程序接⼝)的头⽂件,⽤于在 Windows 平台上进⾏⽹络
1 编程。它包含了 Windows Sockets 2 ( Winsock2 )所需的数据类型、函数声明和结构定义,使得开发者能够创建和使⽤套接字(sockets )进⾏⽹络通信。
在编写使⽤ Winsock2 的程序时,需要在源⽂件中包含 WinSock2.h 头⽂件。这样,编译器就能够识别并理解Winsock2 中定义的数据类型和函数,从⽽能够正确地编译和链接⽹络相关的代码。
此外,与 WinSock2.h 头⽂件相对应的是 ws2_32.lib 库⽂件。在链接阶段,需要将这个库⽂件链接到
程序中,以确保运⾏时能够找到并调⽤ Winsock2 API 中实现的函数。
在 WinSock2.h 中定义了⼀些重要的数据类型和函数,如:
WSADATA :保存初始化 Winsock 库时返回的信息。
SOCKET :表⽰⼀个套接字描述符,⽤于在⽹络中唯⼀标识⼀个套接字。
sockaddr_in : IPv4 地址结构体,⽤于存储 IP 地址和端⼝号等信息。
socket () :创建⼀个新的套接字。
bind () :将套接字与本地地址绑定。
listen () :将套接字设置为监听模式,等待客⼾端的连接请求。
accept () :接受客⼾端的连接请求,并返回⼀个新的套接字描述符,⽤于与客⼾端进⾏通信。
WSAStartup 函数是 Windows Sockets API 的初始化函数,它⽤于初始化 Winsock 库。该函数在应⽤
程序或 DLL 调⽤任何 Windows 套接字函数之前必须⾸先执⾏,它扮演着初始化的⻆⾊。
以下是 WSAStartup 函数的⼀些关键点:
它接受两个参数: wVersionRequested 和 lpWSAData 。 wVersionRequested ⽤于指定所请求的
Winsock 版本,通常使⽤ MAKEWORD (major, minor) 宏,其中 major 和 minor 分别表⽰请求的主版本
号和次版本号。 lpWSAData 是⼀个指向 WSADATA 结构的指针,⽤于接收初始化信息。
如果函数调⽤成功,它会返回 0 ;否则,返回错误代码。
WSAStartup 函数的主要作⽤是向操作系统说明我们将使⽤哪个版本的 Winsock 库,从⽽使得该库⽂件能与当前的操作系统协同⼯作。成功调⽤该函数后,Winsock 库的状态会被初始化,应⽤程序就可以
使⽤ Winsock 提供的⼀系列套接字服务,如地址家族识别、地址转换、名字查询和连接控制等。这些服务使得应⽤程序可以与底层的⽹络协议栈进⾏交互,实现⽹络通信。
在调⽤ WSAStartup 函数后,如果应⽤程序完成了对请求的 Socket 库的使⽤,应调⽤ WSACleanup 函数来解除与Socket 库的绑定并释放所占⽤的系统资源。
山景BP10_128DBG开发板通过蓝牙连接手机放歌实验回顾 最近在学习上海山景公司的BP10_128DBG的开发板,因为网上关于BP10开发板的帖子也比较少,在学习的时候也没有太多的参考资料,基本上都是官方的历程。蓝牙放歌这个demo对我来说也比较复杂,特写下此博客进行记录、总结,大家有需要的可以参考。 阅读详情

相关推荐

防止通达信公式被盗?手把手教你用DLL加密+互联网验证(含易语言后台管理)

本文详细介绍了如何通过DLL加密与互联网验证技术保护通达信公式安全。从DLL加密实现、设备绑定到易语言后台管理,提供了一套完整的防护方案,有效防止公式被盗用。重点讲解了互联网验证登录和绑定设备等核心技术,帮助量化交易者守护核心资产。

weixin_29323551的博客 501

初识网络之UDP网络套接字

本文介绍了udp中常用的系统接口,如socket、recvfrom、sendto等。同时实现了客户端与服务端单向通信、客户端与服务端双向通信和多客户端与服务端通信三个程序。

Masquerena114514的博客 2629

手把手教你用THS4551搭建全差分放大器电路(附ADC驱动实战)

本文详细介绍了如何使用TI的THS4551全差分放大器搭建高性能ADC驱动电路,涵盖芯片选型、电路配置、PCB布局及实战调试等关键环节。THS4551凭借其优异的共模抑制比和宽电源电压范围,成为驱动16位及以上精度ADC的理想选择,特别适合工业应用场景。

weixin_30338481的博客 282

UDP套接字编程

参考:《UNIX 网络编程 · 卷1 : 套接字联网API》 UDP 与 TCP 之间传输存在差异,也导致编写应用程序存在很多差异。UDP 客户端和服务器不建立连接,而是直接使用 sendto 函数给服务器发送数据报。但必须指定目的地的地址作为参数。服务器也不用接受客户端的连接,而是直接调用 recvfrom 函数,等待来自某个客户的到达。recvform 将所接收的数据报和协议地址返回来。 UDP 典型交互图: recvfrom 和 sendto 函数 这两个函数类似于 read 和 write 函数,

code_peak 4784

Python 爬虫实战:全球咖啡期货市场数据采集与分析全攻略

通过本项目,我们实现了对全球咖啡期货市场数据的采集、处理和可视化,为市场分析和决策支持提供了数据基础。未来,可以进一步结合机器学习和深度学习技术,对咖啡期货价格进行预测和建模,提高市场分析的准确性和效率。

2201_76125261的博客 395

RK3588S之CPU--benchmark(一)

本章会基于RK3588S来介绍有关CPU常用的benchmark工具及测试过程

文艺小少年的博客 1499

【网络编程】详解UDP/TCP套接字的创建流程+守护进程

UDP协议和TCP协议创建套接字的全过程。

学了就忘是正常现象 4571

【脑肿瘤分类与迁移学习技术】Swin UNETR:当 Swin Transformer 遇见 3D 医学图像分割——从黑盒到白盒的逐层拆解

医学图像分割的终极目标是让算法成为医生的可靠副手,而非黑箱替代品。理解 Swin UNETR 的每一层如何流动、每一个窗口如何移位、每一次拼接如何融合,不仅是为了调参,更是为了在临床场景中。

u012133341的博客 55

详解 UDP 套接字 & 网络字节序 & IP 地址 & 端口号

UDP 套接字是一种用于实现 UDP 协议的编程接口,允许程序发送和接收数据报。使用 UDP 套接字,开发者可以轻松实现网络通信。

小蜗牛的珍贵百宝箱 1517

YOLO11创新改进系列:卷积,主干 注意力,C3k2融合,检测头等创新机制(已更新110+)

本专栏提供详细的 YOLO11 教程,包括基础知识、源码解析、入门实践、算法改进和项目实战,适合发表YOLO11 学习者订阅。内容包含 100+多篇独家改进机制,适合发表论文,评分高达 96 分。订阅者将获得一键运行的改进文件及答疑交流群支持。 使用服务器的同学还可以私聊获取搭建好的运行环境。

专注于图像领域,主要研究内容包括计算机视觉和深度学习,特别是在图像分类、目标检测和图像生成等方面有深入的研究和实践经验。 1万+

第14.7章 高翔博士最新开源框架Lightning-LM源码解读(七):PoseExtrapolator模块深度剖析,揭秘激光SLAM高频位姿外推核心技术

PoseExtrapolator是Lightning-LM框架中负责高频位姿外推的核心模块,通过融合DR(航位推算)、激光里程计和PGO(位姿图优化)数据实现精准实时定位。模块初始化依赖PGO数据,后续通过DR数据进行高频位姿更新,并利用PGO结果进行误差补偿。关键函数AddDRLocAndExtrapolate处理DR增量位姿,结合速度补偿机制逐步修正误差,保证高频输出的同时提升精度。该模块体现了多传感器融合思想,通过互补性数据(高频DR与低频高精PGO)实现鲁棒定位,代码中使用了SE3位姿表示、互斥锁保

行知SLAM 348

UDP 套接字编程入门

概述在使用TCP编写的应用程序和使用UDP编写的应用程序之间存在一些本质差异,其原因在于这两个传输层之间的差别:UDP是无连接不可靠的数据报协议,不同于TCP提供的面向连接的可靠字节流。从资源的角度来看,相对来说UDP套接字开销较小,因为不需要维持网络连接,而且因为无需花费时间来连接连接,所以UDP套接字的速度也较快。 因为UDP提供的是不可靠服务,所以数据可能会丢失。如果数据对于我们来说非常重要

Leo的博客 1万+

jdk-8u144-linux-x64.tar.gz.

该资源为搭建Linux系统Java环境用,资源的后缀为tar.gz格式的压缩包

【TCP套接字编程,UDP套接字编程】

应用进程使用传输层提供的服务才能交换报文。地点:界面上的SAP(Socket)方式:Socket API。:分布式应用进程之间的门,传输层协议提供的端到端的服务接口。:应用进程与端到端传输协议(TCP或UDP)之间的门户。4)客户端从socket中读取一行字符,然后打印出来。1)客户端从标准输入装置读取一行字符,发送给服务器。3)服务器将字符转换成大写,然后返回给客户端。:从一进程向另一个进程可靠地传输。2)服务器从socket读取字符。UDP 为客户端和服务器提供。不可靠的字节组的传送服务。

forever_youyang的博客 1400

OV9734 DATASHEET,完整规格书

OV9734完整规格书,包括电气参数,尺寸规格,寄存器配置表等

计算机控制技术及应用

《计算机控制技术及应用》通过总结作者的科研成果和吸收国内外的先进理论、方法和技术,以计算机控制系统的网络化、开放化、智能化和集成化发展趋势为主线索,主要介绍计算机控制系统的基本概念和计算机控制系统的典型形式,计算机控制系统的数学描述与基本分析方法,以及数字PID控制器、最少拍控制系统、达林算法、状态变量设计法等设计方法及其各种改进算法。重点突出计算机控制系统输入输出通道接口的设计方法和典型控制系统的设计案例与实现方法等。

【JavaEE】网络编程之TCP套接字、UDP套接字

网络编程的基本概念、TCP套接字和UDP套接字的API使用。

努力 1141

迈瑞BS系列全自动生化分析仪LIS协议接口手册(内含HL7和ASTM)

迈瑞BS系列全自动生化分析仪LIS协议接口手册,内含HL7和ASTM)

UDP套接字的数据传输.套接字的关闭

1.发送数据 #include #include ssize_t  sendto(int s,const void *msg,size_t  len ,int flags,const  struct  sockaddr   *to,socklen_t  tolen); 函数sendto的功能与参数send类似,但函数sendto不需要套接字处于连接状态,所以该函数通常用来发送UDP数据,

杨龙飞的博客 2866

UDP 套接字

UDP套接字编程 服务端流程 代码实现: day4/udp_server.py 创建数据报套接字 sockfd = socket(AF_INET,SOCK_DGRAM) 绑定地址 sockfd.bind(addr) 消息收发 data,addr = sockfd.recvfrom(buffersize) 功能: 接收UDP消息 参数: 每次最多接收多少字节 返回值: data 接收到的内容 a...

weixin_45819784的博客 215

UDP 套接字编程

UDP 套接字编程 UDP 套接字编程相较于TCP 套接字编程会简单一些, UDP 仅提供无连接的不可靠数据报协议,而TCP是面向连接的字节流协议. UDP 函数介绍 这里, 服务器和客户端都不需要调用 TCP 的 connect 方法, 客户端可以直接发送数据, 而服务端仅在为套接字命名(绑定端口)之后, 就可以接收消息. 函数介绍如下: socket 创建一个套接字并返回该套接字的文件描...

LeftShadow 459

UDP————套接字socket

摘要:本文介绍了UDP服务器的实现过程,包括套接字创建、绑定、消息接收与发送等核心功能。通过socket()创建UDP套接字,使用bind()绑定IP和端口,利用recvfrom()/sendto()实现无连接通信。服务器初始化时设置IPv4地址族和端口号,接收客户端消息后通过回调函数处理并返回响应。代码展示了网络字节序转换(htons/ntohs)和地址结构体(sockaddr_in)的使用,以及错误处理和日志记录机制。同时提及了客户端实现要点和智能指针(std::unique_ptr)在服务器管理中的应

m0_75006599的博客 888

详解UDP/TCP套接字

在IP数据包头部中, 有两个IP地址, 分别叫做源IP地址, 和目的IP地址。

一个喜欢猫咪的程序员 2507

UDP套接字的使用介绍

UDP套接字相比于TCP套接字具有不同的特征。     1、UDP套接字在使用之前不需要进行连接,即对应于客户端不需要进行connect,创建socket之后直接发送信息;服务器在绑定IP和端口之后在循环中不需要accept进行阻塞,直接利用接收函数阻塞接收信息,并且在完成通讯之后不需要进行close;     2、UDP是尽力而为的通信。     3、在发送的时候UDP需要指定一个比较合适

从小爱吃鱼的博客 2403

【Linux网络】:套接字之UDP

UDP聊天室

2201_75956982的博客 1386

UDP套接字实现

UDP套接字的相关接口及具体含义的链接. #pragma once #include <unistd.h> #include <cstdio> #include <cstring> #include <iostream> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <sys/types.h> usi

小马同学的博客 496

UDP数据报套接字编程

在前面,我们已经学习了一遍有关网络编程的基础知识,例如OSI七层模型、TCP/IP五层模型等,那么本篇我们就来了解如何使用网络编程。网络编程其实就是使用编程语言和通信协议来网络上进行数据交换和通信的过程。通过IP地址、端口号连接到另一台主机上对应的程序,按照规定的协议进行数据交换。当然,只要是不同的进程,即使在同一台主机上,基于网络来传输数据的,也属于网络编程。Socket(套接字)是网络编程中一个重要的概念,是应用程序进行网络通信的接口。

zhyhgx的博客 1885
上一篇: 动态规划——路径问题
下一篇: 动态规划——简单多状态dp
溪苏.
博客等级 码龄3年 995粉丝 138原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值