서버/IOCP

IOCP 관련 advance

MAKGA 2021. 9. 9. 22:17
320x100

AcceptEx

예전 reactor 방식에서 사용하던 accept 함수 대신 AcceptEx 함수를 사용하자.

BOOL AcceptEx(SOCKET sListenSocket, SOCKET sAcceptSocket, PVOID lpOutputBuffer, DWORD dwReceiveDataLength, DWORD dwLocalAddressLength, DWORD dwRemoteAddressLength, LPDWORD lpdwBytesReceived, LPOVERLAPPED lpOverlapped);

SOCKET sListenSocket: 서버에서 사용하는 listensocket

SOCKET sAcceptSocket: 클라이언트의 연결을 수용할 소켓. 미리 풀을 만들어 둔다.

PVOID lpOutputBuffer: 새로운 연결로부터 전송된 데이터를 바로 송신받고, 그에 덧붙여 서버의 로컬 어드레스와 클라이언트의 리모트 어드레스를 저장받기 위한 버퍼

DWORD dwReceiveDataLength: 실제로 전송받을 데이터를 저장할 크기다. 주소 부분을 제외하고 전송된 데이터에 대해 얼마나 버퍼에 저장할지 정한다. 0으로 설정할 경우 쓰기 작업을 하지 않고 연결을 최대한 빨리 완료 한다.

DWORD dwLocalAddressLength: 로컬 어드레스 정보를 저장하기 위해 예약될 크기다. 반드시 16바이트 이상이여야 한다.

DWORD dwRemoteAddressLength: 리모트 어드레스 정보를 저장하기 위해 예약될 크기다. 반드시 16바이트 이상이여야 한다.

LPDWORD lpdwBytesReceived: 받은 데이터의 양이다. 동기적인 IO일 때만 의미 있다

LPOVERLAPPED lpOverlapped: 요청을 수행하기 위한 OVERLAPPED 구조체의 포인터.

 

최신 Windows SDK에서는 AcceptEx를 지원해주지만 이전 버전은 그렇지 않으므로 WSAloctl 함수를 통해 AcceptEx 함수의 포인터를 구해야 한다.

LPFN_ACCEPTEX lpfnAcceptEx = nullptr;
GUID GuidAcceptEx = WSAID_ACCEPTEX;

DWORD dwBytes;
WSAIoctl(mListenSocket, SIO_GET_EXTENSION_FUNCTION_POINTER, &GuidAcceptEx, sizeof(GuidAcceptEx), lpfnAcceptEx, sizeof(lpfnAcceptEx), &dwBytes, nullptr, nullptr);



SOCKET acceptSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

memset(&overlap, 0, sizeof(overlap));

lpfnAcceptEx(listenSocket, acceptSocket, lpOutputBuf, outBufLen - ((sizeof(sockaddr_in)+16 * 2), sizeof(sockaddr_in) + 16, sizeof(sockaddr_in) + 16, &dwBytes, &overlap);
hCompPort = CreateIoCompletionPort((HANDLE)acceptSocket, hCompPort, 0, 0);

 


ConnectEx

소켓과의 연결을 proactor 방식으로 수행하기 위한 함수다.

BOOL ConnectEx(SOCKET s, const sockaddr* name, int namelen, PVOID lpSendBuffer, DWORD dwSendDataLength, LPDWORD lpdwBytesSent, LPOVERLAPPED lpOverlapped);

SOCKET s

const sockaddr* name

int namelen

PVOID lpSendBuffer

DWORD dwSendDataLength

LPDWORD lpdwBytesSent

LPOVERLAPPED lpOverlapped

 

 

출처: https://www.slideshare.net/namhyeonuk90/iocp-advanced

 

Iocp advanced

IOCP Advanced NHN NEXT 남현욱

www.slideshare.net


Disconnectex


Page Locking

- Zero byte recv

- SO_RCVBUF, SO_SNDBUF 크기를 0으로


IO Recv, Send 카운트 핸들링 해야하는 이유

- 유저 종료시 io count가 남아있다면 진행 중인 io가 남아있으므로, 소켓만 종료하고 session 은 유지시킨 채 남은 작업 처리.

- 어플리케이션에서 한 유저가 Send를 여러번 호출했을 때, 연속으로 WSASend를 호출하지 않기 위해서

(WSASend 호출시 send count를 1 증가시키고, 다음 WSASend 호출되면 아직 io가 진행중이므로 send queue에 넣고 종료, 이전 io 완료시 queue에 있는 데이터를 send)

- WSASend의 결과로 반환되는 바이트 수가 내가 요청한 바이트 수와 다를 수 있다. (non paged pool 고갈 또는 limit 초과시) 버퍼에 담고 데이터를 유지해주자


IOCP WSAsend() 관련

https://gpgstudy.com/forum/viewtopic.php?t=19086%EF%BB%BF

더보기

저는 통신하는 세션의 부류에 따라 좀 달리 해야한다고 봅니다.
일단 저도 같은 상황을 겪어 봤습니다. 완료시 더 보냇다고는 당연히 잘못된거고요 ^^;;;

해당 완료와 관련된 송신요청 이전에 연이어 송신요청을 했다면 이들에 대한 완료도 에러로 통보가 연이어 올겁니다.
이런 경우의 발생 확률은 송신할 내용의 크기와 빈도수에 따라 비례한다는것은 당연히 아실겁니다.
클라이언트라고 과감하게 끊는다는것은 좀 무리가 있겠지만, 서버끼리의 통신에서 저런 상황이 발생했다면 과연 해당 서버의 세션을 끊는다면? 문제가 많겠죠.
만드신 루틴/모듈이 클라이언트/서버 세션에 대해 공통 작동한다면 더 큰 문제가 될겁니다.

저의 경우엔 IOCP관리 모듈/클래스/인스턴스를 세션용도[클라이언트, 서버집단1, 서버집단2]별로 분리해서 유지관리하고, 송신매커니즘을 다르게 사용하게 생성합니다.

서버끼리의 통신에서는 송신할때 어플리케이션계층의 송신버퍼에 쌓아두고 송신요청을 합니다. 하나의 송신완료가 떨어지면 다음 송신버퍼의 내용에서 송신요청을 하고, 송신완료에 에러가 발생시 관련 송신버퍼에서 다시 내용을 가져와 재송신 요청을 합니다.
물론 저도 클라이언트는 바로 끊어 버립니다...


IOCP에서 Send 할 때

https://www.gpgstudy.com/forum/viewtopic.php?p=43020

더보기

질문
IOCP에서 Recv 할 때는 당연히 버퍼(큐)를 만들어서 저장 후 패킷 처리를 하고 있습니다. 
그런데, Send 할 때는 어플리케이션 레벨의 별도의 버퍼를 궂이 만들 필요가 있는지 궁금합니다. 
다시 말해서, 
Q1. 소켓 옵션으로 설정해 준 크기 만큼의 버퍼만이 IOCP에서 관리되는 것인지? 
Q2. 그래서, 아주 많은 Send 오퍼레이션이 요청될 경우, 버퍼 오버플로우로 Send가 실패할 수도 있는지? 
(WSASend()를 콜(비동기) 할 때, 패킷 데이터를 소켓 버퍼로 즉시 메모리 카피 해버리는지.. 
그래서 카피에 실패하면 전송 실패를 리턴하는지..) 

Q2 같은 경우, 전송 실패가 리턴되지 않는다면, Send를 위한 별도의 버퍼를 만드는 것은 
패킷 데이터의 메모리 카피를 쓸데없이 한 번 더 하게 만드는 꼴인것 같아서요..



+



가령 예를 들면, 실제 서비스 되는 상황에서 
1024바이트 크기의 패킷을 한 유저에게 연속해서 1천번을 전송한다고 하면, 
(TCP/IP를 자체 헤더 크기를 제외하고라도 데이터의 크기가 
1M 정도로 소켓 자체의 송신 버퍼보다는 큰 데이터가 되어버린다고 가정합니다.) 
다시 말해서 WSASend(1024바이트 크기의 패킷)를 연속으로 1천번 콜 했을 때(물론 그 이상도..) 
과연 에러가 리턴 되느냐/되지 않느냐를 질문한 겁니다. 



답변들


0) IOCP를 사용해서 Send를 할때는 위와 같은 경우 에러가 리턴되지 않습니다. 

물론 중간에 소켓이 끊어지거나 한다면 에러가 리턴됩니다. 

하지만 IOCP를 이용한 WSASend에서 문제가 되는 것은 WSASend에서 지정한 크기 보다 작은 크기를 보낼 수 있다는 것입니다. 
이때는 에러 상황으로 오는 것이아니라 GetQueuedCompletionStatus에서는 정상 상태에 대한 리턴을 주는 대신 lpNumberOfBytes 가 원래 지정한 값보다 작은 값으로 옮니다. 
그렇기 때문에 하나의 소켓에 대해서 위와 같이 쭉 밀어 넣어 버리게 되면 뭔가 중간에 원하는 데이터가 빠져서 전송될 수 있습니다.


즉 IOCP를 사용할 때 가장 주의해야 할 점이 하나의 소켓에 여러개의 WSARecv나 WSASend가 걸리는 것을 방지해야 한다는 것 입니다. 
자세한 내용은 다음 URL을 참조하세요 (http://www.codeproject.com/internet/reusablesocketserver4.asp)



1) 요 문제는 계속 논란이 되던 부분인데요. 제가 IOCP를 약 4년째 사용해봤는데요.. 수많은 부하테스트에서도 WSASend에서 지정한 크기 보다 작은 크기가 보내졌던 경우는 단 한번도 없었습니다. 어디까지나 제 경험상의 얘기입니다. 


그리고.. "IOCP에서 하나의 소켓에 여러개의 WSARecv나 WSASend가 걸리는 것"에 대한 이슈도.. 저도 첨에는 하나의 세션은 한번의 WSASend를 요청하는식으로 구현했었는데요. 한 세션에 대해서는 너무나도 효율이 떨어졌습니다. 그래서 완료통지 무시하고.. 무조건 WSASend를 요청하는 식으로 테스트 해봤는데 잘되더군요. 너무 많은 WSASend를 요청하면 Non-paaged pool이 모두 소모되서 세션에 심각한 문제가 생기긴 했습니다. 
이것도 제 경험의 얘기입니다.


요약. 
송신 시 못 보내는 경우는 거의 없으나, 지정한 값보다 작은 값을 보낼 수도 있으므로 버퍼를 계속 간직해야 함.

 

출처: https://www.gpgstudy.com/forum/viewtopic.php?p=43020


iocp 서버 개발에 유의할점
https://blog.naver.com/zard9023/220609396561

더보기

chapter#1

현재 개발중인 게임 서버의 소켓이 4년전에 제작한 비동기 이벤트 셀렉트 방식인 관계로 퍼포먼스를 향상시키고자 IOCP 네트워크 구조를 최근 제작하게 되었다. 실제 게임서버에 적용 가능할지는 좀 더 고려해 보아야 하겠지만, IOCP 서버를 만들면서 겪었던 점들을 공유하고자 한다.

시중의 책들과 공개된 소스들을 참고해서 IOCP를 구현해 보면, 항상 과부하 테스트시에 문제가 발생했다. 내가 설정한 과부하 테스트는 다음과 같은 상황이다:

1) 클라이언트 측에서 과도할 정도로 Connect를 시도한다.
2) 서버는 Accept 직후 랜덤하게 연결을 끊어버린다.
3) 클라이언트는 Connect된 직후 서버로 데이터를 전송한다.
4) 서버는 클라이언트로부터 데이터가 수신되면 바로 응답메시지를 전송한다. 이때 응답메시지를 전송할 내부 버퍼(소켓 버퍼 아님)가 모자라는 경우 연결을 끊는다. 이 처리와는 별도로 데이터 수신시 랜덤하게 연결을 끊는다.
5) 클라이언트는 서버로부터 데이터를 수신하면 바로 응답메시지를 전송한다. 이때 응답메시지를 전송할 내부 버퍼(소켓 버퍼 아님)가 모자라는 경우 연결을 끊는다. 이 처리와는 별도로 데이터 수신시 랜덤하게 연결을 끊는다.
6) 클라이언트는 연결이 끊어진 커넥션이 발생하면 그에 대응하는 Connect를 시도한다.

 클라이언트는 초기에 몇천개 이상의 Connect를 시도하고 연결된 커넥션들에 대해 각각 위의 규칙대로 처리를 반복하는 상황을 만들어 테스트 해 보았는데, 여러번의 삽질끝에 발견한 문제점들은 다음과 같다.

일 단, 시중에 떠도는 IOCP소스들의 대부분은 에코(Echo) 서버들이다. 이 소스들은 항상 데이터를 recv한 다음 send를 하므로 참조 카운트가 1 이상 올라가지 않지만, 게임 서버는 그렇지 않다. 걸어놓은 recv에 대한 응답이 안 온 상태에서 send를 할 수 있으므로 소켓 1개에 대해 2개의 참조 카운트가 발생할 수 있다. GetQueuedCompletionStatus가 FALSE를 리턴한 경우, 대부분의 에코서버 소스에서는 바로 소켓을 close한 다음 커넥션 객체를 삭제해 버리는데, 이것은 참조 카운트가 1이상 올라가지 않기 때문이다. 이런 경우 게임서버에서는 그 소켓에 대한 참조카운트가 2라면, 그냥 소켓을 close한 다음 나머지 작업에 대한 실패 통보가 와서 참조 카운트가 0이 되었을때 커넥션 객체를 삭제해야만 한다. 마찬가지로 WSARecv에 대한 리턴이 왔는데 전송 바이트가 0인 경우에도 무조건 객체를 지워서는 안된다.

IOCP에서 WSASend, WSARecv를 사용할 때 소켓에러 없이(IO_PENDING는 에러가 아니므로 제외) 포스팅된 소켓 연산의 결과는 반드시 각각 GetQueuedCompletionStatus에서 리턴된다. 단, 함수의 반환값은 TRUE일수도 FALSE일수도 있다.

한가지 이상한 현상을 발견했는데, close한 소켓을 WSASend나 WSARecv에 사용했을때 에러가 반환되지 않는 경우가 있다. 예 를 들어, 소켓 A에 대해서 WSARecv가 포스팅된 상태에서, 소켓 A를 close했다. 이 때 GetQueue... 에서 WSARecv에 대한 성공 결과가 리턴될 수 있다(close하기 전에 성공한 결과일 수 있으므로), 이때 다시 WSARecv를 걸려고 할 때 close된 소켓이므로 WSARecv가 에러를 발생해야 정상이므로 이 시점에 커넥션을 삭제하고자 했는데, WSARecv가 에러를 발생시키지 않는 거다. 원인은 잘 모르겠지만, 아마도 네트워크 과부하 상황일때 이런 현상이 발생하는게 아닌가 추측하고 있다. 결국 결국 소켓이 닫혔다는 별도의 플래그를 만들어 직접 해결할 수 밖에 없었다. 이런 상황에서 얻은 정보를 써 보자면:

서버에서 소켓을 close하는 것이 closesocket() 호출 이전에 포스팅한 WSASend, WSARecv를 꼭 실패시키지는 않는다. 성공할 수도 있고, 실패할 수도 있다.

서버에서 소켓을 close하면 이전에 포스팅된 연산에 대한 결과는 반드시 GetQueuedCompletionStatus에서 각각 모두 리턴된다.


이런 것들을 이해하고 나니, 과부하 테스트에서도 정확하게 동작하는 IOCP 네트워크 클래스를 제작할 수 있었다.

 

 

chapter #2

 

서버 프로그래밍을 하다 보면 정말 웃지 못할 사건들을 많이 접하게 된다. 클라이언트 접속이 1000개까지는 문제가 없다가 1001번째에 갑자기 크래시(crash)가 발생하기도 하고, 서버를 구동시킨 지 2~3일이 지난 시점부터 조금씩 리소스가 새기도 한다. 부지런한 개발자들은 MSDN의 버그 리포트와 테크니컬 기사를 샅샅이 뒤져보며 운영체제 또는 하드웨어의 버그이길 간절히 바란다. 
그러나 아쉽게도 이런 경우의 대부분이 본인의 실수에서 비롯되므로 유용한 단서를 찾기는 힘들다. 몇몇 뛰어난 프로그래머들은 몇 날 며칠을 밤새워 고생하다 갑자기 무언가 드디어 알아챘다는 듯 멀쩡히 죄 없는 PC를 포맷하기도 한다(필자도 과거에 일말의 희망을 걸고 수차례 포맷해 본 경험이 있다. 물론 다시 설치한다고 해결될 성질의 것이 결코 아니었다). 앞으로의 설명은 서버의 성능이나 최적화 문제보다는 안정적이고 유연한 네트워킹 환경을 구축하는 것에 초점을 맞춰 진행할 것이다.

소켓 API 리뷰
네트워크 개발자들이 겪는 문제들의 많은 부분이 소켓과 TCP의 특성을 제대로 이해하지 못한 것에서 시작한다. 다소 지루할 수도 있겠지만, 먼저 각 소켓 API의 중요한 점을 되짚어 보는 것으로 시작하겠다.
소 켓을 생성하는 함수는 두 가지가 있다. <리스트 1>처럼 WSA Socket 쪽이 좀더 다양한 프로토콜을 열거할 수 있다. 하지만 우리가 만들 서버는 인터넷 TCP 프로토콜만을 사용하기 때문에 어느 것을 사용해도 문제없다. 단 WSASocket으로 소켓을 생성하는 경우 dwFlags 파라미터에 WSA_FLAG_OVERLAPPED를 넘겨 오버랩드 속성을 가지도록 해야 한다. 그렇지 않으면 이후 다룰 WSASend, WSARecv 등의 오버랩드 호출은 무시될 것이다. 오버랩드 I/O에 관한 내용은 추후 자세히 설명하겠다.
 

int bind(
   SOCKET s,
   const struct sockaddr FAR *name,
   int namelen
);


빈 소켓만으로는 아무 것도 할 수 없다. 소켓과 로컬 주소를 연결시킨 뒤에야 비로소 네트워크 통신을 할 수 있는데, bind가 이러한 역할을 해 준다. 먼저 name 파라미터를 자세히 살펴보자(<리스트 2>).
sockaddr 은 bind할 주소를 지정하는데 쓰이는 16바이트 크기의 구조체다. 소켓에는 다양한 주소 패밀리(AF_UNIX, AF_INET, AF_IMPLINK, ...)와 각각의 하위 프로토콜이 존재한다. 각 주소 패밀리에 따라 주소 지정 방법이 다를 수 있는데, 우리는 인터넷 프로토콜(AF_INET)을 사용하므로 AF_INET의 주소 지정을 쉽게 하기 위해 우측의 sockaddr_in을 사용한다.
보통 sockaddr_in의 sin_addr 필드에 ADDR_ANY를 집어넣는데, 이것은 멀티홈드 호스트(예 : 여러 LAN 카드가 꽂혀 있는 호스트)의 특정 네트워크 주소를 선택하지 않겠다는 뜻이다. 그러나 성능이나 보안 측면을 강화시키기 위해 특정 네트워크의 주소를 입력할 수 있다.
윈속이 제공하는 Name Resolution 함수 중 하나인 gethostb yname를 사용해 로컬 호스트의 네트워크를 열거할 수 있다.
 

HOSTENT *he = gethostbyname( host_name );
he->h_addr_list[0]; // 첫 번째(예 : LAN CARD #1)
he->h_addr_list[1]; // 두 번째(예 : LAN CARD #2)
he->h_addr_list[2]; // 세 번째(예 : CABLE MODEM)


다 음으로 sockaddr_in의 포트를 살펴보자. 이 값이 0이면 시스템은 적당한 포트를 찾아 맵핑해 준다. 윈도우 2000에서의 기본 값은 1024~5000 사이의 값인데, 부족할 경우 TCP/IP 관련 레지스트리 키(MaxUserPort)의 최대 값을 변경할 수 있다.
 

MaxUserPort
Key: HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSe\tServices\Tcpip\Parameters
Value Type: REG_DWORD (maximum port number)
Valid Range: 500065534 (decimal)
Default: 0x1388 (5000 decimal)


이 외에도 MaxFreeTcbs와 MaxHashTableSize 등을 조절해 맵핑될 소켓 수를 조절할 수 있다. 관심있는 독자는 「Microsoft Win dows 2000 TCP/IP Implementation Details」를 참고하기 바란다.
그리 고 당연하겠지만 같은 주소로의 두 번째 bind는 실패한다. 간혹, 이전에 bind된 소켓을 분명히 닫았음에도 두 번째 bind가 실패하는 경우가 있는데, 이것은 이전 소켓이 실제로 완전히 닫히지 않고 TIME_WAIT 상태에 머물러 있기 때문이다. 서버를 재시작하는 경우에도 발생할 수 있는데, 이런 상황을 피하려면 setsockopt 함수를 사용해 SO_REUSEADDR 옵션을 셋팅하면 된다. TCP 상태에 대해 잘 모르고 있다면 TCP/IP 서적 등을 참고해 반드시 숙지하기 바란다.
 

int connect(
   SOCKET s,
   const struct sockaddr FAR *name,
   int namelen
);


상 대방 호스트에 접속하기 위해 connect를 호출한다. connect를 호출하기 위해 bind할 필요는 없다. 소켓을 생성한 뒤 바로 connect를 호출하면 자동으로 시스템이 지정하는 포트로 bind되는데, 이 역시 1024~5000 사이의 값을 가진다. 스트레스 테스트(stress test)를 하면 이 정도의 연결이 부족할 수 있다. 따라서 필요하다면 앞서 언급한대로 레지스트리 값을 적당한 값으로 설정해 주자.
 

int listen(
   SOCKET s,
   int backlog
);


listen 은 소켓에 TCP 접속을 받아들일 수 있는 속성을 부여해 준다. backlog 파라미터는 동시에 접속이 몰렸을 때를 처리하기 위한 큐의 크기인데, 보통 시스템의 최대 값을 지정해서 쓴다. 윈속 2.0 이전 버전에서 이 값의 최대 값은 5였는데, 이것은 접속 요청이 최대 5개까지 큐될 수 있다는 것을 뜻한다. 윈속 2가 등장하면서 SO MAXCONN이라는 상수 값을 사용하는데, 내부적으로 윈도우 2000 서버는 200개, 프로는 5개까지 설정된다. 접속 처리를 위해 accept를 호출하면, backlog 큐의 첫 번째 노드가 삭제되면서 다른 접속 요청을 큐에 넣을 수 있다. backlog 큐가 가득 차면 클라이언트의 connect 호출은 WSAECONNREFUSED 에러를 리턴한다.
 

SOCKET accept(
   SOCKET s,
   struct sockaddr FAR *addr,
   int FAR *addrlen
);


accept는 서버 소켓의 접속 큐에서 첫 번째 노드를 가져와 소켓을 생성한 뒤 리턴한다. 리턴된 소켓은 s 파라미터와 동일한 속성을 가진다는 것을 기억해 두자.
TCP 패킷을 주고받을 때 사용한다. 가장 빈번하게 호출되는 함수인 만큼 네트워크 개발자들이 주의해야 하는 부분이다. TCP는 신뢰할 수 있는(reliable) 스트림 기반의 프로토콜이다. 여기서 스트림 기반이라는 것에 주목할 필요가 있다. 수신자는 언제나 송신자가 전송한 만큼 받기 마련이지만, 이것이 곧 send, recv 함수의 호출 횟수까지 같다는 것을 뜻하지는 않는다. 
전송된 패킷은 인터넷의 수많은 게이트웨이를 경유하면서 상대방에게 도착하는데, send 호출과는 관계없이 패킷이 뭉쳐오기도 하고 완전히 조각난 상태로 도착하기도 한다. TCP는 보낸 순서대로 끝까지 도착하는 것을 보장하는 것이지, 전송 횟수까지 보장하는 것은 아니다. 그야말로 스트리밍 송수신이다. 따라서 반드시 송수신자 간에 패킷의 완료 여부를 알 수 있도록 사인을 해 두어야 한다. 보통 패킷의 앞이나 뒤에 이를 확인할 수 있도록 구조를 잡는다. 다음의 의사 코드(pseudo code)는 일반적으로 소켓 수신을 처리하는 방법을 보여준다.
 

// TCP 수신 처리 방법
ret = recv( s, buf, sizeof( buf ), 0 );
if ( ret <= 0 ) // ret 에러 처리

// 패킷이 잘려 올 수 있기 때문에 이전 패킷과 합친다.
queue.add( buf, ret );

// 패킷이 뭉쳐 올 수도 있으므로 완료 패킷이 없어질 때까지 반복한다.
while ( queue.has_completion_packet() )
{
   process_completion_packet( queue.get_completion_packet() );

   // 처리한 패킷은 큐에서 삭제한다.
   queue.remove_completion_packet();
}


또 다른 주의해야 할 점은 send, recv 함수의 리턴 값을 명확히 처리해 두는 것이다. 넌블러킹 소켓에서 send 호출은 우리가 생성한 버퍼(스택 또는 힙)를 커널 버퍼(소켓 버퍼)로 복사하고 커널 버퍼에 복사된 크기를 리턴한다. 이 때 커널 버퍼의 공간이 부족하여 요청한 크기와 리턴된 크기가 다를 수 있는데, 이런 경우 보통 네트워크 지연으로 판단해 접속을 끊거나 사용자가 만든 송신 큐에 임시로 보관해 두고 다음 송신이 가능해졌을 때 재전송하는 방법으로 해결한다. recv 함수는 보통 수신된 패킷 크기를 리턴하며, 0을 리턴하는 경우 정상적으로 접속이 종료되었다는 것으로 볼 수 있다. 단, 서버가 강제로 접속을 끊는 경우 recv는 SOCKET_ERROR를 리턴하면서 GetLastError() 함수로 WSAECONNRESET와 같은 에러 코드를 얻을 수 있다. 에러를 처리해 두면 send, recv 함수가 왜 실패했는지 명확해지기 때문에, 이후 네트워크 에러가 발생했을 때 어떻게 대처해야 할 것인가는 어렵지 않게 판단할 수 있다.
 

int shutdown(
   SOCKET s,
   int how
);

int closesocket(
   SOCKET s
);


서 버 프로그래밍을 할 때 주의해야 할 부분 중 하나가 소켓을 닫을 때의 처리이다. 안전하게 종료하기 위해서는 모든 데이터를 전송한 뒤 접속을 끊으려 할 때 shutdown 호출을 사용해 이 사실을 상대방에게 알려줘야 한다. 물론 상대방도 마찬가지다. 이러한 처리를 Graceful Closure라고 하며 <표 1>처럼 종료 처리를 한다(MSDN의 Graceful Shutdown, Linger Options, and Socket Closure 참조).
<표 1>과 같이 shutdown을 사용하면 남아 있는 데이터를 보낼 기회를 제공함으로써 소켓의 연결 종료를 제어할 수 있다. 그런데 아직 한 가지 고려해 볼 문제가 남아 있다. shutdown이나 closesocket 모두 ACK(TCP Handshake)를 확인하지 않고 리턴한다는 점이다. 그렇다면 어떻게 우리가 전송한 데이터가 정말로 보내졌는지 확인할 수 있을까?
Graceful Closure 설명을 하면서 Linger 옵션에 관한 설명을 빠뜨릴 수 없다. Linger 옵션은 closesocket 호출로 소켓이 닫히면서 남아 있는 데이터 전송을 어떻게 다룰 것인가를 설정한다. TCP는 상대방으로부터 보낸 패킷에 대한 ACK를 받아야 전송이 완료된 것으로 간주한다. <표 1>에서 서버 측을 보면 상대방으로부터 FIN 세그먼트를 확인한 뒤 데이터를 보내고 shutdown 호출 후 closesocket 호출로 마침내 소켓을 닫는다. 정상적인 과정이지만 내부적으로 FIN ACK는 물론 이전에 전송한 데이터조차 ACK를 받지 못했을 가능성이 있다. 다행히도 시스템은 기본적으로 소켓이 닫힌 후의 클로징 핸드세이크(Closing Handshake)를 처리할 시간(2MSL)을 준다. 
Linger 옵션은 이 시간을 조절할 수 있게 하는데 일반적인 경우에 Linger 옵션을 설정할 필요는 없다. Linger를 설정하는 경우 블러킹 소켓에선 closesocket 호출시 블럭될 수 있고, 넌블러킹 소켓은 closesocket에서 WSAEWOULDBLOCK을 리턴하므로 완료되기까지 수차례 호출해야 한다는 단점이 있다. 간혹 이 시간을 0으로 설정하기도 하는데 이것을 하드 클로저(Hard Closure)라고 하며, 이 때 서버는 closesocket 즉시 해당 소켓에 관한 모든 리소스를 반납한다. 이 경우 상대방은 모든 데이터를 수신하지 못한 채 WSAECON NRESET 에러를 받기 때문에 특별한 경우가 아니라면 권장하지 않는다. <표 2>는 MSDN에서 발췌한 것으로 Linger 옵션에 따른 closesocket 작동 방식을 나타낸다.

오버랩드 I/O
오 버랩드(overlapped) I/O란 문자 그대로 중첩된 입출력을 뜻한다. CPU에 비해 디스크나 통신 디바이스의 입출력에 걸리는 속도는 대단히 느리기 때문에 오버랩드 I/O를 사용해 디바이스 입출력시에 걸리는 시간 지연을 피할 수 있다. 물론 윈속은 이미 여러 가지 비동기 입출력 방법을 제공하고 있어, 굳이 오버랩드 I/O를 사용하지 않더라도 거의 같은 성능의 비동기 입출력을 구현할 수 있다. 잠시 후 소개할 IOCP(IO Completion Port)와 함께 사용되기 때문에 한 번쯤 거쳐야 할 관문 정도로만 생각해 두고 부담없이 진행해 나가도록 하자. send, recv 대신 WSASend, WSARecv를 사용해 오버랩드 I/O를 할 수 있다. 
함수 파라미터의 구체적인 사용법은 이후에 논하기로 하고, 지금은 WSAOVERLAPPED 구조체를 사용해 함수를 호출한다는 것 정도만 알아두자. 넌블러킹 소켓에서와 마찬가지로 오버랩드를 사용한 WSASend, WSARecv 호출은 특별한 이상이 없는 한 WSAE WOULDBLOCK을 리턴한다. 오버랩드 I/O의 완료 여부를 확인하려면 다음의 함수를 호출하면 된다. 

BOOL WSAGetOverlappedResult(
   SOCKET s,
   LPWSAOVERLAPPED lpOverlapped,
   LPDWORD lpcbTransfer,
   BOOL fWait,
   LPDWORD lpdwFlags
);


사 실은 바로 IOCP를 설명해도 되지만, 오버랩드 I/O를 설명하면서 그냥 지나치면 WSAGetOverlappedResult가 섭섭해 할까봐 한번 등장시켜 보았다. 바로 뒤에 설명할 IOCP를 사용해 완료 통보를 받게 되면, 더 이상 이 함수는 설 자리가 없어지기 때문에 독자의 기억 속에 그리 오래 머물 것 같진 않다. 오버랩드 I/O의 다양한 쓰임새나 윈속의 다른 비동기 입출력 방법에 대해 자세히 알고 싶다면, 마이크로소프트 프레스의 「Network Programming for Microsoft Windows」를 참고하기 바란다.

디바이스 입출력 완료 통보 포트, IOCP
IOCP는 디바이스의 입출력 완료를 통보하기 위한 포트로서, 빠른 입출력 통보 외에 최적화된 쓰레드 풀링 기술을 포함하고 있다. 디바이스와 IOCP를 연결하는 데 개수 제한이 없고, 최적화된 쓰레드 풀링을 통해 고성능 서버를 구축하는 데 큰 도움이 되기 때문에, 현재 많은 윈도우 서버 프로그래머들의 사랑을 받고 있는 귀여운 녀석이기도 하다. 제공되는 성능에 비해 사용법 자체는 의외로 간단해 프로그래머는 IOCP를 만들고, 적절한 수의 워커 쓰레드를 생성한 다음 입출력 완료 통보를 기다리기만 하면 된다.
 

HANDLE CreateIoCompletionPort (
   HANDLE FileHandle,
   HANDLE ExistingCompletionPort,
   ULONG_PTR CompletionKey,
   DWORD NumberOfConcurrentThreads
);


IOCP 를 만들어 주는 좀 웃기는(?) 함수다. 이 함수는 사실상 두 가지 역할을 하는데, 하나는 이름 그대로 IOCP를 생성하는 것이고(네 번째 파라미터만 사용), 다른 하나는 오버랩드 속성을 지닌 소켓과 IOCP를 연결하는 것이다(앞의 세 파라미터만 사용). 「Program ming Server-Side Applications for Microsoft Windows 2000」의 저자 제프리 리처(Jeffrey Richter)도 언급한 것이지만, 함수를 왜 저렇게 만들어 놨는지 도저히 이해되지 않는 부분이다. 어쨌든 꽤 중요한 함수이기에 다음의 일련의 흐름을 보면서 IOCP 체계를 확실히 이해해 둘 필요가 있다.

짾 IOCP를 만든다 
 

HANDLE h = CreateIoCompletionPort( INVALID_HANDLE_VALUE, NULL, 0, 0 );


처 음엔 당연히 IOCP를 만들어야 한다. 먼저 IOCP를 만들 때는 앞의 세 파라미터가 쓰이지 않으므로 가볍게 INVALI D_HANDLE_ VALUE, 0, 0을 넘겨주자. NumberOfConcurrent Threads에 오버랩드 I/O를 처리하기 위해 동시에 실행할 수 있는 쓰레드의 수를 지정하는데, 0을 넘기면 시스템은 설치된 프로세서(CPU)의 수만큼 할당한다.

짿 IOCP를 감시할 쓰레드를 생성한다
 

SYSTEM_INFO si
GetSystemInfo( &si );

numThreads = si.dwNumberOfProcessors * 2;

for ( i = 0; i < numThreads; i++ )
   _beginthreadex( NULL, 0, WorkerThread, ... );


IOCP 의 완료 통보를 받을 쓰레드를 생성한다. 좀 이상한 부분이 눈에 띄지 않는가? IOCP를 만들 때 CPU 수만큼의 쓰레드가 동시에 돌아갈 수 있도록 그 수를 제한해 놓고선, 정작 쓰레드는 그 두 배만큼 만들고 있다. 이는 워크 쓰레드가 Wait 상태에 다다를 때(예 : Sleep 호출) IOCP가 또 다른 쓰레드에 완료 통보를 해주기 때문에 여분의 쓰레드를 미리 만들어 두는 것이다. 두 배라고 한 것은 필자 맘대로 정한 수치이고 서버의 구현 방법이나 서비스 내용에 따라 적절한 값을 찾는 것이 좋다.

쨁 소켓과 IOCP를 연결시킨다
 

CreateIoCompletionPort( (HANDLE) my_socket, iocp_handle, completion_key, 0 )


오 버랩드 I/O를 IOCP로 통보받기 위해 소켓 핸들과 IOCP 핸들을 연결시켜야 한다. 세 번째 파라미터인 completion_key는 나중에 오버랩드 I/O에 대한 완료 통보를 받을 때, 어떤 소켓으로부터의 완료 통보인지 식별할 수 있게 해주는 것으로 보통 소켓을 포함하고 있는 객체의 주소를 넘긴다. 그리고 앞서 언급했듯 마지막 파라미터는 쓰지 않는다.

쨂 IOCP를 감시(+_+)한다
 

WorkerThread()
{
   while ( TRUE )
   {
      GetQueuedCompletionStatus(
         iocp_handle, // HANDLE CompletionPort
         &bytes_transferred, // LPDWORD lpNumberOfBytes
         &completion_key, // PULONG_PTR lpCompletionKey
         &overlapped, // LPOVERLAPPED *lpOverlapped
         INFINITE ); // WORD dwMilliseconds

      // completion_key와 오버랩드를 보면
      // 어떤 소켓의 오버랩드 I/O인지 구별할 수 있다.
   }
};


처 음 보는 함수가 나타났다. 이미 독자도 예상하고 있겠지만 Get QueuedCompletionStatus가 IOCP의 부름(Thread Wake-Up)을 받기 위해 기다리고 있다. 이 함수를 통해 어떤 소켓의 어떤 호출인지, 또 얼마만큼 전송이 되었고 에러 코드는 무엇인지 등을 확인할 수 있다.

쨃 WSASend, WSARecv 등의 오버랩드 I/O를 시작한다
 

WSASend(
   s, &wsabuf, 1,
   &bytes_transferred, 0, &overlapped, NULL
);

WSARecv(
   s, &wsabuf, 1,
   &bytes_transferred, &flag, &overlapped, NULL
);


패 킷을 주고받기 위해 오버랩드 구조체를 이용한다. WSASend, WSARecv 각각의 파라미터에 주의를 기울일 필요가 있는데, 이에 관한 자세한 설명은 다음 번에 직접 네트워크 라이브러리를 구현하면서 자세히 설명하기로 하고 지금은 IOCP 체계를 이해하는 것에 초점을 맞추자.
두 개의 CPU가 설치된 윈도우 2000에서 <그림 2>와 같은 IOCP 서버가 실행 중이라고 가정해 보자. IOCP를 만들 때 NumberOf ConcurrentThreads에 0을 넘겨 동시 쓰레드(concurrent thread)의 수가 두 개가 되도록 했다. #1은 이미 완료 통보를 받아 해당 객체의 송수신을 처리 중이고, #2가 지금 막 완료 통보를 받고 있다. 이렇게 되면 정확히 두 개의 쓰레드가 동시에 실행중인 것이며, IOCP 큐에 완료 통보가 도착하더라도 IOCP는 다른 쓰레드(#3)에 완료 통보를 하지 않는다. 이 시점에서 발생할 수 있는 두 가지 시나리오를 세워 보았다.

◆ 시나리오 1 - #1이 완료 통보 처리를 마침
완료 처리가 끝났기 때문에, #1은 다시 GetQueuedCompletion Status 함수를 호출한다. 이때 IOCP는 큐에 쌓여 있던 다른 완료 통보를 다시 #1에 넘겨준다. 먼저 기다리고 있던 #3에 넘기지 않는 이유는 쓰레드 컨텍스트 스위칭을 줄이기 위해서다. 

◆ 시나리오 2 - #1이 처리 도중 Sleep을 호출
프 로그래머가 무슨 생각으로 Sleep을 호출했는지는 모르겠지만 어쨌든 쓰레드 Wait 상태에 돌입한다. 이 때 기다리고 있던 #3이 IOCP로부터 완료 통보를 받는다. 이 시점의 실제 동시 쓰레드 수는 2+1(Wait State)이며, #1이 잠에서 깨어날 경우 순간적으로 IOCP를 만들 때 지정했던 쓰레드 수의 범위를 초과할 수 있다. 이후 IOCP는 다시 동시에 실행될 쓰레드 수가 2가 되도록 조절한다. 이러한 이유로 IOCP 생성시에 지정해 준 NumberOfConcurrentThreads의 수보다 실제로 많은 워커 쓰레드를 생성하는 것이다.

IOCP를 이용한 서버 구현시 주의사항
많은 개발자들이 범하는 대부분의 실수는 멀티 쓰레드와 비동기 입출력의 이해 부족에 기인한다. 멀티 쓰레드 프로그래밍만 하더라도 어렵고 복잡한데, 여기에 비동기 입출력까지 더해지니 네트워크 개발자들이 겪을 그 혼란은 충분히 짐작할 만하다. 이번엔 IOCP를 이용해 서버 네트워크 코드를 구현할 때 특히 주의해야 점을 알아보기로 하자.

에러 코드를 반드시 확인한다
WSASend, WSARecv 등을 통해 오버랩드 I/O를 할 때 정상적인 경우 WSAEWOULDBLOCK을 리턴한다. 그러나 원격 호스트가 접속을 끊거나(WSAECONNRESET), 가상 회선에 문제가 발생했을 때(WSAECONNABORTED)와 같은 문제는 빈번히 발생한다. 이 경우 별 수 없이 이쪽에서도 접속을 끊는 수밖에 없다. 골치 아픈 부분은 WSAENOBUFS와 같은 에러를 만나는 경우다. 다음 호에서 구현을 통해 자세히 알아보겠지만, 시스템 리소스(커널 리소스) 제한에 걸리게 되면 오버랩드 I/O는 ‘WSAENOBUFS 에러’를 내뱉으며 실패한다. 마찬가지로 ‘그냥 접속을 끊으면 되는 것 아니냐?’고 반문하겠지만, 그것이 클라이언트가 아니라 대량의 클라이언트가 접속한 상황에서의 서버간 송수신에서 발생하는 것이라면 더욱 심각해진다. 대량의 클라이언트가 접속한 상황에서는 언제든지 시스템 리소스가 바닥날 수 있기 때문에 클라이언트의 연결을 적절히 분산시킬 수 있는 메커니즘이 필요하며, 불가피한 경우 클라이언트의 접속을 제한해야 한다.

참조 카운트를 유지한다
오버랩드 호출을 걸어두고, 완료 통보를 받기도 전에 오버랩드 버퍼나 소켓 객체가 삭제돼서는 안된다. 또한 한 객체에 대해 둘 이상의 오버랩드 호출이 있는 경우엔 반드시 참조 카운트를 유지해야 하며, 객체를 제거해야 하는 경우에 이 참조 카운트가 0인지 확인해야 한다. 참조 카운트를 유지하지 않고 완료 통보가 아직 더 남아있는 상태에서 객체를 삭제하면, 당연한 것이지만 그 다음 완료 통보시 엉뚱한 메모리 위치를(IOCP로 말하자면 CompletionKey나 Overlapped Pointer) 가리켜 크래시를 발생시킨다. 원인을 모르고 객체가 삭제된 것에 분개해 정적 메모리 관리 등으로 당장 급한 불을 끄는 것은 근본적인 해결책이 될 수 없다. 

데드락을 주의한다
IOCP 의 워커 쓰레드만을 이용해 서비스 코드를 구현할 때 주의해야 할 사항이 있다. 주로 샘플 소스로 쓰이는 에코(echo) 서버나, 실제로 IOCP로 구현되어 있는 IIS(Internet Information Server)와 같은 서버는 객체간 상호 참조가 발생되지 않아 이러한 문제는 없다. 그러나 채팅 서버와 같은 상호 참조(즉, 한 객체가 다른 객체에 직접적인 접근이 일어나는 것)가 발생하는 서비스에서는 양방향 상호 참조가 동시에 일어나는 경우에 데드락(dead-lock)이 발생할 수 있다. 따라서 동기화에 각고의 노력을 기울여야 하며, 이것보다는 패킷을 처리하는 전용 쓰레드를 따로 두어 일괄적으로 처리하는 방법을 권한다.

다양한 의견 기다리며
이 번 호에서는 본격적인 구현에 앞서 필요한 내용들을 쭉 살펴봤다. 지면 관계상 조금 빠르게 진행된 감이 있는데 부족한 부분은 참고자료를 살펴보기 바란다. 필자도 부족한 부분이 많기 때문에 오해하고 있는 부분이 있거나, 잘못된 코드를 제공할 수 있다. 이런 부분이 발견되면 즉시 연락해 바로잡을 수 있도록 도와주길 바란다. 그리고 이번 기사에 대한 질책이나 조언, 다양한 의견을 접할 수 있다면 앞으로 좋은 기사를 쓰는 데 큰 도움이 될 것이다. 부담없이 연락해 주길..(^^;)


iocp를 이용한 게임 서버 구현시 wsasend 응용 버퍼 처리 문제

https://gpgstudy.com/forum/viewtopic.php?t=23484

더보기

일정한 크기의 메모리를 생성(약 1.5k)해 놓고...

바로 보내야 되는 상황이면 그냥 보내고,
완료 통지가 안된 경우라면 1.5k까지는 쌓다가, 1.5k가 꽉 차면 바로 보냅니다.
...완료 통지가 된 뒤에 보내는 것과 비교해서 성능에서 차이가 좀 있습니다. 그냥 보내줘도 네트워크에서 받아주면 다 갑니다.
...작은 사이즈 패킷을 계속 보내는 것도 문제가 있습니다. 윈도에서 iocp로 할당된 메모리가 금방 다 찹니다.

완료 통지가 오면, 해당되는 메모리만 해제 하고요...

보내는 양이 네트워크 보다 적기만 하다면 다 보내집니다만...

네트워크가 감당이 안되면, 센드할때 WSAENOBUFS 에러가 납니다.

저의 경우에는 어차피 네트워크가 감당이 안된다면 패킷 량을 조절해야 되니 그냥 씁니다.. ^^;;

게임 서버 쪽에서 쓰시려면...
완료 통지가 안온 패킷이 어느 정도 이상이면 더이상 보내지 않거나
계속 쌓아 두다가 가장 먼저 쌓인 데이터를 순차적으로 날려 주면 될 것 같습니다.
오래된 데이터는 쓸모 없는 데이터일 가능 성이 높으니까요...

서버에서는 보일 포인트가 있어서 패킷량이 어느 정도까지는 잘 가다가,
보일 포인트를 넘어가면 버퍼가 순간적으로 차 오릅니다.. ^^


빨리 쓰다 보니 설명이 엉망이네요...

고쳐 보셔야 할 포인트는...
1. 완료통지가 오기 전에 미리 몇개정도(한 루프에 해당되는 데이터 정도)는 더 보내세요.(iocp성능 향상)

2. 이래도 데이터가 많다면... 끊어지는 것 보다는 데이터 누락이 낳으므로...
데이터를 오래된 순서대로(또는 중요하지 않은 순서대로) 삭제하는 방법을 쓰시고(한 루프에 해당되는 데이터를 한번에 날리는 게 좋을 듯..) 동시에 데이터 량을 줄이세요.


WSASend 이제 그만 얘기 하자

https://blog.naver.com/laster40/49513680

더보기

마소 젤 유명한 넷떡 뉴스그룹입니다. 2008년 부터 2004년까지 다 뒤쳤습니다. 이거 검색이 안댑니다.-_- 일일이 뒤쳤습니다.

 

Alexander Nickolov  라는 분입니다. 대충 해석해 보면 WSASend는 부분적인 성공에 대해서 리턴하지 않는다구 나와있습니다. 강조하고 싶지 않지만 MVP

 

all or nothing 이라는거죠....

 

http://groups.google.com/group/microsoft.public.win32.programmer.networks/browse_thread/thread/ac6ee6903772c2af/1e8ca2b398466eae 중에서

 

Alexander Nickolov    프로필 보기 
  추가 옵션 2007년6월19일, 오전5시24분 

뉴스그룹: microsoft.public.win32.programmer.networks
보낸 사람: "Alexander Nickolov" <agnicko...@mvps.org>
날짜: Mon, 18 Jun 2007 13:24:30 -0700
지역: 2007년6월19일(화) 오전5시24분
제목: Re: IOCP question: ordering of data
작성자에게 회신 | 전달 | 인쇄 | 개별 메시지 | 원문 보기 | 이 메시지 신고 | 이 작성자의 메시지 찾기
Data sequencing synchronization is up to you.

On a side note, WSASend will never return with a partial
success - it's all or nothing. This doesn't even make sense
for overlapped I/O though, since your send is not completed
yet. It's the completion notification that's all or nothing. But,
if you do get a partial completion, that means all subsequent
sends will complete with failure (the socket is toast).


--

--
=====================================
Alexander Nickolov
Microsoft MVP [VC], MCSD
email: agnicko...@mvps.org
MVP VC FAQ: http://vcfaq.mvps.org
=====================================

 

 

Skywing 라는 분입니다. 엇따 이분도 MVP십니다.

 

nonblocked io 소켓은 부분적으로 완료될 수 있다고 나옵니다. 하지만 blocked io과 Overlapped io는 all or nothing라고 나옵니다.


Skywing [MVP]    프로필 보기
  추가 옵션 2007년6월19일, 오전10시39분 

뉴스그룹: microsoft.public.win32.programmer.networks
보낸 사람: "Skywing [MVP]" <skywing_NO_SP...@valhallalegends.com>
날짜: Mon, 18 Jun 2007 21:39:34 -0400
지역: 2007년6월19일(화) 오전10시39분
제목: Re: IOCP question: ordering of data
작성자에게 회신 | 전달 | 인쇄 | 개별 메시지 | 원문 보기 | 이 메시지 신고 | 이 작성자의 메시지 찾기
It can do partial completion immediately for nonblocking-sockets mode.  For
blocking sockets or for overlapped operation, it's all or nothing.

--
Ken Johnson (Skywing)
Windows SDK MVP
http://www.nynaeve.net


 

 

보내는 순서에 대한 예깁니다. 이건 MS문서이니 확실합니다.

 

오브랩io는 중첩이 가능하구요~ 중첩해서 보낼때 다른것두 보낼수 있다는거죠. 보낸시점대로 보내지지만 완료는 다른 순서대로 발생할수도 있다고 하네요....받을때에도 순서대로 잘받아지지만 완료가 발생하는 순서는 다를수 있다는거군요....

 

'aaa' 를 보내고 'bbb' 를 보내면 'aaabbb' 잘간다는 소리죠 근데 'aaa'보내는 완료가 'bbb'보내는 완료보다 먼저 도착하는건 보장할수 없다는거죠.....받을때도 마찬가지구요....받을때 순서가 저러니 약간 nrecv는 처리가 애매하겟네요

 

다만 여기서 조심해야 할께 'aaa'와 'bbb'를 같은 버퍼에 쓰구 다른 쓰레드에서 전송을 하려구 하면 당연히 말이 안대죠....몇몇 분들이 그렇게 테스트 해서 안댄다구 하던데....잘못된거죠


Both send and receive operations can be overlapped. The receive functions may be invoked multiple times
to post receive buffers in preparation for incoming data, and the send functions may be invoked multiple
times to queue up multiple buffers to be sent. Note that while the application can rely upon a series of
overlapped send buffers being sent in the order supplied, the corresponding completion indications may
occur in a different order. Likewise, on the receiving side, buffers will be filled in the order they are
supplied but the completion indications may occur in a different order.

-- http://www.jimprice.com/winsock/winsock2api-withtoc.PDF 중에서
-- http://msdn2.microsoft.com/en-us/library/ms740087(VS.85).aspx?PHPSESSID=lm71lj7i6gj5fjtebtb2srl4n3

 

 

나눠서 보내기가 완료된다는건 불가능하다네요? 젤위분이네요...

 

http://www.tech-archive.net/Archive/Development/microsoft.public.win32.programmer.networks/2007-08/msg00178.html

4. For overlapped non-blocking sockets, in WSASend, same
thing only 1 buffer, is it possible my completion routine is called
with cbTransferred smaller than what I tried to send?? If so,
do I retry or will I get another callback when another chunk is sent?


No, that's not possible, at least for TCP sockets

 

 

 

다시 보낼필요 없다는 겁니다....아 훗... 

 

 


http://www.tech-archive.net/Archive/Development/microsoft.public.win32.programmer.networks/2005-12/msg00350.html

From: "Eugene Gershnik" <gershnik@xxxxxxxxxxx>
Date: Thu, 29 Dec 2005 11:43:02 -0800

--------------------------------------------------------------------------------
Natascha Heumann wrote:
> This is part of MSDN's doc for WSASend:
>
> "Data is copied from the buffer(s) into the transport's buffer. If the
> socket is nonblocking and stream oriented, and there is not sufficient
> space in the transport's buffer, WSASend will return with only part of
> the application's buffers having been consumed"
>
> This is the same behaviour as of send, that means if I want to send
> let's say 100 bytes send may return saying only 60 bytes ar sent so I
> have to repeat the send for the remaining 40 Bytes.

This is about traditional non-overlapped sockets (see previous sentence in
MSDN). When you use IOCP this does not apply. All the data you pass to
WSASend is simply queued in the kernel and will *eventually* be passed to
the buffer mentioned above without your intervention. That is you don't need
to resend anything.

> Thats exactly the problem I'm dealing with and what makes me toss and
> turn :-) And indeed, it happens!

It shouldn't :-) What exact problem do you observe?

 

여기저기 끍어 모은 자료임

 

http://www.eggheadcafe.com/software/aspnet/30166108/iocp-question-ordering-o.aspx

 

 

 

WriteFile Function 을 찾아봤습니다. 드뎌 찾던 내용이 있네요.....OTL 소켓 IO와도 흡사할테죠. 너무 많이 asyncio 하면 에러 난답니다  확실히 Overlapped IO는 완젼히 완료되어야지만 시그널이 셋팅되어 지네요... ERROR_NOT_ENOUGH_MEMORY  에러가 나오면 Locked Paged Limit 보다 커서 그런걸꺼죠
 
 

If hFile was opened with FILE_FLAG_OVERLAPPED, the lpOverlapped parameter must not be NULL. It must point to a valid OVERLAPPED structure. If hFile was opened with FILE_FLAG_OVERLAPPED and lpOverlapped is NULL, the function can incorrectly report that the write operation is complete.

If hFile was opened with FILE_FLAG_OVERLAPPED and lpOverlapped is not NULL, the write operation starts at the offset specified in the OVERLAPPED structure and WriteFile may return before the write operation has been completed. In this case, WriteFile returns FALSE and the :Track('ctl00_rs1_mainContentContainer_ctl00|ctl00_rs1_mainContentContainer_ctl12',this);" href="http://msdn2.microsoft.com/en-us/library/ms679360(VS.85).aspx">GetLastError function returns ERROR_IO_PENDING. This allows the calling process to continue processing while the write operation is being completed. The event specified in the OVERLAPPED structure is set to the signaled state upon completion of the write operation. The caller must adjust the position of the file pointer upon completion.

 

The WriteFile function may fail with ERROR_INVALID_USER_BUFFER or ERROR_NOT_ENOUGH_MEMORY whenever there are too many outstanding asynchronous I/O requests.

 

 

ASIO1.0에서 발취한 내용입니다. 아래는 ASIO의 async_write입니다.

 

  • All of the data in the supplied buffers has been written. That is, the bytes transferred is equal to the sum of the buffer sizes.

 

==> 소스 분석 결과 위의 내용은 사실 asio 내부 모듈(io_service)가 보장 하겟다라는 것임. async io에 대해서 asio는 os에서 지원해 주지 않은 부분은 시뮬레이션 하고 있음. 하지만 iocp의 구현을 내부 구현을 보면 wsasend한번의 요청으로 원하고자 하는 만큼의 용량을 전송해준다고 가정하고 작성되어져 있음 ==> 저 위의 내용은 사실 직접적으로 지원해준다고 얘기 하기 힘들다는 것임. 하지만 iocp의 wsasend는 실제로 저렇게 구현해주기 위해서 아무것도 하지 않아도 됨

 

Start an asynchronous operation to write of all of the supplied data to a stream.

<PRE class=programlisting>template< typename AsyncWriteStream, typename ConstBufferSequence, typename WriteHandler>void async_write( AsyncWriteStream & s, const ConstBufferSequence & buffers, WriteHandler handler);</PRE>

This function is used to asynchronously write a certain number of bytes of data to a stream. The function call always returns immediately. The asynchronous operation will continue until one of the following conditions is true:

  • All of the data in the supplied buffers has been written. That is, the bytes transferred is equal to the sum of the buffer sizes.
  • An error occurred.

 

ACE 는 handle_write_stream에서 보통 bytestowrite 와 bytetransferred 를 비교해서 재전송을 해야한다고 설명되어짐 곳곳의 소스에서 그렇게 실제로 구현하고 있음 ACE_Asynch_Write_Stream::write현재 슈미츠교수님에게 메일을 보내놓은 상태..인데 답장이 없음-_- 확실히 ACE는 예외이고 재전송을 해야한다고 되어 있음

 

http://www.dre.vanderbilt.edu/Doxygen/5.4/html/ace/a01100.html

 

 

CodeProject의 IOCP예제를 받아봤습니다.

http://www.codeproject.com/KB/IP/IOCP_how_to_cook.aspx

http://www.codeproject.com/KB/IP/iocp_server_client.aspx

http://www.codeproject.com/KB/IP/IOCPNet.aspx

 

보낸만큼 안가서 재전송 코드가 어디에도 없습니다. 확인해 보세요 물론 큐잉을 합니다. 이건 보낸만큼 안가서 재전송하는게 아니고 1Send을 위한 큐잉이었습니다.
보내기 완료에서 당연히 사이즈가 같다고 처리하고 있다.

 

 

그럼 왜..... 큐잉을 추천하고 있나 OR 상업용 모듈들은 많이들 큐잉을 하고 있습니다.  한 아티컬에서는 OnWritten이 이럼에도 불구하고 Queuing을 강추하고 있습니다. 그 이유는 다름 아닌 Locked page limit, Non-paged pool limit  때문입니다. 아티클 주소를 까먹었습니다 OTL ... 그래서 Locked Page 를 줄일수 있게 send/recv 를 제한하는거죠. 1recv/1send로 말입니다. 

 

* 여기 한가지 큐잉을 할때 울나라 서적과 몇몇분들의 소스에서 큐잉을 하지만 Locked Page를 크게하도록 큐잉을 합니다. Ring Buffer 같은거 사용해서 한꺼번에 모아 보내기 이런거 하시는거죠. 그러면 사실 1Send의 의미가 퇴색되는게 되더군요. 보통 예제들에서 (코드 플젝과 구글에서 주어온예제) 환형큐가 아니고 패킷단위 큐잉 방식이 많이 사용되더군요.

 

결론 :

 

1 .  Overlapped IO에서 WSASend해서 성공하면  Locked page limit, Non-paged pool limit 에는 안걸렸다는것이고 만약에 걸렸으면 실패하는거죠. 성공하면 GQCS에서 성공or실패 여부가 나오죠...성공했으면 보내기 요청한 만큼 무조건 갔다는게 보장되는거고요 -> WSASend하면 다 간다며 100M보내면 안가는 경우는 어떻게 설명할래? 이게 우선 설명이 됩니다.

 

2 . 전송 순서는 N-Send도 생각보다 비관적이지만은 않았습니다. 전송 순서 보장되고 단 완료 순서가 다를수 있다는 거니깐 말이죠. 그럼 N-Send로 Broadcasting을 하는 서버들두 잘돌았다는것두 설명이 됩니다.

 

3 . 사실 대충 만들어서 환형큐를 써두 잘 돌고 재 전송 안해도 잘돌았고 해도 성능상 문제를 일으키진 않았죠. 하지만 제대로 알고 싶었습니다.

 

4 . 예전보다 자료가 많아 졌네요~ ㅎㅎㅎ

 

기초적인 IOCP 서버 개발 팁. 연결에서 종료까지...
https://yamoe.tistory.com/421
더보기

0. 성능 향상을 위한 참고사항
a. AcceptEx(), GetAcceptExSockaddrs(), TransmitFile(), ConnectEx() 함수를 가급적 mswsock.lib 링크를 통해 mswsock.dll 에서 import하여 호출하지 말고 직접 함포를 얻어내 처리할 것.
환경에 따라선 mswsock.dll 이 해당 함수를 export하지 않을 수 있으며(XP PRO SP2의 경우 뭐 물론 이걸로 서버 돌릴 일은 그리 없다만서두...),
mswsock.dll에 실제 코드가 있는게 아니라, 매 호출시마다 WSAIoctl() 로 함포를 꺼내 호출하는 wrapper라서 씨잘데기 없이 오버헤드가 발생한다.
예제)
LPFN_ACCEPTEX lpfnAcceptEx=NULL; // 타입은 MSWSOCK.H 에 정의되어있다.
GUID GuidAcceptEx=WSAID_ACCEPTEX; // WSAID_ACCEPTEX 또한 MSWSOCK.H 에 정의되어있다.
WSAIoctl(s,
SIO_GET_EXTENSION_FUNCTION_POINTER,
&GuidAcceptEx,
sizeof(GuidAcceptEx),
&lpfnAcceptEx,
sizeof(lpfnAcceptEx),
&dwBytes, NULL, NULL );

b. IFS 와 socket provider 간의 모드 전환에 의한 오버헤드가 발생하는걸 막기 위해 ReadFile()/WriteFile() 대신 WSARecv()/WSASend() 함수를 사용한다.
AcceptEx(), ConnectEx() 로 연결된 소켓은 별도의 처리 없이도 WSARecv()/WSASend() 를 쓸 수 있다.
가끔은 msdn.microsoft.com 에서 최신정보도 찾아보자. 옛날 MSDN 보고 질문부터 하지 말고-_-;

c. 메모리 풀을 쓰고, 임계영역을 최소화하여 개발하라.
동시성을 가진 코드는 빠른 속도보다는 임계영역을 최소화할 수 있도록 코딩하고 스핀락을 걸어라. 특히 임계 영역 내에서 작업을 하도록 하지 말고, reserve 후 commit 하는 구조로 제작하라.
환형 큐는 이러한 조건을 만족하는 가장 이상적인 자료구조다. 그러나 STL의 선택은 최악이다. 귀찮더라두 병렬처리용 자료구조 템플릿을 만들어 쓰는게 좋다....
스핀락 정수 영역은 가급적 다른 데이터와 떨어진 주소공간에 배치하는게 좋다.
임계영역에서 작업중 동시에 접근하는 타 프로세서의 간섭을 피하기 위해 가급적 임계영역 데이터와는 떨어진 별도의 4k 주소공간 영역에 따로 모아놓는다.
물론 물리적 페이지가 논리적 주소와 별도로 인접했다면 소용 없을지 모르지만, 모든 메모리를 순차적으로 한꺼번에 초기화하는 서버의 특성상 그럴 일은 거의 없다.

d. 설치될 서버 환경에 맞게 각 쓰레드의 processor affinity 를 적절히 배치하고, 개별 쓰레드가 접근하는 메모리 영역을 각각 집중시켜 캐쉬 사용을 최적화하라.
가급적 포인터/인덱스 등의 집약된 데이터로써 대부분의 처리를 수행하도록 하고, 분산된 데이터는 최종적으로 접근하게 한다.
그리고 각 쓰레드의 시간상 메모리 접근이 분산되지 않도록 주의한다. 되도록 서비스 프로세스에서는 주소공간상 순차적으로 메모리 접근이 진행되도록 한다.
이를 위해 가급적 힙 상의 동적 할당 대신 전역변수 및 풀로써 모든 객체를 생성하라.
또한 프로세서간의 캐시 동기화 간섭이 발생하는걸 최소화하기 위해, 서로 다른 프로세서가 접근할 데이터들 사이엔 64k 이상의 간격이 발생되도록 배치한다.
이 부분은 코딩이 아니라 아키텍쳐 설계시부터 고려되어야 할 사항이다.
(이게 싫으면 C#, JAVA, PHP 쓰자... 아니 이 좋은 세상에 무슨 큰 죄를 지었다고 c++ 쓰는데?)

e. 분기문 작성시 파이프라인 초기화를 최소화하도록 하라.
분기문 없애란 소리가 아니다. IA-32는 분기문도 예측한다. 그 예측의 적중률이 높도록 코딩한다. (물론 말은 쉽지-_-;)
(조건 전방점프 외엔 모두 예측대상으로 간주하다. 예를 들어 if() {} 의 경우 블럭 내의 코드가 실행되리라 예측한다. for/while 루프는 루프가 반복되리라 예상한다. )
간접호출이 아닌 함수는 예측 대상이 된다면 depth가 너무 깊지 않도록(대략 10단계 정도) 한다.
switch() 문에 사용될 case 문 상수는 가급적 0부터 연속으로 정의되도록 한다.
virtual method, 함포의 호출 횟수는 가급적 최소한으로 줄인다. 간접호출/점프는 대부분 분기 예측 실패를 발생시킨다.
암튼 하고 싶으면 하고, 싫으면 말고-_-; 근데 부하가 순간적으로 몰릴 땐 확실히 차이가 있긴 있다.

f. 서버 개발은 아키텍쳐에서 80을 먹고 버그까지 잡은 후 들어간다. 서버 개발자가 이런저런 컴포넌트나 라이브러리 줏어모으며 코딩부터 하는 스크립터 흉내를 내고 있다면 대략 낭패-_-;



1. 소켓 생성과 연결 처리
소켓 풀을 생성한다. 미리 최대 동접자만큼의 소켓을 만들며, 모든 소켓을 미리 WSA_FLAG_OVERLAPPED 로 생성하고 iocp 에 등록한다.
소켓 풀에 생성할 소켓의 수를 정할 땐 TransmitFile(TF_DISCONNECT | TF_REUSE_SOCKET) 후 TIME_WAIT 상태로 graceful closing 완료통보를 대기하게 될 경우도 고려하여 여유분을 둔다.
만약 ConnectEx() 로 연결할 클라이언트 소켓이라면 반드시 setsockopt(SO_REUSEADDR) 로써 포트를 재사용시키도록 한다. 이유는 '연결 종료 처리' 에서 설명한다.

그리고 AcceptEx() 를 사용하여 비동기로 처리한다. 이 때 포트당 AcceptEx()로 너무 많이 삽입하지 않고 적절한 수를 유지시키도록 한다.(보통 32개 정도)
이렇게 한번 iocp에 등록된 소켓은 나중에 TransmitFile(TF_DISCONNECT | TF_REUSE_SOCKET) 로 재사용한다 하더라도 다시 iocp에 등록할 필요가 없다.

이렇게 연결되어 초기화되는 순간 소켓 초기화 처리보다 먼저 해야 할 일이 바로 AcceptEx() 에 의한 다른 연결자 소켓의 공급이다.
만약 msdn의 AcceptEx()나 ConnectEx() 후 사용 가능한 API 목록에 없는 일반적 socket api를 사용할 계획이라면 반드시 setsockopt( SO_UPDATE_ ACCEPT_CONTEXT ) 로써 초기화해줘야 한다.

ConnectEx()로 연결했다면 getsockopt(SO_CONNECT_TIME) 으로 연결 성공 여부를 확인해야만 한다.

tip) 만약 프록시같은 서버에서 다수의 ConnectEx()를 통한 클라이언트 소켓을 확보해야 한다면 아래의 레지스트리 값을 등록하여 ephemeral port 범위를 증가시키도록 한다.
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters
변수형 : DWORD
변수명 : MaxUserPort
범위 : 10진수 5000 ~ 65534
참고 사항 : 등록 후 재부팅해야 적용된다. ephemeral port 는 각각 커널의 메모리를 할당하므로, 너무 큰 값으로 올리면 커널의 메모리를 소진하게 된다. 필요한 만큼만 올릴 것.


2. WSARecv, WSASend 함수는 항상 요청한 순서대로 송수신한다. (디스크 IO를 하더라도 항상 먼저 요청된놈이 앞쪽의 데이터를 읽거나 쓴다.)
그러므로 tcp window를 제거하는 대신 미리 WSARecv()를 여러번 호출하여 버퍼를 공급할 수 있으며, 송신 또한 미리 블러킹 없이 요청할 수 있다.
하지만 이들 송수신 요청에 대한 완료통보는 항상 순서대로 오지 않는다.
그러므로 완료통보의 순서를 재정렬할 매커니즘이 필요하다. 이는 수신 뿐만 아니라, 송신 실패시 송신을 순서대로 재개할 경우에도 필요하다.


3. WSASend 는 일부만 전송할 가능성이 있다.
IOCP 를 쓰면 zero-copy를 구현하기 위해 socket 송수신 윈도우를 0으로 맞춘다.
그러므로 WSASend 로 공급된 버퍼가 바로 window가 된다.
만약 WSASend 로 공급된 송신측 윈도우보다 수신측 윈도우가 작은 상태에서 수신 윈도우를 채우자 마자 shutdown(recv|send)을 하여 ack과 함께 fin을 보낸다면 일부만 전송된다.
물론 전송된 양에 대한 ack이 오며, 이 ack으로 온 송신량이 GQCS 함수의 전송량 파라미터로 들어온다.
연결 종료시 전송을 재개하여 완전히 마무리지어야 한다면 반드시 염두해둘 것.


4. 연결 종료를 감지하는 시점.
WSARecv 완료통보가 0바이트 수신으로 올 때. 일반적으로 이게 먼저 온다.
WSASend 완료통보의 송신량이 0이거나 요청량보다 작을 때. 이런 경우는 거의 발생하지 않지만, 절대 발생하지 않는건 아니다. 적어도 당신이 교통사고 당할 확률보단 높게 발생한다.


5. 연결 종료 처리
연결 종료를 처리 절차.
a. 요청한 WSARecv() 완료통보가 모두 돌아왔는가?
b. 요청한 WSASend() 완료통보가 모두 돌아왔는가?
c. 위의 조건이 충족되지 않았다면 매 완료통보마다 위의 조건을 검사한다(오래 걸리지 않아 모든 통보가 돌아온다).
d. 위의 조건이 충족되었다면 TransmitFile( s,NULL,0,0,&Overlapped,NULL, TF_DISCONNECT | TF_REUSE_SOCKET ) 로써 연결 종료 및 재사용을 요청한다.
이 때 Overlapped 없이 블러킹 콜을 하는 대신, 반드시 overlapped 포인터를 넘겨주고, 완료통보에서 소켓을 풀로 반납하도록 코딩해야 한다.
만약 remote peer보다 먼저 연결종료를 시도하는 경우라면 TransmitFile(TF_DISCONNECT|TF_REUSE_SOCKET)은 fin_1 송신 후 상대방의 ack과 fin_2 를 기다리며 TIME_WAIT 상태에 들어가게 된다. 즉 graceful closing을 시도한다.
만약 overlapped 포인터를 넘기지 않고 블러킹 콜을 시도하는 상황에서 remote peer에 이상이 생겨 fin_2와 ack를 보내지 못한다면, 기본적으로 최대 240초동안 블러킹을 하게 되어 서버 성능에 치명적 영향을 끼치게 된다.
그러므로 반드시 overlapped를 넘겨주어 ack과 fin_2가 도착하거나 TIME_WAIT 상태가 끝나고 완료통보가 왔을 때 풀로 반납해야만 한다. (이건 DisconnectEx()를 써도 마찬가지)
물론 이상이 발생한다면 기본적으로 240초 뒤에 완료통보가 오게 된다.
e. 만약 ConnectEx()로 연결한 클라이언트 소켓을 TransmitFile(TF_DISCONNECT | TF_REUSE_SOCKET) 로 재사용한다면, 이 소켓은 이미 포트에 바인딩되어 있으며 서버로부터 연결 종료 완료통보가 오기 전엔 일정시간 TIME_WAIT 상태에 있음을 명심하라. 그러므로 소켓 풀은 반드시 FIFO 구조로 만들어져서 TIME_WAIT 시간이 지나 할당되어야 한다.
물론 바인딩되어있다 하더라도, 바로 ConnectEx()에 넘겨 재사용하는건 가능하지만 TIME_WAIT 상태에 있을 때 재사용한다면 에러를 발생시킨다.

주의) TIME_WAIT 직접 때려 잡겠다고 절대로 shutdown 쓰지 말 것. 그럼 TF_REUSE_SOCKET 을 통한 소켓의 재사용이 불가능해진다.

tip) TransmitFile() 을 통한 소켓의 재사용은 치명적 단점이 있다.
악의적 클라이언트가 고의적으로 연속적인 비정상 연결종료를 시도할 경우 240초 내에 소켓 풀 내의 모든 소켓이 고갈될 가능성이 있다.
이를 해결하기 위해 TIME_WAIT 시간을 기본값 240초에서 줄이고 싶다면 아래의 레지스트리 값을 등록한다.
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters
변수형 : DWORD
변수명 : TcpTimedWaitDelay
범위 : 10진수 30 ~ 300 (초)
참고 사항 : 등록 후 재부팅해야 적용된다. 0으로 잡을 경우 네트워크 딜레이에 의한 지연 전달을 전혀 잡지 못하므로 3초 ~ 5초 정도로 잡자.
분산 DOS 공격을 걸어온다면... 일단 뭐 서버 내리던가...
공격 감지하면 모드 전환해서 소켓 풀 포기하고 shutdown(snd|rcv) -> 캔슬 -> closesocket() 초필콤보 후 다시 생성하는거 말곤... 길이 없다.
저놈의 TransmitFile이 엄~ 뷰우리플하고 판~타스틱하고 엘레가앙~스~한데다 원더풀하고 굉~장히 그레이스한 연결종료를 포기하는 어우~ 저질스런 몰상식 옵션을 지원하기 전엔... 줴길...

 

320x100

'서버 > IOCP' 카테고리의 다른 글

page-locking  (0) 2021.09.09
IOCP 관련 정리  (0) 2020.08.23