高性能服务器 - window篇
最初研究網(wǎng)狐是14年的時候,一轉(zhuǎn)眼已經(jīng)是18年了,這幾年也做了寫亂七八糟的開發(fā),期間也做了些網(wǎng)絡(luò)層的開發(fā),自我感覺良好,最近做的項目主要負責服務(wù)器方面,CS架構(gòu)的。一開始寫了個CSocket簡單的服務(wù)器,就是網(wǎng)上常見的結(jié)構(gòu),封裝下,在繼承下,記得13年在深藍培訓時候老師就是這樣寫的,測試的時候發(fā)現(xiàn)批量登錄導出是BUG,經(jīng)不起大規(guī)模的登錄。估計主要是自己對MFC封裝的CSocket了解不深,唉,這能換網(wǎng)絡(luò)內(nèi)核,之前也學了很多IOCP理論知識,也看了很多DEMO,可惜的是這些封裝的都不是很好。這個時候想起國內(nèi)知名VC++寫的遠控gh0st,也就是紅狼遠控,據(jù)說他的網(wǎng)絡(luò)內(nèi)核寫的不錯,自己也看過他的源碼,3.6和3.78版本,像現(xiàn)在市面上常見的遠控,DDOS管理端都是抄寫gh0st的。我把他的網(wǎng)絡(luò)層用到自己的項目上,剛開始還好,后來也發(fā)現(xiàn)了不少致命BUG,
1.他是自己寫的CBuffer管理內(nèi)存,其實這個類不是很安全,CopyMemery的時候就沒有檢查,出現(xiàn)了偶現(xiàn)的拷貝內(nèi)存出界
2.有的時候莫名其妙的進入某個鎖里面出不來了,導致服務(wù)器卡死
以上兩個BUG很可能是自己不正確使用人家的IOCP模塊導致,因為用人家的自己的項目的時候穩(wěn)定的一B啊,用到自己項目就偶現(xiàn)崩潰能,花了1-2天時間不論自己怎么改都沒解決以上兩個BUG,也有可能這個模型他寫的本身就有BUG,只是他自己的項目沒有觸發(fā)而已,因為gh0st這個項目服務(wù)器只會下發(fā)簡單的指令數(shù)據(jù),數(shù)據(jù)量很小,我自己的項目登錄的時候服務(wù)器會下發(fā)幾十K的數(shù)據(jù)給登錄端,可能這樣就會觸發(fā)BUG了吧,只能這樣帥鍋了。由于項目還是挺緊的,沒有足夠時間查找原因只能趕緊換網(wǎng)絡(luò)內(nèi)核。這個時候我想到了網(wǎng)狐6603的IOCP,這個東西就是寫的太規(guī)范了,導致簡單的IOCP代碼很大,附加的輔助類很多。我花了兩天時間把它壓縮成了一個精小版,
下面這個鏈接是我縮減之后的代碼:
網(wǎng)狐IOCP壓縮版-網(wǎng)絡(luò)基礎(chǔ)文檔類資源-CSDN下載
注意:
1.由于不太會使用去掉了網(wǎng)絡(luò)事件(收發(fā)數(shù)據(jù)、網(wǎng)絡(luò)接受、網(wǎng)絡(luò)斷開)進隊列,發(fā)的時候直接發(fā)送,接收的時候直接回調(diào)。不知道原作者都放進隊列里面有哪些確切的好處。
暫時先這樣,后續(xù)更新。。。
----------15:41 2018/6/27---------------
今天使用網(wǎng)狐的時候出現(xiàn)了死鎖,偶現(xiàn)現(xiàn)象,真他媽嚇我一跳。后經(jīng)過定位,死鎖在?pServerSocketItem->GetSignedLock()上面,一個線程收數(shù)據(jù),一個線程發(fā)數(shù)據(jù),兩個都要獲取這個鎖,導致了死鎖。如果把所有的收發(fā)數(shù)據(jù)都放進一個隊列里面,讓一個線程去處理收發(fā)數(shù)據(jù)肯定不會出現(xiàn)類似的錯誤,因為就一個線程嘛,很難死鎖的。那為什么CServerSocketItem要有個鎖呢?多線程都是操作一個Item,這些操作會訪問Item有成員變量屬性,肯定會亂,所以鎖還有必要的。
----------10:13 2018/6/28----------------
想來想去還是加上了消息隊列,去掉消息隊列的話肯定是不穩(wěn)定的。同時也加上了心跳。也公布出來吧,供大家使用。。
IOCP網(wǎng)絡(luò)模型-網(wǎng)絡(luò)基礎(chǔ)代碼類資源-CSDN下載
2.經(jīng)過實驗測試,這個服務(wù)器模型到1400個左右客戶端,消耗內(nèi)存140兆,就到達了上線,新上來的客戶端登錄不上去。唉,這個效果還沒有g(shù)h0st上限高,gh0st上線3000個客戶端松松的沒壓力,但是gh0st就是不穩(wěn)定,看來還要尋找其他高效服務(wù)器。研究下boost的asio吧。
----------18:25 2018/6/23---------------
突發(fā)的猜想,網(wǎng)狐IOCP能接收的客戶端不止1400多個,1400多個主要的原因是自己寫的測試客戶端開了1400多個線程,達到了上限,具體性能有待進一步測試。。。
----------9:46 2018/6/25---------------
經(jīng)過實際測試,網(wǎng)狐IOCP一秒鐘可以接受1000左右的鏈接,成功連接了20000臺終端,消耗內(nèi)存1.4G(由于本機是I3不方便更大量的測試,這個結(jié)果已經(jīng)令人很滿意)
3.找到一個不錯的boost寫的跨平臺服務(wù)器框架,一秒鐘可以接受1000左右的鏈接,成功連接了30000臺終端(可能可以更多,測試機器WIN7+I7)。原文地址:https://blog.csdn.net/wang19840301/article/details/46648559
下載鏈接:跨平臺高性能TCP服務(wù)器框架&boost;-其它文檔類資源-CSDN下載
由于測試網(wǎng)狐的效果還不錯,所以這個網(wǎng)絡(luò)模型就不做進一步研究了,有需要的時候也是不錯的選擇。
4.壓力測試代碼
// TestClient.cpp : 定義控制臺應(yīng)用程序的入口點。 //#include "stdafx.h"#include<WinSock2.h> #pragma comment(lib,"WS2_32.lib")int g_id=0; CRITICAL_SECTION cs;DWORD WINAPI clientfun(LPVOID lp) {SOCKET s = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if(s == INVALID_SOCKET) { printf("error"); ::WSACleanup(); //釋放資源 return 0; } sockaddr_in servAddr; servAddr.sin_family = AF_INET; servAddr.sin_port = htons(10001);//端口號 servAddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");//IP EnterCriticalSection(&cs); //連接 if(::connect(s, (sockaddr*)&servAddr, sizeof(servAddr)) == -1) { printf("%d-error\n",g_id); ::WSACleanup(); //釋放資源 return 0; }else{printf("%d-success\n",g_id); }g_id++;LeaveCriticalSection(&cs);char sendbuf[10]={0,'a','b',','};send(s,sendbuf,10,0);char buff[156];//緩沖區(qū) int nRecv = ::recv(s, buff, 156, 0);//接收數(shù)據(jù) if(nRecv > 0) { buff[nRecv] = '\0'; printf("接受數(shù)據(jù):%s",buff); } return 0;}//單個進程推薦不超過1000個線程。超過1000個的壓力測試量推薦啟動多個進程 #define CLIENT_COUNT 1int _tmain(int argc, _TCHAR* argv[]) {WSADATA wsaData; WORD sockVersion = MAKEWORD(2,0);//指定版本號 ::WSAStartup(sockVersion, &wsaData);//載入winsock的dll InitializeCriticalSection(&cs);for (int i=0;i<CLIENT_COUNT;i++){HANDLE h = CreateThread(NULL,0,clientfun,NULL,0,NULL);}Sleep(5*60*1000);return 0; }5.更新日期 -------------------11:06 2018/8/14-------------------
有個疑惑:首先我們確定TCP協(xié)議是不會丟包的,我們假設(shè)接收端處理速度較慢,或者網(wǎng)速較慢,總而言之,發(fā)送端勢必會造成數(shù)據(jù)堆積情況,send函數(shù)會失敗,錯誤碼為10035,亦即常見的錯誤10035(WSAEWOULDBLOCK),這個時候如果不處理這個錯誤這次發(fā)送肯定是沒有達到接收方的,那不就是造成了丟包現(xiàn)象嗎?
解答(僅僅代表自己的看法,可能會不太正確或準確):
使用重疊IO時候不會產(chǎn)生錯誤10035(WSAEWOULDBLOCK),當出現(xiàn)接受方處理數(shù)據(jù)較慢導致發(fā)送緩沖區(qū)已經(jīng)滿了的時候,會產(chǎn)生錯誤WSA_IO_PENDING(997),即他們兩個錯誤是一個意思,只不過前者是非重疊IO的,后者是重疊IO的。TCP不會丟包是針對發(fā)送成功而言的(發(fā)送成功是指send函數(shù)返回非負值,或者返回失敗但是錯誤碼是WSAEWOULDBLOCK或者WSA_IO_PENDING),發(fā)送失敗的數(shù)據(jù)包肯定不能到達接收端的。那么網(wǎng)狐是怎么處理這個事情的呢?經(jīng)過閱讀源碼,總結(jié)如下:發(fā)送的數(shù)據(jù)會有一個隊列即m_OverLappedSendActive,每次想要發(fā)送數(shù)據(jù)時候先放進隊列里面,每次真正發(fā)送成功都會觸發(fā)OnSendCompleted事件(GetQueuedCompletionStatus獲取到的),底層發(fā)送成功才會調(diào)用下次發(fā)送,這樣保證了上層調(diào)用send肯定是發(fā)送成功的(無論是網(wǎng)絡(luò)阻塞或者接收端處理數(shù)據(jù)太慢都會成功,除非網(wǎng)絡(luò)鏈接斷開),如果一直沒有觸發(fā)OnSendCompleted事件,則上層調(diào)用的send,其實只是放進應(yīng)用程序自己寫的數(shù)據(jù)隊列里面而已。
6.更新日期 -------------------14:11 2021/11/24-------------------
? ?跟心壓力客戶端代碼:
// TestClient.cpp : 定義控制臺應(yīng)用程序的入口點。 // 有個bug,發(fā)送數(shù)據(jù)大小是1000的時候,連接數(shù)最終只有幾十個#include "stdafx.h" #include <time.h> #include <WinSock2.h> #include <string> #pragma comment(lib,"WS2_32.lib")std::string ip ="192.168.10.27"; int port =10000;int SENDSIZE =1024; int CLIENT_COUNT =1; int SENDSLEEP =1000;int g_id=0; CRITICAL_SECTION cs;DWORD WINAPI clientfun(LPVOID lp) {SOCKET s = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if(s == INVALID_SOCKET) { printf("error"); ::WSACleanup(); //釋放資源 return 0; } sockaddr_in servAddr; servAddr.sin_family = AF_INET; servAddr.sin_port = htons(port);//端口號 servAddr.sin_addr.S_un.S_addr = inet_addr(ip.c_str());//IP EnterCriticalSection(&cs); //連接 if(::connect(s, (sockaddr*)&servAddr, sizeof(servAddr)) == -1) { printf("%d-error - %d\n",g_id,WSAGetLastError()); ::WSACleanup(); //釋放資源 exit(0); }else{struct sockaddr_in connAddr;memset(&connAddr, 0, sizeof(struct sockaddr_in));int len = sizeof(connAddr);int ret = ::getsockname(s, (sockaddr*)&connAddr, &len);int port = ntohs(connAddr.sin_port);int opt =1;setsockopt(s,SOL_SOCKET,SO_REUSEADDR,(char*)&opt,sizeof(opt));printf("序號%d - success - local port:%d\n",g_id,port); }g_id++;LeaveCriticalSection(&cs);//mBdT//char sendbuf[10]={'m','B','d','T',0x00,0x00,0x00,0x02,'A','B'};//char sendbuf[10]={'A','B','\r','\n'};sssschar *sendbuf = new char[SENDSIZE];sendbuf[SENDSIZE-2]='\r';sendbuf[SENDSIZE-1]='\n';for(int i=0;i<SENDSIZE-2;i++) {sendbuf[i] = '0' + (i%10);}//小于10一般設(shè)置的是0.代表只做連接不發(fā)數(shù)據(jù)while (SENDSIZE >= 10){int sendsize = 0;for(int i=0; i<1; i++){int t = send(s,sendbuf,SENDSIZE,0);if (t <= 0){printf("%d-error - %d\n",g_id,WSAGetLastError()); exit(0); }else{//printf("%d-send size - %d\n",g_id,sendsize+=t); }}//while (1){char *buff = new char[SENDSIZE];//緩沖區(qū)memset(buff,'a',SENDSIZE);int nRecv = ::recv(s, buff, SENDSIZE, 0);//接收數(shù)據(jù) if(nRecv > 0) { buff[nRecv] = '\0'; //printf("接受數(shù)據(jù):%s\n",buff); }delete [] buff;}Sleep(SENDSLEEP );}if (SENDSIZE >= 10)Sleep(60*60*1000);return 0; }//可以等待WaitForMultipleObjects多64個的的API DWORD SyncWaitForMultipleObjs(HANDLE * handles, size_t count) { int waitingThreadsCount = count; int index = 0; DWORD res = 0; while(waitingThreadsCount >= MAXIMUM_WAIT_OBJECTS) { res = WaitForMultipleObjects(MAXIMUM_WAIT_OBJECTS, &handles[index], TRUE, INFINITE); if(res == WAIT_TIMEOUT || res == WAIT_FAILED) { puts("1. Wait Failed."); return res; } waitingThreadsCount -= MAXIMUM_WAIT_OBJECTS; index += MAXIMUM_WAIT_OBJECTS; } if(waitingThreadsCount > 0) { res = WaitForMultipleObjects(waitingThreadsCount, &handles[index], TRUE, INFINITE); if(res == WAIT_TIMEOUT || res == WAIT_FAILED) { puts("2. Wait Failed."); } } return res; } int _tmain(int argc, _TCHAR* argv[]) {if (argc == 6){ip = argv[1];port = atoi(argv[2]);SENDSIZE = atoi(argv[3]);CLIENT_COUNT = atoi(argv[4]);SENDSLEEP = atoi(argv[5]);}WSADATA wsaData; WORD sockVersion = MAKEWORD(2,0);//指定版本號 ::WSAStartup(sockVersion, &wsaData);//載入winsock的dll InitializeCriticalSection(&cs);HANDLE *m_hEvent = new HANDLE[CLIENT_COUNT]; clock_t start = clock(); for (int i=0;i<CLIENT_COUNT;i++){m_hEvent[i] = CreateThread(NULL,0,clientfun,NULL,0,NULL);}SyncWaitForMultipleObjs(m_hEvent, CLIENT_COUNT);clock_t finish = clock();double duration = (double)(finish - start) / CLOCKS_PER_SEC; printf( "花費時間:%f 秒\n", duration ); Sleep(60*60*1000);return 0; }從以上程序我們可以看出,發(fā)送的數(shù)據(jù)不再任意的了,這是為了和一個叫hany的項目對接。項目地址:https://github.com/yedf/handy
對的,他是非windows高性能網(wǎng)絡(luò)服務(wù)器(我這邊主要在linux下做研究)。
在做服務(wù)器accept能力測試時,我一直以為這個數(shù)據(jù)在1000/s左右,后來用兩個客戶端同時連服務(wù)器,兩個客戶端同時在1秒內(nèi)連上了1000左右的客戶端,這樣說來以前的測試就不太準確了。后來我查閱資料說是windows的客戶端達到了瓶頸,于是有了下面的linux版本:
//編譯命令 : g++ client.cpp -o client -lpthread#include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <unistd.h> #include <time.h> #include <pthread.h> #include <string> #include <atomic>using namespace std;string ip ="192.168.10.27"; int port =10000;int SENDSIZE =1024; int CLIENT_COUNT =1; int SENDSLEEP =1000;atomic<int> g_thread_id; void *workthread(void *arg) //線程函數(shù) {//printf("thread %d run start\n", g_thread_id++);int sockfd, n;struct sockaddr_in servaddr;if( (sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0){printf("create socket error: %s(errno: %d)\n", strerror(errno),errno);exit(0); }memset(&servaddr, 0, sizeof(servaddr));servaddr.sin_family = AF_INET;servaddr.sin_port = htons(port);if( inet_pton(AF_INET, ip.c_str(), &servaddr.sin_addr) <= 0){printf("inet_pton error for %s\n",ip);exit(0); }if( connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0){printf("connect error: %s(errno: %d)\n",strerror(errno),errno);exit(0); }if (SENDSIZE >= 10){char *sendbuf = new char[SENDSIZE];sendbuf[SENDSIZE-2]='\r';sendbuf[SENDSIZE-1]='\n';for(int i=0;i<SENDSIZE-2;i++) {sendbuf[i] = '0' + (i%10);}//小于10一般設(shè)置的是0.代表只做連接不發(fā)數(shù)據(jù)while (SENDSIZE >= 10){int sendsize = 0;for(int i=0; i<1; i++){int t = send(sockfd,sendbuf,SENDSIZE,0);if (t <= 0){printf("send msg error: %s(errno: %d)\n", strerror(errno), errno);exit(0); }else{//printf("%d-send size - %d\n",g_id,sendsize+=t); }}//while (1){char *buff = new char[SENDSIZE];//緩沖區(qū)memset(buff,'a',SENDSIZE);int nRecv = ::recv(sockfd, buff, SENDSIZE, 0);//接收數(shù)據(jù) if(nRecv > 0) { buff[nRecv] = '\0'; //printf("接受數(shù)據(jù):%s\n",buff); }delete [] buff;}sleep(SENDSLEEP);}}if (SENDSIZE >= 10)sleep(60*60);printf("thread %d run over\n", g_thread_id++);return 0; }int main(int argc, char** argv) {g_thread_id = 0;if (argc == 6){ip = argv[1];port = atoi(argv[2]);SENDSIZE = atoi(argv[3]);CLIENT_COUNT = atoi(argv[4]);SENDSLEEP = atoi(argv[5]);}clock_t start = clock();pthread_t id[CLIENT_COUNT] = {0};for (int i=0; i<CLIENT_COUNT; i++){int ret= pthread_create(&id[i],NULL,workthread,(void *)&i);if(ret){printf("pthread_create error: %s(errno: %d)\n", strerror(errno), errno);return 1;}pthread_detach(id[i]);}/*如果不調(diào)用pthread_detach,即可調(diào)用pthread_join,但是有個問題,連續(xù)創(chuàng)建1萬5千個左右的線程就會pthread_create: Resource temporarily unavailable (errno = 11)for (int i=0; i<CLIENT_COUNT; i++){pthread_join(id[i], NULL);}clock_t finish = clock();double duration = (double)(finish - start) / CLOCKS_PER_SEC;printf( "花費時間:%f 秒 run thread count %d\n", duration, g_thread_id++ ); */sleep(60*60);return 0; }由于我在linux下寫socket熟練度不是很高,當客戶端達到2-3W時候總是報錯:thread_create: Resource temporarily unavailable (errno = 11)
我看網(wǎng)上說是創(chuàng)建了進程沒有及時進行jion或者detach,稍微改了下代碼,依然沒有解決問題。有的說是系統(tǒng)資源限制的原因,調(diào)了下系統(tǒng)參數(shù),還是沒搞定,知道的朋友麻煩給點信息。這個問題大概就是線程個數(shù)創(chuàng)建的有點多導致的,于是有了下面的go版本,不創(chuàng)建線程了,用協(xié)程~)~。
//go build client.go package mainimport ("fmt""net""os""strconv""time" )var ip string var port int var SENDSIZE int var CLIENT_COUNT int var SENDSLEEP intfunc work_coroutine() {var server stringserver = ipserver += ":"server += strconv.Itoa(port)conn, err := net.Dial("tcp", server)if err != nil {fmt.Println("net.Dial err : ", err)return}defer conn.Close()if (SENDSIZE >= 10) {sendbuf := make([]byte, SENDSIZE)sendbuf[SENDSIZE-2]='\r'sendbuf[SENDSIZE-1]='\n'i := 0for i < SENDSIZE-2 {sendbuf[i] = '0' + (byte)(i%10)i++}for {_, err := conn.Write([]byte(sendbuf))if err != nil {os.Exit(1)}recvbuf := make([]byte, SENDSIZE)n, err := conn.Read(recvbuf[:])if err != nil {fmt.Println("recv failed, err:", err, n)os.Exit(1)}time.Sleep(time.Duration(SENDSLEEP)*time.Millisecond)}}time.Sleep(time.Duration(10000)*time.Second) }func main() {ip = os.Args[1]port,_ = strconv.Atoi(os.Args[2])SENDSIZE,_ = strconv.Atoi(os.Args[3])CLIENT_COUNT,_ = strconv.Atoi(os.Args[4])SENDSLEEP,_ = strconv.Atoi(os.Args[5])index := 0for index < CLIENT_COUNT {go work_coroutine()index++}time.Sleep(time.Duration(1)*time.Hour) }經(jīng)過簡單測試,linux的服務(wù)器epoll,一秒鐘可以接收8000個左右的客戶端。
?上圖為證(可能大部人不知道這是什么,這是handy運行日志,每31條日志,看到第四條到第五條增加了24000個連接,經(jīng)歷3秒,平均下來就是8000個/秒)
總結(jié)
以上是生活随笔為你收集整理的高性能服务器 - window篇的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 网络流媒体协议之——RTSP协议
- 下一篇: Rust学习资料大全