c语言mongoose普通用户,mongoose.c

// Copyright (c) 2004-2013 Sergey Lyubka

//

// Permission is hereby granted, free of charge, to any person obtaining a copy

// of this software and associated documentation files (the "Software"), to deal

// in the Software without restriction, including without limitation the rights

// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell

// copies of the Software, and to permit persons to whom the Software is

// furnished to do so, subject to the following conditions:

//

// The above copyright notice and this permission notice shall be included in

// all copies or substantial portions of the Software.

//

// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR

// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,

// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE

// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER

// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,

// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN

// THE SOFTWARE.

#define _XOPEN_SOURCE 600 // For flockfile() on Li

#include

#include

#include

#include

#include

#include

#include

#include

#include

#include

#include

#include

#include

#include

#include

#include

#include

#include

#include

#include

#include

#include

#include

#include

#include

#include

#include

#include

#define closesocket(a) close(a)

#define mg_sleep(x) usleep((x) * 1000)

#define ERRNO errno

#define INVALID_SOCKET (-1)

typedef int SOCKET;

#include "mongoose.h"

#define DEBUG

//#define MONGOOSE_VERSION "3.8"

#define BITBOX_VERSION "0.1"

#define PASSWORDS_FILE_NAME ".htpasswd"

#define CGI_ENVIRONMENT_SIZE 4096

#define MAX_CGI_ENVIR_VARS 64

#define MG_BUF_LEN 8192

#define MAX_REQUEST_SIZE 16384

#define ARRAY_SIZE(array) (sizeof(array) / sizeof(array[0]))

#define DEBUG_she(x) do { \

flockfile(stdout); \

printf("*** %lu.%p.%s.%d: ", \

(unsigned long) time(NULL), (void *) pthread_self(), \

__func__, __LINE__); \

printf x; \

putchar('\n'); \

fflush(stdout); \

funlockfile(stdout); \

} while (0)

#define DEBUG_PRINTF(x) do { \

flockfile(stdout); \

printf x; \

fflush(stdout); \

funlockfile(stdout); \

} while (0)

#ifdef DEBUG_TRACE

#undef DEBUG_TRACE

#define DEBUG_TRACE(x)

#else

#if defined(DEBUG)

#define DEBUG_TRACE(x) do { \

flockfile(stdout); \

printf("*** %lu.%p.%s.%d: ", \

(unsigned long) time(NULL), (void *) pthread_self(), \

__func__, __LINE__); \

printf x; \

putchar('\n'); \

fflush(stdout); \

funlockfile(stdout); \

} while (0)

#else

#define DEBUG_TRACE(x)

#endif // DEBUG

#endif // DEBUG_TRACE

#define IP_ADDR_STR_LEN 50 // IPv6 hex string is 46 chars

#if !defined(SOMAXCONN)

#define SOMAXCONN 100

#endif

// Unified socket address. For IPv6 support, add IPv6 address structure

// in the union u.

union usa {

struct sockaddr sa;

struct sockaddr_in sin;

#if defined(USE_IPV6)

struct sockaddr_in6 sin6;

#endif

};

// Describes a string (chunk of memory).

struct vec {

const char *ptr;

size_t len;

};

// Describes listening socket, or socket which was accept()-ed by the master

// thread and queued for future handling by the worker thread.

struct socket {

SOCKET sock; // Listening socket

union usa lsa; // Local socket address

union usa rsa; // Remote socket address

unsigned is_ssl:1; // Is port SSL-ed

unsigned ssl_redir:1; // Is port supposed to redirect everything to SSL port

};

struct mg_context {

volatile int stop_flag; // Should we stop event loop

struct socket *listening_sockets;

int num_listening_sockets;

volatile int num_threads; // Number of threads

pthread_mutex_t mutex; // Protects (max|num)_threads

pthread_cond_t cond; // Condvar for tracking workers terminations

struct socket queue[20]; // Accepted sockets

volatile int sq_head; // Head of the socket queue

volatile int sq_tail; // Tail of the socket queue

pthread_cond_t sq_full; // Signaled when socket is produced

pthread_cond_t sq_empty; // Signaled when socket is consumed

};

struct mg_connection {

// struct mg_request_info request_info;

struct mg_context *ctx;

//SSL *ssl; // SSL descriptor

// SSL_CTX *client_ssl_ctx; // SSL context for client connections

struct socket client; // Connected client

time_t birth_time; // Time when request was received

int64_t num_bytes_sent; // Total bytes sent to client

int64_t content_len; // Content-Length header value

int64_t consumed_content; // How many bytes of content have been read

// char *buf; // Buffer for received data

char *path_info; // PATH_INFO part of the URL

int must_close; // 1 if connection must be closed

int buf_size; // Buffer size

int request_len; // Size of the request + headers in a buffer

int data_len; // Total size of data in a buffer

int status_code; // HTTP reply status code, e.g. 200

int throttle; // Throttling, bytes/sec. <= 0 means no throttle

time_t last_throttle_time; // Last time throttled data was sent

int64_t last_throttle_bytes;// Bytes sent this second

};

static void sockaddr_to_string(char *buf, size_t len,

const union usa *usa) {

buf[0] = '\0';

#if defined(USE_IPV6)

inet_ntop(usa->sa.sa_family, usa->sa.sa_family == AF_INET ?

(void *) &usa->sin.sin_addr :

(void *) &usa->sin6.sin6_addr, buf, len);

#elif defined(_WIN32)

// Only Windoze Vista (and newer) have inet_ntop()

strncpy(buf, inet_ntoa(usa->sin.sin_addr), len);

#else

inet_ntop(usa->sa.sa_family, (void *) &usa->sin.sin_addr, buf, len);

#endif

}

static void cry(struct mg_connection *conn,

PRINTF_FORMAT_STRING(const char *fmt), ...) PRINTF_ARGS(2, 3);

// Print error message to the opened error log stream.

static void cry(struct mg_connection *conn, const char *fmt, ...) {

char buf[MG_BUF_LEN], src_addr[IP_ADDR_STR_LEN];

va_list ap;

FILE *fp;

time_t timestamp;

va_start(ap, fmt);

(void) vsnprintf(buf, sizeof(buf), fmt, ap);

va_end(ap);

// Do not lock when getting the callback value, here and below.

// I suppose this is fine, since function cannot disappear in the

// same way string option can.

fp = (FILE *)conn->ctx ;

if (fp != NULL) {

flockfile(fp);

timestamp = time(NULL);

sockaddr_to_string(src_addr, sizeof(src_addr), &conn->client.rsa);

fprintf(fp, "[%010lu] [error] [client %s] ", (unsigned long) timestamp,

src_addr);

fprintf(fp, "%s", buf);

fputc('\n', fp);

funlockfile(fp);

fclose(fp);

}

}

// Return fake connection structure. Used for logging, if connection

// is not applicable at the moment of logging.

static struct mg_connection *fc(struct mg_context *ctx) {

static struct mg_connection fake_connection;

fake_connection.ctx = ctx;

return &fake_connection;

}

const char *mg_version(void) {

return BITBOX_VERSION;

}

static void set_close_on_exec(int fd) {

fcntl(fd, F_SETFD, FD_CLOEXEC);

}

int mg_start_thread(mg_thread_func_t func, void *param) {

pthread_t thread_id;

pthread_attr_t attr;

int result;

(void) pthread_attr_init(&attr);

(void) pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);

// TODO(lsm): figure out why mongoose dies on Linux if next line is enabled

// (void) pthread_attr_setstacksize(&attr, sizeof(struct mg_connection) * 5);

result = pthread_create(&thread_id, &attr, func, param);

pthread_attr_destroy(&attr);

return result;

}

static int set_non_blocking_mode(SOCKET sock) {

int flags;

flags = fcntl(sock, F_GETFL, 0);

(void) fcntl(sock, F_SETFL, flags | O_NONBLOCK);

return 0;

}

// Parsed Authorization header

static void close_all_listening_sockets(struct mg_context *ctx) {

int i;

for (i = 0; i < ctx->num_listening_sockets; i++) {

closesocket(ctx->listening_sockets[i].sock);

}

free(ctx->listening_sockets);

}

// Valid listening port specification is: [ip_address:]port[s]

// Examples: 80, 443s, 127.0.0.1:3128, 1.2.3.4:8080s

// TODO(lsm): add parsing of the IPv6 address

static int set_port(struct socket *so) {

memset(so, 0, sizeof(*so));

so->lsa.sin.sin_family = AF_INET;

so->lsa.sin.sin_port = htons(9735);

return 1;

}

static int set_ports_option(struct mg_context *ctx) {

int on = 1, success = 1;

struct vec vec;

struct socket so, *ptr;

if (!set_port(&so)) {

cry(fc(ctx), "%s: %.*s: invalid port spec. Expecting list of: %s",

__func__, (int) vec.len, vec.ptr, "[IP_ADDRESS:]PORT[s|p]");

success = 0;

} else if ((so.sock = socket(so.lsa.sa.sa_family, SOCK_STREAM, 6)) ==

INVALID_SOCKET ||

// On Windows, SO_REUSEADDR is recommended only for

// broadcast UDP sockets

setsockopt(so.sock, SOL_SOCKET, SO_REUSEADDR,

(void *) &on, sizeof(on)) != 0 ||

#if defined(USE_IPV6)

setsockopt(so.sock, IPPROTO_IPV6, IPV6_V6ONLY, (void *) &off,

sizeof(off)) != 0 ||

#endif

bind(so.sock, &so.lsa.sa, sizeof(so.lsa)) != 0 ||

listen(so.sock, SOMAXCONN) != 0) {

cry(fc(ctx), "%s: cannot bind to %.*d: %s", __func__,

4, 9735, strerror(ERRNO));

closesocket(so.sock);

success = 0;

} else if ((ptr = realloc(ctx->listening_sockets,

(ctx->num_listening_sockets + 1) *

sizeof(ctx->listening_sockets[0]))) == NULL) {

closesocket(so.sock);

success = 0;

} else {

set_close_on_exec(so.sock);

ctx->listening_sockets = ptr;

ctx->listening_sockets[ctx->num_listening_sockets] = so;

ctx->num_listening_sockets++;

}

printf("[%s] success is %d \n",__func__,success);

if (!success) {

close_all_listening_sockets(ctx);

}

return success;

}

static void close_socket_gracefully(struct mg_connection *conn) {

struct linger linger;

// Set linger option to avoid socket hanging out after close. This prevent

// ephemeral port exhaust problem under high QPS.

linger.l_onoff = 1;

linger.l_linger = 1;

setsockopt(conn->client.sock, SOL_SOCKET, SO_LINGER,

(char *) &linger, sizeof(linger));

// Send FIN to the client

shutdown(conn->client.sock, SHUT_WR);

set_non_blocking_mode(conn->client.sock);

// Now we know that our FIN is ACK-ed, safe to close

closesocket(conn->client.sock);

}

static void close_connection(struct mg_connection *conn) {

conn->must_close = 1;

if (conn->client.sock != INVALID_SOCKET) {

close_socket_gracefully(conn);

conn->client.sock = INVALID_SOCKET;

}

}

void mg_close_connection(struct mg_connection *conn) {

close_connection(conn);

free(conn);

}

typedef enum playsrc_type{

WIFI=0,

USB=1,

AUX=2,

AUX1 = 3,

OPT = 4,

COA = 5,

BL = 6,

HDMI = 10,

SHORTCUT = 11,

SD= 9

}playsrc_t;

typedef enum networkType_t{

TCP_SWITCHING,

TCP_AP,

TCP_STA,

TCP_NONE,

}networkType_t;

typedef struct msg_header{

int cmdType;

playsrc_t audioInputSource;

networkType_t networkType;

unsigned short totalPage;/*total page */

unsigned short playindex;/*current play index */

unsigned short pageIndex;/*current paging Index*/

unsigned short curplayPage;/*curplayPage */

char ssid[20];

char key[20];

char playListId[64];

int playlistlen;

int playmode;

}msg_header_t;

static int data_recv(int fd, void *buf, int len)

{

/*********************************/

int n=0;

int nrecved=0;

/*********************************/

/** 1s delay

*when the client doesn't shut down,

*and waiting for the data,recv

*will block.

struct timeval tv_out;

tv_out.tv_sec = 4;

tv_out.tv_usec = 0;

setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv_out, sizeof(tv_out));

**/

while((n=recv(fd,buf+nrecved,len-nrecved,0))> 0) {

nrecved += n;

//printf(RED"[debug]nread is %d,n is %d\r\n"NONE,nrecved,n);

if(nrecved == len) {

break;/*break for recv block*/

}

}

return nrecved;

}

static void process_new_connection(struct mg_connection *conn) {

conn->data_len = 0;

DEBUG_TRACE(("[%s] is start \n", __func__));

do {

#if 1

char buf[sizeof(msg_header_t)];

int num;

num = data_recv(conn->client.sock,(void *)buf,sizeof(msg_header_t));

msg_header_t msgRecved;

memcpy(&msgRecved,buf,sizeof(msg_header_t));

DEBUG_TRACE(("[%s] num:%d msgRecved.cmdType is %d \n", __func__,num,msgRecved.cmdType));

#else

if (!getreq(conn, ebuf, sizeof(ebuf))) {

send_http_error(conn, 500, "Server Error", "%s", ebuf);

conn->must_close = 1;

} else if (!is_valid_uri(conn->request_info.uri)) {

snprintf(ebuf, sizeof(ebuf), "Invalid URI: [%s]", ri->uri);

send_http_error(conn, 400, "Bad Request", "%s", ebuf);

} else if (strcmp(ri->http_version, "1.0") &&

strcmp(ri->http_version, "1.1")) {

snprintf(ebuf, sizeof(ebuf), "Bad HTTP version: [%s]", ri->http_version);

send_http_error(conn, 505, "Bad HTTP version", "%s", ebuf);

}

if (ebuf[0] == '\0') {

handle_request(conn);

if (conn->ctx->callbacks.end_request != NULL) {

conn->ctx->callbacks.end_request(conn, conn->status_code);

}

log_access(conn);

}

if (ri->remote_user != NULL) {

free((void *) ri->remote_user);

// Important! When having connections with and without auth

// would cause double free and then crash

ri->remote_user = NULL;

}

// NOTE(lsm): order is important here. should_keep_alive() call

// is using parsed request, which will be invalid after memmove's below.

// Therefore, memorize should_keep_alive() result now for later use

// in loop exit condition.

keep_alive = conn->ctx->stop_flag == 0 && keep_alive_enabled &&

conn->content_len >= 0 && should_keep_alive(conn);

// Discard all buffered data for this request

discard_len = conn->content_len >= 0 && conn->request_len > 0 &&

conn->request_len + conn->content_len < (int64_t) conn->data_len ?

(int) (conn->request_len + conn->content_len) : conn->data_len;

assert(discard_len >= 0);

memmove(conn->buf, conn->buf + discard_len, conn->data_len - discard_len);

conn->data_len -= discard_len;

assert(conn->data_len >= 0);

assert(conn->data_len <= conn->buf_size);

#endif

} while (0);

}

// Worker threads take accepted socket from the queue

static int consume_socket(struct mg_context *ctx, struct socket *sp) {

(void) pthread_mutex_lock(&ctx->mutex);

DEBUG_TRACE(("going idle"));

// If the queue is empty, wait. We're idle at this point.

while (ctx->sq_head == ctx->sq_tail && ctx->stop_flag == 0) {

pthread_cond_wait(&ctx->sq_full, &ctx->mutex);

}

// If we're stopping, sq_head may be equal to sq_tail.

if (ctx->sq_head > ctx->sq_tail) {

// Copy socket from the queue and increment tail

*sp = ctx->queue[ctx->sq_tail % ARRAY_SIZE(ctx->queue)];

ctx->sq_tail++;

DEBUG_TRACE(("grabbed socket %d, going busy", sp->sock));

// Wrap pointers if needed

while (ctx->sq_tail > (int) ARRAY_SIZE(ctx->queue)) {

ctx->sq_tail -= ARRAY_SIZE(ctx->queue);

ctx->sq_head -= ARRAY_SIZE(ctx->queue);

}

}

(void) pthread_cond_signal(&ctx->sq_empty);

(void) pthread_mutex_unlock(&ctx->mutex);

return !ctx->stop_flag;

}

static void *worker_thread(void *thread_func_param) {

struct mg_context *ctx = thread_func_param;

struct mg_connection *conn;

conn = (struct mg_connection *) calloc(1, sizeof(*conn)/* + MAX_REQUEST_SIZE*/);

if (conn == NULL) {

cry(fc(ctx), "%s", "Cannot create new connection struct, OOM");

} else {

// conn->buf_size = MAX_REQUEST_SIZE;

// conn->buf = (char *) (conn + 1);

conn->ctx = ctx;

// conn->request_info.user_data = ctx->user_data;

// Call consume_socket() even when ctx->stop_flag > 0, to let it signal

// sq_empty condvar to wake up the master waiting in produce_socket()

while (consume_socket(ctx, &conn->client)) {

conn->birth_time = time(NULL);

// Fill in IP, port info early so even if SSL setup below fails,

// error handler would have the corresponding info.

// Thanks to Johannes Winkelmann for the patch.

// TODO(lsm): Fix IPv6 case

// conn->request_info.remote_port = ntohs(conn->client.rsa.sin.sin_port);

// memcpy(&conn->request_info.remote_ip,

// &conn->client.rsa.sin.sin_addr.s_addr, 4);

// conn->request_info.remote_ip = ntohl(conn->request_info.remote_ip);

process_new_connection(conn);

close_connection(conn);

}

free(conn);

}

// Signal master that we're done with connection and exiting

(void) pthread_mutex_lock(&ctx->mutex);

ctx->num_threads--;

(void) pthread_cond_signal(&ctx->cond);

assert(ctx->num_threads >= 0);

(void) pthread_mutex_unlock(&ctx->mutex);

DEBUG_TRACE(("exiting"));

return NULL;

}

// Master thread adds accepted socket to a queue

static void produce_socket(struct mg_context *ctx, const struct socket *sp) {

(void) pthread_mutex_lock(&ctx->mutex);

// If the queue is full, wait

while (ctx->stop_flag == 0 &&

ctx->sq_head - ctx->sq_tail >= (int) ARRAY_SIZE(ctx->queue)) {

(void) pthread_cond_wait(&ctx->sq_empty, &ctx->mutex);

}

if (ctx->sq_head - ctx->sq_tail < (int) ARRAY_SIZE(ctx->queue)) {

// Copy socket to the queue and increment head

ctx->queue[ctx->sq_head % ARRAY_SIZE(ctx->queue)] = *sp;

ctx->sq_head++;

DEBUG_TRACE(("queued socket %d", sp->sock));

}

(void) pthread_cond_signal(&ctx->sq_full);

(void) pthread_mutex_unlock(&ctx->mutex);

}

static int set_sock_timeout(SOCKET sock, int milliseconds) {

#ifdef _WIN32

DWORD t = milliseconds;

#else

struct timeval t;

t.tv_sec = milliseconds / 1000;

t.tv_usec = (milliseconds * 1000) % 1000000;

#endif

return setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, (void *) &t, sizeof(t)) ||

setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, (void *) &t, sizeof(t));

}

static void accept_new_connection(const struct socket *listener,

struct mg_context *ctx) {

struct socket so;

socklen_t len = sizeof(so.rsa);

int on = 1;

if ((so.sock = accept(listener->sock, &so.rsa.sa, &len)) == INVALID_SOCKET) {

} else {

// Put so socket structure into the queue

DEBUG_TRACE(("Accepted socket %d", (int) so.sock));

getsockname(so.sock, &so.lsa.sa, &len);

// Set TCP keep-alive. This is needed because if HTTP-level keep-alive

// is enabled, and client resets the connection, server won't get

// TCP FIN or RST and will keep the connection open forever. With TCP

// keep-alive, next keep-alive handshake will figure out that the client

// is down and will close the server end.

// Thanks to Igor Klopov who suggested the patch.

setsockopt(so.sock, SOL_SOCKET, SO_KEEPALIVE, (void *) &on, sizeof(on));

set_sock_timeout(so.sock, 4000);

DEBUG_TRACE(("produce_socket %d", (int) so.sock));

produce_socket(ctx, &so);

}

}

static void *master_thread(void *thread_func_param) {

struct mg_context *ctx = thread_func_param;

struct pollfd *pfd;

int i;

pfd = calloc(ctx->num_listening_sockets, sizeof(pfd[0]));

while (pfd != NULL && ctx->stop_flag == 0) {

for (i = 0; i < ctx->num_listening_sockets; i++) {

pfd[i].fd = ctx->listening_sockets[i].sock;

pfd[i].events = POLLIN;

}

if (poll(pfd, ctx->num_listening_sockets, 200) > 0) {

for (i = 0; i < ctx->num_listening_sockets; i++) {

// NOTE(lsm): on QNX, poll() returns POLLRDNORM after the

// successfull poll, and POLLIN is defined as (POLLRDNORM | POLLRDBAND)

// Therefore, we're checking pfd[i].revents & POLLIN, not

// pfd[i].revents == POLLIN.

if (ctx->stop_flag == 0 && (pfd[i].revents & POLLIN)) {

accept_new_connection(&ctx->listening_sockets[i], ctx);

}

}

}

}

free(pfd);

DEBUG_TRACE(("stopping workers"));

// Stop signal received: somebody called mg_stop. Quit.

close_all_listening_sockets(ctx);

// Wakeup workers that are waiting for connections to handle.

pthread_cond_broadcast(&ctx->sq_full);

// Wait until all threads finish

(void) pthread_mutex_lock(&ctx->mutex);

while (ctx->num_threads > 0) {

(void) pthread_cond_wait(&ctx->cond, &ctx->mutex);

}

(void) pthread_mutex_unlock(&ctx->mutex);

// All threads exited, no sync is needed. Destroy mutex and condvars

(void) pthread_mutex_destroy(&ctx->mutex);

(void) pthread_cond_destroy(&ctx->cond);

(void) pthread_cond_destroy(&ctx->sq_empty);

(void) pthread_cond_destroy(&ctx->sq_full);

DEBUG_TRACE(("exiting"));

// Signal mg_stop() that we're done.

// WARNING: This must be the very last thing this

// thread does, as ctx becomes invalid after this line.

ctx->stop_flag = 2;

return NULL;

}

static void free_context(struct mg_context *ctx) {

// Deallocate context itself

free(ctx);

}

void mg_stop(struct mg_context *ctx) {

ctx->stop_flag = 1;

// Wait until mg_fini() stops

while (ctx->stop_flag != 2) {

(void) mg_sleep(10);

}

free_context(ctx);

}

struct mg_context *mg_start(void) {

struct mg_context *ctx;

int i;

// Allocate context and initialize reasonable general case defaults.

// TODO(lsm): do proper error handling here.

if ((ctx = (struct mg_context *) calloc(1, sizeof(*ctx))) == NULL) {

return NULL;

}

(void) pthread_mutex_init(&ctx->mutex, NULL);

(void) pthread_cond_init(&ctx->cond, NULL);

(void) pthread_cond_init(&ctx->sq_empty, NULL);

(void) pthread_cond_init(&ctx->sq_full, NULL);

// Start master (listening) thread

if (!set_ports_option(ctx) )

return NULL;

mg_start_thread(master_thread, ctx);

// Start worker threads

for (i = 0; i < 20; i++) {

if (mg_start_thread(worker_thread, ctx) != 0) {

cry(fc(ctx), "Cannot start worker thread: %ld", (long) ERRNO);

} else {

ctx->num_threads++;

}

}

return ctx;

}

一键复制

编辑

Web IDE

原始数据

按行查看

历史

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值