最近需求需要開發一款 HTTP ,然后由於先前接觸過Qt,就直接用Qt寫HTTP服務器了,也是為了當作練手,要不然是直接上HTTP框架的。
后端用C++ Qt框架 前端為了練手 當然是純生的 js html css
具體的HTTP 實現過程我就不累贅描述了,這個Http協議解析基本上大部分人都知道原理。
主要是記錄一下開發中遇到的各種問題。
首先最開始開發的時候,一路順風,我的設計模式是 層次 設計模式,一層層獨立互不相干互不干涉。嚴格的只管理好自己的所在層。
數據包是一層層往上傳輸,到達 Logic 層 指令處理完畢之后 返回要顯示的數據(比如HTML),於是再一層層往下返回,一層層加報頭;
是不是有點類似於 七層協議?
由於軟件本身只是后台界面使用,所以並沒有考慮到 線程池,直接使用多線程。
在制作過程中,最經常遇到的莫過於就是編碼問題,本身應該是一個很簡單的問題,但是有時候確實出現的次數比較多。雖然說解決也簡單。
首先我們統一編碼。內部程序和源代碼和html文件均為 UTF-8。
在開發到 60%,也就是在設計 身份識別的地方,我們想了一個辦法,為了保證其安全性,我們用了一直理論上我們沒有找到什么缺陷的方法:
根據一段時間的討論,在不考慮Cookie被盜取(Cookie是以會話的形式存在)的情況下,似乎沒有發現什么問題。
以下為 Cookie 生成算法:
QString Cookie::getRandCookie(QString & name, QString & pass){ QByteArray bb; QString temp; QString md5_pass; QString randcookie; bb = QCryptographicHash::hash(name.toUtf8(), QCryptographicHash::Md5); temp = bb.toHex(); bb = QCryptographicHash::hash(temp.toUtf8(), QCryptographicHash::Md5); randcookie = bb.toHex(); bb = QCryptographicHash::hash(pass.toUtf8(), QCryptographicHash::Md5); temp = bb.toHex(); bb = QCryptographicHash::hash(temp.toUtf8(), QCryptographicHash::Md5); md5_pass = bb.toHex(); QTime t; t = QTime::currentTime(); qsrand(t.msec() + t.second() * 65535 / 2); int n = qrand(); QString tmp = QString::number(n, 1000); bb = QCryptographicHash::hash(tmp.toUtf8(), QCryptographicHash::Md5); tmp = bb.toHex(); randcookie = randcookie + tmp + md5_pass; //目前總共有 32 位,為了防止用戶Cookie被XSS以免 碰撞機碰撞。 所以將返回不完整的Md5 // 6~28位是 用戶名 33位~64位是隨機數無用 QString userhead = randcookie.mid(6, 28); //取用戶名前面這一段 不完整 QString rand = randcookie.mid(33,97); //隨機數 QString passhead = randcookie.mid(72, 20); //這里去掉前8位 //取中間這一段 不給完整的 MD5碼 randcookie = rand+ userhead + passhead;//隨機數 賬號 密碼 randcookie += "_Hi_hacker"; //向大牛打聲招呼 return randcookie; }
如果要是再看的你發現了漏洞,一定要留言告訴我,我將立即改進。
隨后我們遇到了編碼問題。
一個問題就是,因為我們的需求包括了 網頁操作控制台(用匿名管道實現,詳情可以看我的這篇文章:http://www.cnblogs.com/suwings/p/5754943.html,順帶一提,Qt框架也可以實現,只是由於時間問題,沒法再做描述)
但是Windows 控制台默認是 GBK 編碼,這將導致一個問題的出現,中文輸入的數據可能將亂碼。輸出的數據也可能將亂碼。
不過在我們測試的發現居然忘記URL中文解碼了,但是Qt自帶的解碼有個問題就是 英文有時候也會一起解碼。
於是在網上找到了如下實現方法:
1 /************************************************************************/ 2 /* URL解碼 英文可不解 */ 3 /************************************************************************/ 4 std::string urlDecode(const std::string & _szToDecode) 5 { 6 std::string result; 7 int hex = 0; 8 for (size_t i = 0; i < _szToDecode.length(); ++i) 9 { 10 switch (_szToDecode[i]) 11 { 12 case '+': 13 result += ' '; 14 break; 15 case '%': 16 if (isxdigit(_szToDecode[i + 1]) && isxdigit(_szToDecode[i + 2])) 17 { 18 std::string hexStr = _szToDecode.substr(i + 1, 2); 19 hex = strtol(hexStr.c_str(), 0, 16); 20 //字母和數字[0-9a-zA-Z]、一些特殊符號[$-_.+!*'(),] 、以及某些保留字[$&+,/:;=?@] 21 //可以不經過編碼直接用於URL 22 if (!((hex >= 48 && hex <= 57) || //0-9 23 (hex >= 97 && hex <= 122) || //a-z 24 (hex >= 65 && hex <= 90) || //A-Z 25 hex == 0x21 || hex == 0x24 || hex == 0x26 || hex == 0x27 || hex == 0x28 || hex == 0x29 26 || hex == 0x2a || hex == 0x2b || hex == 0x2c || hex == 0x2d || hex == 0x2e || hex == 0x2f 27 || hex == 0x3A || hex == 0x3B || hex == 0x3D || hex == 0x3f || hex == 0x40 || hex == 0x5f 28 ////一些特殊符號及保留字[$-_.+!*'(),] [$&+,/:;=?@] 29 )) 30 { 31 result += char(hex); 32 i += 2; 33 }else if{esult += '%';}else{result += '%';} 34 break; 35 default: 36 result += _szToDecode[i]; 37 break; 38 } 39 } 40 return result; 41 }
於是很開心的完成了URL解碼,開始專注 到控制台的編碼問題:
從 QString UTF-8 轉到 Windows cmd GBK:
1 string UTF8ToGBK(const char* strUTF8) 2 { 3 int len = MultiByteToWideChar(CP_UTF8, 0, strUTF8, -1, NULL, 0); 4 wchar_t* wszGBK = new wchar_t[len + 1]; 5 memset(wszGBK, 0, len * 2 + 2); 6 MultiByteToWideChar(CP_UTF8, 0, strUTF8, -1, wszGBK, len); 7 len = WideCharToMultiByte(CP_ACP, 0, wszGBK, -1, NULL, 0, NULL, NULL); 8 char* szGBK = new char[len + 1]; 9 memset(szGBK, 0, len + 1); 10 WideCharToMultiByte(CP_ACP, 0, wszGBK, -1, szGBK, len, NULL, NULL); 11 string strTemp(szGBK); 12 if (wszGBK) delete[] wszGBK; 13 if (szGBK) delete[] szGBK; 14 return strTemp; 15 }
以及輸出: 從 Windows CMD GBK 轉回 UTF-8:
1 void Pipe::loop(){ 2 char outbuff[4096]; //輸出緩沖 3 DWORD byteread; 4 while (true) 5 { 6 memset(outbuff, '\0', 4096); 7 if (ReadFile(this->hpiperead, outbuff, 4095, &byteread, NULL) == NULL)break; 8 9 QTextCodec *gbk1 = QTextCodec::codecForName("GBK"); //Windows 默認編碼 GBK 轉成 UTF-8 //主要是看這里 10 QString tmp = gbk1->toUnicode(outbuff); //主要是看這里 11 while(tmp.indexOf('\b') != -1)tmp.replace('\b',""); //替換管道可能出現的亂碼 12 //這樣 Qstring tmp 就可以使用了。 13 memset(outbuff, '\0', 4096); 14 } 15 }
然后差不多幾個重點的問題解決了。於是繼續愉快的code
但是好景不長,后來發現返回的數據在 HTTP 響應頭里面總是 少了,也就是說 四個漢字 “啊啊啊啊” 變成了 “啊啊”;
我原先一直以為是編碼問題,在TCP層我多層換編碼輸出,用UTF-8,GBK gb等等一些編碼。都無果。
后來發現 是一句代碼坑了這里:
//------處理數據長度-------- QString tmp_read_len; //int i; 用前面用過的i,無需要重新那個 //Body 是QList類型 i = 0; for (line = 0; line < body.size(); line++) //這個循環是 循環加入 從上層返回的數據 { i = i + body[line].toLocal8Bit().length(); //字節數,判斷中文/英文 就是因為少了toLocal8Bit,所以導致中文判斷也以為是一個,實際上可能是 2 個或 4個 (UTF-8) } QString tmp_int_len = QString::number(i); QString ContentLen = "Content-Length: " + tmp_int_len; //加入 Content-Length (*list) << ContentLen;
於是解決之后,網頁終於能顯示“啊啊啊啊”了,於是又開始愉快的code。
可惜好景不長,在一個及其簡單的地方,出現了差錯。我需要實現一個 在控制台也可以操作的需求,這個很簡單,用一個線程專門讀取用戶輸入就好了
對...是很簡單
1 //循環等待輸入 2 void LoopCin(){ 3 std::string com; 4 while (true){ 5 char ch = '\0'; 6 ch = getchar(); 7 if (ch == '\n'){ 8 //考慮多一點 9 if (PIPE != NULL){ //PIPE 是管道 10 std::cout << ">>" << com.c_str()<< std::endl; 11 PIPE->sendCommand(com.c_str()); //向管道發送命令 管道已經是封裝好了的 12 com = ""; 13 }else{ 14 std::cout << "[程序]" << "服務器未開啟,無法執行命令.請去網頁后台開啟您的服務器."<< std::endl; 15 } 16 } 17 else 18 { 19 com = com + ch; //如果不是回車 就加入char 20 } 21 } 22 }
於是寫完這些代碼之后,用C++11 的Thread 類創建線程(別問我為什么不用QThread 類,為了實現一個這個還用着那個,而且據說這個使用起來需要謹慎)
可是 用Thread創建的線程卻 毫無效果,明明可以等待用戶輸入了,卻將主線程個阻塞了,很是詫異。。
我也不是專門走C++這條路的,所以沒有詳細的去調查為什么,於是我用代替方法,直接使用了 WIndows API 創建線程。
1 PIPE_cin_hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)LoopCin, NULL, 0, &PIPE_cin_ThreadID);//創建輸入循環線程
奇跡般的不知道為什么的突然就可以了。莫非 Thread 創建的線程不可靠?不是沒有啟動,確實啟動了,但是卻阻塞了主線程,整個進程在等待我輸入,網頁也加載不出來了。
如果你知道這個原理,還煩請告訴我一下,謝謝。
於是又開始愉快的進行code。
雖然后面還有點小插曲,但是都一一解決,完成了這個項目。關於Javascript 編寫那里遇到的坑其實也沒多少,就不寫了。
不論是否對你有幫助,謝謝你的耐心閱讀