1. 소켓에 옵션 설정하기
// 소켓 옵션 설정
// setsockopt(소켓, level, option name, option value, option len);
::setsockopt(listenSocket, )
(1) 소켓 옵션(Level)
- IPPROTO_IP, IPV6, TCP, UDP ; IPv4, IPv6, TCP, UDP 수준에서 적용할 수 있는 소켓 옵션
- SOL_SOCKET ; 소켓 수준에서 적용할 수 있는 소켓 옵션
(2) 소켓 수준의 옵션 값(Option Value)
- SO_KEEPALIVE(불리언); 소켓 연결에 대해 연결 유지 패킷을 보낸다.
bool enable = true;
::setsockopt(listenSocket, SOL_SOCKET, SO_KEEPALIVE, (char*)&enable, sizeof(enable));
enable의 값에 따라 SO_KEEPALIVE가 적용 / 비적용 된다.
주기적으로 연결 유지 패킷을 보내 클라이언트가 서버에 아직도 연결되어있는지 확인할 수 있다.
TCP에서 작동한다.
- SO_LINGER(inger) ; 송신 버퍼의 데이터를 보낼지 설정한다.
- SO_SNDBUF, SO_RCVBUF(정수) ; 송신 버퍼와 수신 버퍼의 크기를 가져온다.
int32 sendBufferSize;
int32 optionLen = sizeof(sendBufferSize);
::getsockopt(listenSocket, SOL_SOCKET, SO_SNDBUF, (char*)&sendBufferSize, &optionLen);
- SO_REUSEADDR(불리언) ; 서버 소켓이 이미 사용되었던 주소와 포트에 바인딩할 수 있다.
bool enable = true;
::setsockopt(listenSocket, SOL_SOCKET, SO_REUSEADDR, (char*)&enable, sizeof(enable));
기존에 켰던 서버를 종료 후에 다시 키면, 기존의 IP와 포트를 사용 중이었기 때문에 서버가 켜지지 않을 수 있다.
이 때 이 옵션을 사용하면 서버 소켓이 이전에 사용했던 주소와 포트를 그대로 쓸 수 있다.
(3) TCP 수준의 옵션 값(Value)
- TCP_NODELAY ; Nagle 알고리즘을 작동할 지 선택한다.
bool enable = true;
::setsockopt(listenSocket, IPPROTO_TCP, TCP_NODELAY, (char*)&enable, sizeof(enable));
해당 옵션을 켜면 Nagle 알고리즘을 비활성화 한다.
* Nagle 알고리즘
작은 패킷을 모아서 버퍼 크기의 어떤 수준을 넘으면 한 번에 전송한다.
실시간 게임 서버에서는 작은 크기의 데이터를 빠르게 전송하는 것이 중요하기 때문에 Nagle 알고리즘을 끄는 것이 더 유리할 수 있다.
2. SocketUtil 클래스
Listen, Binding, setsockopt를 이용하여 TCP_NODELAY를 키는 함수 등 소켓을 조작하는 대부분의 코드가 여기에 들어간다.
#pragma once
/*----------------
SocketUtils
-----------------*/
class SocketUtils
{
public:
static LPFN_CONNECTEX ConnectEx;
static LPFN_DISCONNECTEX DisconnectEx;
static LPFN_ACCEPTEX AcceptEx;
public:
static void Init();
static void Clear();
static bool BindWindowsFunction(SOCKET socket, GUID guid, LPVOID* fn);
static SOCKET CreateSocket();
static bool SetLinger(SOCKET socket, uint16 onoff, uint16 linger);
static bool SetReuseAddress(SOCKET socket, bool flag);
static bool SetRecvBufferSize(SOCKET socket, int32 size);
static bool SetSendBufferSize(SOCKET socket, int32 size);
static bool SetTcpNoDelay(SOCKET socket, bool flag);
static bool SetUpdateAcceptSocket(SOCKET socket, SOCKET listenSocket);
static bool Bind(SOCKET socket, SOCKADDR_IN netAddr);
static bool BindAnyAddress(SOCKET socket, uint16 port);
static bool Listen(SOCKET socket, int32 backlog = SOMAXCONN);
static void Close(SOCKET& socket);
};
template<typename T>
static inline bool SetSockOpt(SOCKET socket, int32 level, int32 optName, T optVal)
{
return SOCKET_ERROR != ::setsockopt(socket, level, optName, reinterpret_cast<char*>(&optVal), sizeof(T));
}
3. 블로킹(Blocking)
소켓 서버에서 소켓 연산이 완료(소켓의 송, 수신 등)될 때까지 대기하는 생태이다.
서버가 단일 연결을 처리하는 동안 또 다른 클라이언트와의 통신이 불가능하기 때문에 다중 클라이언트 상황에서는 사용할 수 없다(MMORPG와 같은 다중 접속 멀티 온라인게임).
(1) 논-블로킹 소켓(Non-Blocking Socket) - Server
소켓 연산이 완료되지 않더라도 대기하지 않고 즉시 다음 코드로 진행되는 소켓.
// 논 블로킹 소켓
u_long on = 1;
if (::ioctlsocket(listenSocket, FIONBIO, &on) == INVALID_SOCKET)
return 0;
ioctlsocket 함수를 사용하면 논 블로킹 소켓을 만들 수 있다. on은 flag로 동작한다.
(2) TCP 서버 구현
// 이전 주소 재사용
SocketUtils::SetReuseAddress(listenSocket, true);
// 바인딩
if (SocketUtils::BindAnyAddress(listenSocket, 7777) == false)
return 0;
// 리슨
if (SocketUtils::Listen(listenSocket) == false)
return 0;
while (true)
{
SOCKADDR_IN clientAddr;
int32 addrLen = sizeof(clientAddr);
// 수락, 패킷을 받지 못해도 다음 코드로 진행
SOCKET clientSocket = ::accept(listenSocket, (SOCKADDR*)&clientAddr, &addrLen);
if (clientSocket == INVALID_SOCKET)
{
// 아무도 접속을 안했을 때, 패킷을 받지 못했을 때 스킵
if (WSAGetLastError() == WSAEWOULDBLOCK)
continue;
}
cout << "클라이언트가 연결됨." << endl;
// RecvBuff
while (true)
{
char recvBuffer[100];
int32 recvLen = ::recv(clientSocket, recvBuffer, sizeof(recvBuffer), 0);
if (recvLen == SOCKET_ERROR)
{
// 아무도 접속을 안했을 때 패킷을 받지 못했을 때 스킵
if (WSAGetLastError() == WSAEWOULDBLOCK)
continue;
// 기타 에러 TODO
break;
}
cout << "Recv Data = " << recvBuffer << endl;
cout << "Recv Data Len = " << recvLen << endl;
}
}
논 블로킹 소켓은 블로킹 소켓과는 다르게 패킷을 받지 못해도 코드가 진행이 된다.
따라서 패킷을 받지 못한게 클라이언트가 접속을 안해서 그런건지 한 번 더 걸러줘야 한다.
아직 클라이언트로부터 패킷을 받지 못했다면 continue로 인해 코드의 처음으로 돌아가게 된다.
이후에 클라이언트가 연결되었다면 반복문을 한번 더 돌면서 리시브 버퍼에 해당 패킷을 받아준다.
(3) 클라이언트에 TCP 통신 구현
// Windows Socket 시작
SocketUtils::Init();
// 소켓 생성
SOCKET clientSocket = ::socket(AF_INET, SOCK_STREAM, 0);
if (clientSocket == INVALID_SOCKET)
return 0;
// 논 블로킹 소켓 설정
u_long on = 1;
if (::ioctlsocket(clientSocket, FIONBIO, &on) == INVALID_SOCKET)
return 0;
// 서버 주소와 포트 가져옴
SOCKADDR_IN serverAddr;
::memset(&serverAddr, 0, sizeof(serverAddr));
serverAddr.sin_family = AF_INET;
::inet_pton(AF_INET, "127.0.0.1", &serverAddr.sin_addr);
serverAddr.sin_port = htons(7777); // 80 : HTTP
// 연결
while (true)
{
if (::connect(clientSocket, (SOCKADDR*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR)
{
// 서버에 접속하지 못했을 때 스킵
if (WSAGetLastError() == WSAEWOULDBLOCK)
continue;
// 커넥션을 요청했는데 또 요청한 경우 -> 이미 연결된 상태라고 가정
if (WSAGetLastError() == WSAEISCONN)
break;
}
}
// 패킷 전송, 1초 마다
while (true)
{
char sendBuffer[100] = "나는 클라이언트야.";
int32 sendLen = sizeof(sendBuffer);
if (::send(clientSocket, sendBuffer, sendLen, 0) == SOCKET_ERROR)
{
// 서버에 접속하지 못했을 때 스킵
if (WSAGetLastError() == WSAEWOULDBLOCK)
continue;
}
cout << "데이터 전송 선공, Len = " << sendLen << endl;
this_thread::sleep_for(1s);
}
// 종료
SocketUtils::Clear();
클라이언트는 서버와 다르게 연결 요청 후 또 요청을 보낼 수 있다. 이런 경우에는 연결이 성사되었다고 가정한다.
블로킹 소켓과는 다르게 멀티 클라이언트 작업이 가능해지지만, 예외 사항을 체크하는 부분이 계속 들어가므로 코드가 복잡해진다는 문제가 있다.
또한 아무도 서버로 데이터를 보내지 않는 상황이면 다시 코드의 처음으로 돌아가야된다는 문제가 있다.
- 해결 방법
Select, WSAEventSelect를 사용하면 accept와 recv를 패킷을 받았을 때만 실시할 수 있다.
'서버 프로그래밍 > 네트워크' 카테고리의 다른 글
2-6. 소켓 입출력 모델 (2) - Overlapped 모델 (0) | 2023.12.01 |
---|---|
2-5. 소켓 입출력 모델 (1) - Select 모델 (Select, WSAEventSelect) (0) | 2023.11.30 |
2-3. TCP와 UDP (0) | 2023.11.29 |
2-2. 소켓 통신 - WinSock API (0) | 2023.11.29 |
2-1. 프로젝트 설정, 스레드 매니저(ThreadManager) (0) | 2023.11.29 |