客戶端以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協議大致格式可以描述如下:
-
GET或Post請求方法 請求的資源路徑 http協議版本號\r\n
-
字段名1:值1\r\n
-
字段名2:值2\r\n
-
字段名3:值3\r\n
-
字段名4:值4\r\n
-
字段名5:值5\r\n
-
字段名6:值6\r\n
-
\r\n
也就是是http協議的頭部是一行一行的,每一行以\r\n表示該行結束,最後多出一個空行以\r\n結束表示頭部的結束。接下來就是包體的大小了(如果有的話,上文的例子沒有包體)。一般get方法會將參數放在請求的資源路徑後面,像這樣
http://wwww.hootina.org/index.php?變量1=值1&變量2=值2&變量3=值3&變量4=值4
網址後面的問號表示參數開始,每一個參數與參數之間用&隔開
還有一種post的請求方法,這種數據就是將數據放在包體裏面了,例如:
-
POST /otn/login/loginAysnSuggest HTTP/1.1\r\n
-
Host: kyfw.12306.cn\r\n
-
Connection: keep-alive\r\n
-
Content-Length: 96\r\n
-
Accept: */*\r\n
-
Origin: https://kyfw.12306.cn\r\n
-
X-Requested-With: XMLHttpRequest\r\n
-
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.75\r\n
-
Content-Type: application/x-www-form-urlencoded; charset=UTF-8\r\n
-
Referer: https://kyfw.12306.cn/otn/login/init\r\n
-
Accept-Encoding: gzip, deflate, br\r\n
-
Accept-Language: zh-CN,zh;q=0.8\r\n
-
\r\n
-
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函數裏面做了如下初始化工作,僞碼如下:
第1點,建立任務隊列我們前面系列的文章已經介紹過了。
第2點,代碼如下:
-
g_fileManager = FileManager::getInstance(listen_ip, base_dir, fileCnt, filesPerDir);
-
int ret = g_fileManager->initDir();
-
int FileManager::initDir() {
-
bool isExist = File::isExist(m_disk);
-
if (!isExist) {
-
u64 ret = File::mkdirNoRecursion(m_disk);
-
if (ret) {
-
log("The dir[%s] set error for code[%d], \
-
its parent dir may no exists", m_disk, ret);
-
return -1;
-
}
-
}
-
-
-
char first[10] = {0};
-
char second[10] = {0};
-
for (int i = 0; i <= FIRST_DIR_MAX; i++) {
-
snprintf(first, 5, "%03d", i);
-
string tmp = string(m_disk) + "/" + string(first);
-
int code = File::mkdirNoRecursion(tmp.c_str());
-
if (code && (errno != EEXIST)) {
-
log("Create dir[%s] error[%d]", tmp.c_str(), errno);
-
return -1;
-
}
-
for (int j = 0; j <= SECOND_DIR_MAX; j++) {
-
snprintf(second, 5, "%03d", j);
-
string tmp2 = tmp + "/" + string(second);
-
code = File::mkdirNoRecursion(tmp2.c_str());
-
if (code && (errno != EEXIST)) {
-
log("Create dir[%s] error[%d]", tmp2.c_str(), errno);
-
return -1;
-
}
-
memset(second, 0x0, 10);
-
}
-
memset(first, 0x0, 10);
-
}
-
-
return 0;
-
}
下面,我們直接來看如何處理客戶端的http請求,當連接對象CHttpConn收到客戶端數據後,調用OnRead方法:
-
void CHttpConn::OnRead()
-
{
-
for (;;)
-
{
-
uint32_t free_buf_len = m_in_buf.GetAllocSize()
-
- m_in_buf.GetWriteOffset();
-
if (free_buf_len < READ_BUF_SIZE + 1)
-
m_in_buf.Extend(READ_BUF_SIZE + 1);
-
-
int ret = netlib_recv(m_sock_handle,
-
m_in_buf.GetBuffer() + m_in_buf.GetWriteOffset(),
-
READ_BUF_SIZE);
-
if (ret <= 0)
-
break;
-
-
m_in_buf.IncWriteOffset(ret);
-
-
m_last_recv_tick = get_tick_count();
-
}
-
-
-
char* in_buf = (char*) m_in_buf.GetBuffer();
-
uint32_t buf_len = m_in_buf.GetWriteOffset();
-
in_buf[buf_len] = '\0';
-
-
-
-
-
m_HttpParser.ParseHttpContent(in_buf, buf_len);
-
-
if (m_HttpParser.IsReadAll())
-
{
-
string strUrl = m_HttpParser.GetUrl();
-
log("IP:%s access:%s", m_peer_ip.c_str(), strUrl.c_str());
-
if (strUrl.find("..") != strUrl.npos) {
-
Close();
-
return;
-
}
-
m_access_host = m_HttpParser.GetHost();
-
if (m_HttpParser.GetContentLen() > HTTP_UPLOAD_MAX)
-
{
-
-
log("content is too big");
-
char url[128];
-
snprintf(url, sizeof(url), "{\"error_code\":1,\"error_msg\": \"上傳文件過大\",\"url\":\"\"}");
-
log("%s",url);
-
uint32_t content_length = strlen(url);
-
char pContent[1024];
-
snprintf(pContent, sizeof(pContent), HTTP_RESPONSE_HTML, content_length,url);
-
Send(pContent, strlen(pContent));
-
return;
-
}
-
-
int nContentLen = m_HttpParser.GetContentLen();
-
char* pContent = NULL;
-
if(nContentLen != 0)
-
{
-
try {
-
pContent =new char[nContentLen];
-
memcpy(pContent, m_HttpParser.GetBodyContent(), nContentLen);
-
}
-
catch(...)
-
{
-
log("not enough memory");
-
char szResponse[HTTP_RESPONSE_500_LEN + 1];
-
snprintf(szResponse, HTTP_RESPONSE_500_LEN, "%s", HTTP_RESPONSE_500);
-
Send(szResponse, HTTP_RESPONSE_500_LEN);
-
return;
-
}
-
}
-
Request_t request;
-
request.conn_handle = m_conn_handle;
-
request.method = m_HttpParser.GetMethod();;
-
request.nContentLen = nContentLen;
-
request.pContent = pContent;
-
request.strAccessHost = m_HttpParser.GetHost();
-
request.strContentType = m_HttpParser.GetContentType();
-
request.strUrl = m_HttpParser.GetUrl() + 1;
-
CHttpTask* pTask = new CHttpTask(request);
-
if(HTTP_GET == m_HttpParser.GetMethod())
-
{
-
g_GetThreadPool.AddTask(pTask);
-
}
-
else
-
{
-
g_PostThreadPool.AddTask(pTask);
-
}
-
}
-
}
該方法先收取數據,接着解包,然後根據客戶端發送的http請求到底是get還是post方法,分別往對應的get和post任務隊列中丟一個任務CHttpTask。任務隊列開始處理這個任務。我們以get請求的任務爲例(Post請求與此類似):
-
void CHttpTask::run()
-
{
-
-
if(HTTP_GET == m_nMethod)
-
{
-
OnDownload();
-
}
-
else if(HTTP_POST == m_nMethod)
-
{
-
OnUpload();
-
}
-
else
-
{
-
char* pContent = new char[strlen(HTTP_RESPONSE_403)];
-
snprintf(pContent, strlen(HTTP_RESPONSE_403), HTTP_RESPONSE_403);
-
CHttpConn::AddResponsePdu(m_ConnHandle, pContent, strlen(pContent));
-
}
-
if(m_pContent != NULL)
-
{
-
delete [] m_pContent;
-
m_pContent = NULL;
-
}
-
}
處理任務時,根據請求類型判斷到底是客戶端下載圖片還是上傳圖片,如果是下載圖片則從本機緩存的圖片信息中找到該圖片,並讀取該圖片數據,因爲是聊天圖片,所以一般不會很大,所以這裏都是一次性讀取圖片字節內容,然後發出去。
-
void CHttpTask::OnDownload()
-
{
-
uint32_t nFileSize = 0;
-
int32_t nTmpSize = 0;
-
string strPath;
-
if(g_fileManager->getAbsPathByUrl(m_strUrl, strPath ) == 0)
-
{
-
nTmpSize = File::getFileSize((char*)strPath.c_str());
-
if(nTmpSize != -1)
-
{
-
char szResponseHeader[1024];
-
size_t nPos = strPath.find_last_of(".");
-
string strType = strPath.substr(nPos + 1, strPath.length() - nPos);
-
if(strType == "jpg" || strType == "JPG" || strType == "jpeg" || strType == "JPEG" || strType == "png" || strType == "PNG" || strType == "gif" || strType == "GIF")
-
{
-
snprintf(szResponseHeader, sizeof(szResponseHeader), HTTP_RESPONSE_IMAGE, nTmpSize, strType.c_str());
-
}
-
else
-
{
-
snprintf(szResponseHeader,sizeof(szResponseHeader), HTTP_RESPONSE_EXTEND, nTmpSize);
-
}
-
int nLen = strlen(szResponseHeader);
-
char* pContent = new char[nLen + nTmpSize];
-
memcpy(pContent, szResponseHeader, nLen);
-
g_fileManager->downloadFileByUrl((char*)m_strUrl.c_str(), pContent + nLen, &nFileSize);
-
int nTotalLen = nLen + nFileSize;
-
CHttpConn::AddResponsePdu(m_ConnHandle, pContent, nTotalLen);
-
}
-
else
-
{
-
int nTotalLen = strlen(HTTP_RESPONSE_404);
-
char* pContent = new char[nTotalLen];
-
snprintf(pContent, nTotalLen, HTTP_RESPONSE_404);
-
CHttpConn::AddResponsePdu(m_ConnHandle, pContent, nTotalLen);
-
log("File size is invalied\n");
-
-
}
-
}
-
else
-
{
-
int nTotalLen = strlen(HTTP_RESPONSE_500);
-
char* pContent = new char[nTotalLen];
-
snprintf(pContent, nTotalLen, HTTP_RESPONSE_500);
-
CHttpConn::AddResponsePdu(m_ConnHandle, pContent, nTotalLen);
-
}
-
}
這裏需要說明一下的就是FileManager::getAbsPathByUrl在獲取本地文件時,用了一個鎖,該鎖是爲了防止同一個進程同時讀取同一個文件,這個鎖是“建議性”的,必須自己主動檢測有沒有上鎖:
-
int FileManager::getAbsPathByUrl(const string &url, string &path) {
-
string relate;
-
if (getRelatePathByUrl(url, relate)) {
-
log("Get path from url[%s] error", url.c_str());
-
return -1;
-
}
-
path = string(m_disk) + relate;
-
return 0;
-
}
-
u64 File::open(bool directIo) {
-
assert(!m_opened);
-
int flags = O_RDWR;
-
#ifdef __linux__
-
m_file = open64(m_path, flags);
-
#elif defined(__FREEBSD__) || defined(__APPLE__)
-
m_file = ::open(m_path, flags);
-
#endif
-
if(-1 == m_file) {
-
return errno;
-
}
-
#ifdef __LINUX__
-
if (directIo)
-
if (-1 == fcntl(m_file, F_SETFL, O_DIRECT))
-
return errno;
-
#endif
-
struct flock lock;
-
lock.l_type = F_WRLCK;
-
lock.l_start = 0;
-
lock.l_whence = SEEK_SET;
-
lock.l_len = 0;
-
if(fcntl(m_file, F_SETLK, &lock) < 0) {
-
::close(m_file);
-
return errno;
-
}
-
-
m_opened = true;
-
u64 size = 0;
-
u64 code = getSize(&size);
-
if (code) {
-
close();
-
return code;
-
}
-
m_size = size;
-
m_directIo = directIo;
-
return 0;
-
}
注意上面的fcntl函數設置的flock鎖。這個是Linux特有的,應該學習掌握。
圖片上傳的邏輯和下載邏輯大致類似,這裏就不再分析了。
當然,發送圖片數據的包和前面的發送邏輯也是一樣的,在OnWrite裏面發送。發送完畢後會調用CHttpConn::OnSendComplete,在這個函數裏面關閉http連接。這也就是說msfs與客戶端的http連接也是短連接。
-
void CHttpConn::OnSendComplete()
-
{
-
Close();
-
}
關於msfs也就這麼多內容了。不知道你有沒有發現,在搞清楚db_proxy_server和msg_server之後,每個程序框架其實都是一樣的,只不過業務邏輯稍微有一點差別。後面介紹的file_server和route_server都是一樣的。我們也着重分析其業務代碼。
好了,msfs服務就這麼多啦。