Я написал очень простой сокет-сервер на C ++ (MinGW), используя такие общие функции, как
socket( PF_INET, SOCK_STREAM, 0 )...
setsockopt( s, SOL_SOCKET, SO_REUSEADDR, &OptVal, sizeof( OptVal ) )...
bind( s, ( struct sockaddr * ) &ServerAddress, sizeof( ServerAddress ) )...
listen( s, 10 )...
Обработка нескольких клиентских подключений выполняется
select( s, &FileDescriptorClient, NULL, NULL, &tv )...
accept( Server->GetSocketHandle(), (struct sockaddr*) &ClientAddress, &Length )...
Все выглядело очень хорошо и красиво, ... пока я не решил стресс-тест мой сервер.
Моим первым тестом был очень простой клиент, который делал только одно: подключался и отключался бесконечным циклом - как можно быстрее. Хотя этот тест был чрезвычайно прост, он сразу же провалился.
Для меня не было большим сюрпризом, что сервер будет задыхаться от стольких переключающихся соединений, поэтому я добавил Sleep (5) (миллисекунд) в клиент перед каждым подключением и отключением, и все было в порядке. На данный момент.
Мои вопросы:
- Как правильно обрабатывать эти переподключения?
- А как правильно
провести стресс-тест приложения сервера сокетов?
Сейчас процедура выглядит следующим образом:
- клиент: подключается к серверу с помощью connect (...)
- сервер: новое соединение распознается с помощью select (...) и accept (...). Каждое новое соединение сохраняется в std :: vector.
- клиент: отключается от сервера с помощью closesocket (...) (MinGW ...)
- сервер: recv (...) читает 0 байтов, что означает, что клиент отключился от сервера
- сервер: выполняет closesocket (...) и удаляет соединение из std :: vector
- Перейти к 1
Как уже упоминалось: это будет работать только тогда, когда я задушу клиента с помощью сна. Как только я сокращаю время ожидания, сервер начинает пропускать pt. 4 (разъединяет) и запасает открытые соединения по линии.
Какой смысл я упускаю?
Edit:
- сервер работает в одном потоке
- клиентский сокет не блокируется
Редактировать 2:
По запросу: исходный код (сокращен до минимальной версии):
main.cpp сервера:
#include <iostream>
#include "Server.h"
using namespace std;
int main()
{
try
{
ServerSocket srv;
while ( true )
{
srv.Open();
srv.Run();
}
}
catch( ServerException const &e )
{
std::cout << "failed to run the server" << e.what() << std::endl;
}
return 0;
}
server.h:
#ifndef SERVER_H
#define SERVER_H
#define SOCKETBUFLEN 10
#include <string>
#include <cstdio>
#include <vector>
#include <memory>
#include <unistd.h>
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <winsock2.h>
#include <ws2tcpip.h>
#include <sys/types.h>
class Exception : public std::exception
{
private:
std::string Message;
public:
template< typename ... Arguments >
Exception( const char * AFormat, Arguments&& ... AArguments )
{
char Buffer[ 1024 ];
sprintf( Buffer, AFormat, std::forward<Arguments>( AArguments ) ... );
Message = std::string( Buffer );
};
const char* what() const noexcept{ return Message.c_str(); }
};
class ServerException : public Exception
{
public:
template< typename ... Arguments >
ServerException( const char * AFormat, Arguments&& ... AArguments ) : Exception( AFormat, std::forward< Arguments >( AArguments ) ... ){}
};
class ClientException : public Exception
{
public:
template< typename ... Arguments >
ClientException( const char * AFormat, Arguments&& ... AArguments ) : Exception( AFormat, std::forward< Arguments >( AArguments ) ... ){}
};
class Client
{
public:
Client();
virtual ~Client();
virtual bool Authenticate( const std::string & AClientId ) = 0;
virtual bool Process( const std::string & AMessage ) = 0;
protected:
std::vector< std::string > Clients;
private:
};
class ServerSocket;
class ClientSocket;
class Socket;
class ClientSockets
{
private:
ServerSocket *OwnerServer;
fd_set FileDescriptorClient;
std::vector< ClientSocket * > Items;
std::vector< Socket * > SocketsRead;
void Append( ClientSocket * );
void OnClientConnect( SOCKET ASocketHandle, struct sockaddr_in AClientAddress );
public:
ClientSockets( ServerSocket * );
int Count( void );
void Remove( SOCKET ASocketHandle );
bool HandleConnnections( void );
void Read( void );
};
class Socket
{
protected:
SOCKET SocketHandle;
public:
Socket( SOCKET ASocketHandle = 0 ) { SocketHandle = ASocketHandle; };
SOCKET GetSocketHandle( void ) { return SocketHandle; };
virtual void Close( void ) = 0;
};
class ServerSocket : public Socket
{
public:
ServerSocket( const std::string &AHost = "127.0.0.1", const int &APort = 24442 );
virtual ~ServerSocket();
void Open( void );
void Close( void ) {};
void Run( void );
fd_set FileDescriptorServer;
private:
std::string Host;
int Port;
ClientSockets *clientSockets;
};
class ClientSocket : public Socket
{
private:
ClientSockets *FClientSockets;
public:
ClientSocket( ClientSockets *, SOCKET ASocketHandle );
virtual ~ClientSocket() {};
void Close( void );
};
#endif // SERVER_H
server.cpp:
#include "Server.h"
#include <cstring>
#include <cstdarg>
#include <cstdio>
#include <algorithm>
#include <iostream>
#include <thread>
#include <chrono>
#define LOG_CONNECTION_AMOUNT 100
ServerSocket::ServerSocket( const std::string &AHost, const int &APort ) :
Host( AHost ),
Port( APort ),
clientSockets( new ClientSockets( this ) ) {}
ServerSocket::~ServerSocket()
{
WSACleanup();
}
void ServerSocket::Open( void )
{
WSADATA wsa;
if ( WSAStartup( MAKEWORD( 2, 2 ), &wsa ) != 0 )
{
throw ServerException( "WSAStartup failed" );
}
if ( ( SocketHandle = socket( PF_INET, SOCK_STREAM, 0 ) ) == INVALID_SOCKET )
{
throw ServerException( "%s: unable to get a socket handle. errno: %ld", __PRETTY_FUNCTION__, errno );
}
char OptVal = 1;
if ( setsockopt( SocketHandle, SOL_SOCKET, SO_REUSEADDR, &OptVal, sizeof( OptVal ) ) == -1 )
{
throw ServerException( "%s: setsockopt failed", __PRETTY_FUNCTION__ );
}
struct sockaddr_in ServerAddress;
memset( & ServerAddress, 0x0, sizeof( ServerAddress ) );
ServerAddress.sin_family = AF_INET;
ServerAddress.sin_addr.s_addr = inet_addr( Host.c_str() );
ServerAddress.sin_port = htons( Port );
if ( bind( SocketHandle, ( struct sockaddr * ) &ServerAddress, sizeof( ServerAddress ) ) == -1 )
{
throw ServerException( "%s: binding server address failed", __PRETTY_FUNCTION__ );
}
if ( listen( SocketHandle, 10 ) == -1 )
{
throw ServerException( "%s: listen socket failed", __PRETTY_FUNCTION__ );
}
FD_ZERO( &FileDescriptorServer );
FD_SET( SocketHandle, &FileDescriptorServer );
std::cout << "socket server fd '" << SocketHandle << "' listening on " << Host << ":" << Port << std::endl;
}
void ServerSocket::Run( void )
{
std::cout << __PRETTY_FUNCTION__ << std::endl;
do
{
try
{
if ( !clientSockets->HandleConnnections() )
{
continue;
}
clientSockets->Read();
}
catch( ServerException const &e )
{
int ErrorCode = WSAGetLastError();
std::cout << "error while server running: " << e.what() << ", code: " << ErrorCode << std::endl;
if ( ErrorCode == 10038 ) // no socket error
{
// continue; // not sure if this is the correct way to do...
}
// if ( ErrorCode == 10014 ) {} // bad address error
// if ( ErrorCode == 10054 ) {} // connection reset by peer
break;
}
catch( ... )
{
std::cout << "some exception occurred" << std::endl;
}
} while ( true );
}
ClientSockets::ClientSockets( ServerSocket * AOwnerServer )
{
OwnerServer = AOwnerServer;
}
void ClientSockets::Append( ClientSocket *AClientSocket )
{
Items.push_back( AClientSocket );
}
int ClientSockets::Count( void )
{
return Items.size();
}
bool ClientSockets::HandleConnnections( void )
{
FD_ZERO( & FileDescriptorClient );
SocketsRead.clear();
SocketsRead.push_back( OwnerServer );
for ( auto i : Items )
{
SocketsRead.push_back( i );
}
for ( auto Socket : SocketsRead )
{
if ( FD_ISSET( Socket->GetSocketHandle(), & OwnerServer->FileDescriptorServer ) )
{
FD_SET( Socket->GetSocketHandle(), & FileDescriptorClient );
}
}
struct timeval tv;
tv.tv_sec = 10;
tv.tv_usec = 3;
int SocketChangedCount = 0;
if ( ( SocketChangedCount = select( 0, &FileDescriptorClient, NULL, NULL, &tv ) ) == -1 )
{
throw ServerException( "%s: select failed", __PRETTY_FUNCTION__ );
}
static int scc = 0;
if ( scc != SocketChangedCount )
{
scc = SocketChangedCount;
std::cout << "socket changes: " << SocketChangedCount << std::endl;
}
if ( FD_ISSET( OwnerServer->GetSocketHandle(), &FileDescriptorClient ) )
{
struct sockaddr_in ClientAddress;
socklen_t Length = sizeof( ClientAddress );
SOCKET SocketHandle = 0;
if ( ( SocketHandle = accept( OwnerServer->GetSocketHandle(), (struct sockaddr*) &ClientAddress, &Length ) ) == INVALID_SOCKET )
{
throw ServerException( "%s: accept failed", __PRETTY_FUNCTION__ );
}
else
{
OnClientConnect( SocketHandle, ClientAddress );
}
FD_SET( SocketHandle, &OwnerServer->FileDescriptorServer );
}
return ( SocketChangedCount > 0 );
}
void ClientSockets::OnClientConnect( SOCKET ASocketHandle, struct sockaddr_in AClientAddress )
{
#if LOG_CONNECTION_AMOUNT > 0
static int ClientConnectionCounter = 0;
if ( ClientConnectionCounter++ % LOG_CONNECTION_AMOUNT == 0 )
{
std::cout << "connect fd: [" << ASocketHandle << "], serial: [" << ClientConnectionCounter << "], cnt: [" << Count() << "]" << std::endl;
}
#endif
ClientSocket *clientSocket = new ClientSocket( this, ASocketHandle );
Append( clientSocket );
}
void ClientSockets::Read( void )
{
for ( auto clientSocket : Items )
{
if ( FD_ISSET( clientSocket->GetSocketHandle(), &FileDescriptorClient ) )
{
char buf[ SOCKETBUFLEN + 1 ];
memset( buf, 0x0, sizeof( buf ) );
int BytesReceived = recv( clientSocket->GetSocketHandle(), buf, SOCKETBUFLEN, 0 );
if ( BytesReceived > 0 )
{
std::string Message = ( std::string ) buf;
if ( Message.substr( 0, 4 ) == "quit" )
{
clientSocket->Close();
break;
}
else
{
// ... do fancy stuff here...
std::cout << "read from socket: " << Message << std::endl;
}
}
else if ( BytesReceived == 0 )
{
clientSocket->Close();
break;
}
else
{
throw ServerException( "reading data failed" );
}
}
}
}
void ClientSockets::Remove( SOCKET ASocketHandle )
{
FD_CLR( ASocketHandle, & FileDescriptorClient );
std::vector< ClientSocket * >::iterator it =
Items.erase(
std::remove_if(
Items.begin(),
Items.end(),
[ ASocketHandle ]( Socket *sock ){ return sock->GetSocketHandle() == ASocketHandle; }
)
);
delete *it;
}
void ClientSocket::Close( void )
{
#if LOG_CONNECTION_AMOUNT > 0
static int ClientDisconnectionCounter = 0;
if ( ClientDisconnectionCounter++ % LOG_CONNECTION_AMOUNT == 0 )
{
std::cout << "disconnnect fd: [" << SocketHandle << "], serial: [" << ClientDisconnectionCounter << "], cnt: [" << FClientSockets->Count() << "]" << std::endl;
}
#endif
if ( closesocket( SocketHandle ) == SOCKET_ERROR )
{
throw ClientException( "closing socket failed: %ld", errno );
}
FClientSockets->Remove( SocketHandle );
}
ClientSocket::ClientSocket( ClientSockets *AClientSockets, SOCKET ASocketHandle ) :
Socket( ASocketHandle ),
FClientSockets( AClientSockets ) {}
Тестирующее клиентское приложение очень примитивно: я использовал Embarcadero C ++ Builder с компонентом TClientSocket, который непрерывно переключает свое состояние соединения (в режиме без блокировки). 3 флажка используются, чтобы указать приложению работать в циклическом режиме (1), переходить в спящий режим перед подключением (2) и переходить в спящий режим перед отключением (3), а 1 TEdit используется для изменения продолжительности ожидания в миллисекундах.
Код файла cpp:
#include <vcl.h>
#pragma hdrstop
#include "UnitFormMain.h"
#pragma package(smart_init)
#pragma resource "*.dfm"
TFormMain *FormMain;
//---------------------------------------------------------------------------
__fastcall TFormMain::TFormMain(TComponent* Owner)
: TForm(Owner), Trigger( false ), sock( 0 )
{
}
//---------------------------------------------------------------------------
void __fastcall TFormMain::ClientSocket1Connect(TObject *Sender, TCustomWinSocket *Socket)
{
static int ConnectionCounter = 0;
if ( ConnectionCounter++ % 100 == 0 )
{
Memo1->Lines->Add( System::Sysutils::Format( "connectionCounter '%d'", ARRAYOFCONST(( ConnectionCounter )) ) );
}
if ( CheckBoxRunLoopComponent->Checked )
{
Trigger = true;
}
}
//---------------------------------------------------------------------------
void __fastcall TFormMain::ClientSocket1Disconnect(TObject *Sender, TCustomWinSocket *Socket)
{
if ( CheckBoxRunLoopComponent->Checked )
{
Trigger = true;
}
}
//---------------------------------------------------------------------------
void __fastcall TFormMain::ButtonToggleConnectionComponentClick(TObject *Sender)
{
ClientSocket1->Active = !ClientSocket1->Active;
}
//---------------------------------------------------------------------------
void __fastcall TFormMain::CheckBoxRunLoopComponentClick(TObject *Sender)
{
if ( dynamic_cast< TCheckBox * >( Sender )->Checked )
{
for (;;)
{
Application->ProcessMessages();
if ( !dynamic_cast< TCheckBox * >( Sender )->Checked )
{
break;
}
ToggleConnection();
}
}
}
void TFormMain::ToggleConnection( void )
{
if ( Trigger )
{
Trigger = false;
if ( ClientSocket1->Active )
{
if ( CheckBoxSleepOnDisconnect )
{
Sleep( StrToInt( Edit1->Text ) );
}
ClientSocket1->Close();
}
else
{
if ( CheckBoxSleepOnConnect->Checked )
{
Sleep( StrToInt( Edit1->Text ) );
}
ClientSocket1->Open();
}
}
}
//---------------------------------------------------------------------------
Намного интереснее вывод тестового прогона (сервера):
Каждый сотый тумблер протоколируется.
"cnt" - это количество соединений в std :: vector <>.
Тест начался с 5 миллисекундного сна при каждом переключении. Где-то около 1500-го переключения я сократил время ожидания до 1 миллисекунды, что привело к немедленному увеличению активных соединений в списке серверов. Через несколько тысяч соединений select()
завершается с 10038
.
socket server fd '276' listening on 127.0.0.1:24442
void ServerSocket::Run()
socket changes: 1
connect fd: [280], serial: [1], cnt: [0]
disconnnect fd: [280], serial: [1], cnt: [1]
connect fd: [284], serial: [101], cnt: [0]
disconnnect fd: [284], serial: [101], cnt: [1]
connect fd: [280], serial: [201], cnt: [0]
disconnnect fd: [280], serial: [201], cnt: [1]
connect fd: [288], serial: [301], cnt: [0]
disconnnect fd: [288], serial: [301], cnt: [1]
connect fd: [292], serial: [401], cnt: [0]
disconnnect fd: [292], serial: [401], cnt: [1]
connect fd: [296], serial: [501], cnt: [0]
disconnnect fd: [296], serial: [501], cnt: [1]
connect fd: [308], serial: [601], cnt: [0]
disconnnect fd: [308], serial: [601], cnt: [1]
connect fd: [296], serial: [701], cnt: [0]
disconnnect fd: [296], serial: [701], cnt: [1]
connect fd: [296], serial: [801], cnt: [0]
disconnnect fd: [296], serial: [801], cnt: [1]
connect fd: [288], serial: [901], cnt: [0]
disconnnect fd: [288], serial: [901], cnt: [1]
connect fd: [300], serial: [1001], cnt: [0]
disconnnect fd: [300], serial: [1001], cnt: [1]
connect fd: [312], serial: [1101], cnt: [0]
disconnnect fd: [312], serial: [1101], cnt: [1]
connect fd: [300], serial: [1201], cnt: [0]
disconnnect fd: [300], serial: [1201], cnt: [1]
connect fd: [300], serial: [1301], cnt: [0]
disconnnect fd: [300], serial: [1301], cnt: [1]
connect fd: [280], serial: [1401], cnt: [0]
disconnnect fd: [280], serial: [1401], cnt: [1]
connect fd: [280], serial: [1501], cnt: [0]
disconnnect fd: [280], serial: [1501], cnt: [1]
connect fd: [316], serial: [1601], cnt: [0]
socket changes: 2
disconnnect fd: [316], serial: [1601], cnt: [2]
socket changes: 1
socket changes: 2
socket changes: 1
socket changes: 2
socket changes: 1
connect fd: [312], serial: [1701], cnt: [3]
socket changes: 2
socket changes: 1
disconnnect fd: [312], serial: [1701], cnt: [4]
connect fd: [324], serial: [1801], cnt: [3]
socket changes: 2
socket changes: 1
disconnnect fd: [312], serial: [1801], cnt: [4]
connect fd: [324], serial: [1901], cnt: [3]
socket changes: 2
socket changes: 1
disconnnect fd: [324], serial: [1901], cnt: [4]
connect fd: [320], serial: [2001], cnt: [3]
socket changes: 2
socket changes: 1
disconnnect fd: [320], serial: [2001], cnt: [4]
socket changes: 2
socket changes: 1
connect fd: [336], serial: [2101], cnt: [3]
disconnnect fd: [336], serial: [2101], cnt: [4]
connect fd: [344], serial: [2201], cnt: [3]
disconnnect fd: [344], serial: [2201], cnt: [4]
connect fd: [344], serial: [2301], cnt: [3]
socket changes: 2
socket changes: 1
disconnnect fd: [336], serial: [2301], cnt: [4]
socket changes: 2
socket changes: 1
connect fd: [344], serial: [2401], cnt: [3]
socket changes: 2
socket changes: 1
disconnnect fd: [336], serial: [2401], cnt: [4]
connect fd: [344], serial: [2501], cnt: [3]
socket changes: 2
socket changes: 1
disconnnect fd: [344], serial: [2501], cnt: [4]
connect fd: [344], serial: [2601], cnt: [3]
socket changes: 2
socket changes: 1
disconnnect fd: [344], serial: [2601], cnt: [4]
connect fd: [332], serial: [2701], cnt: [3]
socket changes: 2
socket changes: 1
disconnnect fd: [332], serial: [2701], cnt: [4]
connect fd: [344], serial: [2801], cnt: [3]
socket changes: 2
socket changes: 1
disconnnect fd: [332], serial: [2801], cnt: [4]
socket changes: 2
socket changes: 1
connect fd: [328], serial: [2901], cnt: [3]
socket changes: 2
socket changes: 1
disconnnect fd: [324], serial: [2901], cnt: [4]
socket changes: 2
socket changes: 1
connect fd: [328], serial: [3001], cnt: [3]
socket changes: 2
socket changes: 1
disconnnect fd: [324], serial: [3001], cnt: [4]
socket changes: 2
socket changes: 1
connect fd: [328], serial: [3101], cnt: [3]
socket changes: 2
socket changes: 1
disconnnect fd: [324], serial: [3101], cnt: [4]
connect fd: [324], serial: [3201], cnt: [3]
disconnnect fd: [324], serial: [3201], cnt: [4]
connect fd: [324], serial: [3301], cnt: [3]
disconnnect fd: [324], serial: [3301], cnt: [4]
socket changes: 2
socket changes: 1
error while server running: bool ClientSockets::HandleConnnections(): select failed, code: 10038
socket server fd '324' listening on 127.0.0.1:24442
void ServerSocket::Run()
socket changes: 0
В этом протоколе я также вижу, что select()
возвращает значение> 1 («Socket changes: n»), что означает, что более 1 сокета изменили свой статус. Я думаю, что это может быть указатель на правильное решение, но сейчас я не знаю, как с этим справиться.
Редактировать 3: Блокировка / отсутствие блокировки: в CPPBuilder вы можете использовать эти компоненты времени разработки, где вы можете установить свойства во время разработки:
![TClientSocktet property editor](https://i.stack.imgur.com/5emLG.png)