高性能服务器 - window篇

本文记录了作者在IOCP网络模型开发过程中遇到的问题及解决方案,包括内存管理、死锁问题等,并对比了不同网络模型的性能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

最初研究网狐是14年的时候,一转眼已经是18年了,这几年也做了写乱七八糟的开发,期间也做了些网络层的开发,自我感觉良好,最近做的项目主要负责服务器方面,CS架构的。一开始写了个CSocket简单的服务器,就是网上常见的结构,封装下,在继承下,记得13年在深蓝培训时候老师就是这样写的,测试的时候发现批量登录导出是BUG,经不起大规模的登录。估计主要是自己对MFC封装的CSocket了解不深,唉,这能换网络内核,之前也学了很多IOCP理论知识,也看了很多DEMO,可惜的是这些封装的都不是很好。这个时候想起国内知名VC++写的远控gh0st,也就是红狼远控,据说他的网络内核写的不错,自己也看过他的源码,3.6和3.78版本,像现在市面上常见的远控,DDOS管理端都是抄写gh0st的。我把他的网络层用到自己的项目上,刚开始还好,后来也发现了不少致命BUG,
1.他是自己写的CBuffer管理内存,其实这个类不是很安全,CopyMemery的时候就没有检查,出现了偶现的拷贝内存出界
2.有的时候莫名其妙的进入某个锁里面出不来了,导致服务器卡死
以上两个BUG很可能是自己不正确使用人家的IOCP模块导致,因为用人家的自己的项目的时候稳定的一B啊,用到自己项目就偶现崩溃能,花了1-2天时间不论自己怎么改都没解决以上两个BUG,也有可能这个模型他写的本身就有BUG,只是他自己的项目没有触发而已,因为gh0st这个项目服务器只会下发简单的指令数据,数据量很小,我自己的项目登录的时候服务器会下发几十K的数据给登录端,可能这样就会触发BUG了吧,只能这样帅锅了。由于项目还是挺紧的,没有足够时间查找原因只能赶紧换网络内核。这个时候我想到了网狐6603的IOCP,这个东西就是写的太规范了,导致简单的IOCP代码很大,附加的辅助类很多。我花了两天时间把它压缩成了一个精小版,


下面这个链接是我缩减之后的代码:

网狐IOCP压缩版-网络基础文档类资源-CSDN下载

注意:
1.由于不太会使用去掉了网络事件(收发数据、网络接受、网络断开)进队列,发的时候直接发送,接收的时候直接回调。不知道原作者都放进队列里面有哪些确切的好处。

暂时先这样,后续更新。。。

----------15:41 2018/6/27---------------

今天使用网狐的时候出现了死锁,偶现现象,真他妈吓我一跳。后经过定位,死锁在 pServerSocketItem->GetSignedLock()上面,一个线程收数据,一个线程发数据,两个都要获取这个锁,导致了死锁。如果把所有的收发数据都放进一个队列里面,让一个线程去处理收发数据肯定不会出现类似的错误,因为就一个线程嘛,很难死锁的。那为什么CServerSocketItem要有个锁呢?多线程都是操作一个Item,这些操作会访问Item有成员变量属性,肯定会乱,所以锁还有必要的。

----------10:13 2018/6/28----------------

想来想去还是加上了消息队列,去掉消息队列的话肯定是不稳定的。同时也加上了心跳。也公布出来吧,供大家使用。。

IOCP网络模型-网络基础代码类资源-CSDN下载

2.经过实验测试,这个服务器模型到1400个左右客户端,消耗内存140兆,就到达了上线,新上来的客户端登录不上去。唉,这个效果还没有gh0st上限高,gh0st上线3000个客户端松松的没压力,但是gh0st就是不稳定,看来还要寻找其他高效服务器。研究下boost的asio吧。

----------18:25 2018/6/23---------------

突发的猜想,网狐IOCP能接收的客户端不止1400多个,1400多个主要的原因是自己写的测试客户端开了1400多个线程,达到了上限,具体性能有待进一步测试。。。

----------9:46 2018/6/25---------------

经过实际测试,网狐IOCP一秒钟可以接受1000左右的链接,成功连接了20000台终端,消耗内存1.4G(由于本机是I3不方便更大量的测试,这个结果已经令人很满意)

3.找到一个不错的boost写的跨平台服务器框架,一秒钟可以接受1000左右的链接,成功连接了30000台终端(可能可以更多,测试机器WIN7+I7)。原文地址:https://blog.csdn.net/wang19840301/article/details/46648559

下载链接:跨平台高性能TCP服务器框架&boost;-其它文档类资源-CSDN下载

由于测试网狐的效果还不错,所以这个网络模型就不做进一步研究了,有需要的时候也是不错的选择。

4.压力测试代码

// TestClient.cpp : 定义控制台应用程序的入口点。
//

#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];//缓冲区  
	int nRecv = ::recv(s, buff, 156, 0);//接收数据  
	if(nRecv > 0)  
	{  
		buff[nRecv] = '\0';  
		printf("接受数据:%s",buff);  
	}  

	return 0;

}

//单个进程推荐不超过1000个线程。超过1000个的压力测试量推荐启动多个进程
#define		CLIENT_COUNT		1

int _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协议是不会丢包的,我们假设接收端处理速度较慢,或者网速较慢,总而言之,发送端势必会造成数据堆积情况,send函数会失败,错误码为10035,亦即常见的错误10035(WSAEWOULDBLOCK),这个时候如果不处理这个错误这次发送肯定是没有达到接收方的,那不就是造成了丢包现象吗?
解答(仅仅代表自己的看法,可能会不太正确或准确):
使用重叠IO时候不会产生错误10035(WSAEWOULDBLOCK),当出现接受方处理数据较慢导致发送缓冲区已经满了的时候,会产生错误WSA_IO_PENDING(997),即他们两个错误是一个意思,只不过前者是非重叠IO的,后者是重叠IO的。TCP不会丢包是针对发送成功而言的(发送成功是指send函数返回非负值,或者返回失败但是错误码是WSAEWOULDBLOCK或者WSA_IO_PENDING),发送失败的数据包肯定不能到达接收端的。那么网狐是怎么处理这个事情的呢?经过阅读源码,总结如下:发送的数据会有一个队列即m_OverLappedSendActive,每次想要发送数据时候先放进队列里面,每次真正发送成功都会触发OnSendCompleted事件(GetQueuedCompletionStatus获取到的),底层发送成功才会调用下次发送,这样保证了上层调用send肯定是发送成功的(无论是网络阻塞或者接收端处理数据太慢都会成功,除非网络链接断开),如果一直没有触发OnSendCompleted事件,则上层调用的send,其实只是放进应用程序自己写的数据队列里面而已。

6.更新日期 -------------------14:11 2021/11/24-------------------

   跟心压力客户端代码:

// TestClient.cpp : 定义控制台应用程序的入口点。
// 有个bug,发送数据大小是1000的时候,连接数最终只有几十个

#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'};ssss
	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一般设置的是0.代表只做连接不发数据
	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];//缓冲区
			memset(buff,'a',SENDSIZE);
			int nRecv = ::recv(s, buff, SENDSIZE, 0);//接收数据  
			if(nRecv > 0)  
			{  
				buff[nRecv] = '\0';  
				//printf("接受数据:%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;
}

从以上程序我们可以看出,发送的数据不再任意的了,这是为了和一个叫hany的项目对接。项目地址:https://github.com/yedf/handy

对的,他是非windows高性能网络服务器(我这边主要在linux下做研究)。

在做服务器accept能力测试时,我一直以为这个数据在1000/s左右,后来用两个客户端同时连服务器,两个客户端同时在1秒内连上了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) //线程函数
{
    //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一般设置的是0.代表只做连接不发数据
	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];//缓冲区
			memset(buff,'a',SENDSIZE);
			int nRecv = ::recv(sockfd, buff, SENDSIZE, 0);//接收数据  
			if(nRecv > 0)  
			{  
				buff[nRecv] = '\0';  
				//printf("接受数据:%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]);
    }

/*
    如果不调用pthread_detach,即可调用pthread_join,但是有个问题,连续创建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)

我看网上说是创建了进程没有及时进行jion或者detach,稍微改了下代码,依然没有解决问题。有的说是系统资源限制的原因,调了下系统参数,还是没搞定,知道的朋友麻烦给点信息。这个问题大概就是线程个数创建的有点多导致的,于是有了下面的go版本,不创建线程了,用协程~)~。

//go build client.go
package main

import (
    "fmt"
    "net"
    "os"
    "strconv"
    "time"
)

var       ip     		string
var       port       		int
var       SENDSIZE   		int
var       CLIENT_COUNT		int 	    
var 	  SENDSLEEP     	int

func work_coroutine() {
    var server string
    server = ip
    server += ":"
    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 := 0
    	for 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 := 0
    for index < CLIENT_COUNT {
    	go work_coroutine()
    	index++
    }
    
    time.Sleep(time.Duration(1)*time.Hour)
}

经过简单测试,linux的服务器epoll,一秒钟可以接收8000个左右的客户端。

 上图为证(可能大部人不知道这是什么,这是handy运行日志,每31条日志,看到第四条到第五条增加了24000个连接,经历3秒,平均下来就是8000个/秒)

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值