サーバーベースクラス(2012/09/02)


サーバーベースクラスとは

WebSocketのサーバークライアント間の通信は一定の手順に則って行われる。
新たなサーバープログラムを書く際に、それらの一定の手順を毎回コーディングするのは面倒であるため、今回のプロジェクトではサーバーベースクラスを作成する。
サーバーベースクラスでは、WebSocketクライアントからリクエストが来た際のハンドシェイクの実行と、データのやり取りをするためのベースフレーミングプロトコルの実装、通信終了時の遮断処理を行い、派生クラスである各サーバープログラムがそれらのWebSocketのプロトコルを意識すること無く通信プログラムを記述できるようにすることを目的とする。

ハンドシェイクの実行

資料にあげたRFC-6455によれば、WebSocketの接続開始時は、クライアントから「Sec-WebSocket-Key」フィールドにハンドシェイクに必要なキー情報がセットされる。このキーに「258EAFA5-E914-47DA-95CA-C5AB0DC85B11」という文字列を追加した文字列のsha-1ハッシュをとり、そのハッシュ値をbase64でコード化したものを、レスポンスとして「Sec-WebSocket-Accept」で返さなければならない。ハンドシェイク実行後に送信されるデータはWebSocketのデータとして扱われる。
このように、ハンドシェイクの実行のこうしたルーチンは如何なるWebSocketサーバープログラムであっても一定であるため、ベースクラスによる実装を行う。
ただし、「Sec-WebSocket-Protocol」に記述されるプロトコル情報が不一致である場合などにはサーバー側の判断で接続を切断する必要が出てくる。そういった判断は派生先のサーバークラスでしか定義できないため、サーバークラス側でも接続の継続や拒否を行うことが出来るようにする。

ベースフレーミングプロトコル

ハンドシェイク実行後、WebSocketサーバーとWebSocketクライアントはデータのやり取りを行う。データのやり取りは無手順ではなく、ベースフレーミングプロトコルを用いて行う。
ベースフレーミングプロトコルはヘッダー情報やデータ長、マスクからなる。サーバーからクライアントへのデータの送信はマスキングされないが、クライアントからサーバーへのデータは常にマスキングされる。
ただ、データの内容はあくまで「データ長」と「データ」という単純なものであるため、各ヘッダーの追加やマスキングの解除(クライアントからサーバーへのデータのみマスキングされるため、サーバープログラム側ではマスキング処理は行わない)などを行い、ベースフレーミングプロトコル形式への変換を行うのはサーバーベースクラスとなる。

その他の通信

クライアントから不正なデータが送られてきたり、特定の条件下で通信を終了する際、WebSocketサーバー及びWebSocketクライアントは通信終了指示を送信して通信を切断する(この通信終了指示もベースフレーミングプロトコルの仕様に含まれる)。 また、ベースフレーミングプロトコルには「ping」と「pong」も含まれているため、クライアントからのping要求に対して(派生したサーバークラス側でpingを拒否するように成っていない場合)、pongを返す処理など、一定の処理はサーバーベースクラス側で実装する。

ソースコード

サーバーベースクラスのソースコードを以下に示す。ソースコード掲載後、少しのコメントを述べる。

SPWebSocketServerBase.h
#pragma once

#include <winsock2.h>
#include "HttpHeaderParser.h"
#include "SPSendManager.h"
#include "SPBaseFramingDataSender.h"
#include "SPBaseFramingDataReciever.h"

//
//    サーバーパラメータ
//
class CServerParameter
{
private:
    sockaddr_in m_sockAddr;
    SOCKET m_sockClient;

public:
    void SetSockAddr( sockaddr_in sockAddr )
    {
        m_sockAddr = sockAddr;
    }
    sockaddr_in * GetSockAddr()
    {
        return &m_sockAddr;
    }

    void SetSocketClient( SOCKET sockClient )
    {
        m_sockClient = sockClient;
    }
    SOCKET GetSockClient()
    {
        return m_sockClient;
    }
};

//
//    サーバースレッドのベースクラス
//
class CSPWebSocketServerBase
{
protected:
    CSPWebSocketServerBase();
    CHttpHeaderParser m_headerParser;
    CServerParameter * m_pParameter;

    //送信データリスト
    CSPSendManager m_sendManager;

    //受信データリスト
    CSPSendManager m_recieveData;

    //データを送信する
    bool WriteSendBuffer(bool bAllSendForced = false);

    bool GetHandshakeFlag();
    void Disconnect();
    bool AddAndWriteSender( CSPSendElement * pSender , bool bAllSendForced = false);

    //オーバーライド可能
    virtual bool ProcessHandshakeData();
    virtual bool DoHandshake();
    virtual bool ProcessBaseFrameData();

    //派生クラス側で追加実装可能用
    virtual bool EnableHandshake();
    virtual bool AdditionalHandshake( char ** ppszHeaderData, const char * pcsAcceptKey );

    virtual bool HandshakeDone();
    virtual bool Polling();
    virtual bool Closing();
    virtual bool FrameDataRecieved(CSPBaseFramingDataReciever * pRecieveData, bool & bSaveData);

private:
    static CSPWebSocketServerBase * CreateServerInstance( );
    char * m_pszReadBuffer;
    size_t m_sizeReadData;
    bool m_bHandshake;
    bool ReadData( );
    bool DoAction();
    void SetServerParameter( CServerParameter * pParameter );
    bool SendClosing();

    HANDLE m_hSocketMutex;
    bool m_bStopThread;
    
public:
    static DWORD WINAPI ServerThread( LPVOID lpVoid );
    ~CSPWebSocketServerBase();

};

SPWebSocketServerBase.cpp
#include "SPWebSocketServerBase.h"
#include "sha1.h"
#include "SPBase64.h"
#include "Setting.h"

DWORD WINAPI CSPWebSocketServerBase::ServerThread( LPVOID lpVoid )
{
    CSPWebSocketServerBase * pServer = CSPWebSocketServerBase::CreateServerInstance();
    pServer->SetServerParameter( (CServerParameter *)lpVoid );
    pServer->DoAction();
    delete pServer;

    return (DWORD)0;
}

//
//    コンストラクタ
//
//        パラメータのセット
//
CSPWebSocketServerBase::CSPWebSocketServerBase( )
{
    m_bHandshake = false;
    m_pszReadBuffer = NULL;
    m_sizeReadData = (size_t)0;
    m_bStopThread = false;

    m_hSocketMutex = CreateMutex( NULL , FALSE , NULL );
}

void CSPWebSocketServerBase::SetServerParameter( CServerParameter * pParameter )
{
    m_pParameter = pParameter;
}

//
//    デストラクタ
//
//        パラメータの開放
//
CSPWebSocketServerBase::~CSPWebSocketServerBase()
{
    if( m_pParameter )
    {
        delete m_pParameter;
        m_pParameter = NULL;
    }

    //ソケット用ミューテックスの破棄
    WaitForSingleObject( m_hSocketMutex , INFINITE );
    CloseHandle( m_hSocketMutex );
}

//
//    サーバースレッドループ
//
bool CSPWebSocketServerBase::DoAction()
{
    bool bRet = true;
    //イベントの作成
    HANDLE hEvent = WSACreateEvent();
    if( hEvent == WSA_INVALID_EVENT )
    {
        closesocket( m_pParameter->GetSockClient() );
        return false;
    }

    //イベントとソケットの接続
    int nRet = WSAEventSelect( m_pParameter->GetSockClient() , hEvent , FD_READ|FD_CLOSE );
    if( nRet == SOCKET_ERROR )
    {
        closesocket( m_pParameter->GetSockClient() );
        CloseHandle( hEvent);
        return false;
    }

    //スレッドが終了するまでループする
    bool bLoopEnd = false;
    while( !bLoopEnd && !m_bStopThread )
    {
        DWORD dwRet = WSAWaitForMultipleEvents( 1 , &hEvent , FALSE , CSetting::Inst()->GetLoopWait() , FALSE );
        WSANETWORKEVENTS events;
        int nRet = WSAEnumNetworkEvents( m_pParameter->GetSockClient() , hEvent , &events );

        //スレッドの終了が指定されている場合
        if( m_bStopThread )
        {
            break;
        }

        //ポーリング時の処理
        if( !Polling() )
        {
            bRet = false;
            break;
        }

        //読み込みイベントが発生した場合
        if( events.lNetworkEvents & FD_READ )
        {
            if( !ReadData( ) )
            {
                bRet = false;
                break;
            }
        }

        //クローズイベントが発生した場合
        if( events.lNetworkEvents & FD_CLOSE )
        {
            m_bHandshake = false;
            break;
        }

        //ライトバッファがあれば処理する
        if( !WriteSendBuffer() )
        {
            bRet = false;
            break;
        }
    }

    Closing();

    //ハンドシェイクが成立しているときのみ、クローズリクエストの送信
    if( m_bHandshake )
    {
        SendClosing();
    }

    closesocket( m_pParameter->GetSockClient() );
    CloseHandle( hEvent );

    return bRet;
}

//
//    データの読み込み処理
//
//        読み込まれたデータのバッファリング
//        バッファリングデータを使用してハンドシェイクまたはデータ送受信処理
//
bool CSPWebSocketServerBase::ReadData()
{
    bool bRet = true;

    //4Kずつデータを読み込む
    char buff[4096];

    //データの取得処理(複数のスレッドで同時にソケットを使わないようにする)
    WaitForSingleObject( m_hSocketMutex , INFINITE );

    int iRet = recv( m_pParameter->GetSockClient(), buff, 4096, 0 );

    ReleaseMutex( m_hSocketMutex );

    if( iRet == SOCKET_ERROR )
    {
        bRet = false;
    }

    if( bRet )
    {
        //既に読み込みバッファに登録済みの場合
        if( m_pszReadBuffer )
        {
            char * pszReadBufferTemp = new char[ m_sizeReadData + iRet ];
            memcpy_s( pszReadBufferTemp, m_sizeReadData + iRet, m_pszReadBuffer, m_sizeReadData );
            memcpy_s( pszReadBufferTemp + m_sizeReadData, iRet, buff , iRet );
            delete[] m_pszReadBuffer;
            m_pszReadBuffer = pszReadBufferTemp;
        }
        //始めてのデータ登録の場合
        else
        {
            m_pszReadBuffer = new char[ iRet ];
            memcpy_s( m_pszReadBuffer, iRet, buff, iRet );
        }
        m_sizeReadData += iRet;

        //ハンドシェイクが終了している場合
        if( m_bHandshake )
        {
            bRet = ProcessBaseFrameData( );
        }
        //ハンドシェイクリクエストの場合
        else
        {
            bRet = ProcessHandshakeData( );
        }
    }

    return bRet;
}

//
//    ハンドシェイクデータの処理
//
bool CSPWebSocketServerBase::ProcessHandshakeData()
{
    bool bRet = true;

    //最後の\r\nのポジションを取得する
    int iLength;
    for( iLength = (int)m_sizeReadData - 1 ; iLength >= 0 ; iLength-- )
    {
        if( m_pszReadBuffer[iLength] == '\r'
            && iLength != m_sizeReadData-1 && m_pszReadBuffer[iLength+1] == '\n' )
        {
            iLength += 2;
            break;
        }
    }

    bool bDoHandshake = false;

    //改行のみのデータの場合
    if( iLength == 2 )
    {
        bDoHandshake = true;
    }
    //データを処理する場合
    else if( iLength >= 0 )
    {
        m_headerParser.AddData( m_pszReadBuffer, iLength );

        if( iLength >= 4 &&
            memcmp( m_pszReadBuffer + iLength - 4 , "\r\n\r\n", 4 ) == 0 )
        {
            bDoHandshake = true;
        }
    }

    //処理済みのデータを削除する
    if( iLength >= 0 )
    {
        size_t sizeNewBuffer = m_sizeReadData - (size_t)iLength;
        char * pszTempBuffer = new char[ sizeNewBuffer ];

        memcpy_s( pszTempBuffer, sizeNewBuffer, m_pszReadBuffer+iLength, sizeNewBuffer );
        delete[] m_pszReadBuffer;
        m_pszReadBuffer = pszTempBuffer;
        m_sizeReadData = sizeNewBuffer;
    }

    //ハンドシェイクの実行
    if( bDoHandshake )
    {
        m_bHandshake = true;
        DoHandshake();

        delete[] m_pszReadBuffer;
        m_pszReadBuffer = NULL;
        m_sizeReadData = (size_t)0;
    }

    return bRet;
}

//
//    ハンドシェイクの実行
//
//        m_headerParserの登録内容に併せてハンドシェイクを実行する
//
bool CSPWebSocketServerBase::DoHandshake()
{
    bool bRet = true;

    //ハンドシェイク可能な場合
    if( EnableHandshake() )
    {
        char * pszKeyBase = NULL;
        CTool::SetStringA( &pszKeyBase, m_headerParser.GetFieldValue( "Sec-WebSocket-Key" ) );
        int iLength = (int)strlen( pszKeyBase );
        char * pszKey = NULL;

        //キーの取得とGUIDの連結
        CTool::SetStringA( &pszKey, pszKeyBase );
        CTool::AddStringA( &pszKey, "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" );

        CTool::ReleaseStringA( &pszKeyBase );

        //SHA1ハッシュの取得
        uint8_t hash[SHA1HashSize];
        SHA1Context context;
        SHA1Reset( &context );
        SHA1Input( &context, (const uint8_t *)pszKey , (unsigned int)strlen( pszKey ) );
        SHA1Result( &context, hash );

        //SHA1ハッシュをBase64に変換
        size_t sizeLength;
        CSPBase64 base64;
        base64.EncodeData( hash, SHA1HashSize );
        base64.GetChangedData( sizeLength );
        const char * pcszAcceptKey = (const char*)base64.GetChangedData( sizeLength );

        //標準ヘッダーデータのセット
        char * pszHeaderData = NULL;
        CTool::SetStringA( &pszHeaderData , "HTTP/1.1 101 Switching Protocols\r\n"
                                            "Upgrade: websocket\r\n"
                                            "Connection: Upgrade\r\n"
                                            "Sec-WebSocket-Accept: " );
        CTool::AddStringA( &pszHeaderData , pcszAcceptKey );
        CTool::AddStringA( &pszHeaderData , "\r\n" );

        //追加ヘッダーの取得
        if( AdditionalHandshake( &pszHeaderData, pcszAcceptKey ) )
        {
            CTool::AddStringA( &pszHeaderData , "\r\n" );
        }
        else
        {
            bRet = false;
        }

        //レスポンスヘッダーの送信処理
        CSPSendElement * pElement = new CSPSendElement();
        pElement->SetData( pszHeaderData, strlen( pszHeaderData ) );

        m_sendManager.AddElement( pElement );

        //バッファの送信処理
        if( !WriteSendBuffer() )
        {
            bRet = false;
        }
        else
        {
            //ハンドシェイク実行時の処理
            bRet = HandshakeDone();
        }

    }
    //ハンドシェイク不可能な場合
    else
    {
        bRet = false;
    }

    return bRet;
}

//
//    ベースフレームデータの処理
//
bool CSPWebSocketServerBase::ProcessBaseFrameData()
{
    bool bRet = true;
    bool bFrameReadEnd = false;

    do
    {
        bFrameReadEnd = false;

        //レシーバーに受信データをセットする
        CSPBaseFramingDataReciever * pReciever = new CSPBaseFramingDataReciever();
        if( !pReciever->SetRecievedData( &m_pszReadBuffer, m_sizeReadData ) )
        {
            delete pReciever;
            bRet = false;
            break;
        }

        //1フレーム分のデータが到達しているかどうか
        bFrameReadEnd = pReciever->IsFrameDataRecieved();

        //1フレーム分のデータが到達している場合
        if( bFrameReadEnd )
        {
            //受信したフレームデータは処理後に削除する(デフォルト)
            bool bSaveData = false ;

            //フレームデータ受信処理に失敗した場合
            if( !FrameDataRecieved( pReciever, bSaveData ) )
            {
                delete pReciever;
                bRet = false;
                break;
            }

            //フレームデータの保存が指定されている場合
            if( bSaveData )
            {
                m_recieveData.AddElement( pReciever );
            }
            //フレームデータの削除が指定されている場合
            else
            {
                delete pReciever;
            }
        }

    //1フレーム分のデータが到達している場合にはcontinueする
    }while( bFrameReadEnd );

    return bRet;
}

//
//    データを送信する
//
//        m_sendManagerに登録されている全データを送信する
//
bool CSPWebSocketServerBase::WriteSendBuffer(bool bAllSendForced)
{
    bool bRet = true;

    CSPSendElement * pSend;
    while( (pSend = m_sendManager.GetAndDeleteFirst() ) != NULL )
    {
        size_t sizeLength;
        const char * pcszData = pSend->GetData( sizeLength );
        int iSeparateCount = 0;

        size_t sizeTop = (size_t)0;

        //データを完全に送信できるまでループする
        while( sizeTop < sizeLength )
        {
            //データの送信リクエスト(複数のスレッドで同時にソケットを使用しないようにする)
            WaitForSingleObject( m_hSocketMutex , INFINITE );

            int iRet = send( m_pParameter->GetSockClient(), pcszData + sizeTop , (int)(sizeLength - sizeTop) , 0 );

            ReleaseMutex( m_hSocketMutex );

            if( iRet == SOCKET_ERROR )
            {
                bRet = false;
                break;
            }

            sizeTop += (size_t)iRet;
            iSeparateCount++;
        }

        //分割送信が行われた場合
        if( iSeparateCount > 1 && !bAllSendForced)
        {
            //次の送信タイミングで次データ送信をトライする
            break;
        }

    }

    return bRet;
}

//
//    m_bHandshakeフラグのゲッタ
//
bool CSPWebSocketServerBase::GetHandshakeFlag()
{
    return m_bHandshake;
}

//
//    通信切断処理
//
void CSPWebSocketServerBase::Disconnect()
{
    m_bStopThread = true;
}

//
//    コネクションクローズメッセージの送信処理
//
bool CSPWebSocketServerBase::SendClosing()
{
    CSPBaseFramingDataSender * pSender = new CSPBaseFramingDataSender();
    pSender->SetOpcode( OPCODE_DISCONNECT );
    pSender->SetData( NULL , (size_t)0 );
    AddAndWriteSender( pSender , true );

    return true;
}

bool CSPWebSocketServerBase::AddAndWriteSender( CSPSendElement * pSender , bool bAllSendForced)
{
    m_sendManager.AddElement( pSender );
    return WriteSendBuffer( bAllSendForced );
}

///////////////////////////////////////////////////////////////////////////////
//        virtual functions
///////////////////////////////////////////////////////////////////////////////

//
//    ハンドシェイクの可否
//
//    戻り値:
//        ==true    ハンドシェイク可能
//        ==false    ハンドシェイク不可能
//
//        m_headerParserに登録されたリクエスト・メッセージを見て、
//        通信可能かどうかを判定する
//
bool CSPWebSocketServerBase::EnableHandshake()
{
    return true;
}

//
//    ハンドシェイク時の応答に追加する文字列の設定
//
//    引数:
//        ppszHeaderData    (IN/OUT)    応答ヘッダーが格納された文字列のポインタへのポインタ
//                                    この文字列にヘッダーを追加するかもしくは
//                                    文字列自体を書き換えることが出来る
//        pcszAcceptKey    (IN)        クライアントから送信されたキーに対するアクセプトキー
//                                    応答ヘッダーを書き換える際には計算済みのこのキーを使うことも出来る
//
//    戻り値:
//        ==true    ハンドシェイク続行
//        ==false    ハンドシェイク中止
//
bool CSPWebSocketServerBase::AdditionalHandshake( char ** ppszHeaderData, const char * pcsAcceptKey )
{
    return true;
}

//
//    ハンドシェイク完了時の処理
//
bool CSPWebSocketServerBase::HandshakeDone()
{
    return true;
}

//
//    ポーリング時の処理
//
//        接続後CSetting::GetLoopWaitで設定した間隔ごとに呼び出される。
//
bool CSPWebSocketServerBase::Polling()
{
    return true;
}

//
//    コネクションクローズ時の処理
//
bool CSPWebSocketServerBase::Closing()
{
    return true;
}

//
//    フレームデータを受信した際の処理
//
bool CSPWebSocketServerBase::FrameDataRecieved(CSPBaseFramingDataReciever * pRecieveData, bool & bSaveData)
{
    return true;
}

派生クラスの実装予定

本サーバーベースクラスには以下の仮想関数が存在する。派生クラスでは、これらの派生クラスを実装して、希望のサーバープログラムを書き出すことになる。

bool EnableHandshake();
ハンドシェイクの際に通信を継続するか、拒否するかを判断するための関数となる。
この関数の呼び出し時にはクライアントから送信されたHTTPヘッダーがHttpHeaderParserのインスタンスである内部変数m_headerParser(protectedで宣言済み)にセットされているため、特定のフィールドデータ(例えばSec-WebSocket-Protocolなど)の内容を確認して通信を継続するか切断するかを判断し、true/falseで結果を返す。

bool AdditionalHandshake( char ** ppszHeaderData, const char * pcsAcceptKey );
ハンドシェイク時にサーバーベースクラスから返されるレスポンスヘッダーの内容はHTTPバージョン、Upgrade、Connection、Sec-WebSocket-Acceptフィールドのみの最低限のもののみである。故に追加のハンドシェイク用HTTPレスポンスヘッダーが必要な際には、この関数で定義する。

bool HandshakeDone(); ハンドシェイクのためのHTTPレスポンスヘッダーがサーバーからクライアントへ返された直後に呼び出される関数。各サーバーで初期化処理が必要な場合などはこの関数内に書くことが出来る。

bool Polling();
データの受信時、または受信データが無い場合、は一定間隔で呼び出される関数。特定タイミングでのメッセージの送信が必要な場合などはこの部分に記述できる。

bool Closing();
通信の終了時に呼び出される関数。この関数で終了処理などを記述できる。通信の終了をキャンセルすることなどは行えない。

bool FrameDataRecieved(CSPBaseFramingDataReciever * pRecieveData, bool & bSaveData);
フレームデータが受信された時に呼び出される関数。サーバープログラムのデータ受信時の処理をこの部分に記述する。


通常デフォルトで動作させて欲しいvirtual関数

以下の関数はデフォルトで動作が定義されているが、必要性があれば派生クラス側で書き換えることも出来る。

bool ProcessHandshakeData();
ハンドシェイクデータ到達後のハンドシェイク処理を行うための関数。

bool DoHandshake();
ハンドシェイク実行用関数(ハッシュの作成、ハンドシェイクデータの送信など。

bool ProcessBaseFrameData();
ベースフレーミングデータの処理用関数。受信データを確認して、1フレームのデータがきていれば処理を行う。