Network bridge 방식으로 증권사 API 활용하기-2

반응형

Network bridge 방식으로 증권사 API 활용하기-2-Memory Pool Implemantation

이전 글에서 언급한 내용은 아래와 같습니다.

  1. 증권사 API 가 32비트로 제공되는데 따른 프로그램 개발 제약사항
  2. Network bridge 방식으로 트레이딩프로그램과 API커넥터 두 개의 프로그램 개발 필요성
  3. 두 프로그램 간 데이터 통신용 패킷관리를 위한 메모리풀 구조 스케치

이번 글에서는 이전 글에서 언급한 메모리풀을 실제 코드로 구현하고 동작까지 확인하겠습니다.

 

In the previous article, we mentioned the following:

  1. Program development constraints due to the 32-bit provision of the stock API
  2. The need to develop two programs, a trading program and an API connector, in a network bridge manner
  3. Sketch of the memory pool structure for packet management for data communication between the two programs

In this article, we will implement the memory pool mentioned in the previous article with actual code and verify its operation.

 

1. 데이터 타입 정의 및 Member Function(Init, Alloc, Free) 작성

앞서 DMSS는 Data Management Services System의 약자라고 하였습니다. 그래서 관련 클래스파일 이름을 CDmssItem으로 하고 정의하는 데이터타입 명은 prefix DSM을 붙였습니다.

  • CDmssItem.h

CDmssItem

#define DMSS_DSM_ITEM_POOL_SIZE	(512)
#define DMSS_DSM_ITEM_PAYLOAD_SIZE	(1450)

typedef struct _dsm_item {
	_dsm_item* pNext;	다음 객체를 가리킬 포인터
	WORD wItemNo;			Array에서의 자기 자신 IndexNo, 진단, 디버깅 용도
	UINT16 uSizeAllocated;	lpPayload가 가리키고 있는 Chunk의 할당 사이즈
	UINT16 uUsedLenth;		lpPayload에 기록된 유효 데이터 Length
	PCHAR lpPayload;		Memory Chunk를 가리키는 포인터
	UINT8 uRefCnt;			이 객체를 참조하고 있는 레퍼런스 카운트(아직은 미사용)
	UINT8 uProtocol;		이 객체가 담고있는 데이터(패킷) 타입
	UINT8 uPriority;		이 객체의 전송큐 우선순위(아직은 미사용)

	_dsm_item() {
		ZeroMemory(this, sizeof(_dsm_item));
	}
} TST_DSM_ITEM, * PTST_DSM_ITEM;

typedef struct _dsm_item_pool {
	SRWLOCK srwlock;		Reader-Writer 락
	WORD wFreeItemsCnt;		메모리풀 가용량
	PTST_DSM_ITEM pFreeItems;	메모리 객체 head 포인터
	TST_DSM_ITEM arPool[DMSS_DSM_ITEM_POOL_SIZE];	메모리 객체 배열 512개
	CHAR arPayloadPool[DMSS_DSM_ITEM_POOL_SIZE][DMSS_DSM_ITEM_PAYLOAD_SIZE]; 메모리청크 512개
} TST_DSM_ITEM_POOL, *PTST_DSM_ITEM_POOL;
  • CDmssItem.cpp

생성자 호출되면 메모리풀 초기화 실행
메모리풀 초기화

void CDmssItem::_vInit_TST_DSM_ITEM_POOL(PTST_DSM_ITEM_POOL pPool)
{
	PTST_DSM_ITEM p;

	if (nullptr == pPool)
		return;

	InitializeSRWLock(&(pPool->srwlock)); 락 초기화
    CAutoScopeSRWLOCKExclusive lock(&(pPool->srwlock)); 쓰기락 걸고 초기화 시작
    p = nullptr; 단방향 linked list이므로 head가 아닌 tail부터 초기화 
    for (int i = DMSS_DSM_ITEM_POOL_SIZE - 1; i >= 0; i--) {
    pPool->arPool[i].pNext = p;		첫 진입시 tail의 pNext는 nullptr로 초기화 됨
    p = &(pPool->arPool[i]); 이후 N-1 노드가 N 노드를 가리키며 0노드까지 순환함				
		p->wItemNo = i;		메모리풀에서의 ARRAY INDEX 저장(디버깅, 진단 목적)		
        p->uSizeAllocated = DMSS_DSM_ITEM_PAYLOAD_SIZE;	할당 사이즈 1450 저장		
        p->uUsedLenth = 0;													
        p->lpPayload = pPool->arPayloadPool[i];								
        p->uRefCnt = 0;														
		p->uProtocol = 0; /* 0 - UNKNOWN */									
        p->uPriority = 0;													
	}
    pPool->pFreeItems = p;
    pPool->wFreeItemsCnt = DMSS_DSM_ITEM_POOL_SIZE;

	return;
   }

pAlloc_PTST_DSM_ITEM_CHAIN

alloc 함수의 인자는 할당할 메모리 사이즈를 의미하는 게 아닌 사전 할당된 규격의 chunk가 몇 개 연결된 Chain의 Head 포인터를 반환받을지를 의미한다.

PTST_DSM_ITEM CDmssItem::pAlloc_PTST_DSM_ITEM_CHAIN(const WORD wQuantity)
{
	PTST_DSM_ITEM pHead = nullptr;
	PTST_DSM_ITEM pTail = nullptr;
	PTST_DSM_ITEM_POOL pPool = &_m_poolDSMITEM;
	WORD wAssigned;

	CAutoScopeSRWLOCKExclusive lock(&(pPool->srwlock));

	if ((wQuantity <= 0) || (pPool->wFreeItemsCnt < wQuantity)) {
		DPRINTF("[ERROR] pAlloc_PTST_DSM_ITEM_CHAIN wQuantity(%d), pPool->wFreeItemsCnt(%d)\n", 
        wQuantity, pPool->wFreeItemsCnt);
		return nullptr;
	}

	pHead = pPool->pFreeItems;	후입선출로 반환이므로 얘는 Chain의 Head가 됨
	pTail = pPool->pFreeItems;	얘는 wQuantity만큼 다음 객체로 넘어가서
    마지막에 도달하면 pNext를 nullptr 로 마무리함

	wAssigned = 0;
	while (pTail) {
		DPRINTF("DBG] Allocated PTST_DSM_ITEM::wItemNo(%d)\n", pTail->wItemNo);
		wAssigned++; 루프 진입전 풀에서 헤드 할당 했으므로 1개 할당
		if (wQuantity == wAssigned) { 요청받은 수만큼 할당 됐나 확인
			break;
		}

		pTail = pTail->pNext; 다음 메모리풀 객체로 이동
	};

	if (wAssigned < wQuantity) {	/* Should never reach here */ 
		DPRINTF("[ERROR] pAlloc_PTST_DSM_ITEM_CHAIN wQuantity(%d), wAssigned(%d), 
        pPool->wFreeItemsCnt(%d)\n", wQuantity, wAssigned, pPool->wFreeItemsCnt);
		return nullptr;
	}

	pPool->pFreeItems = pTail->pNext; 할당 체인의 다음 객체가 메모리풀의 헤드가 됨
	pPool->wFreeItemsCnt -= wAssigned;할당한 만큼 빈 객체 수 차감
	pTail->pNext = nullptr; Tail의 Next를 nullptr로 봉인함

	DPRINTF("DBG] Head ItemNo(%d) PTST_DSM_ITEM::wFreeItemsCnt(%d)\n", 
    pPool->pFreeItems->wItemNo, pPool->wFreeItemsCnt);

	return pHead;

}

vFree_PTST_DSM_ITEM_CHAIN

Free는 할당때 처럼 Linked List 순환 시 메모리풀에 락을 걸지 않고 먼저 파라미터로 입력받은 노드 체인의 Tail을 찾아낸 다음에 메모리풀에 락을 걸고 순식간에 반환하고 끝냅니다. 노드 1개씩 메모리풀에 반환하지 않고 사용했던 노드 체인의 맨 끝을 찾아내서 그걸 메모리풀 맨 앞에 붙이고 끝낸다는.

void CDmssItem::vFree_PTST_DSM_ITEM_CHAIN(PTST_DSM_ITEM p)
{
	PTST_DSM_ITEM_POOL pPool = &_m_poolDSMITEM;
	PTST_DSM_ITEM pTail;
	WORD wFreeCnt = 0;

	if (nullptr == p)
		return;

	pTail = p;
	pTail->uUsedLenth = 0;
	wFreeCnt++;
	DPRINTF("DBG] Being Free PTST_DSM_ITEM::wItemNo(%d)\n", pTail->wItemNo);
	while (pTail->pNext) {
		pTail = pTail->pNext; 실제 메모리풀 건드리지 않고 일단 체인의 끝을 찾음
		pTail->uUsedLenth = 0;
		wFreeCnt++;
		DPRINTF("DBG] Being Free PTST_DSM_ITEM::wItemNo(%d)\n", pTail->wItemNo);
	};

	AcquireSRWLockExclusive(&(pPool->srwlock)); 실제 메모리풀 건드릴것이므로 락검

	pTail->pNext = pPool->pFreeItems; 반환 체인의 다음을 FreeItem 연결함
	pPool->pFreeItems = p; 메모리풀의 head를 반환체인의 head로 바꿈
	pPool->wFreeItemsCnt += wFreeCnt; 체인에 포함된 객체 수 만큼 프리카운트 증가시킴

	ReleaseSRWLockExclusive(&(pPool->srwlock)); 락 해제

	DPRINTF("DBG] Head ItemNo(%d) PTST_DSM_ITEM::wFreeItemsCnt(%d)\n", pPool->pFreeItems->wItemNo, pPool->wFreeItemsCnt);

	return;
}

2. 메모리 풀 사용한 트레이딩프로그램 Login 테스트

API 커넥터가 서버(트레이딩 프로그램)로 접속시 가장 먼저 전송해야 할 Initial Packet은 로그인정보로 결정했습니다.

API 커넥터가 트레이딩 프로그램으로 접속할 수 있는 조건은 증권사에 접속한 상태에서 증권사 서버에 조회한 사용자 닉네임과 계정명(그러므로 API 커넥터 실행자가 임의 조작 불가)과 트레이딩프로그램에서 별도 생성해 준 트레이딩프로그램의 계정과 비밀번호입니다. 트레이딩프로그램의 계정은 이메일주소 형식을 따를 예정이고 계정생성 시 이메일 인증할 예정입니다. 앞서 정의한 메모리풀과 별개로 로그인프로토콜을 정의해야 합니다. 로그인프로토콜 패킷이 메모리풀에 담겨 전송큐에 들어가면 전송스레드가 소켓을 통해 전송할 것입니다.

아래 프로토콜 내용은 말그대로 프로토콜이므로 서버와 클라이언트 양측이 똑같은 내용을 가지고 있어야 합니다.

프로토콜 패킷 정의 부분은 메모리 최적화를 못하도록 컴파일러지시자 #pragma를 사용하여 메모리패킹값 1로 지정합니다.(#pragma pack(1))

RIL Protocol, RIL_AUTH_REQUEST_PARAMS_TYPE

RIL은 Radio Interface Layer의 약자입니다.

API커넥터가 서버 연결에 성공 후 가장 먼저 전송하는 패킷은 _auth_request_params_type이고

서버 측에서 프로토콜 파악을 할 수 있도록 패킷 가장 앞에 TEN_RIL_RQ_CAT을 두고 그 값은

ESTABLISH_CLIENT_CONNECTION으로 정했습니다. 서버 입장에서는 지금 접속한 API커넥터가 어느 증권사를 지원하는지 알아야 하므로 TEN_STOCK_API_CAT에 값 XING을 넘기기로 하였습니다. 그리고 증권사 계정별로 API에서 거래 가능한 마켓이 다르므로 거래가능 마켓은 MASK에 표기하여 서버측에 전달하기로 하였습니다. 그리고 네 개의 TCHAR 문자열은 계정명과 패스워드, 증권사 계정명과 닉네임입니다.

 

테스트코드 작성에 앞서 테스트 시나리오를 적어봅니다.

  1. 클라이언트가 지정된 서버 IP, 포트로 TCP 연결 체결, Connect함수 성공값 반환받으면 
  2. Initial Packet(로그인 패킷, _auth_request_params_type) 작성하여 전송
  3. 서버 측에서 클라이언트가 전송한 파라미터 그대로 알아들었는지 수신 패킷 디버그 메시지 출력

테스트코드를 작성해 봅니다.

API 커넥터 로그인 창

API커넥터에 로그인창을 만들었습니다. 로그인 버튼을 누르면 위 테스트 코드가 실행됩니다.

OnBnClickedNcBtnLogin()

	errRet = theApp.m_RilClient.RilInit(_T("127.0.0.1"), 65500);
	if (NOYECUBE_ESUCCESS == errRet) {
		cstrTemp.Append(_T("Established. Account authentication in progress"));

		PTST_DSM_ITEM pItem = theApp.m_RilClient.m_DMSS.pAlloc_PTST_DSM_ITEM_CHAIN(1);
		if (pItem != nullptr) {
			PTST_RIL_AUTH_REQUEST_PARAMS_TYPE p = reinterpret_cast<PTST_RIL_AUTH_REQUEST_PARAMS_TYPE>(pItem->lpPayload + pItem->uUsedLenth);

			p->enRilRequest = TEN_RIL_RQ_CAT::ESTABLISH_CLIENT_CONNECTION;
			p->enApiCat = TEN_STOCK_API_CAT::XING;
			p->u8TradeSupportMask |= API_TRADE_SUPPORT_STOCK;

			_tcsncpy_s(p->tchNoyeNetId, _countof(TST_RIL_AUTH_REQUEST_PARAMS_TYPE::tchNoyeNetId), _T("TEST_NAME"), 10);
			_tcsncpy_s(p->tchNoyeNetPasswd, _countof(TST_RIL_AUTH_REQUEST_PARAMS_TYPE::tchNoyeNetPasswd), _T("TEST_PASS"), 10);
			_tcsncpy_s(p->tchStockId, _countof(TST_RIL_AUTH_REQUEST_PARAMS_TYPE::tchStockId), _T("TEST_IDID"), 10);
			_tcsncpy_s(p->tchUserName, _countof(TST_RIL_AUTH_REQUEST_PARAMS_TYPE::tchUserName), _T("TEST_NICK"), 10);

			pItem->uUsedLenth += sizeof(TST_RIL_AUTH_REQUEST_PARAMS_TYPE);
			pItem->uProtocol = static_cast<UINT8>(TEN_PROTO_CAT::TST_RIL_AUTH_REQUEST_PARAMS_TYPE);

			theApp.m_RilClient.m_qDataTx.push(pItem);
			SetEvent(theApp.m_RilClient.m_hEvent_Wakeup_Tx);
		}

	}

서버 연결에 성공하면 메모리풀에서 메모리객체 한 개를 할당받습니다.

PTST_DSM_ITEM pItem = theApp.m_RilClient.m_DMSS.pAlloc_PTST_DSM_ITEM_CHAIN(1);

Chunk에 로그인패킷을 기록할 거니까 CHAR* 타입의 Chunk를 아래와 같이 타입 캐스팅 합니다.

캐스팅할 때 포인터 시작점이 lpPayload가 아니라 lpPayload+uUsedLength 인 점 유의 바랍니다.

메모리풀의 객체는 패킷을 1개만 담는 용도가 아니라 가용가능한 1450 바이트의 사이즈까지 최대한 채우기 위한

용도로 정의되었으므로 새로운 데이터를 기록할 위치는 Chunk의 시작점이 아니라 Chunk의 시작점 + 유효데이터길이만큼 지난 그다음지점부터입니다.

PTST_RIL_AUTH_REQUEST_PARAMS_TYPE p = reinterpret_cast<PTST_RIL_AUTH_REQUEST_PARAMS_TYPE>(pItem->lpPayload + pItem->uUsedLenth);

로그인 패킷 셋업 부분입니다. RIL Request는 ESTABLISH_CLIENT_CONNECTINON으로 지정했고

API 카테고리는 xingAPI, 지원하는 마켓은 주식 현물을 의미하는 API_TRADE_SUPPORT_STOCK으로 지정했습니다.

나머지 네 줄은 계정정보인데 테스트값을 입력하였습니다.

			p->enRilRequest = TEN_RIL_RQ_CAT::ESTABLISH_CLIENT_CONNECTION;
			p->enApiCat = TEN_STOCK_API_CAT::XING;
			p->u8TradeSupportMask |= API_TRADE_SUPPORT_STOCK;

			_tcsncpy_s(p->tchNoyeNetId, _countof(TST_RIL_AUTH_REQUEST_PARAMS_TYPE::tchNoyeNetId), _T("TEST_NAME"), 10);
			_tcsncpy_s(p->tchNoyeNetPasswd, _countof(TST_RIL_AUTH_REQUEST_PARAMS_TYPE::tchNoyeNetPasswd), _T("TEST_PASS"), 10);
			_tcsncpy_s(p->tchStockId, _countof(TST_RIL_AUTH_REQUEST_PARAMS_TYPE::tchStockId), _T("TEST_IDID"), 10);
			_tcsncpy_s(p->tchUserName, _countof(TST_RIL_AUTH_REQUEST_PARAMS_TYPE::tchUserName), _T("TEST_NICK"), 10);

프로토콜 패킷(RIL Packet)이 아닌 메모리풀 관리 정보를 마지막에 메모리 객체에 입력해 줍니다. 현재 메모리 객체에 어떤 정보가 담겨있는지에 대한 정보입니다. 사용한만큼 uUsedLength 값을 증가시켜주고 프로토콜에 어떤 프로토콜인지 입력해줍니다.

			pItem->uUsedLenth += sizeof(TST_RIL_AUTH_REQUEST_PARAMS_TYPE);
			pItem->uProtocol = static_cast<UINT8>(TEN_PROTO_CAT::TST_RIL_AUTH_REQUEST_PARAMS_TYPE);

패킷 작성이 완료되었으면 아래와 같이 패킷 Chain의 head를 전송큐에 push 합니다.

			theApp.m_RilClient.m_qDataTx.push(pItem);
			SetEvent(theApp.m_RilClient.m_hEvent_Wakeup_Tx);

전송큐에 데이터 삽입 후 전송스레드에 wakeup 이벤트를 발생시켜 깨우면 전송스레드가 깨어나서 패킷을 서버로 전송합니다. 아래와 같이 패킷 Chain head 포인터 p가 유효하면 uUsedLength 만큼씩 send를 호출하고 데이터 전송이 완료되면 루프를 탈출 후 vFree_PTST_DSM_ITEM_CHAIN을 호출하여 메모리 객체 노드를 다시 반환합니다.

Tx Thread 패킷 send 호출 부분

진짜 테스트, 메모리풀 제대로 동작하는지 보려고 데이터 Chunk 요청수를 1이 아닌 3으로 해보겠습니다. 그럼 Chunk 두 개는 데이터가 없이 0이므로 빈 패킷이 전송될 것입니다.

서버 대기 화면

서버 측 대기 화면

클라이언트 접속 요청 화면

디버깅 메시지를 확인하면 vAllock_PTST_DSM_ITEM_CHAIN(3)이 호출되어 실제로 메모리 객체 0번, 1번, 2번 총 3개를 할당했고 1번 객체에 로그인패킷이 기록되어 서버로 전송되었습니다. 그 뒤에 다시 vFree_PTST_DSM_ITEM_CHAIN이 호출되어 0, 1, 2번 객체가 제대로 반환되어 wFreeItemCnt값이 전체 메모리 풀 객체 수 512를 표시하는 것을 볼 수 있습니다.

네트워크 로그

다시 서버 화면을 보면 클라이언트에서 전송한 로그인패킷을 제대로 전송받은 것을 확인할 수 있습니다.

클라이언트 접속 로그

서버 측 테스트 코드입니다.

서버 테스트 코드

				DPRINTF("[DBG][%d] *** Client sent data.(%s)\n", GetCurrentThreadId(), psi->_buff);
				{
					PTST_RIL_AUTH_REQUEST_PARAMS_TYPE p = reinterpret_cast<PTST_RIL_AUTH_REQUEST_PARAMS_TYPE>(psi->_buff);
					if (p->enRilRequest == TEN_RIL_RQ_CAT::ESTABLISH_CLIENT_CONNECTION) {
						DPRINTF("TEN_RIL_RQ_CAT: (%d)\nTEN_STOCK_API_CAT: (%d)\nTradeSupportMask : (%d)\ntchNoyeNetId : (%s)\ntchNoyeNetPasswd: (%s)\ntchStockId: (%s)\ntchUserName: (%s)\n",
							static_cast<UINT>(p->enRilRequest), static_cast<UINT>(p->enApiCat), static_cast<UINT>(p->u8TradeSupportMask),
							p->tchNoyeNetId, p->tchNoyeNetPasswd, p->tchStockId, p->tchUserName);
					}

				}

 

이상으로 메모리 풀을 구현해 보았습니다.

반응형