ノンブロッキングソケットを備えたクロスプラットフォームhttpsサーバー。 パート2

この記事は記事の続きです:

SSLをサポートする最もシンプルなクロスプラットフォームサーバー

ノンブロッキングソケットを備えたクロスプラットフォームhttpsサーバー

これらの記事では、OpenSSLの一部である単純な例から、本格的なシングルスレッドWebサーバーを徐々に作成しようとしています。

前回の記事では、1つのクライアントからの接続を受け入れ、リクエストヘッダー付きのhtmlページを送り返すようサーバーに「教え」ました。

今日は、1つのスレッドで任意の数のクライアントからの接続を処理できるようにサーバーコードを修正します。



最初に、コードを2つのファイルに分割します:serv.cppとserver.h

この場合、serv.cppファイルには次のような「高度にインテリジェントな」コードが含まれます。

#include "server.h" int main() { server::CServer(); return 0; }
      
      







はい、あなたは私を蹴ることができますが、私がまだ書いている、書いている、そしてそれが私にとって都合が良いならヘッダーファイルにコードを書くでしょう。 そのため、実際にはC ++が大好きで、選択の自由が与えられますが、これは別の会話です...



server.hファイルに移動します

最初に、以前にserv.cppにあったすべてのヘッダー、マクロ、および定義を移動し、STLからいくつかのヘッダーを追加しました。



 #ifndef _SERVER #define _SERVER #include <stdio.h> #include <stdlib.h> #include <memory.h> #include <errno.h> #include <sys/types.h> #ifndef WIN32 #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <netdb.h> #else #include <io.h> #include <Winsock2.h> #pragma comment(lib, "ws2_32.lib") #endif #include <openssl/rsa.h> /* SSLeay stuff */ #include <openssl/crypto.h> #include <openssl/x509.h> #include <openssl/pem.h> #include <openssl/ssl.h> #include <openssl/err.h> #include <vector> #include <string> #include <sstream> #include <map> #include <memory> #ifdef WIN32 #define SET_NONBLOCK(socket) \ if (true) \ { \ DWORD dw = true; \ ioctlsocket(socket, FIONBIO, &dw); \ } #else #include <fcntl.h> #define SET_NONBLOCK(socket) \ if (fcntl( socket, F_SETFL, fcntl( socket, F_GETFL, 0 ) | O_NONBLOCK ) < 0) \ printf("error in fcntl errno=%i\n", errno); #define closesocket(socket) close(socket) #define Sleep(a) usleep(a*1000) #define SOCKET int #define INVALID_SOCKET -1 #endif /* define HOME to be dir for key and cert files... */ #define HOME "./" /* Make these what you want for cert & key files */ #define CERTF HOME "ca-cert.pem" #define KEYF HOME "ca-cert.pem" #define CHK_ERR(err,s) if ((err)==-1) { perror(s); exit(1); }
      
      







次に、最初にネームスペースサーバー内にCServerクラスとCClientクラスを作成します。

 using namespace std; namespace server { class CClient { //   SOCKET m_hSocket; //        vector<unsigned char> m_vRecvBuffer; //        vector<unsigned char> m_vSendBuffer; //    OpenSSL SSL_CTX* m_pSSLContext; SSL* m_pSSL; //       explicit CClient(const CClient &client) {} public: CClient(const SOCKET hSocket) : m_hSocket(hSocket), m_pSSL(NULL), m_pSSLContext(NULL) {} ~CClient() { if(m_hSocket != INVALID_SOCKET) closesocket(m_hSocket); if (m_pSSL) SSL_free (m_pSSL); if (m_pSSLContext) SSL_CTX_free (m_pSSLContext); } }; class CServer { //      map<SOCKET, shared_ptr<CClient> > m_mapClients; //       explicit CServer(const CServer &server) {} public: CServer() {} }; } #endif
      
      







ご覧のとおり、これはサーバーにとって空欄です。 このワークピースをコードでゆっくり埋めていきます。そのほとんどはすでに前の記事で説明しています

クライアントごとに、独自のSSLコンテキストが開始されます。これは、明らかにCClientクラスのコンストラクターで実行する必要があります

  CClient(const SOCKET hSocket) : m_hSocket(hSocket), m_pSSL(NULL), m_pSSLContext(NULL) { #ifdef WIN32 const SSL_METHOD *meth = SSLv23_server_method(); #else SSL_METHOD *meth = SSLv23_server_method(); #endif m_pSSLContext = SSL_CTX_new (meth); if (!m_pSSLContext) ERR_print_errors_fp(stderr); if (SSL_CTX_use_certificate_file(m_pSSLContext, CERTF, SSL_FILETYPE_PEM) <= 0) ERR_print_errors_fp(stderr); if (SSL_CTX_use_PrivateKey_file(m_pSSLContext, KEYF, SSL_FILETYPE_PEM) <= 0) ERR_print_errors_fp(stderr); if (!SSL_CTX_check_private_key(m_pSSLContext)) fprintf(stderr,"Private key does not match the certificate public key\n"); }
      
      







ライブラリの初期化、リスニングソケットの作成とバインドを、CServerコンストラクターへの最小限の変更で転送します。

  CServer() { #ifdef WIN32 WSADATA wsaData; if ( WSAStartup( MAKEWORD( 2, 2 ), &wsaData ) != 0 ) { printf("Could not to find usable WinSock in WSAStartup\n"); return; } #endif SSL_load_error_strings(); SSLeay_add_ssl_algorithms(); /* ----------------------------------------------- */ /* Prepare TCP socket for receiving connections */ SOCKET listen_sd = socket (AF_INET, SOCK_STREAM, 0); CHK_ERR(listen_sd, "socket"); SET_NONBLOCK(listen_sd); struct sockaddr_in sa_serv; memset (&sa_serv, '\0', sizeof(sa_serv)); sa_serv.sin_family = AF_INET; sa_serv.sin_addr.s_addr = INADDR_ANY; sa_serv.sin_port = htons (1111); /* Server Port number */ int err = ::bind(listen_sd, (struct sockaddr*) &sa_serv, sizeof (sa_serv)); CHK_ERR(err, "bind"); /* Receive a TCP connection. */ err = listen (listen_sd, 5); CHK_ERR(err, "listen"); }
      
      







さらに同じコンストラクターで、着信TCP接続を受け入れることをお勧めします。

それに対する議論を誰もまだ与えていないので、前の記事のように、無限ループでTCP接続をリッスンします。

各コールを受け入れた後、コールバック関数を呼び出すことにより、新しく接続されたクライアントと既に接続されたクライアントで何かを行うことができます。

listen関数の後、CServerコンストラクターに次のコードを追加します。



 while(true) { Sleep(1); struct sockaddr_in sa_cli; size_t client_len = sizeof(sa_cli); #ifdef WIN32 const SOCKET sd = accept (listen_sd, (struct sockaddr*) &sa_cli, (int *)&client_len); #else const SOCKET sd = accept (listen_sd, (struct sockaddr*) &sa_cli, &client_len); #endif Callback(sd); }
      
      







そして、コンストラクターの直後、実際のコールバック関数:

  private: void Callback(const SOCKET hSocket) { if (hSocket != INVALID_SOCKET) m_mapClients[hSocket] = shared_ptr<CClient>(new CClient(hSocket)); //   auto it = m_mapClients.begin(); while (it != m_mapClients.end()) //   { if (!it->second->Continue()) // -   m_mapClients.erase(it++); //   false,    else it++; } }
      
      







これでCServerクラスコードが完成しました! 他のすべてのアプリケーションロジックは、CClientクラスに含まれます。

ループ内のすべてのクライアントを列挙するのではなく、速度に重要なプロジェクトの場合、ソケットが読み取りまたは書き込みの準備ができているクライアントのみをソートする必要があることに注意することが重要です。

このブルートフォースは、Windowsの一部の機能またはLinuxのepollで簡単に実行できます。 次の記事でこれを行う方法を示します。

それまでの間(再び批判を受ける危険性がある)、すべて同じように、私は単純なサイクルに自分自身を制限します。



サーバーのメインの「主力」であるCClientクラスに渡します。

CClientクラスは、そのソケットに関する情報だけでなく、サーバーとの対話がどの段階にあるかに関する情報もそれ自体に保存する必要があります。

次のコードをCClientクラス定義に追加します。

  private: //    .     . enum STATES { S_ACCEPTED_TCP, S_ACCEPTED_SSL, S_READING, S_ALL_READED, S_WRITING, S_ALL_WRITED }; STATES m_stateCurrent; //    //      void SetState(const STATES state) {m_stateCurrent = state;} const STATES GetState() const {return m_stateCurrent;} public: //      const bool Continue() { if (m_hSocket == INVALID_SOCKET) return false; switch (GetState()) { case S_ACCEPTED_TCP: break; case S_ACCEPTED_SSL: break; case S_READING: break; case S_ALL_READED: break; case S_WRITING: break; case S_ALL_WRITED: break; default: return false; } return true; }
      
      







ここで、Continue()はこれまでのスタブ関数に過ぎませんが、少し下の方で、接続されたクライアントですべてのアクションを実行する方法を説明します。



コンストラクターで、以下を変更します。

 CClient(const SOCKET hSocket) : m_hSocket(hSocket), m_pSSL(NULL), m_pSSLContext(NULL)
      
      







 CClient(const SOCKET hSocket) : m_hSocket(hSocket), m_pSSL(NULL), m_pSSLContext(NULL), m_stateCurrent(S_ACCEPTED_TCP)
      
      







現在の状態に応じて、クライアントは異なる関数を呼び出します。 クライアントの状態は、コンストラクターとContinue()関数でのみ変更できることに同意します。これにより、コードのサイズがわずかに増加しますが、デバッグが大幅に容易になります。



したがって、クライアントがコンストラクターで作成されたときに受け取る最初の状態:S_ACCEPTED_TCP。

この状態になるまで、クライアントによって呼び出される関数を作成します。

この行の場合:

  case S_ACCEPTED_TCP: break;
      
      







以下に変更します。

  case S_ACCEPTED_TCP: { switch (AcceptSSL()) { case RET_READY: printf ("SSL connection using %s\n", SSL_get_cipher (m_pSSL)); SetState(S_ACCEPTED_SSL); break; case RET_ERROR: return false; } return true; }
      
      







また、次のコードをCClientクラスに追加します。

  private: enum RETCODES { RET_WAIT, RET_READY, RET_ERROR }; const RETCODES AcceptSSL() { if (!m_pSSLContext) //     SSL return RET_ERROR; if (!m_pSSL) { m_pSSL = SSL_new (m_pSSLContext); if (!m_pSSL) return RET_ERROR; SSL_set_fd (m_pSSL, m_hSocket); } const int err = SSL_accept (m_pSSL); const int nCode = SSL_get_error(m_pSSL, err); if ((nCode != SSL_ERROR_WANT_READ) && (nCode != SSL_ERROR_WANT_WRITE)) return RET_READY; return RET_WAIT; }
      
      







これで、暗号化された接続が発生するかエラーが発生するまで、クライアントによってAcceptSSL()関数が呼び出されます。



1.エラーの場合、CClient :: AcceptSSL()関数は、それを呼び出すCClient :: Continue()関数にRET_ERRORコードを返します。この場合、呼び出したCServer :: Callback関数にfalseを返します。この場合、サーバーのメモリからクライアントを削除します

2.接続が成功すると、CClient :: AcceptSSL()関数は、呼び出したCClient :: Continue()関数にRET_READYコードを返します。この場合、クライアントの状態をS_ACCEPTED_SSLに変更します。



次に、状態処理関数S_ACCEPTED_SSLを追加します。 この行について

 case S_ACCEPTED_SSL: break;
      
      







以下を修正します。

  case S_ACCEPTED_SSL: { switch (GetSertificate()) { case RET_READY: SetState(S_READING); break; case RET_ERROR: return false; } return true; }
      
      







そして、CClientに関数を追加します。

  const RETCODES GetSertificate() { if (!m_pSSLContext || !m_pSSL) //     SSL return RET_ERROR; /* Get client's certificate (note: beware of dynamic allocation) - opt */ X509* client_cert = SSL_get_peer_certificate (m_pSSL); if (client_cert != NULL) { printf ("Client certificate:\n"); char* str = X509_NAME_oneline (X509_get_subject_name (client_cert), 0, 0); if (!str) return RET_ERROR; printf ("\t subject: %s\n", str); OPENSSL_free (str); str = X509_NAME_oneline (X509_get_issuer_name (client_cert), 0, 0); if (!str) return RET_ERROR; printf ("\t issuer: %s\n", str); OPENSSL_free (str); /* We could do all sorts of certificate verification stuff here before deallocating the certificate. */ X509_free (client_cert); } else printf ("Client does not have certificate.\n"); return RET_READY; }
      
      







この関数は、前の関数とは異なり、1回だけ呼び出され、RET_ERRORまたはRET_READYをCClient :: Continueに返します。 したがって、CClient :: Continueはfalseを返すか、クライアントの状態をS_READINGに変更します。



その後、すべてが同じです:コードを変更します

  case S_READING: break; case S_ALL_READED: break; case S_WRITING: break;
      
      







これについて:

  case S_READING: { switch (ContinueRead()) { case RET_READY: SetState(S_ALL_READED); break; case RET_ERROR: return false; } return true; } case S_ALL_READED: { switch (InitRead()) { case RET_READY: SetState(S_WRITING); break; case RET_ERROR: return false; } return true; } case S_WRITING: { switch (ContinueWrite()) { case RET_READY: SetState(S_ALL_WRITED); break; case RET_ERROR: return false; } return true; }
      
      







そして、対応する状態処理関数を追加します。

  const RETCODES ContinueRead() { if (!m_pSSLContext || !m_pSSL) //     SSL return RET_ERROR; unsigned char szBuffer[4096]; const int err = SSL_read (m_pSSL, szBuffer, 4096); //      if (err > 0) { //     m_vRecvBuffer m_vRecvBuffer.resize(m_vRecvBuffer.size()+err); memcpy(&m_vRecvBuffer[m_vRecvBuffer.size()-err], szBuffer, err); //  http     const std::string strInputString((const char *)&m_vRecvBuffer[0]); if (strInputString.find("\r\n\r\n") != -1) return RET_READY; return RET_WAIT; } const int nCode = SSL_get_error(m_pSSL, err); if ((nCode != SSL_ERROR_WANT_READ) && (nCode != SSL_ERROR_WANT_WRITE)) return RET_ERROR; return RET_WAIT; } const RETCODES InitRead() { if (!m_pSSLContext || !m_pSSL) //     SSL return RET_ERROR; //      const std::string strInputString((const char *)&m_vRecvBuffer[0]); // html     const std::string strHTML = "<html><body><h2>Hello! Your HTTP headers is:</h2><br><pre>" + strInputString.substr(0, strInputString.find("\r\n\r\n")) + "</pre></body></html>"; //    http  std::ostringstream strStream; strStream << "HTTP/1.1 200 OK\r\n" << "Content-Type: text/html; charset=utf-8\r\n" << "Content-Length: " << strHTML.length() << "\r\n" << "\r\n" << strHTML.c_str(); // ,    m_vSendBuffer.resize(strStream.str().length()); memcpy(&m_vSendBuffer[0], strStream.str().c_str(), strStream.str().length()); return RET_READY; } const RETCODES ContinueWrite() { if (!m_pSSLContext || !m_pSSL) //     SSL return RET_ERROR; int err = SSL_write (m_pSSL, &m_vSendBuffer[0], m_vSendBuffer.size()); if (err > 0) { //    ,      if (err == m_vSendBuffer.size()) return RET_READY; //    ,      ,     vector<unsigned char> vTemp(m_vSendBuffer.size()-err); memcpy(&vTemp[0], &m_vSendBuffer[err], m_vSendBuffer.size()-err); m_vSendBuffer = vTemp; return RET_WAIT; } const int nCode = SSL_get_error(m_pSSL, err); if ((nCode != SSL_ERROR_WANT_READ) && (nCode != SSL_ERROR_WANT_WRITE)) return RET_ERROR; return RET_WAIT; }
      
      







これまでのサーバーは、httpリクエストのヘッダーをクライアントに表示することのみを目的としています。

サーバーはその目的を達成した後、接続を閉じてクライアントを忘れることができます。

したがって、最後の小さな変更はコードに残ります。

 case S_ALL_WRITED: break;
      
      







修正する必要があります

  case S_ALL_WRITED: return false;
      
      







以上です! これで、非ブロッキングソケットにクロスプラットフォームのシングルスレッドhttpsサーバーがあり、任意の(メモリとオペレーティングシステムの設定によってのみ制限される)接続数を処理できます。



Visual Studio 2012のプロジェクトのアーカイブは、こちらからダウンロードできます: 01.3s3s.org

Linuxでコンパイルするには、ファイルを1つのディレクトリ(serv.cpp、server.h、ca-cert.pem)にコピーし、コマンドラインに入力する必要があります。「g ++ -std = c ++ 0x -L / usr / lib -lssl -lcrypto serv .cpp»



継続



All Articles