Socket 의 입출력에 관한 옵션은, 말 그대로 Socket IO 전체에 개입하는 옵션이다.
소켓의 옵션을 설정하고나 값을 가져올때는 아래의 개념이 필요합니다.
level | optname | optval | get | set | meaning |
SOL_SOCKET | SO_TYPE | int | yes | no | 소켓 유형 ( 예 : SOCKET_STREAM ) |
SOL_SOCKET | SO_RCVBUF | int | yes | yes | 수신을 위해 예약된 버퍼공간 |
SOL_SOCKET | SO_SNDBUF | int | yes | yes | 전송을 위해 예약된 버퍼공간 |
SOL_SOCKET | SO_REUSEADDR | bool | yes | yes |
소켓을 이미 사용중인 주소에 바인딩 할수 있도록 합니다.
( Server Listen Socket 에 사용 ) |
IPPROTO_TCP | TCP_NODELAY | DWORD(Boolean) | yes | yes |
TCP 소켓에 대한 Nagle 알고리즘 활성화 여부를 설정할 수 있습니다. (default : false) 활성화시 buffer 에 차는것을 기다리지 않고 바로 보낼수 있도록 설정할 수 있는것 같다. (검증필요) |
int getsockopt(
[in] SOCKET s,
[in] int level,
[in] int optname,
[out] char *optval,
[in, out] int *optlen
);
int setsocketopt(
[in] SOCKET s,
[in] int level,
[in] int optname, // TCP_NODELAY 로 설정하면, Buffer 에 꽉 채워지도록 기다리지 않는다.
[in] const char *optval,
[in] int optlen
)
패킷 전송지연을 사용하지 않도록 하여 recv, send 시 Buffer에 데이터가 충분히 채워질때까지 기다리지 않고, 바로보내도록 설정한다.
int opt = 1;
::setsocketopt (hSocket, IPPROTO_TCP, TCP_NODELAY, (char*)&nOpt, sizeof(nOpt))
// HelloTcpipClient.cpp : 콘솔 응용 프로그램에 대한 진입점을 정의합니다.
#include "stdafx.h"
#include <winsock2.h>
#pragma comment(lib, "ws2_32")
int _tmain(int argc, _TCHAR* argv[])
{
//※윈속 초기화
WSADATA wsa = { 0 };
if (::WSAStartup(MAKEWORD(2, 2), &wsa) != 0)
{
puts("ERROR: 윈속을 초기화 할 수 없습니다.");
return 0;
}
//1. 접속대기 소켓 생성
SOCKET hSocket = ::socket(AF_INET, SOCK_STREAM, 0);
if (hSocket == INVALID_SOCKET)
{
puts("ERROR: 소켓을 생성할 수 없습니다.");
return 0;
}
//2. 포트 바인딩 및 연결
SOCKADDR_IN svraddr = { 0 };
svraddr.sin_family = AF_INET;
svraddr.sin_port = htons(25000);
svraddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
if (::connect(hSocket,
(SOCKADDR*)&svraddr, sizeof(svraddr)) == SOCKET_ERROR)
{
puts("ERROR: 서버에 연결할 수 없습니다.");
return 0;
}
//2.1
int nOpt = 1;
::setsockopt(hSocket, IPPROTO_TCP, TCP_NODELAY,
(char*)&nOpt, sizeof(nOpt));
//3. 채팅 메시지 송/수신
char szBuffer[128] = { 0 };
while (1)
{
//사용자로부터 문자열을 입력 받는다.
gets_s(szBuffer);
if (strcmp(szBuffer, "EXIT") == 0) break;
//사용자가 입력한 문자열을 서버에 전송한다.
//::send(hSocket, szBuffer, strlen(szBuffer) + 1, 0);
int nLength = strlen(szBuffer);
for (int i = 0; i < nLength; ++i)
::send(hSocket, szBuffer + i, 1, 0);
//서버로부터 방금 보낸 문자열에 대한 에코 메시지를 수신한다.
memset(szBuffer, 0, sizeof(szBuffer));
::recv(hSocket, szBuffer, sizeof(szBuffer), 0);
printf("From server: %s\n", szBuffer);
}
//4. 소켓을 닫고 종료.
::shutdown(hSocket, SD_BOTH);
::closesocket(hSocket);
//※윈속 해제
::WSACleanup();
return 0;
}
서버쪽 특화 옵션 IP 주소를 재 사용하겠다는 옵션이다. socket 이라고 하는것은 자원이다. 그리고 이 소켓자원에 접근하는 process 는, socket 에 ip, port 를 묶게 되고, 만약 다른 Process 가 이미 사용중인 IP, Port 는 묶을 수 없다. 근데. 이미 사용된 IP 주소와 Port 번호를 이것을 사용해야 할때가 있다. Socket의 종료를 요청하는 쪽에서는 필연적으로 TIME_WAIT 가 발생하고 TIME_WAIT 가 짧게는 수초에서 길게는 몇분동안 일어난다. Server 에서 문제가 발생하여 Server 쪽에서 연결을 끊자, TIME_WAIT 가 발생하였고, 다시 Server 를 재시작해야하는데, 아직까지 Socket 이 회수가 안되어, 서버가 재시작을 못하는경우가 발생할 수 있다. 그 시간동안 Client 가 접속을 하지 못한다는 것이 되는것이다., 그것은 큰 문제가 될 수 있다. Server 의 생명은 1번이 안정성, 2번이 성능이다. 안정성을 최우선으로 생각해야 한다. 게이머들이 게임하다 팅겼는데 2분동안 접속이 안된다면,, 엄청난 문제가 될것이다.
// 바인딩 전에 IP 주소와 포트를 재사용 하도록 소켓 옵션을 변경한다.
BOOL bOption = TRUE;
if(SOCKET_ERROR ==::setsockopt(hSocket, SOL_SOCKET, SO_REUSEADDR, (char*)&bOption, sizeof(BOOL)) )
{
puts("소켓 옵션을 변경할 수 없습니다.");
return 0;
}
위 옵션을 사용하면 똑같은 IP, PORT 번호를 사용하는 Server 를 n 개 실행할 수 있다. 그렇다면 만약 똑같은 주소를 사용하는 Server1, Server2 총 2개가 있고, Client 가 해당 주소로 연결하려고 할때, 어느쪽으로 연결이 될까. 이렇게 되면, 먼저 실행된 Server1 에 우선적으로 연결이 되고, 만약 그 Server1 이 죽으면, Client 재시작시, 그 다음으로 실행된 Server2 에 연결이 된다.
// SockReuse.cpp : 콘솔 응용 프로그램에 대한 진입점을 정의합니다.
#include "stdafx.h"
#include <winsock2.h>
#pragma comment(lib, "ws2_32")
int _tmain(int argc, _TCHAR* argv[])
{
WSADATA wsa = { 0 };
if (::WSAStartup(MAKEWORD(2, 2), &wsa) != 0)
{
puts("ERROR: 윈속을 초기화 할 수 없습니다.");
return 0;
}
SOCKET hSocket = ::socket(AF_INET, SOCK_STREAM, 0);
if (hSocket == INVALID_SOCKET)
{
puts("ERROR: 접속 대기 소켓을 생성할 수 없습니다.");
return 0;
}
//※ 바인딩 전에 IP주소와 포트를 재사용하도록 소켓 옵션을 변경한다.
BOOL bOption = TRUE;
if (::setsockopt(hSocket, SOL_SOCKET,
SO_REUSEADDR, (char*)&bOption, sizeof(BOOL)) == SOCKET_ERROR)
{
puts("ERROR: 소켓 옵션을 변경할 수 없습니다.");
return 0;
}
//바인딩
SOCKADDR_IN svraddr = { 0 };
svraddr.sin_family = AF_INET;
svraddr.sin_port = htons(25000);
// svraddr.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
svraddr.sin_addr.S_un.S_addr = inet_addr("192.168.77.2");
if (::bind(hSocket,
(SOCKADDR*)&svraddr, sizeof(svraddr)) == SOCKET_ERROR)
{
puts("ERROR: 소켓에 IP주소와 포트를 바인드 할 수 없습니다.");
return 0;
}
if (::listen(hSocket, SOMAXCONN) == SOCKET_ERROR)
{
puts("ERROR: 리슨 상태로 전환할 수 없습니다.");
return 0;
}
puts("Echo 서버를 시작합니다.");
SOCKADDR_IN clientaddr = { 0 };
int nAddrLen = sizeof(clientaddr);
SOCKET hClient = 0;
char szBuffer[128] = { 0 };
int nReceive = 0;
while ((hClient = ::accept(hSocket,
(SOCKADDR*)&clientaddr, &nAddrLen)) != INVALID_SOCKET)
{
puts("새 클라이언트가 연결되었습니다.");
while ((nReceive = ::recv(hClient,
szBuffer, sizeof(szBuffer), 0)) > 0)
{
::send(hClient, szBuffer, sizeof(szBuffer), 0);
puts(szBuffer);
memset(szBuffer, 0, sizeof(szBuffer));
}
::closesocket(hClient);
puts("클라이언트 연결이 끊겼습니다.");
}
::closesocket(hSocket);
::WSACleanup();
return 0;
}
127.0.0.1 과 원래 주소 192.168.0.4 는 다른 주소로 처리가 된다. 가급적 htonl(INADDR_ANY) 를 사용하지말고, inet_addr ("192.168.0.4") 로 사용하라 Secure 관련 편의성과 보안성을 반비례한다.