libcurl長連接高並發高性能
自己開發了一個股票智能分析軟件,功能很強大,需要的點擊下面的鏈接獲取:
https://www.cnblogs.com/bclshuai/p/11380657.html
掃碼關注公眾號
目錄
1 背景介紹
2 長短連接實測分析
2.1 長連接參數設置說明
2.2 長短連接區別
2.2.1 短連接
2.2.2 長連接
2.3 長短連接測試分析
2.3.1 短連接調用url1
2.3.2 長連接調用url1
2.3.3 長連接調用url2
2.3.4 長連接調用兩次不同的url
2.3.5 總結分析
2.3.6 源碼下載地址
1 背景介紹
項目中需要用到Curl頻繁調用的情況,發現curl接口調用速度緩慢。為了實現curl高性能,高並發,需要研究如何實現高性能高並發。研究方向有三個。
(1) 長連接。考慮采用長連接的方式去開發。首先研究下長連接和短連接的性能區別。curl內部是通過socket去連接通訊。socket每次連接最為耗時,如果能夠復用連接,長時間連接,減少每次socket連接的時間,則可以大大減少時間,提高效率。
(2) 多線程。單個線程下載速度畢竟有限,使用多線程去調用接口。實現高並發高性能,需要考慮資源分配和沖突的問題。
(3) 異步調用。和socket異步調用的原理類似。同步調用會阻塞等待,造成CPU占用率高,電腦卡死等問題。異步調用則是數據接收完成后才會取通知調用成功,處理數據。
2 長短連接實測分析
2.1 長連接參數設置說明
Curl提供了三個參數來設置
/* 設置TCP連接為長連接 */
curl_easy_setopt(curl, CURLOPT_TCP_KEEPALIVE, 1L);
/* 設置長連接的休眠時間*/
curl_easy_setopt(curl, CURLOPT_TCP_KEEPIDLE, 120L);
/* 設置心跳發送時間,心使得socket長時間保活,小於KEEPIDLE時間 */
curl_easy_setopt(curl, CURLOPT_TCP_KEEPINTVL, 20L);
/* 設置連接的超時時間,大於心跳時間*/
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30);
2.2 長短連接區別
2.2.1 短連接
短連接一般分為4步驟:初始化、設置參數、執行請求、清理資源。即使用curl_easy_setopt設置該curl為長連接,因為最后被curl_easy_cleanup(curl),所以這個socket連接會被中斷銷毀,不會保持長連接。具體步驟如下:
(1)CURL* curl = curl_easy_init();//創建一個curl對象
(2)curl_easy_setopt(curl,……);//可以設置多個參數url,result
(3)res = curl_easy_perform(curl);//執行請求
(4)curl_easy_cleanup(curl);//清除curl
實例代碼如下:
int CHttpClient::Get(const std::string & strUrl, std::string & strResponse)
{
int res;
CURL* curl = curl_easy_init();
if (NULL == curl)
{
return CURLE_FAILED_INIT;
}
if (m_bDebug)
{
curl_easy_setopt(curl, CURLOPT_VERBOSE, 1);
curl_easy_setopt(curl, CURLOPT_DEBUGFUNCTION, OnDebug);
}
curl_easy_setopt(curl, CURLOPT_URL, strUrl.c_str());
curl_easy_setopt(curl, CURLOPT_READFUNCTION, NULL);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, OnWriteData);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void *)&strResponse);
/* enable TCP keep-alive for this transfer */
curl_easy_setopt(curl, CURLOPT_TCP_KEEPALIVE, 1L);
/* keep-alive idle time to 120 seconds */
curl_easy_setopt(curl, CURLOPT_TCP_KEEPIDLE, 120L);
/* interval time between keep-alive probes: 60 seconds */
curl_easy_setopt(curl, CURLOPT_TCP_KEEPINTVL, 20L);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30);
/**
* 當多個線程都使用超時處理的時候,同時主線程中有sleep或是wait等操作。
* 如果不設置這個選項,libcurl將會發信號打斷這個wait從而導致程序退出。
*/
//curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L);
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 20);
res = curl_easy_perform(curl);
if (res != 0)
{
//FIRE_ERROR(" Get error %d", res);
}
//CurlMutiTreadMutex::GetInstance()->muti_curl_easy_cleanup(curl);
curl_easy_cleanup(curl);
return res;
}
2.2.2 長連接
長連接是我們創建了curl對象之后,不立刻使用curl_easy_cleanup清理掉,而是保存起來,下一個請求,只要重新設置url,執行請求,就可以復用以前的socket連接。
示例代碼如下
頭文件
CURL* GetCurl();
CURL* CreateCurl();
void PutCurl(CURL* curl);
QVector<CURL*> m_VectCurl;
QMutex m_mutex;
源文件
CURL* RestClientPool::GetCurl()
{
CURL* curl = NULL;
m_mutex.lock();
if (m_VectCurl.size()>0)
{
curl = m_VectCurl.front();
m_VectCurl.pop_front();
}
m_mutex.unlock();
if(curl==NULL)
{
curl = CreateCurl();
}
return curl;
}
CURL* RestClientPool::CreateCurl()
{
CURL* curl = curl_easy_init();
if (NULL == curl)
{
return NULL;
}
if (m_bDebug)
{
curl_easy_setopt(curl, CURLOPT_VERBOSE, 1);
curl_easy_setopt(curl, CURLOPT_DEBUGFUNCTION, OnDebug);
}
//curl_easy_setopt(curl, CURLOPT_URL, strUrl.c_str());
curl_easy_setopt(curl, CURLOPT_READFUNCTION, NULL);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, OnWriteData);
//curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void *)&strResponse);
/* enable TCP keep-alive for this transfer */
curl_easy_setopt(curl, CURLOPT_TCP_KEEPALIVE, 1L);
/* keep-alive idle time to 120 seconds */
curl_easy_setopt(curl, CURLOPT_TCP_KEEPIDLE, 300L);
/* interval time between keep-alive probes: 60 seconds */
curl_easy_setopt(curl, CURLOPT_TCP_KEEPINTVL, 20L);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30);
/**
* 當多個線程都使用超時處理的時候,同時主線程中有sleep或是wait等操作。
* 如果不設置這個選項,libcurl將會發信號打斷這個wait從而導致程序退出。
*/
curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L);
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 20);
return curl;
}
void RestClientPool::PutCurl(CURL* curl)
{
m_mutex.lock();
m_VectCurl.push_back(curl);
m_mutex.unlock();
}
int RestClientPool::Get(const std::string & strUrl, std::string & strResponse)
{
int res;
//CURL* curl = CurlMutiTreadMutex::GetInstance()->muti_curl_easy_init();
CURL* curl = GetCurl();
if (NULL == curl)
{
return CURLE_FAILED_INIT;
}
curl_easy_setopt(curl, CURLOPT_URL, strUrl.c_str());
curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void *)&strResponse);
res = curl_easy_perform(curl);
if (res != 0)
{
printf("req error %d",res);
}
PutCurl(curl);
return res;
}
2.3 長短連接測試分析
用上述的長連接和短連接進行測試,分四種情況進行測試分析。
(1) shot連接循環調用1000次url1;
(2) long連接循環調用1000次url1;
(3) long連接循環調用1000次url2;
(4) long連接循環調用1000次,每次循環中各調用一次url1和一次url2;
測試程序代碼
#include <QtCore/QCoreApplication>
#include"RestClientPool.h"
#include "RestClient.h"
#include <QDateTime>
#include <string>
using namespace std;
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
CHttpClient m_shotclient;
RestClientPool m_longClient;
QDateTime StartTime = QDateTime::currentDateTime();
string strUrl = "http://qt.gtimg.cn/q=sz002415";
string strUrl2= "http://hq.sinajs.cn/list=sz002415";
string strResponse = "";
for (int i=0;i<1000;i++)
{
m_longClient.Get(strUrl, strResponse);
m_longClient.Get(strUrl, strResponse);
}
QDateTime timeEnd = QDateTime::currentDateTime();
int time = timeEnd.toTime_t()- StartTime.toTime_t();
printf("using time %d", time);
return a.exec();
}
2.3.1 短連接調用url1
如下圖所示,短連接每次調用都會創建一個socket連接。
輸出
[WSPConnect] Socket ip 120.204.10.232:80[WSPConnect] Socket ip 127.0.0.1:60102[WSPConnect] Socket ip 127.0.0.1:60104線程 0x89b4 已退出,返回值為 0 (0x0)。 [WSPConnect] Socket ip 120.204.10.232:80[WSPConnect] Socket ip 127.0.0.1:60107[WSPConnect] Socket ip 127.0.0.1:60109線程 0x8de8 已退出,返回值為 0 (0x0)。 [WSPConnect] Socket ip 120.204.10.232:80[WSPConnect] Socket ip 127.0.0.1:60112[WSPConnect] Socket ip 127.0.0.1:60114線程 0x7d20 已退出,返回值為 0 (0x0)。 [WSPConnect] Socket ip 120.204.10.232:80[WSPConnect] Socket ip 127.0.0.1:60118[WSPConnect] Socket ip 127.0.0.1:60120線程 0x7e1c 已退出,返回值為 0 (0x0)。 [WSPConnect] Socket ip 120.204.10.232:80[WSPConnect] Socket ip 127.0.0.1:60124[WSPConnect] Socket ip 127.0.0.1:60126線程 0xa328 已退出,返回值為 0 (0x0)。 [WSPConnect] Socket ip 120.204.10.232:80[WSPConnect] Socket ip 127.0.0.1:60129[WSPConnect] Socket ip 127.0.0.1:60132線程 0x9a68 已退出,返回值為 0 (0x0)。 [WSPConnect] Socket ip 120.204.10.232:80[WSPConnect] Socket ip 127.0.0.1:60137[WSPConnect] Socket ip 127.0.0.1:60140線程 0xbd80 已退出,返回值為 0 (0x0)。 [WSPConnect] Socket ip 120.204.10.232:80[WSPConnect] Socket ip 127.0.0.1:60151[WSPConnect] Socket ip 127.0.0.1:60153線程 0x7360 已退出,返回值為 0 (0x0)。 [WSPConnect] Socket ip 120.204.10.232:80[WSPConnect] Socket ip 127.0.0.1:60156[WSPConnect] Socket ip 127.0.0.1:60158線程 0xbfac 已退出,返回值為 0 (0x0)。 [WSPConnect] Socket ip 120.204.10.232:80[WSPConnect] Socket ip 127.0.0.1:60161[WSPConnect] Socket ip 127.0.0.1:60163線程 0xd18 已退出,返回值為 0 (0x0)。 [WSPConnect] Socket ip 120.204.10.232:80[WSPConnect] Socket ip 127.0.0.1:60166[WSPConnect] Socket ip 127.0.0.1:60168線程 0x8ca8 已退出,返回值為 0 (0x0)。 [WSPConnect] Socket ip 120.204.10.232:80[WSPConnect] Socket ip 127.0.0.1:60171[WSPConnect] Socket ip 127.0.0.1:60174線程 0xbc88 已退出,返回值為 0 (0x0)。 [WSPConnect] Socket ip 120.204.10.232:80[WSPConnect] Socket ip 127.0.0.1:60177[WSPConnect] Socket ip 127.0.0.1:60179線程 0x90b0 已退出,返回值為 0 (0x0)。 [WSPConnect] Socket ip 120.204.10.232:80[WSPConnect] Socket ip 127.0.0.1:60183[WSPConnect] Socket ip 127.0.0.1:60185線程 0x8c38 已退出,返回值為 0 (0x0)。 [WSPConnect] Socket ip 120.204.10.232:80[WSPConnect] Socket ip 127.0.0.1:60189[WSPConnect] Socket ip 127.0.0.1:60191線程 0xa8d0 已退出,返回值為 0 (0x0)。 [WSPConnect] Socket ip 120.204.10.232:80[WSPConnect] Socket ip 127.0.0.1:60194[WSPConnect] Socket ip 127.0.0.1:60196線程 0x76a0 已退出,返回值為 0 (0x0)。 [WSPConnect] Socket ip 120.204.10.232:80[WSPConnect] Socket ip 127.0.0.1:60200[WSPConnect] Socket ip 127.0.0.1:60202線程 0x7c6c 已退出,返回值為 0 (0x0)。 [WSPConnect] Socket ip 120.204.10.232:80[WSPConnect] Socket ip 127.0.0.1:60205[WSPConnect] Socket ip 127.0.0.1:60208線程 0x8618 已退出,返回值為 0 (0x0)。 [WSPConnect] Socket ip 120.204.10.232:80[WSPConnect] Socket ip 127.0.0.1:60211[WSPConnect] Socket ip 127.0.0.1:60213線程 0xa300 已退出,返回值為 0 (0x0)。 [WSPConnect] Socket ip 120.204.10.232:80[WSPConnect] Socket ip 127.0.0.1:60218[WSPConnect] Socket ip 127.0.0.1:60220線程 0xa3f8 已退出,返回值為 0 (0x0)。 [WSPConnect] Socket ip 120.204.10.232:80[WSPConnect] Socket ip 127.0.0.1:60223[WSPConnect] Socket ip 127.0.0.1:60225線程 0xb81c 已退出,返回值為 0 (0x0)。 [WSPConnect] Socket ip 120.204.10.232:80[WSPConnect] Socket ip 127.0.0.1:60228[WSPConnect] Socket ip 127.0.0.1:60230線程 0xa554 已退出,返回值為 0 (0x0)。 [WSPConnect] Socket ip 120.204.10.232:80[WSPConnect] Socket ip 127.0.0.1:60233[WSPConnect] Socket ip 127.0.0.1:60235線程 0xa0f0 已退出,返回值為 0 (0x0)。
2.3.2 長連接調用url1
如下圖所示,長連接調用1000次url,只創建了一個socket連接。所用的時間也大幅減少,只有27秒的時間。
輸出
WSPStartup ===> D:\Project\CurlHighSpeed\Win32\Release\CurlHighSpeed.exe使用鏈式SPI[WSPConnect] Socket ip 127.0.0.1:60584[WSPConnect] Socket ip 127.0.0.1:60586WSPStartup ===> D:\Project\CurlHighSpeed\Win32\Release\CurlHighSpeed.exe“CurlHighSpeed.exe”(Win32): 已加載“C:\Program Files (x86)\Sangfor\SSL\ClientComponent\2_SangforNsp.dll”。模塊已生成,不包含符號。 “CurlHighSpeed.exe”(Win32): 已卸載“C:\Program Files (x86)\Sangfor\SSL\ClientComponent\2_SangforNsp.dll” “CurlHighSpeed.exe”(Win32): 已加載“C:\Program Files (x86)\Sangfor\SSL\ClientComponent\2_SangforNsp.dll”。模塊已生成,不包含符號。 “CurlHighSpeed.exe”(Win32): 已加載“C:\Windows\SysWOW64\dbghelp.dll”。“包括”/“排除”設置禁用了加載功能。 “CurlHighSpeed.exe”(Win32): 已加載“C:\Windows\SysWOW64\rasadhlp.dll”。“包括”/“排除”設置禁用了加載功能。 “CurlHighSpeed.exe”(Win32): 已加載“C:\Windows\SysWOW64\FWPUCLNT.DLL”。“包括”/“排除”設置禁用了加載功能。 “CurlHighSpeed.exe”(Win32): 已加載“C:\Windows\SysWOW64\bcrypt.dll”。“包括”/“排除”設置禁用了加載功能。 線程 0x9adc 已退出,返回值為 0 (0x0)。 [WSPConnect] Socket ip 120.204.10.232:80[WSPConnect] Socket ip 120.204.10.232:80[WSPConnect] Socket ip 120.204.10.232:80[WSPConnect] Socket ip 120.204.10.232:80[WSPConnect] Socket ip 120.204.10.232:80[WSPConnect] Socket ip 120.204.10.232:80[WSPConnect] Socket ip 120.204.10.232:80[WSPConnect] Socket ip 120.204.10.232:80[WSPConnect] Socket ip 120.204.10.232:80[WSPConnect] Socket ip 120.204.10.232:80“CurlHighSpeed.exe”(Win32): 已加載“C:\Windows\SysWOW64\uxtheme.dll”。“包括”/“排除”設置禁用了加載功能。
2.3.3 長連接調用url2
如下圖所示調用不同的url2,調用1000次,用時40秒。所用的時間和url1是不同的,這個和請求的服務器以及請求的數據不一致,所以會有不同的耗時。
WSPStartup ===> D:\Project\CurlHighSpeed\Win32\Release\CurlHighSpeed.exe使用鏈式SPI[WSPConnect] Socket ip 127.0.0.1:58122[WSPConnect] Socket ip 127.0.0.1:58126
WSPStartup ===> D:\Project\CurlHighSpeed\Win32\Release\CurlHighSpeed.exe“CurlHighSpeed.exe”(Win32): 已加載“C:\Program Files (x86)\Sangfor\SSL\ClientComponent\2_SangforNsp.dll”。模塊已生成,不包含符號。
2.3.4 長連接調用兩次不同的url
如下圖所示,長連接調用兩個不同的url。會創建兩個socket連接。不會因為切換不同的url,重新創建socket連接。對於每個url會對應一個socket連接。用時82秒,之前分別調用url1和url2所用的時間之和是27+40=67秒,多出來的15秒時間,應該是連接之間的切換時間,所以為了減少時間,可以一種url,用一個curl對象,避免切換。
WSPStartup ===> D:\Project\CurlHighSpeed\Win32\Release\CurlHighSpeed.exe使用鏈式SPI[WSPConnect] Socket ip 127.0.0.1:58345[WSPConnect] Socket ip 127.0.0.1:58347
2.3.5 總結分析
調用情況 |
用時 |
connect連接次數 |
請求次數 |
單次用時 |
Shot連接url1 |
147秒 |
1000次 |
1000 |
0.147 |
Long連接url1 |
27秒 |
1次 |
1000 |
0.027 |
Long 調用url2 |
40秒 |
1次 |
1000 |
0.04 |
Long url1和url2 |
82秒 |
2次 |
2000 |
0.041 |
綜上所述可以得出結論:
(1) curl初始化,設置參數、調用url、清理cleanup,整個過程會創建一個socket連接。可以先創建,設置為長連接,不清理cleanup,重復使用該curl對象,復用已創建的curl對象和socket連接。可以提高5倍的速度。
(2) 調用不同的url,會因為服務器性能和請求數據量,耗時也會不同。
(3) 一個長連接curl調用兩個不同的url(不同的網址),會創建兩個socket連接。保持兩個socket長連接。不會因為切換不同的url,而重復創建socket連接。切換連接會造成耗時,降低速度20%左右。所以對不同的url,可以用不用的對象和連接,避免切換。提高性能。
2.3.6 源碼下載
https://download.csdn.net/download/baochunlei1/12863616
3. libcurl多線程高並發
3 curl線程池並發執行
多線程一直是提高性能和速度的關鍵技術,繼承QT的QRunable類,定義一個線程任務,用QThreadPool線程池去調用url;
3.1 測試程序
采用如下的程序進行測試,采用毫秒計時。
3.1.1 測試主程序
#include <QtCore/QCoreApplication>
#include"RestClientPool.h"
#include "RestClient.h"
#include <QDateTime>
#include <string>
#include <QThreadPool>
#include "MultTask.h"
extern RestClientPool g_restpool;
using namespace std;
//RestClientPool g_restPool;
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
///CHttpClient m_shotclient;
RestClientPool m_longClient;
QDateTime StartTime = QDateTime::currentDateTime();
qint64 istarttimems = StartTime.toMSecsSinceEpoch();
/*string strUrl = "http://qt.gtimg.cn/q=sz002415";
string strUrl2= "http://hq.sinajs.cn/list=sz002415";
string strResponse = "";
for (int i=0;i<1000;i++)
{
m_longClient.Get(strUrl2, strResponse);
m_longClient.Get(strUrl, strResponse);
}*/
QThreadPool qThreadPool;
qThreadPool.setMaxThreadCount(10);
for (int i=0;i<1000;i++)
{
MultTask * p = new MultTask();
qThreadPool.start(p);
}
qThreadPool.waitForDone();
QDateTime timeEnd = QDateTime::currentDateTime();
qint64 iendtimems = timeEnd.toMSecsSinceEpoch();
int time = iendtimems - istarttimems;
//int time = timeEnd.toTime_t()- StartTime.toTime_t();
printf("using time %d\n", time);
printf("curl number %d\n ",g_restpool.getcurlsize());
return a.exec();
}
3.1.2 線程類定義
線程類頭文件
#ifndef MULTTASK_H
#define MULTTASK_H
#include <QObject>
#include <QRunnable>
#include "RestClientPool.h"
class MultTask : public QObject,public QRunnable
{
Q_OBJECT
public:
MultTask();
~MultTask();
void run();
private:
};
#endif // MULTTASK_H
線程類源文件
#include "MultTask.h"
#include<string>
using namespace std;
RestClientPool g_restpool;//全局變量
MultTask::MultTask()
{
setAutoDelete(true);
}
MultTask::~MultTask()
{
}
void MultTask::run()
{
string strUrl = "http://qt.gtimg.cn/q=sz002415";
string strResponse = "";
g_restpool.Get(strUrl,strResponse);
}
1.1.3 運行測試結果
如下圖所示,采用10個線程去調用1000次url1。用時3868毫秒。創建curl的數量是10個,創建的socket連接的數量是10個。平均每次調用時間是0.003868秒。而單線程平均每次調用耗時0.027秒。按理說10個線程,每次調用應該是0.0027秒,但是0.003868秒大於0.0027秒。線程之間的資源競爭和切換也會耗時。而且1000次調用接口中會出現不定數量的錯誤6,錯誤碼解釋是CURLE_COULDNT_RESOLVE_HOST(6)無法bai解析主機。給定的遠程主機沒有得到解決。可能是多線程訪問太快,服務器無法響應。
輸出框中顯示的創建的10個socket連接:
WSPStartup ===> D:\Project\CurlHighSpeed\Win32\Release\CurlHighSpeed.exe使用鏈式SPI[WSPConnect] Socket ip 127.0.0.1:54517[WSPConnect] Socket ip 127.0.0.1:54518[WSPConnect] Socket ip 127.0.0.1:54520[WSPConnect] Socket ip 127.0.0.1:54522[WSPConnect] Socket ip 127.0.0.1:54524[WSPConnect] Socket ip 127.0.0.1:54525[WSPConnect] Socket ip 127.0.0.1:54527[WSPConnect] Socket ip 127.0.0.1:54531[WSPConnect] Socket ip 127.0.0.1:54533[WSPConnect] Socket ip 127.0.0.1:54536WSPStartup ===> D:\Project\CurlHighSpeed\Win32\Release\CurlHighSpeed.exe[WSPConnect] Socket ip 127.0.0.1:54540[WSPConnect] Socket ip 127.0.0.1:54539[WSPConnect] Socket ip 127.0.0.1:54542[WSPConnect] Socket ip 127.0.0.1:54544[WSPConnect] Socket ip 127.0.0.1:54546[WSPConnect] Socket ip 127.0.0.1:54551[WSPConnect] Socket ip 127.0.0.1:54553“CurlHighSpeed.exe”
3.1.4 不同線程數量調用耗時
為了研究線程數量和調用耗時的關系,采用不同的線程數量去執行10000次的調用;每次消耗的時間如下所示。隨着線程數量的增加,多線程處理速度和性能會大幅提高。但是當線程數量達到一定數量之后,線程池的性能反而下降,這是因為線程之間的競爭資源和線程CPU切換導致的。
線程數量 |
總用時(ms) |
每次用時(ms) |
Curl數量 |
錯誤數 |
10 |
27690 |
2.7690 |
10 |
6 |
20 |
18080 |
1.808 |
20 |
8 |
50 |
9593 |
0.9593 |
50 |
7 |
100 |
6200 |
0.62 |
100 |
9 |
200 |
7183 |
0.7183 |
200 |
19 |
300 |
12431 |
1.2431 |
300 |
11 |
400 |
11687 |
11687 |
400 |
12 |
500 |
21990 |
2.1990 |
500 |
13 |
3.1.5 測試程序源碼下載地址