1. 소켓(Socket) 통신
소켓은 IP 주소와 포트 번호로 네트워크를 통해 통신할 수 있는 기능을 제공한다.
주로 TCP/IP 기반의 프로토콜이 사용된다.
여기서는 윈도우가 기본으로 제공하는 API인 WinSock을 사용한다.
(1) CorePch.h 수정
#pragma once
#include "Types.h"
#include "CoreMacro.h"
#include "CoreTLS.h"
#include "CoreGlobal.h"
#include <vector>
#include <list>
#include <queue>
#include <stack>
#include <map>
#include <set>
#include <unordered_map>
#include <unordered_set>
#include <iostream>
using namespace std;
#include <winsock2.h>
#include <ws2tcpip.h>
#include <windows.h>
#pragma comment(lib, "ws2_32.lib")
소켓 통신을 위하여 winsock2, ws2tcpip를 추가한다.
단, 헤더 순서가 올바르지 않으면 오류가 발생하므로, windows 헤더를 최하단에 위치시킨다.
2. 서버에서 소켓 통신하기
(1) 순서
- 새로운 소켓 생성 (Socket)
- 소켓에 주소 및 포트 번호 설정 (Bind)
- 신호 대기 (Listen)
- 클라이언트 접속 (Accept)
- 클라이언트와 통신
(2) WinSock 실행 - Server -> Server.cpp
int main()
{
// Windows Socket 시작
WSADATA wsaData;
// 2.2버전 사용
if (::WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
return 0;
// 종료
::WSACleanup();
}
위의 코드가 실행되지 않으면 헤더의 위치를 변경하여야한다.
(3) IPv4, TCP 방식으로 소켓 생성
// IPv4를 사용, TCP 사용(<->UDP : SOCK_DGRAM), protocol
// int32 errorCode = ::WSAGetLastError();
SOCKET listenSocket = ::socket(AF_INET, SOCK_STREAM, 0);
if (listenSocket == INVALID_SOCKET)
return 0;
(4) 주소 및 포트 번호 설정(Bind)
// 바인딩
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); // 80 : HTTP
if (::bind(listenSocket, (SOCKADDR*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR)
return 0;
(4-1) sin_family
IP주소 유형을 설정한다. AF_INET은 IPv4를 의미한다.
(4-2) sin_addr.s_addr
어떤 ip를 통과시킬 것인지 설정한다. INADDR_ANY는 가능한 모든 IP주소를 뜻한다.
(4-3) sin_port
어떤 포트를 통하여 통신할 것인지 설정한다.
(4-4) bind
IP와 포트 번호를 listenSocket에 연결한다.
(5) 클라이언트 연결 요청 대기(Listen)
// 리슨
if (::listen(listenSocket, SOMAXCONN) == SOCKET_ERROR)
return 0;
(6) 클라이언트 접속(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;
}
클라이언트의 소켓을 통해 통신을 하게 된다.
이제 여기에 코드를 추가하여 클라이언트의 IP를 가져오는 실습을 한다.
// 클라이언트 접속
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 << "클라이언트가 연결됨. IP = " << ip << endl;
// TODO
}
이제 클라이언트가 접속하기 전까지 accept에서 대기한다.
3. 클라이언트에서 서버로 접속하기(DummyClient 프로젝트)
(1) 순서
- 소켓 생성
- 서버에 연결 요청
- 통신
(2) 소켓 생성(Socket)
// 서버의 주소와 포트 번호 설정
SOCKADDR_IN serverAddr;
::memset(&serverAddr, 0, sizeof(serverAddr));
serverAddr.sin_family = AF_INET;
//serverAddr.sin_addr.s_addr = ::htonl(INADDR_ANY);
::inet_pton(AF_INET, "127.0.0.1", &serverAddr.sin_addr);
serverAddr.sin_port = ::htons(7777); // 80 : HTTP
127.0.0.1은 루프백 주소로 내 컴퓨터(로컬)의 주소를 가리킨다.
즉 내 컴퓨터 내에서만 사용하는 IP주소이다.
(3) 서버 주소와 포트에 연결(Connect)
// 서버에 연결
if (::connect(clientSocket, (SOCKADDR*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR)
return 0;
// 연결 성공
cout << "서버에 연결함." << endl;
while (true)
{
}
이제 서버의 주소를 가져와 클라이언트 소켓을 통해 서버에 연결한다.
(4) 마무리
// 소켓 닫음
::closesocket(clientSocket);
// 종료
::WSACleanup();
}
서버에 정상적으로 연결되었다면 서버 쪽과 클라이언트 쪽 콘솔에 정상적으로 로그가 떠야한다.
(5) htonl, htons
각 머신마다 0x12345678을 읽는 방법이 다를 수 있다.
ex) 리틀 엔디언 vs 빅 엔디언
리틀 엔디언 방식 : low - [0x78] [0x56] [0x34] [0x12] - high
빅 엔디언 방식 : low - [0x12] [0x34] [0x56] [0x78] - high
htonl은 32비트, htons는 16비트 정수를 네트워크 규격에 맞도록 엔디언 방식을 변환하여 출력한다.
4. 서버-클라이언트 통신하기
(1) 데이터 전송 - DummyClient
while (true)
{
char sendBuffer[100] = "클라이언트 메세지 전송 테스트";
int32 resultCode = ::send(clientSocket, sendBuffer, sizeof(sendBuffer), 0);
if (resultCode == SOCKET_ERROR)
return 0;
this_thread::sleep_for(1s);
}
클라이언트의 루프를 수정한다.
(2) 데이터 수신 - Server
// TODO
while (true)
{
char recvBuffer[100];
int32 recvLen = ::recv(clientSocket, recvBuffer, sizeof(recvBuffer), 0);
if (recvLen <= 0)
return 0;
cout << "Recv Data : " << recvBuffer << endl;
cout << "Recv Data Len : " << recvLen << endl;
}
클라이언트가 보낸 데이터를 출력한다.
(3) 에코 서버 - Server
서버가 받은 데이터를 클라이언트에게 돌려준다.
cout << "Recv Data : " << recvBuffer << endl;
cout << "Recv Data Len : " << recvLen << endl;
int32 resultCode = ::send(clientSocket, recvBuffer, recvLen, 0);
if (resultCode == SOCKET_ERROR)
return 0;
(4) 데이터 수신 - DummyClient
while (true)
{
char sendBuffer[100] = "클라이언트 메세지 전송 테스트";
int32 resultCode = ::send(clientSocket, sendBuffer, sizeof(sendBuffer), 0);
if (resultCode == SOCKET_ERROR)
return 0;
char recvBuffer[100];
int32 recvLen = ::recv(clientSocket, recvBuffer, sizeof(recvBuffer), 0);
if (recvLen <= 0)
return 0;
cout << "Echo Data : " << recvBuffer << endl;
this_thread::sleep_for(1s);
}
기존에 서버에서 데이터를 받는 부분을 그대로 복사해서 클라이언트에 추가한다.
서로가 데이터를 주고 받으면서 통신을 한다. 이를 사용하여 클라이언트 간의 상호작용을 서버를 통해 계산하고 주고 받을 수 있다.
5. 소켓 통신 원리
(1) 블로킹(Blocking)
위에서 클라이언트의 recv send 함수를 보면, 데이터를 보내거나 받을 때까지 멈춰있는 것을 보았을 것이다.
즉, 위의 소켓 서버는 블로킹 상태를 사용하기 때문에, 소켓 연산이 완료될 때까지 무한정 대기한다.
이는 기본적인 소켓 서버는 다중 접속 온라인 게임에서 사용될 수 없음을 뜻한다.
따라서 비-블로킹(Non-Blocking) 소켓을 구성하는 등의 방법을 사용하여야 한다.
(2) 클라이언트가 일방적으로 패킷을 보내는 경우
서버가 데이터를 받지 않을 때(recv 하지 않을 때) 클라이언트가 Send를 하면 정상적으로 Send가 되는 것을 알 수 있다.
이유는 소켓을 만들때 커널 단계에서 클라이언트와 서버에 리시브 버퍼와 샌드 버퍼가 동시에 생성이 되기 때문이다.
즉, 클라이언트는 본인의 샌드 버퍼에 데이터를 저장하면 성공적으로 Send 했다고 판단하게 된다.
실제 통신은 클라이언트의 샌드 버퍼가 서버의 리시브 버퍼에 패킷을 보내는 것이다.
이후에 리시브 버퍼의 데이터를 복사하여 가지고 와서 출력한다.
(3) Send가 실패하는 경우
클라이언트의 샌드 버퍼가 꽉 차게되면 다음으로 데이터를 보낼 때 데이터를 정상적으로 Send할 수 없다고 판단하게 되어 Send가 실패하게 된다.
(4) 서버에서 데이터를 받는 recvBuffer의 동작
// TODO
while (true)
{
char recvBuffer[100];
int32 recvLen = ::recv(clientSocket, recvBuffer, sizeof(recvBuffer), 0);
서버에서 받는 데이터를 100바이트만 가져온다.
100바이트보다 커도 100바이트 만큼만 가져온다.
100바이트보다 작으면 해당 사이즈 만큼만 가져온다.
'서버 프로그래밍 > 네트워크' 카테고리의 다른 글
2-6. 소켓 입출력 모델 (2) - Overlapped 모델 (0) | 2023.12.01 |
---|---|
2-5. 소켓 입출력 모델 (1) - Select 모델 (Select, WSAEventSelect) (0) | 2023.11.30 |
2-4. 소켓 옵션 설정, 논-블로킹 소켓(Non-Blocking Socket) (0) | 2023.11.30 |
2-3. TCP와 UDP (0) | 2023.11.29 |
2-1. 프로젝트 설정, 스레드 매니저(ThreadManager) (0) | 2023.11.29 |