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

소켓 옵션과 필수 이론

Tony Lim 2023. 11. 19. 14:03

소켓 입/출력 버퍼

  • bps = bit per sec
  • pps = packet per sec
  • mtu = 1500bytes 정도하는데 패킷을 꽉채우지 않고 pps 만 늘면 별로 좋지 않다. bps가 높아야 좋다고 볼 수 있다.
  • Nagle algorithm = pps를 줄이고 한번에 패킷을 꽉채워서 보내자 , tcp 수준에서 기본적으로 적용되는 알고리즘
  • 결국은 buffered i/o에서 하고자 하는것은 i/o 신호가 올때마다 매번 반응하는것이 아니라 한꺼번에 한번씩 보내자
  • tcp implementation 이 엄청많다. (딱히 언제까지 기다릴것인가에 대한 규약이 정해져있지않다.)
	//소켓의 '송신' 버퍼의 크기를 확인하고 출력한다.
	int nBufSize = 0, nLen = sizeof(nBufSize);
	if (::getsockopt(hSocket, SOL_SOCKET,
		SO_SNDBUF, (char*)&nBufSize, &nLen) != SOCKET_ERROR)
		printf("Send buffer size: %d\n", nBufSize);

	//소켓의 '수신' 버퍼의 크기를 확인하고 출력한다.
	nBufSize = 0;
	nLen = sizeof(nBufSize);
	if (::getsockopt(hSocket, SOL_SOCKET,
		SO_RCVBUF, (char*)&nBufSize, &nLen) != SOCKET_ERROR)
		printf("Receive buffer size: %d\n", nBufSize);

 


send()와 recv()는 1:1로 매핑되는가?

1대1 맵핑은 아니다. (send 1번 호출하면 recv 1번 호출되는 관계가 아니다)

send를 1byte씩 3번 한것을 recv (128 bytes) 로 한번에 읽어 들일 수 있다.

		//사용자가 입력한 문자열을 서버에 전송한다.
		//::send(hSocket, szBuffer, strlen(szBuffer) + 1, 0);
		int nLength = strlen(szBuffer);
		for (int i = 0; i < nLength; ++i) {
			::send(hSocket, szBuffer + i, 1, 0);
		}

client에서 hello를 1글자씩 여러번 보내면 send를 5번 보내게 될것이다.

output stream buffer에서 반응을 어떻게 하냐에 따라  1글자만 전송되거나 여러글자가 전송될 수 있다.

받는 server 쪽도 마찬가지이다. client가 준만 큼 받는게 아니것이다. 128byte만큼 recv 하려할텐데 client가 보낸게 그보다 많을 수도 적을 수도 있다.


소켓 입/출력 버퍼와 TCP_NODELAY 옵션

경우에 따라서 진짜로 1글짜씩 바로바로 buffering 없이 보내야 할 필요가 있을 때가 있다.

게임에서 그러한 경우가 존재한다. 바로바로 반응을 해야하는것이 게임하기 편하기 때문이다.

	int nOpt = 1;
	::setsockopt(hSocket, IPPROTO_TCP, TCP_NODELAY,
		(char*)&nOpt, sizeof(nOpt));

TCP_NODELAY를 통해 버퍼링 하지않게 설정할 수 있다.

하지만 server에서는 버퍼링을 하면 client가 아무리 1개씩보내도 1개씩 읽지 않을 수 있다.


서버 소켓과 SO_REUSEADDR 옵션

 

server는 항상 passive하게 동작해야한다. client가 close를 먼저하는것도 마찬가지이다.

server가 close를 하게되면 TIME WAIT 과정을 겪게 되고 closed 될때까지 시간이 많이 걸리게 된다. (몇초 ~ 몇분)

server가 죽었는데 TIME WAIT인 녀석이 많아서 재시작을 했을 경우 socket이 안열리는 경우가 발생한다.

 

	//※ 바인딩 전에 IP주소와 포트를 재사용하도록 소켓 옵션을 변경한다.
	BOOL bOption = TRUE;
	if (::setsockopt(hSocket, SOL_SOCKET,
		SO_REUSEADDR, (char*)&bOption, sizeof(BOOL)) == SOCKET_ERROR)
	{
		puts("ERROR: 소켓 옵션을 변경할 수 없습니다.");
		return 0;
	}

SO_REUSEADDR 옵션을 통해 서버를 2개를 기동한후에 client의 연결을 시도하면 1개만 연결이 잘되고 

이후 server 1개를 중지하고 client가 재접속을 시도하면 나머지 1개에게 바로 연결이 된다.

//	svraddr.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
	svraddr.sin_addr.S_un.S_addr = inet_addr("192.168.77.2");

위 옵션을 쓸시 bind할 때 정확한 ip주소를 적어줘야한다. 

안그러면 A라는 프로세스가 any로 바인딩 했으면 B라는 악성 프로세스가 reuse option을 통해 A가 연 ip port로 바인딩을 해놓으면 A가 언젠가 종료가 되는 순간 악성 코드 B가 client들을 받게 된다.


멀티스레드 에코 서버

	//4.1 클라이언트 연결을 받아들이고 새로운 소켓 생성(개방)
	while ((hClient = ::accept(hSocket,
		(SOCKADDR*)&clientaddr,
		&nAddrLen)) != INVALID_SOCKET)
	{
		//4.2 새 클라이언트와 통신하기 위한 스레드 생성
		//클라이언트 마다 스래드가 하나씩 생성된다.
		hThread = ::CreateThread(
			NULL,	//보안속성 상속
			0,		//스택 메모리는 기본크기(1MB)
			ThreadFunction,		//스래드로 실행할 함수이름
			(LPVOID)hClient,	//새로 생성된 클라이언트 소켓
			0,				//생성 플래그는 기본값 사용
			&dwThreadID);	//생성된 스레드ID가 저장될 변수주소

		::CloseHandle(hThread);
	}

ThreadFunction에게 accept를 통해 연결된 socket (hClient)를 넘겨줘서 새로운 스레드에서 recv, 하도록 변경했다.