상세 컨텐츠

본문 제목

대용량 소켓 서버 프로그래밍 - Connection Object

프로젝트/네트워크

by ryujt 2012. 10. 26. 04:56

본문

대용량 소켓 서버 프로그래밍 두 번째 주제로 "Connection Object"에 대한 이야기를 해볼 까 합니다.  


새로 맡은 프로젝트를 진행 중에 기존 소켓 라이브러리에서 잦은 문제가 발생하자, 저는 IOCP를 이용해서 새로운 소켓을 작성하여 사용 중 입니다.  그리고, 그 동안 가장 맘에 안들었던 컨넥션 관리 알고리즘을 변경하였습니다.  테스트 결과는 상당히 만족스럽습니다.  



컨넥션 객체의 재사용

객체나 메모리를 할당 받고 삭제하는 것을 반복하는 것보다는 재사용하는 쪽이 성능에 당연히 유리합니다.  그리고, 이것이 빈번 할 경우에는 상당한 차이가 벌어질 수도 있습니다.  따라서, 생성했던 객체를 삭제하지 않고 객체 풀에 보관하다가, 필요 할 때 다시 재사용하면서 다소 성능에 이득을 볼 수가 있습니다. 


또한, 이미 언급했었던 패킷 풀과 같은 원리로, 컨넥션이 끊어지고 나서 해당 컨넥션 객체에 접근하더라도 Access Violation 에러를 내지 않고 유연하게 대체 할 수 있게 됩니다.  이러한 점을 이용해서 아래 "특정 컨넥션 객체 찾기"에서는, ID를 통해서 임계 영역없이 그리고 반복문도 없이 바로 컨넥션 객체에 찾아내고 접근하는 방법을 설명하고 있습니다.   


재사용이 너무 빠르면 이전 참조와 충돌하는 문제로 인해서 풀에서 바로 꺼내지 않고 한 참 후에 꺼내도록 하고 있습니다.  패킷 풀에서와 마찬가지로 풀에서 잠자는 시간보다 컨넥션 객체 참조의 라이프 사이클이 더 길면 문제가 됩니다.



컨넥션 객체의 컨테이너

제가 사용했던 대부분의 소켓 라이브러리는 ArrayList 형태를 취하고 있습니다.  델파이에서는 TList가 대부분입니다. 이것이 저는 항상 의아했습니다.  그리고, 대충 [소스 1]과 같이 컨넥션 객체를 접근하고 있습니다.  


문제는 새로운 접속이 들어오고, 또는 접속이 해제 됐을 때, 해당 객체를 제거하는 데에 CPU를 많이 사용하게 된다는 점 입니다.   따라서, TList를 제거하고 Linked-List로 데이터 구조를 변경하여 테스트하였더니, 상당히 만족스러운 결과를 얻을 수가 있었습니다.  


다만, 접속자 수가 많지 않거나 Connected와 Disconnected가 빈번하지 않은 경우에는 큰 차이가 없었습니다.  (당연히, 그리고 오히려 그러한 경우에는 TList가 유리합니다)  100, 500, 1000, 2000의 접속자 수에 대해서 각각 테스트 하였습니다. 


[소스 1]

procedure TfmMain.btSendToAllClick(Sender: TObject);
var
  List : TList;
  Loop: Integer;
begin
  List := IdTCPServer.Contexts.LockList;
  try
    for Loop := 0 to List.Count-1 do
      TIdContext(List[Loop]).Connection.IOHandler.WriteLn('');
  finally
    IdTCPServer.Contexts.UnlockList;
  end;
end;

[그림 1]은 TList와 Linked-List의 성능 비교 입니다.  데이터가 앞에서 부터 삭제되는 경우에는 아주 큰 차이를 보이고 있으며, 뒤에서부터 차례로 삭제되는 것은 TList가 두 배 정도 앞서고 있습니다.  이것은 실제 사용에서 좀 더 큰 차이가 나타날 수가 있습니다.  (메모리가 한 곳에 몰려 있지 않고 분산 될 경우 특히)


[그림 1]



특정 컨넥션 객체 찾기

귓속말을 보내는 과정을 생각해보도록 하겠습니다.  필자는 [소스 2]와 같이 컨넥션 객체마다 일련번호를 부여하고, 클라이언트에서 해당 일련 번호를 이용해서 서버의 객체를 반복문 없이 바로 사용하는 방법을 사용합니다.  





[소스 2]

var
  ID, Count : integer;
  ExchangeResult : pointer;
  FConnections : array [0..$FFFF] of TConnection;
begin
  // Get serial number and skip when it is zero or has taken.
  Count := 0;
  while true do begin
    Count := Count + 1;
    if Count > $FFFF then 
      raise Exception.Create('TConnectionList.Add: Connection limited.');

    ID := InterlockedIncrement(FConnectionID);

    // "ID = 0" means that Connection is not assigned.
    if ID = 0 then Continue;

    // This will helps you to find TConnection with ID without CriticalSection.
    ExchangeResult :=
      InterlockedCompareExchangePointer(
        Pointer(FConnections[Word(ID)]), AConnection, nil
      );
    if ExchangeResult = nil then begin
      AConnection.FID := ID;
      Break;
    end;
  end;


[소스 2]는 실제 코드가 아닌 설명을 위한 코드입니다.  


일련 번호는 FConnectionID에 저장합니다.  새로운 접속이 있을 때마다 하나씩 증가시키며, 혹시 해당 번호가 이미 사용 중이면 다음 번호를 찾도록 합니다.  


4: 그리고, FConnections은 각 컨넥션 객체를 관리하는 컨테이너입니다.  


19-22: 일련 번호가 선택되면 해당 번호에서 하위 2바이트만을 실제 인댁스로 사용하게 됩니다.  FConnectionID는 Integer로  선언되어있지만, Word 만큼만 사용합니다.  FConnections에 선택된 일련 번호에 해당하는 위치에 현재 컨넥션 객체의 주소를 저장합니다.  겹치는 영역들은 무시하고 다시 일련 번호를 찾기 때문에 문제 없습니다.


24: 실제 일련 번호를 저장합니다.


[소스 3]

function TConnectionList.GetConnection(ID: integer): TConnection;
begin
  Result := FConnections[Word(ID)];
  if (Result <> nil) and (Result.FID <> ID) then Result := nil;
end;


[소스 3]은 ID를 이용해서 컨넥션 객체를 찾아내는 코드 입니다.  클라이언트에서 ID를 알고 있는 경우, 반복문을 사용하지 않고 한 번에 객체에 접근 할 수 있도록 하였습니다.  또한, 해당 접속이 끊어지지 않는 이상 ID는 불변이기 때문에 특정 사용자에게만 메시지를 전달하고자 했을 경우 유용하게 사용 할 수가 있습니다.


4: ID가 가리키는 곳이라고 해도 nil이거나 FID와 완전히 일치하지 않으면 해당 ID는 할당 받은 적이 없거나 삭제된 컨넥션 객체입니다.  따라서, 결과를 nil로 리턴합니다.



관련글 더보기