成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

聊聊No.js---基于V8和io_uring的JS運行時

開發 前端
本文介紹運行時No.js的一些設計和實現,取名No.js一來是受Node.js的影響,二來是為了說明不僅僅是JS,也就是利用V8拓展了JS的功能,同時,前端開發者要學習的知識也不僅僅是JS了。

[[421775]]

前言:閱讀Node.js的源碼已經有一段時間了,最近也看了一下新的JS運行時Just的一些實現,就產生了自己寫一個JS運行時的想法,雖然幾個月前就基于V8寫了一個簡單的JS運行時,但功能比較簡單,這次廢棄了之前的代碼,重新寫了一遍,寫這個JS運行時的目的最主要是為了學習,事實也證明,寫一個JS運行時的確可以學到很多東西。本文介紹運行時No.js的一些設計和實現,取名No.js一來是受Node.js的影響,二來是為了說明不僅僅是JS,也就是利用V8拓展了JS的功能,同時,前端開發者要學習的知識也不僅僅是JS了。

1 為什么選io_uring

io_uring是Linux下新一代的高性能異步IO框架,也是No.js的核心。在No.js中,io_uring用于實現事件循環。為什么不選用epoll呢?因為epoll不支持文件IO,如果選用epoll,還需要自己實現一個線程池,還需要實現線程和主線程的通信,以及線程池任務和事件循環的融合,No.js希望把事件變得純粹,簡單。而io_uring是支持異步文件IO的,并且io_uring是真正的異步IO框架,支持的功能也非常豐富,比如在epoll里我們監聽一個socket后,需要把socket fd注冊到epoll中,等待有連接時執行回調,然后調用accept獲取新的fd,而io_uring直接就幫我們獲取新的fd,io_uring通知我們的時候,我們就已經拿到新的fd了,epoll時代,epoll通知我們可以做什么事情了,然后我們自己去做,io_uring時代,io_uring通知我們什么事情完成了。

2 No.js框架的設計

No.js目前的實現比較清晰簡單,所有的功能都通過c和c++實現,然后通過V8暴露給JS實現。No.cc是初始化的入口,core目錄是所有功能實現的地方,core下面按照模塊功能劃分。下面我們看看整體的框架實現。

  1. int main(int argc, char* argv[]) { 
  2.   // ... 
  3.   Isolate* isolate = Isolate::New(create_params); 
  4.   { 
  5.     Isolate::Scope isolate_scope(isolate); 
  6.     HandleScope handle_scope(isolate); 
  7.     // 創建全局對象 
  8.     Local<ObjectTemplate> global = ObjectTemplate::New(isolate); 
  9.     // 創建執行上下文 
  10.     Local<Context> context = Context::New(isolate, nullptr, global); 
  11.     Environment * env = new Environment(context); 
  12.     Context::Scope context_scope(context); 
  13.     // 創建No,核心對象 
  14.     Local<Object> No = Object::New(isolate); 
  15.     // 注冊c、c++模塊 
  16.     register_builtins(isolate, No); 
  17.     // 獲取全局對象 
  18.     Local<Object> globalInstance = context->Global(); 
  19.     // 設置全局屬性 
  20.     globalInstance->Set(context, String::NewFromUtf8Literal(isolate, "No",  
  21.     NewStringType::kNormal), No); 
  22.     // 設置全局屬性global指向全局對象 
  23.     globalInstance->Set(context, String::NewFromUtf8Literal(isolate,  
  24.       "global",  
  25.       NewStringType::kNormal), globalInstance).Check(); 
  26.     { 
  27.       // 打開文件 
  28.       int fd = open(argv[1], O_RDONLY); 
  29.       struct stat info; 
  30.       // 取得文件信息 
  31.       fstat(fd, &info); 
  32.       // 分配內存保存文件內容 
  33.       char *ptr = (char *)malloc(info.st_size + 1); 
  34.       read(fd, (void *)ptr, info.st_size); 
  35.       // 要執行的js代碼 
  36.       Local<String> source = String::NewFromUtf8(isolate, ptr, 
  37.                           NewStringType::kNormal, 
  38.                           info.st_size).ToLocalChecked(); 
  39.  
  40.       // 編譯 
  41.       Local<Script> script = Script::Compile(context, source).ToLocalChecked(); 
  42.       // 解析完應該沒用了,釋放內存 
  43.       free(ptr); 
  44.       // 執行 
  45.       Local<Value> result = script->Run(context).ToLocalChecked(); 
  46.       // 進入事件循環 
  47.       Run(env->GetIOUringData()); 
  48.     } 
  49.   } 
  50.   return 0; 
  51.  

大部分邏輯都是V8初始化的標準流程,添加的內容主要包括注冊c、c++模塊、掛載No到全局作用域、開啟事件循環。

2.1 注冊模塊

No在初始化的時候會把所有C++模塊注冊到No中,因為No是全局屬性,所以在JS里可以直接訪問C++模塊,不需要require。我們看看register_builtins。

  1. void No::Core::register_builtins(Isolate * isolate, Local<Object> target) { 
  2.     FS::Init(isolate, target);  
  3.     TCP::Init(isolate, target);  
  4.     Process::Init(isolate, target);  
  5.     Console::Init(isolate, target); 
  6.     IO::Init(isolate, target); 
  7.     Net::Init(isolate, target); 
  8.     UDP::Init(isolate, target); 
  9.     UNIX_DOMAIN::Init(isolate, target); 
  10.     Signal::Init(isolate, target); 
  11.     Timer::Init(isolate, target); 
  12.  

register_builtins會調用各個模塊的Init函數,各個模塊自己實現需要掛載的功能,從代碼中可以看到目前實現的功能。我們隨便找一個模塊看看初始化的邏輯。

  1. void No::FS::Init(Isolate* isolate, Local<Object> target) { 
  2.   Local<ObjectTemplate> fs = ObjectTemplate::New(isolate); 
  3.   setMethod(isolate, fs, "open"No::FS::Open); 
  4.   setMethod(isolate, fs, "openat"No::FS::OpenAt); 
  5.   setMethod(isolate, fs, "close"No::IO::Close); 
  6.   setMethod(isolate, fs, "read"No::IO::Read); 
  7.   setMethod(isolate, fs, "write"No::IO::Write); 
  8.   setMethod(isolate, fs, "readv"No::IO::ReadV); 
  9.   setMethod(isolate, fs, "writev"No::IO::WriteV); 
  10.   setObjectValue(isolate, target, "fs", fs->NewInstance(isolate->GetCurrentContext()).ToLocalChecked()); 
  11.  

掛載的邏輯就是新建一個對象,然后設置對象的屬性,最后把這個對象作為No對象的一個屬性掛載到No中,最后形成如下一個結構。

  1. var No = { 
  2.     fs: {}, 
  3.     tcp: {} 
  4.  

這就完成了所有核心模塊的注冊。

2.2 執行JS

注冊完核心模塊后就是執行業務JS。我們隨便看個例子。

  1. const { 
  2.     fs, 
  3.     console 
  4. } = No;const fd = fs.open('./test/file/1.txt');const arr = new ArrayBuffer(100); 
  5. fs.readv(fd,arr , 0, (res) => {console.log(res)}); 
  6. console.log(new Uint8Array(arr)); 

以上是讀取一個文件的例子,從中也可以看到No的使用方式。No沒有實現類似Node.js的Buffer,是直接使用V8的ArrayBuffer的,ArrayBuffer使用的是V8堆外內存,readv是C++層實現的函數,我們一會單獨介紹。

2.3 開啟事件循環

執行完JS后,最后進入事件循環。

  1. void No::io_uring::RunIOUring(struct io_uring_info *io_uring_data) { 
  2.     struct io_uring* ring = &io_uring_data->ring; 
  3.     struct io_uring_cqe* cqe; 
  4.     struct request* req; 
  5.     while(io_uring_data->stop != 1 && io_uring_data->pending != 0) { 
  6.         // 提交請求給內核 
  7.         int count = io_uring_submit_and_wait(ring, 1); 
  8.         // 處理每一個完成的請求 
  9.         while (1) {  
  10.             io_uring_peek_cqe(ring, &cqe); 
  11.             if (cqe == NULL
  12.                 break; 
  13.             --io_uring_data->pending; 
  14.             // 拿到請求上下文 
  15.             req = (struct request*) (uintptr_t) cqe->user_data; 
  16.             req->res = cqe->res; 
  17.             io_uring_cq_advance(ring, 1); 
  18.             // 執行回調 
  19.             if (req->cb != nullptr) { 
  20.                 req->cb((void *)req); 
  21.             } 
  22.         } 
  23.     } 
  24.  

從事件循環的代碼中大致可以看到原理,首先判斷事件循環是不是停止或者可以停止了,如果還沒有停止,則等待任務完成,然后取出任務執行任務的對象。

3 任務的封裝和處理

io_uring的任務是以結構體io_uring_sqe表示的,但是io_uring_sqe只是記錄了和io_uring框架本身相關的一些數據結構,因為是異步的模式,所以在任務完成的時候,我們需要知道,這個任務關聯的上下文和回調。io_uring_sqe提供了user_data字段用于保存請求對應的上下文。流程如下。設置和提交請求

  1. // 獲取一個io_uring的請求結構體 
  2.  struct io_uring_sqe *sqe = io_uring_get_sqe(&io_uring_data->ring); 
  3.  // 自定義結構體 
  4.  struct io_request * file_req = (struct io_request *)req; 
  5.  // 設置請求的字段 
  6.  io_uring_prep_read(sqe, file_req->fd, file_req->buf, file_req->len, file_req->offset); 
  7.  // 保存請求上下文,響應的時候用 
  8.  io_uring_sqe_set_data(sqe, (void *)req); 
  9.  // 提交請求 
  10.  io_uring_submit(&io_uring_data->ring); 

我們看到提交請求的時候,設置了請求上下文是我們自定義的結構體,具體結構體類型根據操作類型而不同。我們看看請求完成時是如何處理的。

  1. struct io_uring_cqe* cqe;io_uring_peek_cqe(ring, &cqe);// 拿到請求上下文 
  2. req = (struct request*) (uintptr_t) cqe->user_data;// 記錄請求結果 
  3. req->res = cqe->res; 
  4. req->cb((void *)req); 

以上就是一個No請求和響應的處理過程。No為不同的操作類型封裝了不同的結構體。首先封裝了一個請求的基類。

  1. #define REQUEST \ 
  2.         int op; \ 
  3.         // io_uring執行的回調 
  4.         request_cb cb; \ 
  5.         // io_uring請求的結果 
  6.         int res;\ 
  7.         // 業務上下文 
  8.         void * data; \ 
  9.         int flag; 

類似io_uring通過user_data字段關聯請求響應上下文。REQUEST 里通過data關聯請求和響應上下文,通過user_data字段,我們在任務完成時可以執行應該執行哪個回調以及對應的上下文。但是執行某個回調時,該回調函數需要的上下文可能不僅僅是io_uring返回的結果,這時候就可以使用data字段記錄額外的上下文。一會會具體介紹。基于REQUEST,針對不同的操作封裝了不同的結構體,比如文件請求。

  1. struct io_request { 
  2.     REQUEST 
  3.     int fd;  
  4.     int offset;  
  5.     void *buf; 
  6.     int len;  
  7. }; 

下面我們分析一個具體請求的過程,這里以read為例。

  1. void read_write_request(V8_ARGS, int op) {  
  2.     V8_ISOLATE 
  3.     int fd = args[0].As<Uint32>()->Value(); 
  4.     int offset = 0; 
  5.     if (args.Length() > 2 && args[2]->IsNumber()) { 
  6.         offset = args[2].As<Integer>()->Value(); 
  7.     } 
  8.     Local<ArrayBuffer> arrayBuffer = args[1].As<ArrayBuffer>(); 
  9.     std::shared_ptr<BackingStore> backing = arrayBuffer->GetBackingStore(); 
  10.     V8_CONTEXT 
  11.     Environment *env = Environment::GetEnvByContext(context); 
  12.     struct io_uring_info *io_uring_data = env->GetIOUringData(); 
  13.     struct request *req; 
  14.     // 文件操作對應的request結構體 
  15.     struct io_request *io_req = (struct io_request *)malloc(sizeof(struct io_request)); 
  16.     memset(io_req, 0, sizeof(*io_req)); 
  17.     io_req->buf = backing->Data(); 
  18.     io_req->len = backing->ByteLength(); 
  19.     io_req->fd = fd; 
  20.     io_req->offset = offset; 
  21.     req = (struct request *)io_req; 
  22.     // JS層回調 
  23.     req->cb = makeCallback<onread>; 
  24.     req->op = op; 
  25.     // 保存回調上下文 
  26.     if (args.Length() > 3 && args[3]->IsFunction()) { 
  27.         Local<Object> obj = Object::New(isolate); 
  28.         Local<String> key = newStringToLcal(isolate, onread); 
  29.         obj->Set(context, key, args[3].As<Function>()); 
  30.         req->data = (void *)new RequestContext(env, obj); 
  31.     } else { 
  32.         req->data = (void *)new RequestContext(env, Local<Function>()); 
  33.     } 
  34.     // 提交請求 
  35.     SubmitRequest((struct request *)req, io_uring_data);   

初始化請求的上下文后,調用SubmitRequest提交任務和io_uring。我們看看SubmitRequest。

  1. void No::io_uring::SubmitRequest(struct request * req, struct io_uring_info *io_uring_data) { 
  2.     // 獲取一個io_uring的請求結構體 
  3.     struct io_uring_sqe *sqe = io_uring_get_sqe(&io_uring_data->ring); 
  4.     // 填充請求 
  5.     switch (req->op) 
  6.     { 
  7.  
  8.         case IORING_OP_READ: 
  9.             { 
  10.                 struct io_request * file_req = (struct io_request *)req; 
  11.                 io_uring_prep_read(sqe, file_req->fd, file_req->buf, file_req->len, file_req->offset); 
  12.                 break; 
  13.             } 
  14.         default
  15.             return
  16.     } 
  17.     ++io_uring_data->pending; 
  18.     // 保存請求上下文,響應的時候用 
  19.     io_uring_sqe_set_data(sqe, (void *)req); 
  20.     io_uring_submit(&io_uring_data->ring); 
  21.  

SubmitRequest根據不同的操作設置io_uring的請求結構體,并保存對應的請求上下文。當任務完成時執行回調makeCallback。makeCallback是模板函數。

  1. template <const char * event> 
  2.   void makeCallback(void * req) { 
  3.        struct request * _req = (struct request *)req; 
  4.        RequestContext* ctx =(RequestContext *)_req->data; 
  5.        if (!ctx->object.IsEmpty()) { 
  6.            Local<Object> object = ctx->object.Get(ctx->env->GetIsolate()); 
  7.            Local<Value> cb; 
  8.            Local<Context> context = ctx->env->GetContext(); 
  9.            Local<String> onevent = newStringToLcal(ctx->env->GetIsolate(), event);        
  10.            object->Get(context, onevent).ToLocal(&cb); 
  11.            if (cb->IsFunction()) {   
  12.                Local<Value> argv[] = { 
  13.                    Integer::New(context->GetIsolate(), _req->res) 
  14.                }; 
  15.                // 執行JS層回調 
  16.                cb.As<v8::Function>()->Call(context, object, 1, argv); 
  17.            } 
  18.        } 
  19.    }; 

makeCallback做的事情就是執行JS回調。

4 非io_uring的處理

io_uring目前已經支持了非常多的操作,但我們也不可避免地會碰到io_uring不支持的操作,比如信號的處理。No里目前定時器和信號不是使用io_uring處理的。定時器目前使用內核的posix timer實現的,io_uring有個timeout類型的請求,可能會使用io_uring的,信號處理io_uring就無能無力了。因為No是單線程的架構,所以非io_uring的任務完成后也需要通過io_uring事件循環執行,下面看一下非io_uring支持的操作如何處理的。在業務里,我們可能需要監聽一個信號。

  1. const { 
  2.     signal, 
  3.     console, 
  4.     process, 
  5.     timer,} = No
  6.  
  7. signal.on(signal.constant.SIG.SIGUSR1, () => { 
  8.     process.exit(); 
  9.  
  10. }); 
  11.  
  12. // for keep process alive 
  13.  
  14. timer.setInterval(() => {},10000, 10000); 

可以通過signal模塊的on實現監聽信號,接下來看看具體實現。

  1. void No::Signal::RegisterSignal(V8_ARGS) { 
  2.     V8_ISOLATE 
  3.     V8_CONTEXT 
  4.     Environment *env = Environment::GetEnvByContext(context); 
  5.     Local<Object> obj = Object::New(isolate); 
  6.     Local<String> key = newStringToLcal(isolate, onsignal); 
  7.     obj->Set(context, key, args[1].As<Function>()); 
  8.     int sig = args[0].As<Integer>()->Value();  
  9.     // 新建一個上下文 
  10.     shared_ptr<SignalRequestContext> ctx = make_shared<SignalRequestContext>(env, obj, sig); 
  11.     auto ret = signalMap.find(sig); 
  12.     // 是否在map里,不是則新建一個vector,否則直接追加 
  13.     if (ret == signalMap.end()) { 
  14.         signal(sig, signalHandler); 
  15.         vector<shared_ptr<SignalRequestContext>> vec; 
  16.         vec.push_back(ctx); 
  17.         signalMap.insert(map<int, vector<shared_ptr<SignalRequestContext>>>::value_type (sig, vec));   
  18.         return
  19.     } 
  20.     ret->second.push_back(ctx); 
  21.  

No使用一個map管理信號和監聽函數,因為支持多個監聽函數,所以map的key是信號的值,value是一個回調函數數組。如果是第一次注冊該信號,則調用signal注冊該信號的處理函數,所有信號的處理函數都是signalHandler。接著看信號產生時的處理邏輯。

  1. static void signalHandler(int signum){    
  2.     auto vec = signalMap.find(signum); 
  3.     if (vec != signalMap.end()) { 
  4.         vector<shared_ptr<SignalRequestContext>>::iterator it; 
  5.         for(it=vec->second.begin();it!=vec->second.end(); it++) 
  6.         { 
  7.             struct signal_request * req = (struct signal_request *)malloc(sizeof(*req));  
  8.             memset(req, 0, sizeof(*req)); 
  9.             req->cb = signal_cb; 
  10.             req->data = (void *)(*it).get(); 
  11.             req->op = IORING_OP_NOP; 
  12.             SubmitRequest((struct request *)req, (*it).get()->env->GetIOUringData()); 
  13.         } 
  14.     } 
  15.  

信號產生時,從map中找到對應的處理函數列表,然后生成一個io_uring請求,這樣在事件循環時就會被執行,也實現了非io_uring任務和io_uring任務的整合,這里主要是利用了io_uring提供了nop類型的請求,這個類型的請求不做任何操作,主要是用于測試io_uring請求和響應鏈路,利用這點恰好可以實現我們的需求。從代碼中可以看到io_uring事件循環時會執行信號處理的回調signal_cb,signal_cb會回調JS層。

5 上下文的設計

因為No各種請求都是異步的,所以避免不了需要保持請求和響應的上下文。類似Node.js,No里也存在一個env作為整個進程級的上下文。

  1. enum { 
  2.     CONTEXT_INDEX 
  3. } ENV_INDEX; 
  4.  
  5. class Environment { 
  6.     public
  7.         Environment(Local<Context> context); 
  8.         static Environment * GetEnvByContext(Local<Context> context); 
  9.         struct io_uring_info * GetIOUringData() { 
  10.             return io_uring_data; 
  11.         } 
  12.         Isolate * GetIsolate() const { 
  13.             return _isolate; 
  14.         } 
  15.          Local<Context> GetContext() const { 
  16.             return PersistentToLocal::Strong(_context); 
  17.         } 
  18.     private: 
  19.         struct io_uring_info *io_uring_data; 
  20.         Global<Context> _context; 
  21.         Isolate * _isolate; 
  22.  
  23. }; 

env目前的功能還不多,只要負責管理context、isolate、io_uring等數據結構。另外還有一些和具體操作相關的上下文。

  1. struct RequestContext { 
  2.    RequestContext(Environment * passEnv, Local<Object> _object) 
  3.    : env(passEnv),  object(passEnv->GetIsolate(), _object) {} 
  4.    ~RequestContext() { 
  5.        if (!object.IsEmpty()) { 
  6.            object.Reset(); 
  7.        } 
  8.    } 
  9.    Environment * env; 
  10.    Global<Object> object; 
  11.  
  12. }; 
  13.  
  14.  
  15.  
  16. struct SignalRequestContext: public RequestContext 
  17.  
  18.    SignalRequestContext(Environment * passEnv, Local<Object> _object, int _sig) 
  19.    : RequestContext(passEnv, _object), sig(_sig) {} 
  20.    int sig; 
  21.  
  22. }; 

前面介紹過io_uring層的上下文request,request主要是用于io_uring任務完成時,知道執行哪個回調函數,并且記錄了少量的上下文,但是reuqest的字段不一定夠用,所以RequestContext主要記錄額外的上下文,其實把RequestContext的字段合進request也是可以的。

6 事件循環的設計

No的事件循環是io_uring實現的,事件循環的本質就是在一個循環里不斷等待任務和執行任務,那么什么時候結束呢?

  1. while(io_uring_data->stop != 1 && io_uring_data->pending != 0) { 
  2.         // 等待和處理任務 
  3.  

目前可以通過設置stop直接停止事件循環,正常情況下,沒有任務了就會結束事件循環,通過pending字段記錄,比如發起一個讀取文件的請求,pending就是1,讀完后就會減一,這時候,事件循環就會結束,相對Node.js的handle和request,No里是沒有的,No里通過控制pending的值去控制事件循環的狀態。

7 如何使用

No是基于Linux的io_uring的,目前在Linux5.5及以上的系統可以運行,可以安裝ubuntu21.04及以上的虛擬機使用,具體可以參考倉庫說明(https://github.com/theanarkh/No.js)。目前支持了TCP、UCP、Unix域、文件、信號、定時器、log,進程還沒有寫完,總體只是支持一些簡單的操作,后續慢慢更新。

8 后記

寫No是一個讓人非常深刻的過程,已經很多年沒有正經寫過c、c++代碼,或許代碼里有不對的用法,但是整個過程里的思考、編碼和調試讓我學到了很多東西,也給我了一段深刻的時光。目前實現的功能還不多,也不足以用起來,還有很多事情需要做,紙上得來終覺淺,絕知此事要躬行。后續慢慢學習、慢慢思考、慢慢更新!

 

責任編輯:姜華 來源: 編程雜技
相關推薦

2023-10-20 06:26:51

Libuvio_uring

2021-07-03 08:04:10

io_uringNode.js異步IO

2024-03-21 09:15:58

JS運行的JavaScrip

2023-02-07 19:46:35

NIOCQ內核

2023-10-10 10:23:50

JavaScriptV8

2021-08-27 00:21:19

JSJust源碼

2021-07-07 23:38:05

內核IOLinux

2025-06-27 01:44:00

2021-07-11 23:25:29

Libuvepoll文件

2022-10-08 00:00:00

V8channel對象

2023-04-12 18:36:20

IO框架內核

2023-09-12 17:38:41

2024-01-29 08:07:42

FlinkYARN架構

2022-01-19 08:50:53

設備樹Linux文件系統

2016-04-18 09:33:52

nodejswebapp

2021-07-10 07:39:38

Node.js C++V8

2022-10-08 00:06:00

JS運行V8

2023-12-28 11:24:29

IO系統請求

2021-10-14 09:53:38

鴻蒙HarmonyOS應用

2021-09-07 11:19:42

操作系統華為鴻蒙
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 日韩在线精品 | 涩涩视频在线观看 | 日本精品视频一区二区三区四区 | 久久久久久久久久久爱 | 国产精品毛片一区二区在线看 | 一区二区三区网站 | 在线免费国产视频 | 亚洲国产欧美在线人成 | 国产精品123区 | 九九热免费观看 | 国产精品99久久久久久久vr | 久久久久久成人 | 一本综合久久 | 狠狠爱视频 | 成人在线一级片 | 日韩成人在线播放 | a久久久久久 | 99reav| 欧美一级黄视频 | 国产精品成人69xxx免费视频 | 亚洲国产精品久久久久久 | 国产激情视频在线 | 特黄色一级毛片 | 久热久热| 超碰高清| 国产精品久久久久久久久久软件 | 天天操天天射综合网 | 国产女人与拘做视频免费 | 午夜电影网址 | 天天影视亚洲综合网 | 亚洲成人久久久 | 免费三级网 | 亚洲网址| 国产做a爱片久久毛片 | 亚洲精品99 | 国产精品久久久久aaaa樱花 | 亚洲一区二区三区在线视频 | 成人网在线观看 | 欧美日韩视频 | 久久中文字幕在线 | 免费三级网站 |