소켓 입/출력 버퍼
- 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, 하도록 변경했다.
'Network > Windows 소켓 프로그래밍 입문에서 고성능 서버까지!' 카테고리의 다른 글
파일 송/수신과 프로토콜 설계 (0) | 2023.11.27 |
---|---|
파일 송/수신 (0) | 2023.11.23 |
TCP 채팅 서버 - 성능 개선 (0) | 2023.11.22 |
TCP 채팅 서버 - 기본 이론 (0) | 2023.11.20 |
TCP 소켓 프로그래밍 입문 (0) | 2023.11.16 |