Network/Windows 소켓 프로그래밍 입문에서 고성능 서버까지!

TCP 채팅 서버 - 성능 개선

Tony Lim 2023. 11. 22. 20:44

I/O 멀티플렉싱 채팅 서버로 개선

  • 여러 입/출력 요청이 한 채널에 동시에 혼재 할 수 있는 입/출력 구조
  • 개발자 관점에서 보면 송신과 수신이 동시에 발생하는 것
  • 모든 입/출력은 프로세스가 아니라 OS가 주도 한다는것이 핵심

 

https://tonylim.tistory.com/465

비동기 I/O는 callback , Event 같은 방식으로 디스크 쓰기 가 완료되었다는 것을 알려준다.

멀티플렉싱에서는 OS가 채널에 정보가 들어오는것을 알고 있기때문에 모종의 방식으로 user에게 어떤 채널에 정보가
준비되었는지 알려주게 된다.

 

중첩된 파일 입/출력에서 한 파일의 여러 다른 부위에 비동기적으로 write요청을 보낼시

순차적으로 될지 알 수가 없다.

 

I/O 멀티플렉싱 서버 - 변화 감시

	//5. 소켓의 변화를 감시를 위한 반복문
	UINT nCount;
	FD_SET fdRead;
	std::list<SOCKET>::iterator it;

	puts("I/O 멀티플렉싱 채팅 서버를 시작합니다.");
	do
	{
		//5-1. 클라이언트 접속 및 정보수신 변화 감시셋 초기화
		FD_ZERO(&fdRead);
		for (it = g_listClient.begin(); it != g_listClient.end(); ++it)
			FD_SET(*it, &fdRead);

		//5-2. 변화가 발생할 때까지 대기
		::select(0, &fdRead, NULL, NULL, NULL);

		//5-3. 변화가 감지된 소켓 셋 확인
		nCount = fdRead.fd_count;
		for (int nIndex = 0; nIndex < nCount; ++nIndex)
		{
			//소켓에 변화 감시 플래그가 세트 되었는가?
			if (!FD_ISSET(fdRead.fd_array[nIndex], &fdRead))
				continue;

			//5-3-1. 서버의 listen 소켓이 세트되었는가?
			//즉, 누군가 연결을 시도했는가?
			if (fdRead.fd_array[nIndex] == g_hSocket)
			{
				//새 클라이언트의 접속을 받는다.
				SOCKADDR_IN clientaddr = { 0 };
				int nAddrLen = sizeof(clientaddr);
				SOCKET hClient = ::accept(g_hSocket,
									(SOCKADDR*)&clientaddr, &nAddrLen);
				if (hClient != INVALID_SOCKET)
				{
					FD_SET(hClient, &fdRead);
					g_listClient.push_back(hClient);
				}
			}

			//5-3-2 클라이언트가 전송한 데이터가 있는 경우.
			else
			{
				char szBuffer[1024] = { 0 };
				int nReceive = ::recv(fdRead.fd_array[nIndex],
								(char*)szBuffer, sizeof(szBuffer), 0);
				if (nReceive <= 0)
				{
					//연결 종료.
					::closesocket(fdRead.fd_array[nIndex]);
					FD_CLR(fdRead.fd_array[nIndex], &fdRead);
					g_listClient.remove(fdRead.fd_array[nIndex]);
					puts("클라이언트가 연결을 끊었습니다.");
				}
				else
				{
					//채팅 메시지 전송
					SendMessageAll(szBuffer, nReceive);
				}
			}
		} //for()
	} while (g_hSocket != NULL);

0으로 fd 배열을 초기화한다음에 fdRead에 fd들을 걸어서 select에서 변화가 생길때까지 대기하게 된다. 
최대64개의 fd(channel)까지 감시가 가능하다.

변화했다는 것을 기록해주는 주체는 OS이다.

java 에서 selector를 사용하는 방법과 유사하다.

싱글 스레드로 context swtiching 없이 운영할 수 있는 장점이 있다.


Win32 Event select

	while (TRUE)
	{
		/////////////////////////////////////////////////////////////////
		//소켓 이벤트를 기다린다.
		dwIndex = ::WSAWaitForMultipleEvents(
						g_nListIndex + 1,	//감시할 이벤트 개수
						g_aListEvent,		//이벤트 배열
						FALSE,	//전체에 대해 대기하지는 않는다.
						100,	//100ms 동안 대기한다.
						FALSE);	//호출자 스레드 상태를 변경하지 않는다.
		if (dwIndex == WSA_WAIT_FAILED)
			continue;

		//이벤트가 발생한 소켓의 인덱스 및 이벤트 발생 이유를 확인한다.
		if (::WSAEnumNetworkEvents(g_aListSocket[dwIndex],
				g_aListEvent[dwIndex], &netEvent) == SOCKET_ERROR)
			continue;

기존의 select 는 64개의 fd중 1개라도 준비가되면 반응하는 방식이었다.

event select 방식은 64개를 다 loop를 도는게 아니라 여러 event들을 한번에  감시할 수 있다. 

WaitForMultipleEvents에서 반환하는것이 조금더 빨리진다. 반환받은 event가 어떤것에 대한 event인지는 if문으로 기존과 동일하게 분기처리를 하여 확인하게 된다.