teamtalkのconnフレームワークの概要とnetlibスレッドのセキュリティ問題

8385 ワード

最近チームトークのconn_mapはスマートポインタに変更されましたが、変更するには多方面にわたって問題があるかどうかを試してみなければなりません.コンパイルに合格することはできません.正常に起動できれば万事順調です.だからshell clientクライアントを書いて機能のテストを行いました.
ttの公式前回リリースにはtestディレクトリがあり、簡易なテストクライアントが書かれています.しかし、このtestは全然使えません.コードが間違っているのではなく、機能が欠けているので、私は自分でやり直すしかありません.
やっているうちにclientが発起したconnectがたまに接続できないことが発見され、このたまにの確率は非常に低いが、発生した以上、それは問題があるに違いない.そこで調べてみましょう...
testのテストクライアントは、コマンドラインshellのクライアントに相当します.つまり、グラフィックインタフェースがなく、あなたの機能は端末にコマンドを入力することで完了します.現在、登録とログインしか実現していません.将来的にはチャットなど様々な機能も行う予定で、コマンドラインを実現したクライアントに相当する差は少ない.TTにはwindows、mac、ios、androidの全プラットフォームクライアントがあり、コマンドラインのクライアントになって何の役に立つのかと聞かれるかもしれません.もちろん役に立ちます.テスト機能は便利ですね.インタフェースを振り回すことを考えずにいろいろな機能を測定することができます.将来の追加機能もテストを書くのに便利です.例えば、私は今登録機能を追加して、このコマンドラインにreg xx ooを入力すると、xxというユーザー名がパスワードでooとしてデータベースに登録されます.コマンドの解析はインタフェースのイベント応答関数よりずっと便利です.
はい、今問題が来て、shellコマンドの入力は1つのデッドサイクルを繰り返してユーザーの入力を待つ必要がありますが、ttの非同期ネットワークフレームワークはもう1つのデッドサイクルを必要とします.もし2つのデッドサイクルが同じスレッドに入れられたら明らかにだめなので、ユーザーの入力を受けたデッドサイクルを別のスレッドに入れました.では、ユーザーがreg xx ooを入力すると、このコマンドをユーザー名とパスワードを解析し、登録プロセスを開始します.これはすべて別のスレッドで行われます.
登録の流れはどうなっていますか?ここではまずTTのconnフレームワークについてお話しします.
TTの下位非同期ネットワークライブラリはsocketとepollをnetlibライブラリにカプセル化し、非同期ネットワークに関する操作はnetlibを呼び出すことによって実現されます.しかし、netlibは元のtcpメッセージに対して受信した非同期ライブラリにすぎません.インスタント通信を行うには、その上で通信プロトコルを実装し、これらのプロトコルの操作を完了するためにインタフェースのセットをカプセル化する必要があります.
そこでTTは
CImConnのクラスで、このクラスはimconnに定義されています.hの中.
皆さんが読みやすいように、ここにコードの一部を抜きます.
class CImConn : public CRefObject
{
public:
	CImConn();
	virtual ~CImConn();
	int Send(void* data, int len);
	virtual void OnRead();
	virtual void OnWrite();
	
	bool IsBusy() { return m_busy; }
	int SendPdu(CImPdu* pPdu) { return Send(pPdu->GetBuffer(), pPdu->GetLength()); }

	virtual void OnConnect(net_handle_t handle) { m_handle = handle; }
	virtual void OnConfirm(){}
	virtual void OnClose(){}
	virtual void OnTimer(uint64_t){}
    virtual void OnWriteCompelete(){}
	virtual void HandlePdu(CImPdu*){}

インタフェースの意味は明らかで、OnConnectは接続アクセスイベントの応答関数であり、OnConfirmという意味は少し曖昧で、実はあなたがnetlib_を開始したのです.connect、このconnect接続の確立が完了したときに呼び出される関数.
ここでは理解を容易にするために、クラス比を作って、android開発をしたことがある場合は、アプリケーションを書くたびに最もよく使われるプロセスを考えてみましょう.クラス継承Activityを定義し、overrideのonCreateなどのxxメソッドは似ていますか?もちろん、アンドロイドの開発経験がなければ、iosを比べてみましょう.iosもそうです.iosもやったことがなければ、大丈夫です.下を見続けてください.
ここでCImConnは実はあなたに継承されたもので、あなたが継承した後、中の対応するメンバー関数を実現してください.
サービス側であれば、ユーザーのアクセスに応答するためにOnConnectを実現する必要があります.クライアントであれば、サーバに接続した後の操作を定義するためにOnConfirmが必要です.他のいくつかのインタフェースサービス側とクライアントは共通です.
だから、ここを見終わったらmsg_serverディレクトリの下には、なぜDBServ,FileServ,LoginServ,RouteServ,PushServおよびMsgConnがあるのか.
前のいくつかはメッセージサーバが他のいくつかのサーバに自発的に開始したクライアント接続であり、最後の1つはメッセージサーバ自身のサービス側Connであり、ユーザーのアクセスを待つためにOnConnect関数を実現する必要がある.
そしてlogin_serverの中のHttpConnとLoginConnの意味も明らかで、一つはhttp要求に応答するために使用され、もう一つはメッセージサーバlogin情報登録要求に応答するために使用される.他のいくつかのサーバのconnもこのようにしています.
前にTTに対してとても乱れていると感じた友达は突然自分が悟ったと感じましたか?感謝してください.
もう一つの疑問は、TTのimconnフレームワークがこのCImConnとnetlibをどのように接続しているのかということです.
ここではDBSrConnを例に説明します.コードを見てください.
void CDBServConn::Connect(const char* server_ip, uint16_t server_port, uint32_t serv_idx)
{
	log("Connecting to DB Storage Server %s:%d ", server_ip, server_port);

	m_serv_idx = serv_idx;
	m_handle = netlib_connect(server_ip, server_port, imconn_callback, (void*)&g_db_server_conn_map);

	if (m_handle != NETLIB_INVALID_HANDLE) {
		g_db_server_conn_map.insert(make_pair(m_handle, this));
	}
}

CDBServConnは、データベース・エージェントのようにメッセージ・サーバが接続を開始するときに継承する必要があるCImConnクラスで、接続を開始するときに呼び出されるConnect関数です.中にはnetlibに異動しています接続し、imconn_に転送callbackとg_db_server_conn_map.
この2つのパラメータはimconnとnetlibを接続する鍵です.g_db_server_conn_mapはCDBServConnに定義されたstaticグローバルmapマッピングテーブルで、何を保存しますか?次の一言
g_db_server_conn_map.insert(make_pair(m_handle, this))
は、このマッピングテーブルが、接続されるたびにsocketハンドル(m_handle)とimconnオブジェクト(this)のマッピング関係を保存していることが明らかである.
TT最下位のイベント配布器がイベントを生成すると、imconn_が呼び出されます.callback、中にはFindImConnが対応するConnを反転して呼び出し、ConnオブジェクトのOnConfirmなどの関数を呼び出します.これらの関数はあなたが前にCImConnを継承して自分で実現したものです.運転時の多態有木有?TTのフレームワークはなかなかいいと思いますか.connオブジェクトのOnReadは、ビジネスコードがここで自分で実現されるため、最も重要な関数です.
void imconn_callback(void* callback_data, uint8_t msg, uint32_t handle, void* pParam)
{
	NOTUSED_ARG(handle);
	NOTUSED_ARG(pParam);

	if (!callback_data)
		return;

	ConnMap_t* conn_map = (ConnMap_t*)callback_data;
	CImConn* pConn = FindImConn(conn_map, handle); // socket imconn
	if (!pConn)
		return;

	//log("msg=%d, handle=%d ", msg, handle);

	switch (msg)
	{
	case NETLIB_MSG_CONFIRM:
		pConn->OnConfirm();  //connect pConn OnConfirm() 
		break;
	case NETLIB_MSG_READ:
		pConn->OnRead(); // 
		break;
	case NETLIB_MSG_WRITE:
		pConn->OnWrite();
		break;
	case NETLIB_MSG_CLOSE:
		pConn->OnClose();
		break;
	default:
		log("!!!imconn_callback error msg: %d ", msg);
		break;
	}

	pConn->ReleaseRef();
}

OnReadコードを見てみるとHandlePduが入っています
void CImConn::OnRead()
{
	for (;;)
	{
		uint32_t free_buf_len = m_in_buf.GetAllocSize() - m_in_buf.GetWriteOffset();
		if (free_buf_len < READ_BUF_SIZE)
			m_in_buf.Extend(READ_BUF_SIZE);

		int ret = netlib_recv(m_handle, m_in_buf.GetBuffer() + m_in_buf.GetWriteOffset(), READ_BUF_SIZE);
		if (ret <= 0)
			break;

		m_recv_bytes += ret;
		m_in_buf.IncWriteOffset(ret);

		m_last_recv_tick = get_tick_count();
	}

    CImPdu* pPdu = NULL;
	try
    {
		while ( ( pPdu = CImPdu::ReadPdu(m_in_buf.GetBuffer(), m_in_buf.GetWriteOffset()) ) )
		{
            uint32_t pdu_len = pPdu->GetLength();
            
			HandlePdu(pPdu);  // 

			m_in_buf.Read(NULL, pdu_len);
			delete pPdu;
            pPdu = NULL;
//			++g_recv_pkt_cnt;
		}
	} catch (CPduException& ex) {
		log("!!!catch exception, sid=%u, cid=%u, err_code=%u, err_msg=%s, close the connection ",
				ex.GetServiceId(), ex.GetCommandId(), ex.GetErrorCode(), ex.GetErrorMsg());
        if (pPdu) {
            delete pPdu;
            pPdu = NULL;
        }
        OnClose();
	}
}

CDBServConnのHandlePduを1段抜きます
void CDBServConn::HandlePdu(CImPdu* pPdu)
{
	switch (pPdu->GetCommandId()) {
        case CID_OTHER_HEARTBEAT:
            break;
        case CID_OTHER_VALIDATE_RSP:
            _HandleValidateResponse(pPdu );
            break;
        case CID_LOGIN_RES_DEVICETOKEN:
            _HandleSetDeviceTokenResponse(pPdu);
            break;
        case CID_MSG_UNREAD_CNT_RESPONSE:
            _HandleUnreadMsgCountResponse( pPdu );
            break;
        case CID_MSG_LIST_RESPONSE:
            _HandleGetMsgListResponse(pPdu);
            break;
        case CID_MSG_GET_BY_MSG_ID_RES:
            _HandleGetMsgByIdResponse(pPdu);
            break;
        case CID_MSG_DATA:
            _HandleMsgData(pPdu);
            break;
        case CID_MSG_GET_LATEST_MSG_ID_RSP:
            _HandleGetLatestMsgIDRsp(pPdu);
            break;

中のhandlerは異なるプロトコルに対応するプロセッサです.だから、これではよくわかりません.ほとんどの場合、CImConnを継承してhandlerを書くことです.
TTのconnフレームワークの紹介はここまでですが、実際には自分でコードをほじくって、ゆっくりしなければならない細部がたくさんあります.
最初に言った別のスレッドで登録プロセスを開始すると、プロセス全体がどのように行われているかがよくわかりますが、実はCImConnを継承し、そこで接続を開始し、接続処理を受け入れます.ここで私のコードを取り出します
net_handle_t CClientConn::Connect(const char* ip, uint16_t port, uint32_t idx)
{
	m_handle = netlib_connect(ip, port, imconn_callback_sp, (void*)&s_client_conn_map);
	log("connect handle %d", m_handle);
	if (m_handle != NETLIB_INVALID_HANDLE) {
	    log("in invalid %d", m_handle);
        s_client_conn_map.insert(make_pair(m_handle, shared_from_this()));// !!!
	}
    return  m_handle;
}

ここで私自身のコードは、前に与えられたTTソースコードとは少し違います.imconn_callback_spは私がスマートポインタに変更したバージョンで、conn_を挿入します.mapテーブルは元のthisポインタではなくshared_ptr.
この操作はサブスレッドで行われるのでnetlib_接続するとimconn_callback_spは最下位レベルのイベント配布器に追加されて傍受され、イベント配布器はメインスレッドで実行されるループであり、このループはsocketファイルハンドルで読み書きイベントが発生した後、あなたが追加した関数をコールバックします.だからnetlib_connect会でimconn_callbackは、プライマリ・スレッドのリスナーを追加します.プライマリ・スレッドは、イベントが発生するとすぐにこの関数を呼び出します.この関数の
CImConn* pConn = FindImConn(conn_map, handle);

conn_mapはnetlib_connect後にinsertするので、FindImConnが現れる可能性があります.conn_mapにはinsertという関係が間に合わず、たまにconnectが発生した後も後続のOnConfirm関数を呼び出し続けず、サービス側に走ってみると、connectは確かに成功する奇妙な現象が発生します.マルチスレッドって大変ですね...
では、この問題をどう解決すればいいのでしょうか.これは本文の言うことではありませんて、各位は兴味があって自分で解决の方法を考えてください、ここで友情のヒント、ロックをかけるのは役に立ちません.