TeamTalk源碼分析之msfs

客戶端以http的方式來上傳和下載聊天圖片。

可能很多同學對http協議不是很熟悉,或者說一知半解。這裏大致介紹一下http協議,http協議其實也是一種應用層協議,建立在tcp/ip層之上,其由包頭和包體兩部分組成(不一定要有包體),看個例子:

比如當我們用瀏覽器請求一個網址http://www.hootina.org/index.PHP,實際是瀏覽器給特定的服務器發送如下數據包,包頭部分如下:

GET /index.php HTTP/1.1\r\n
Host: www.hootina.org\r\n
Connection: keep-alive\r\n
Cache-Control: max-age=0\r\n
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8\r\n
User-Agent: Mozilla/5.0\r\n
\r\n

這個包沒有包體。

從上面我們可以看出一個http協議大致格式可以描述如下:

[plain] view plain copy
  1. GET或Post請求方法  請求的資源路徑 http協議版本號\r\n  
  2. 字段名1:值1\r\n  
  3. 字段名2:值2\r\n  
  4. 字段名3:值3\r\n  
  5. 字段名4:值4\r\n  
  6. 字段名5:值5\r\n  
  7. 字段名6:值6\r\n  
  8. \r\n  

也就是是http協議的頭部是一行一行的,每一行以\r\n表示該行結束,最後多出一個空行以\r\n結束表示頭部的結束。接下來就是包體的大小了(如果有的話,上文的例子沒有包體)。一般get方法會將參數放在請求的資源路徑後面,像這樣

http://wwww.hootina.org/index.php?變量1=值1&變量2=值2&變量3=值3&變量4=值4

網址後面的問號表示參數開始,每一個參數與參數之間用&隔開

還有一種post的請求方法,這種數據就是將數據放在包體裏面了,例如:

[plain] view plain copy
  1. POST /otn/login/loginAysnSuggest HTTP/1.1\r\n  
  2. Host: kyfw.12306.cn\r\n  
  3. Connection: keep-alive\r\n  
  4. Content-Length: 96\r\n  
  5. Accept: */*\r\n  
  6. Origin: https://kyfw.12306.cn\r\n  
  7. X-Requested-With: XMLHttpRequest\r\n  
  8. User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.75\r\n   
  9. Content-Type: application/x-www-form-urlencoded; charset=UTF-8\r\n  
  10. Referer: https://kyfw.12306.cn/otn/login/init\r\n  
  11. Accept-Encoding: gzip, deflate, br\r\n  
  12. Accept-Language: zh-CN,zh;q=0.8\r\n  
  13. \r\n  
  14. loginUserDTO.user_name=balloonwj%40qq.com&userDTO.password=xxxxgjqf&randCode=184%2C55%2C37%2C117  

上述報文中loginUserDTO.user_name=balloonwj%40qq.com&userDTO.password=2032_scsgjqf&randCode=184%2C55%2C37%2C117其實包體內容,這個包是我的一個12306買票軟件發給12306服務器的報文。這裏拿來做個例子。

因爲對方收到http報文的時候,如果包體有內容,那麼必須告訴對方包體有多大。這個最常用的就是通過包頭的Content-Length字段來指定大小。上面的例子中Content-Length等於96,正好就是字符串loginUserDTO.user_name=balloonwj%40qq.com&userDTO.password=xxxxgjqf&randCode=184%2C55%2C37%2C117的長度,也就是包體的大小。

還有一種叫做http chunk的編碼技術,通過對http包內容進行分塊傳輸。這裏就不介紹了(如果你感興趣,可以私聊我)。

常見的對http協議有如下幾個誤解:

1. html文檔的頭就是http的頭

 這種認識是錯誤的,html文檔的頭部也是http數據包的包體的一部分。正確的http頭是長的像上文介紹的那種。

2. 關於http頭Connection:keep-alive字段

  一端指定了這個字段後,發http包給另外一端。這個選項只是一種建議性的選項,對端不一定必須採納,對方也可能在實際實現時,將http連接設置爲短連接,即不採納這個字段的建議。

3. 每個字段都是必須的嗎?

不是,大多數字段都不是必須的。但是特定的情況下,某些字段是必須的。比如,通過post發送的數據,就必須設置Content-Length。不然,收包的一端如何知道包體多大。又比如如果你的數據採取了gzip壓縮格式,你就必須指定Accept-Encoding: gzip,然對方如何解包你的數據。

好了,http協議就暫且介紹這麼多,下面回到正題上來說msfs的源碼。

msfs在main函數裏面做了如下初始化工作,僞碼如下:

[cpp] view plain copy
  1. //1. 建立一個兩個任務隊列,分別處理http get請求和post請求  
  2.   
  3. //2. 創建名稱爲000~255的文件夾,每個文件夾裏面會有000~255個子目錄,這些目錄用於存放聊天圖片  
  4.   
  5. //3. 在8700端口上監聽客戶端連接  
  6.   
  7. //4. 啓動程序消息泵  


第1點,建立任務隊列我們前面系列的文章已經介紹過了。

第2點,代碼如下:

[cpp] view plain copy
  1. g_fileManager = FileManager::getInstance(listen_ip, base_dir, fileCnt, filesPerDir);  
  2. int ret = g_fileManager->initDir();  


[cpp] view plain copy
  1. int FileManager::initDir() {  
  2.         bool isExist = File::isExist(m_disk);  
  3.         if (!isExist) {  
  4.             u64 ret = File::mkdirNoRecursion(m_disk);  
  5.             if (ret) {  
  6.                 log("The dir[%s] set error for code[%d], \  
  7.                     its parent dir may no exists", m_disk, ret);  
  8.                 return -1;  
  9.             }  
  10.         }  
  11.           
  12.         //255 X 255   
  13.         char first[10] = {0};  
  14.         char second[10] = {0};  
  15.         for (int i = 0; i <= FIRST_DIR_MAX; i++) {  
  16.             snprintf(first, 5, "%03d", i);  
  17.             string tmp = string(m_disk) + "/" + string(first);  
  18.             int code = File::mkdirNoRecursion(tmp.c_str());  
  19.             if (code && (errno != EEXIST)) {  
  20.                 log("Create dir[%s] error[%d]", tmp.c_str(), errno);  
  21.                 return -1;  
  22.             }  
  23.             for (int j = 0; j <= SECOND_DIR_MAX; j++) {  
  24.                 snprintf(second, 5, "%03d", j);  
  25.                 string tmp2 = tmp + "/" + string(second);  
  26.                 code = File::mkdirNoRecursion(tmp2.c_str());  
  27.                 if (code && (errno != EEXIST)) {  
  28.                     log("Create dir[%s] error[%d]", tmp2.c_str(), errno);  
  29.                     return -1;  
  30.                 }  
  31.                 memset(second, 0x0, 10);  
  32.             }  
  33.             memset(first, 0x0, 10);  
  34.         }  
  35.           
  36.         return 0;  
  37.     }  



下面,我們直接來看如何處理客戶端的http請求,當連接對象CHttpConn收到客戶端數據後,調用OnRead方法:

[cpp] view plain copy
  1. void CHttpConn::OnRead()  
  2. {  
  3.     for (;;)  
  4.     {  
  5.         uint32_t free_buf_len = m_in_buf.GetAllocSize()  
  6.                 - m_in_buf.GetWriteOffset();  
  7.         if (free_buf_len < READ_BUF_SIZE + 1)  
  8.             m_in_buf.Extend(READ_BUF_SIZE + 1);  
  9.   
  10.         int ret = netlib_recv(m_sock_handle,  
  11.                 m_in_buf.GetBuffer() + m_in_buf.GetWriteOffset(),  
  12.                 READ_BUF_SIZE);  
  13.         if (ret <= 0)  
  14.             break;  
  15.   
  16.         m_in_buf.IncWriteOffset(ret);  
  17.   
  18.         m_last_recv_tick = get_tick_count();  
  19.     }  
  20.   
  21.     // 每次請求對應一個HTTP連接,所以讀完數據後,不用在同一個連接裏面準備讀取下個請求  
  22.     char* in_buf = (char*) m_in_buf.GetBuffer();  
  23.     uint32_t buf_len = m_in_buf.GetWriteOffset();  
  24.     in_buf[buf_len] = '\0';  
  25.   
  26.     //log("OnRead, buf_len=%u, conn_handle=%u", buf_len, m_conn_handle); // for debug  
  27.   
  28.   
  29.     m_HttpParser.ParseHttpContent(in_buf, buf_len);  
  30.   
  31.     if (m_HttpParser.IsReadAll())  
  32.     {  
  33.         string strUrl = m_HttpParser.GetUrl();  
  34.         log("IP:%s access:%s", m_peer_ip.c_str(), strUrl.c_str());  
  35.         if (strUrl.find("..") != strUrl.npos) {  
  36.             Close();  
  37.             return;  
  38.         }  
  39.         m_access_host = m_HttpParser.GetHost();  
  40.         if (m_HttpParser.GetContentLen() > HTTP_UPLOAD_MAX)  
  41.         {  
  42.             // file is too big  
  43.             log("content  is too big");  
  44.             char url[128];  
  45.             snprintf(url, sizeof(url), "{\"error_code\":1,\"error_msg\": \"上傳文件過大\",\"url\":\"\"}");  
  46.             log("%s",url);  
  47.             uint32_t content_length = strlen(url);  
  48.             char pContent[1024];  
  49.             snprintf(pContent, sizeof(pContent), HTTP_RESPONSE_HTML, content_length,url);  
  50.             Send(pContent, strlen(pContent));  
  51.             return;  
  52.         }  
  53.   
  54.         int nContentLen = m_HttpParser.GetContentLen();  
  55.         char* pContent = NULL;  
  56.         if(nContentLen != 0)  
  57.         {  
  58.             try {  
  59.                 pContent =new char[nContentLen];  
  60.                 memcpy(pContent, m_HttpParser.GetBodyContent(), nContentLen);  
  61.             }  
  62.             catch(...)  
  63.             {  
  64.                 log("not enough memory");  
  65.                 char szResponse[HTTP_RESPONSE_500_LEN + 1];  
  66.                 snprintf(szResponse, HTTP_RESPONSE_500_LEN, "%s", HTTP_RESPONSE_500);  
  67.                 Send(szResponse, HTTP_RESPONSE_500_LEN);  
  68.                 return;  
  69.             }  
  70.         }  
  71.         Request_t request;  
  72.         request.conn_handle = m_conn_handle;  
  73.         request.method = m_HttpParser.GetMethod();;  
  74.         request.nContentLen = nContentLen;  
  75.         request.pContent = pContent;  
  76.         request.strAccessHost = m_HttpParser.GetHost();  
  77.         request.strContentType = m_HttpParser.GetContentType();  
  78.         request.strUrl = m_HttpParser.GetUrl() + 1;  
  79.         CHttpTask* pTask = new CHttpTask(request);  
  80.         if(HTTP_GET == m_HttpParser.GetMethod())  
  81.         {  
  82.             g_GetThreadPool.AddTask(pTask);  
  83.         }  
  84.         else  
  85.         {  
  86.             g_PostThreadPool.AddTask(pTask);  
  87.         }  
  88.     }  
  89. }  

該方法先收取數據,接着解包,然後根據客戶端發送的http請求到底是get還是post方法,分別往對應的get和post任務隊列中丟一個任務CHttpTask。任務隊列開始處理這個任務。我們以get請求的任務爲例(Post請求與此類似):

[cpp] view plain copy
  1. void CHttpTask::run()  
  2. {  
  3.   
  4.     if(HTTP_GET == m_nMethod)  
  5.     {  
  6.         OnDownload();  
  7.     }  
  8.     else if(HTTP_POST == m_nMethod)  
  9.     {  
  10.        OnUpload();  
  11.     }  
  12.     else  
  13.     {  
  14.         char* pContent = new char[strlen(HTTP_RESPONSE_403)];  
  15.         snprintf(pContent, strlen(HTTP_RESPONSE_403), HTTP_RESPONSE_403);  
  16.         CHttpConn::AddResponsePdu(m_ConnHandle, pContent, strlen(pContent));  
  17.     }  
  18.     if(m_pContent != NULL)  
  19.     {  
  20.         delete [] m_pContent;  
  21.         m_pContent = NULL;  
  22.     }  
  23. }  

處理任務時,根據請求類型判斷到底是客戶端下載圖片還是上傳圖片,如果是下載圖片則從本機緩存的圖片信息中找到該圖片,並讀取該圖片數據,因爲是聊天圖片,所以一般不會很大,所以這裏都是一次性讀取圖片字節內容,然後發出去。

[cpp] view plain copy
  1. void  CHttpTask::OnDownload()  
  2. {  
  3.         uint32_t  nFileSize = 0;  
  4.         int32_t nTmpSize = 0;  
  5.         string strPath;  
  6.         if(g_fileManager->getAbsPathByUrl(m_strUrl, strPath ) == 0)  
  7.         {  
  8.             nTmpSize = File::getFileSize((char*)strPath.c_str());  
  9.             if(nTmpSize != -1)  
  10.             {  
  11.                 char szResponseHeader[1024];  
  12.                 size_t nPos = strPath.find_last_of(".");  
  13.                 string strType = strPath.substr(nPos + 1, strPath.length() - nPos);  
  14.                 if(strType == "jpg" || strType == "JPG" || strType == "jpeg" || strType == "JPEG" || strType == "png" || strType == "PNG" || strType == "gif" || strType == "GIF")  
  15.                 {  
  16.                     snprintf(szResponseHeader, sizeof(szResponseHeader), HTTP_RESPONSE_IMAGE, nTmpSize, strType.c_str());  
  17.                 }  
  18.                 else  
  19.                 {  
  20.                     snprintf(szResponseHeader,sizeof(szResponseHeader), HTTP_RESPONSE_EXTEND, nTmpSize);  
  21.                 }  
  22.                 int nLen = strlen(szResponseHeader);  
  23.                 char* pContent = new char[nLen + nTmpSize];  
  24.                 memcpy(pContent, szResponseHeader, nLen);  
  25.                 g_fileManager->downloadFileByUrl((char*)m_strUrl.c_str(), pContent + nLen, &nFileSize);  
  26.                 int nTotalLen = nLen + nFileSize;  
  27.                 CHttpConn::AddResponsePdu(m_ConnHandle, pContent, nTotalLen);  
  28.             }  
  29.             else  
  30.             {  
  31.                 int nTotalLen = strlen(HTTP_RESPONSE_404);  
  32.                 char* pContent = new char[nTotalLen];  
  33.                 snprintf(pContent, nTotalLen, HTTP_RESPONSE_404);  
  34.                 CHttpConn::AddResponsePdu(m_ConnHandle, pContent, nTotalLen);  
  35.                 log("File size is invalied\n");  
  36.                   
  37.             }  
  38.         }  
  39.         else  
  40.         {  
  41.             int nTotalLen = strlen(HTTP_RESPONSE_500);  
  42.             char* pContent = new char[nTotalLen];  
  43.             snprintf(pContent, nTotalLen, HTTP_RESPONSE_500);  
  44.             CHttpConn::AddResponsePdu(m_ConnHandle, pContent, nTotalLen);  
  45.         }  
  46. }  


這裏需要說明一下的就是FileManager::getAbsPathByUrl在獲取本地文件時,用了一個鎖,該鎖是爲了防止同一個進程同時讀取同一個文件,這個鎖是“建議性”的,必須自己主動檢測有沒有上鎖:

[cpp] view plain copy
  1. int FileManager::getAbsPathByUrl(const string &url, string &path) {  
  2.     string relate;  
  3.     if (getRelatePathByUrl(url, relate)) {  
  4.         log("Get path from url[%s] error", url.c_str());  
  5.         return -1;  
  6.     }  
  7.     path = string(m_disk) + relate;  
  8.     return 0;  
  9. }  

[cpp] view plain copy
  1. u64 File::open(bool directIo) {  
  2.     assert(!m_opened);  
  3.     int flags = O_RDWR;  
  4. #ifdef __linux__      
  5.     m_file = open64(m_path, flags);  
  6. #elif defined(__FREEBSD__) || defined(__APPLE__)  
  7.     m_file = ::open(m_path, flags);  
  8. #endif    
  9.     if(-1 == m_file) {  
  10.         return errno;  
  11.     }  
  12. #ifdef __LINUX__  
  13.     if (directIo)  
  14.         if (-1 == fcntl(m_file, F_SETFL, O_DIRECT))  
  15.             return errno;   
  16. #endif    
  17.     struct flock lock;  
  18.     lock.l_type = F_WRLCK;  
  19.     lock.l_start = 0;  
  20.     lock.l_whence = SEEK_SET;  
  21.     lock.l_len = 0;  
  22.     if(fcntl(m_file, F_SETLK, &lock) < 0) {  
  23.         ::close(m_file);  
  24.         return errno;  
  25.     }  
  26.   
  27.     m_opened = true;  
  28.     u64 size = 0;  
  29.     u64 code = getSize(&size);  
  30.     if (code) {  
  31.         close();  
  32.         return code;  
  33.     }  
  34.     m_size = size;  
  35.     m_directIo = directIo;  
  36.     return 0;  
  37. }  


注意上面的fcntl函數設置的flock鎖。這個是Linux特有的,應該學習掌握。


圖片上傳的邏輯和下載邏輯大致類似,這裏就不再分析了。


當然,發送圖片數據的包和前面的發送邏輯也是一樣的,在OnWrite裏面發送。發送完畢後會調用CHttpConn::OnSendComplete,在這個函數裏面關閉http連接。這也就是說msfs與客戶端的http連接也是短連接。

[cpp] view plain copy
  1. void CHttpConn::OnSendComplete()  
  2. {  
  3.     Close();  
  4. }  


關於msfs也就這麼多內容了。不知道你有沒有發現,在搞清楚db_proxy_server和msg_server之後,每個程序框架其實都是一樣的,只不過業務邏輯稍微有一點差別。後面介紹的file_server和route_server都是一樣的。我們也着重分析其業務代碼。


好了,msfs服務就這麼多啦。

發佈了56 篇原創文章 · 獲贊 95 · 訪問量 51萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章