게임서버

게임서버) 6. 소켓프로그래밍

PJNull 2023. 1. 11.
728x90
반응형

소켓프로그래밍

//TODO

 

 

서버 소켓프로그래밍

 

순서

 

  • 0.소켓초기화
  • 1.새로운 소켓 생성
  • 2.소켓에 주소/포트 번호 바인딩
  • 3.Listen설정
  • 4.Accept설정
  • 5.통신

 

 

//필요한 헤더파일


#include <winsock2.h>
#include <ws2tcpip.h>
#include <windows.h>//<<<<<<순서가 중요!

#pragma comment(lib,"ws2_32.lib")

※주의:winsock와 windows헤더파일을 불러올때 순서가 매우 중요하다. windows가 winsock보다 위에 있을 경우 winsock를 인식하지 못할수도 있다.

 

 

소켓 생성

 

0.소켓 초기화

 

int main()
{
	WSADATA wsaData;
	if(::WSAStartup(MAKEWORD(2,2),&wsaData)!=0)return 0;
    //2.2버젼으로 winsock을 초기화하는 단계
}

 

 

WSADATA (winsock.h) - Win32 apps

The WSADATA (winsock.h) structure contains information about the Windows Sockets implementation.

learn.microsoft.com

 

 

1. 소켓 생성

 

SOCKET listenSocket = ::socket(AF_INET,SOCK_STREAM,0);
if (listenSocket == INVALID_SOCKET)return 0;

 

 

소켓함수는 SOCKET socket(int domain, int type, int protocol)으로 되어 있으며, 각각의 매개변수는 다음과 같다.

Domain:어떤 영역에서 통신할 것인지에 대한 영역(프로토콜 family)을 지정하는 단계

Address Family(AF_INET=IPv4,AF_INET6=IPv6)

 

type:어떤 타입의 프로토콜을 사용할 것인지에 대해 설정. TCP(SOCKET_STREAM)  / UDP(SOCKET_DGRAM)

 

protocol :어떤 프로토콜의 값을 결정.IPPROTO_TCP(TCP 일때), IPPROTO_UDP(UDP 일때)

 

return값: 해당 소켓을 가리키는 소켓 디스크립터(socket descriptor)를 반환한다.

return -1 :소켓 생성 실패

return >=0: socket descriptor 반환.

 

ErrorCode: 각종 에러들을 나타내주는 코드를 int로 반환해준다 int error=::WSAGetLastError()

 

Windows 소켓 오류 코드(Winsock2.h) - Win32 apps

WSAGetLastError 함수에서 반환된 Windows Sockets(Winsock) 오류 코드입니다.

learn.microsoft.com

 

 

 

2.주소/포트 번호 바인딩

 

	SOCKADDR_IN serverAddr;
	::memset(&serverAddr,0,sizeof(serverAddr));
	serverAddr.sin_family = AF_INET;
	serverAddr.sin_addr.s_addr = ::htonl(INADDR_ANY);
	serverAddr.sin_port = ::htons(7777);
   	if (::bind(listenSocket, (SOCKADDR*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR)
   	return 0;

 

SOCKETADDR구조체: sockaddr 구조체는 소켓의 주소를 담는 기본 구조체 역할을 한다.

struct sockaddr 
{
	u_short    sa_family;     // address family, 2 bytes
	char    sa_data[14];     // IP address + Port number, 14 bytes
};

sa_family:주소체계를 구분하기 위한 변수

sa_data: 실제 주소를 저장하기 위한 변수

 

SOCKETADDR_IN구조체:sockaddr 구조체에서 sa_family가 AF_INET인 경우에 사용하는 구조체이며,sockaddr을 그대로 사용할 경우, sa_data에 IP주소와 Port번호가 조합되어 있어 쓰거나 읽기를 할때 불편할수 있기에 SOCKETADDR_IN을 사용한다.

struct sockaddr_in 
{
	short    sin_family;          // 주소 체계: AF_INET
	u_short  sin_port;            // 16 비트 포트 번호, network byte order
	struct   in_addr  sin_addr;   // 32 비트 IP 주소
	char     sin_zero[8];         // 전체 크기를 16 비트로 맞추기 위한 dummy
};

struct  in_addr 
{
	u_long  s_addr;     // 32비트 IP 주소를 저장 할 구조체, network byte order
};

sin_family : 항상 AF_INET을 설정한다.

sin_port : 포트번호를 가진다. 2bytes 이다. 즉, 포트번호는 0~65535 의 범위를 갖는 숫자 값이다. 이 변수에 저장되는 값은 network byte order이어야 한다. 본 프로젝트에서는 사용하지 않을것 같은 7777포트를 설정하였다.

여기서 포트번호는 WellKnown포트,Registered포트, Dynamic/Private포트로 나눌수 있다.

 

WellKnown포트:0~1023번까지를WellKnown포트라 하며, 특권포트,privileged port 혹은 reserved port 라고 불리우며, 예약된 포트를 의미한다. 이 영역내에 있는 포트를 이용하는 프로그램을 작동하려면 반드시 root 권한으로 작동하여야 한다.

Registered포트:1024~49151까지를 Registered포트라 하며,비특권 포트이며, 사용자가 직접 등록할 수 있는 포트를 의미한다. root권한을 가지지 않는 유저라면 이 Registered포트부터 바인딩이 가능하다.

Dynamic/Private포트: 49152~65535까지를 Dynamic/Private포트라고 하며, 수시로 변경되는 포트이다. 레지스터포트와 마찬가지로 비특권 포트이며, 레지스트포트와 다르게 지정하지 않더라도 알아서 포트가 할당된다.

 

sin_addr : 호스트 IP주소이다.
이 변수에는 INADDR_ 로 시작하는 값, 예를 들면 'INADDR_ANY'(사용가능한 랜카드의 IP주소) 와 같은 것이 저장되어야 한다. 혹은 inet_aton( ), inet_addr( ), inet_makeaddr( ) 과 같은 라이브러리가 제공하는 함수의 반환값이 저장되어야 한다. 혹은 name resolver를 통해 직접 설정도 가능하다. gethostbyname( )을 통해 host의 IP를 얻어 올 수도 있다.

sin_zero : 8 bytes dummy data이다. 반드시 모두 0으로 채워져 있어야 한다. sockaddr_in가 sin_zero를 제외한 크기(sin_family + sin_port + sin_addr)가 8 bytes이므로, 총합이 16 bytes이다. struct sockaddr구조체와 크기를 일치시키려는 목적으로 사용한다. padding bytes 혹은 padding data 라고도 한다.

 

 

htonl/htons

htonl htons는 현재 우리 PC에서 사용되고 있는 것을 네트워크환경에 맞게 변환시켜주는 함수이다. 이 둘을 사용하는 이유를 알기 위해서는 엔디언에 대해서 알아야된다. 먼저 엔디언은 1차원의 공간에 여러 개의 연속된 대상을 배열하는 방법을 뜻하며, BigEndian LettleEndian 그리고 두가지 모두 지원하는 MiddleEndian이 있다.

LettleEndian:리틀엔디언의 장점은 수학적 연산이 쉽다는 것이다. 따라서 연산이 빈번하게 일어나는 개인PC환경에서는 대부분 리틀엔디언을 사용하고 있다.

BigEndian:빅엔디언의 장점은 숫자 비교가 빠르다는 것이다. IP주소등의 숫자를 비교가 빈번하게 일어나는 네트워크 환경에서는 대부분 빅엔디언을 사용하고 있다.

htonl: 호스트 바이트 정렬 방식의 4바이트 데이터를 네트워크 바이트 정렬 방식으로 변환   u_long htonl(u_long hostlong); 

htons: 호스트 바이트 정렬 방식의 2바이트 데이터를 네트워크 바이트 정렬 방식으로 변환 u_short htons(u_short hostshort);

 

 

3.Listen설정

 

 

	if (::listen(listenSocket, SOMAXCONN) == SOCKET_ERROR)
		return 0;

 

 

4.Accept설정

 

while (true)
	{
		SOCKADDR_IN clientAddr;		
		::memset(&clientAddr, 0, sizeof(clientAddr));
		int32 addrLen = sizeof(clientAddr);

		SOCKET clientSocket=::accept(listenSocket,(SOCKADDR*)&clientAddr,&addrLen);
		if (clientSocket == INVALID_SOCKET) return 0;

		char ip[16];
		::inet_ntop(AF_INET,&clientAddr.sin_addr,ip,sizeof(ip));
		cout << "Client Connect: " << ip << endl;


	}

 

accept

accept함수는 해당 소켓에 연결 요청이 왔을 때 연결을 받아들이는 함수(블로킹 소켓)이며, 연결 요청 대기 큐에서 대기 중인클라이언트의 연결 요청을 수락하는 기능의 함수이다. 따라서 연결이 되지않으면 계속해서 대기하고 있다. 연결이 성공적으로 이루어졌을 때 리턴되는 값은 연결을 받아들인 새로운 소켓 디스크립터이다.

 

 

int accept(int sockect, struct sockaddr* addr, socklent_t *addrlen);

int socket: 연결을 기다리는 소켓 디스크립터.

struct sockaddr* addr: 받아들인 Client 주소 및 포트 정보가 저장될 구조체의 주소값이다.

socklent_t* addrlen: sockaddr 구조체의 길이가 저장된 변수의 주소값이다.

 

 

 

5.통신

 

while (true)
{
	char recvbuffer[100];
	int32 recvLen=::recv(clientSocket, recvbuffer, sizeof(recvbuffer),0);
	if (recvLen <= 0)return 0;

	cout << "Server: " << recvbuffer << endl;
}

 

 

클라이언트 소켓프로그래밍 

 

 

순서

  • 0.소켓 초기화
  • 1.소켓생성
  • 2.서버 연결 요청
  • 3.통신

 

소켓생성

 

 

0. 소켓 초기화

 

 

int main()
{
	WSADATA wsaData;
	if(::WSAStartup(MAKEWORD(2,2),&wsaData)!=0)return 0;
    //2.2버젼으로 winsock을 초기화하는 단계
}

 

 

1.소켓생성

 

 

	SOCKET ClientSocket = ::socket(AF_INET, SOCK_STREAM, 0);
	if (ClientSocket == INVALID_SOCKET)return 0;

 

2. 서버 연결 요청

 

	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);

	if (::connect(ClientSocket, (SOCKADDR*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR)
	{
		cout << "error" << endl;
		return 0;
	}

 

inet_pton: 이 함수는 Address Family가 가르키는 네트워크 주소를 src의 문자열 값으로 변환하고, dest에 복사한다.

int inet_pton(int af,const char* src,void *dest)

af : Address family를 지정한다. src의 문자열이 IPv4 주소를 나타내는지, IPv6 주소를 나타내는지를 함수에 알린다. 본프로젝트에서는 IPv4를 사용하였다.

src: 문자열 형태의 IP주소를 넣는다. 여기서 자신의 IP/포트 번호가 필요로 하지 않는 이유는 운영체제가 자동적으로 IP주소와 포트번호를 지정하여 호스트에 보내주기 때문이다.

dst : src를 binary형태로 변환 후 복사한 메모리의 포인터.

 

 

3.통신

 

	while (true)
	{
		char sendBuffer[100] = Client!";
		int32 result = ::send(ClientSocket, sendBuffer, sizeof(sendBuffer),0);
		if (result == SOCKET_ERROR)return 0;
	}

 

 

 

블로킹 소켓 Send/Receive 매커니즘

 

블로킹 소켓이란 디바이스에 처리 요청을 걸어 놓고 응답을 대기하는 함수를 호출할 때 스레드에서 발생하는 대기 현상을 의미한다. 위에서 언급한 Accept와 같이 send와 receive또한 블로킹방식이다. 그렇다면 이러한 블로킹 소켓은 어떤식으로 작동할까?

 

우리가 생각하고 있는 전송방식은 밑의 그림과 같이 send를 보내면 서버에 다이렉트로 간다고 생각하기 쉽다.

우리가 생각하는 전송방식

 

 

하지만 send를 보내게 되면 커널의 sendBuffer로 이동하게 된다.

 

그 후 sendBuffer에서 우리가 지정했던 IP주소의 RecvBuffer로 이동하게 된다.

 

 

RecvBuffer에 대기하고 있던 메시지는 ::Recv()에 의해 서버로 복사되어 전달되는 것이다.

 

※유의사항: 위에서 언급한것과 같이 sendBuffer과 RecvBuffer를 통해 데이터 전송이 이루어진다. 즉, Send는 Recv가 없어도 상대의 RecvBuffer로 이동이 가능하지만, Recv는 상대의 Send가 없으면 RecvBuffer가 비어있기 때문에 수행이 불가능하다.

 

send/receive 함수

 

int send(int socket, const void *msg, size_t len, int flags);
int recv(int socket, void *buf, size_t len, int flags);

 

send: 정보를 받을 소켓 디스크립터 주소
recv: 정보를 보내는 소켓 디스크립터 주소

int socket: 통신의 주체가 되는 소켓 디스크립터

const void *msg : 상대에게 보낼 자료의 포인터

void *buf : 받은 메세지를 저장할 버퍼 포인터

size_t len : 전송되는 메세지의 크기 (byte 단위)

int flags :  

  • MSG_DONTWAIT:전송 전에 대기 상태가 필요하다면 기다리지 않고 -1을 반환하면서 복귀. 수신을 위해 대기가 필요하다면 기다리지 않고 -1을 반환하면서 바로 복귀
  • MSG_NOSIGNAL:상대방과 연결이 끊겼을 때 , SIGPIPE 시그널을 받지 않도록 한다.

 

 

 

에코 서버 테스트

 

양방향으로 잘 작동하는지 확인을 위해 에코서버를 테스트한다.

//Client.cpp

while (true)
	{
		char sendBuffer[100] = "Hello Client!";
		int32 result = ::send(ClientSocket, sendBuffer, sizeof(sendBuffer),0);
		if (result == SOCKET_ERROR)return 0;

		char recvbuffer[100];
		int32 recvLen = ::recv(ClientSocket, recvbuffer, sizeof(recvbuffer), 0);
		if (recvLen <= 0)return 0;

		cout << "Eco Client: " << recvbuffer << endl;
		
		this_thread::sleep_for(1s);


	}
//Server.cpp

while (true)
		{
			char recvbuffer[100];
			int32 recvLen=::recv(clientSocket, recvbuffer, sizeof(recvbuffer),0);
			if (recvLen <= 0)return 0;

			cout << "Server: " << recvbuffer << endl;

			int32 result = ::send(clientSocket,recvbuffer,recvLen,0);
			if (result == SOCKET_ERROR)return 0;
		}

 

 

 

소켓옵션

 

네트워크 통신이 하나의 서버에 하나의 클라이언트만으로 이루어진다고 할수 없으며, 네트워크 환경을 모두 예측하기 어렵기 때문에 세부적인 사항들을 조절해야될 필요가 있다.

소켓옵션을 사용하는것에는 getsocketopt와 setsocketopt 두 함수를 사용한다.

GetSocketopt:

int getsockopt(int socket, int level, int option_name, void *restrict option_value, socklen_t *restrict option_len);

SetSocketopt:

int setsockopt(int socket, int level, int option_name, const void *option_value, socklen_t option_len);

 

변수

 socket: socket으로 소켓 디스크립터

level: 프로토콜 레벨을 의미합니다. (SOL_SOCKET, IPPROTO_IP, IPPROTO_TCP, IPPROTO_IPV6)

option_name: 옵션의 세부대상을 의미하며, Level에 따라서 다양한 옵션이 있다. 

 

option_value: void 포인터 타입으로, 각각의 옵션에 따라서 다양하게 타입을 넣을 수 있다.

option_len: option_value의 크기

 

 

옵션

option_name에 설정하는 값으로써 옵션을 설정함으로써 다양한 기능을 활용할수 있다. 이중에서 중요하다고 생각되는 옵션은 SO_REUSEADDR,TCP_NODELAY,SO_KEEPALIVE가 있다.

 

SO_REUSEADDR:  이 옵션을 설정하면 커널이 소켓을 사용하는 중에도 계속해서 사용할 수 있다. 이 옵션은 서버프로그램이 종료된 후에도 커널이 소켓의 포트를 아직 점유 중인 경우에 서버 프로그램을 다시 구동해야 할 때 매우 유용하다.

TCP_NODELAY:이 옵션을 이해하려면 Nagle알고리즘에 대해서 이해를 해야 한다.  Nagle 알고리즘이 적용되면, 운영체제는 패킷을 ACK가 오기를 기다렸다가 도착하면, 그 동안 쌓여있던 데이터를 한꺼번에 보내게 된다. 
이러한 방식을 사용하게 되면, 대역폭이 낮은 WAN에서 빈번한 전송을 줄이게 됨으로 효과적인 대역폭활용이 가능해진다.
대부분의 경우에 있어서 Nagle 알고리즘은 효율적으로 작동하긴 하지만, 빈번한 응답이 중요한 서비스의 경우에는 적당하지 않은 경우가 발생한다. 실시간적인 반응이 중요한 온라인 게임에서는 서버에서는 Nagle 알고리즘을 제거하는게 좋고, 필요할 경우 컨텐츠쪽에서 처리를 하는것이 좋다.

SO_KEEPALIVE:TCP환경에서 주기적으로 연결상태를 확인하는 옵션이다. 온라인게임에서 연결이 끊어진것과 대기상태를 구분하기 위해서 사용된다. 이 옵션은 Ping패킷으로 대체가 가능하다.

 

 

setsockopt function (winsock.h) - Win32 apps

The setsockopt function (winsock.h) sets a socket option.

learn.microsoft.com

 

 

커스텀 SetSocketOpt

 

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));
}

bool SetLinger(SOCKET socket, uint16 onoff, uint16 linger)
{
	LINGER option;
	option.l_onoff = onoff;
	option.l_linger = linger;
	return SetSockOpt(socket, SOL_SOCKET, SO_LINGER, option);
}

bool Bind(SOCKET socket, SOCKADDR_IN sockAddr)
{
	return SOCKET_ERROR != ::bind(socket, (SOCKADDR*)&sockAddr, sizeof(SOCKADDR_IN));
}

bool SocketUtils::BindAnyAddress(SOCKET socket, uint16 port)
{	
	SOCKADDR_IN myAddress;
	myAddress.sin_family = AF_INET;
	myAddress.sin_addr.s_addr = ::htonl(INADDR_ANY);
	myAddress.sin_port = ::htons(port);

	return SOCKET_ERROR != ::bind(socket, reinterpret_cast<const SOCKADDR*>(&myAddress), sizeof(myAddress));
}



bool SetTcpNoDelay(SOCKET socket, bool flag)
{
	return SetSockOpt(socket, SOL_SOCKET, TCP_NODELAY, flag);
}//나머지는 옵션들은 이와같은 형식을 가짐

 

728x90
반응형

'게임서버' 카테고리의 다른 글

게임서버) 8. Select모델  (0) 2023.01.13
게임서버) 7. 논블로킹 소켓  (0) 2023.01.12
게임서버) 3.멀티쓰레드 공유자원  (0) 2023.01.06
게임서버) 2. 멀티쓰레드  (0) 2023.01.06
게임서버) 1. 서버개론  (0) 2023.01.06

댓글