이번 강의는 소켓 프로그래밍에 대한 내용이다.
이 내용은 열혈 TCP/IP 책에서 숙지한 내용들과 겹치는 부분이 많아서 숙지하는데 어렵지 않았고
내용을 복기하고 디테일을 잡아가는 마음으로 공부했다.
서버
/*소켓 초기화*/
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData))
return 0;
- WSADATA는 WSA + DATA이며, WSA는 Windows Socket API의 약자이다.
- WSAStartup함수를 호출할 때 필요한 정보를 저장하고 전달하는데 사용되는 구조체다.
SOCKET listenSocket = socket(AF_INET/*IPv4*/, SOCK_STREAM/*TCP 방식 사용*/, 0);
if (listenSocket == INVALID_SOCKET)
return 0;
- 소켓을 생성하는 코드
- socket 함수의 첫 번째 인자는 address family로 IPv4, IPv6과 같은 주소체계를 의미하는 인자를 받는다.
- 두 번째 인자는 TCP인지 UDP인지 타입에 관한 정보를 받는다.
- 세번째 인자는 프로토콜인데 첫번째 인자에 따라 선택할 수 있는 세부 사항이다

- 이 값을 0으로 주게 되면 서버측의 프로토콜을 따르게 된다.

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);
- SOCKADDR_IN 구조체는 소켓을 바인딩하기위한 구조체, 주소 체계 뿐만 아니라 IP 주소, 포트 번호 등을 지정한다
- socket에서 af 인자에 AF_INET을 넣었으므로 여기도 동일하게 AF_INET을 넣어주어야 한다.
- htonl은 host to network long이라는 의미로 보통 컴퓨터에서 주소를 저장할 때 리틀엔디안 방식을 사용하는데,
네트워크에서는 빅 엔디안 방식을 사용하므로 이를 맞춰주기 위해 htonl을 통해 변환해주는 것. (자매품으로 htons도 있음)
- INADDR_ANY는 일반적으로 자기 자신의 주소를 넣어주게 된다.
- port는 이 ip 주소를 통해 들어올 수 있는 문을 세팅하는 것, 마찬가지로 htonl을 통해 리틀엔디안->빅엔디안으로 변환한다
// 서버 소켓에 ip 주소와 포트 번호를 할당
if (::bind(listenSocket, (SOCKADDR*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR)
return 0;
// 서버 소켓을 대기 상태로 전환 -> 이 과정을 거쳐야지만 accept에서 대기할 수 있다
if (::listen(listenSocket, SOMAXCONN) == SOCKET_ERROR)
return 0;
- bind는 위의 SOCKADDR_IN 구조체를 직접적으로 소켓에 연결시키는 것
- 강의에서의 비유를 빌리자면 소켓은 직원이며, bind는 직원을 교육시키는 것
- listen은 서버 소켓을 대기 상태로 전환하기 위한 과정, 이 과정을 거쳐야지 accept에서 대기할 수 있게 된다.
- 이 과정을 거치지 않으면 accept에서 INVALID_SOCKET을 반환하며 로직에 따라 프로그램이 종료된다.
이후 과정은 while문을 통해 서버로서 클라이언트가 접속하는 것에 대한 처리이다.
별도의 설명이 없다면 while문 안에서 반복되는 코드라고 생각하면 된다.
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;
- 비유를 하자면 입구에서 손님을 위한 메뉴판 등을 준비해 놓는 것 (강의 내용과 상관x)
- accept가 값을 반환했다는 것은 클라이언트가 접속했다는 것이며, 그 클라이언트가 접속함과 동시에 소켓이 만들어진다.
- 이 소켓은 해당 클라이언트를 위한 소켓이며, 클라이언트에게 개인 종업원을 붙여준다고 생각해도 좋을 것이다.
- 이제 클라이언트와의 통신은 이 소켓을 통해 하게 된다.
아래 나오는 내용은 클라이언트가 접속한 이후의 작업이며, 클라이언트와 서버가 통신하는 과정이다.
char ip[16];
::inet_ntop(AF_INET, &clientAddr.sin_addr, ip, sizeof(ip));
cout << "Client connected! IP = " << ip << endl;
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;
int32 resultCode = ::send(clientSocket, recvBuffer, recvLen, 0);
if (resultCode == SOCKET_ERROR)
return 0;
}
- 클라이언트의 ip를 받아서 출력하고, while문을 통해 클라이언트와 계속 통신한다.
- send와 recv는 대칭적인 함수인데, 인자 자체는 같고 목적만 다르다.
- send는 어떤 소켓에 어떤 버퍼로부터 얼만큼의 데이터를 보낼 것인가를 인자로 받는다.
- recv는 어떤 소켓으로부터 어떤 버퍼에 얼만큼의 데이터를 받을 것인가를 인자로 받는다.
클라이언트
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
return 0;
SOCKET clientSocket = ::socket(AF_INET, SOCK_STREAM, 0);
if (clientSocket == INVALID_SOCKET)
return 0;
- 앞서 설명한 서버에서 작성한 내용과 겹치는 부분이 많다.
- 소켓을 초기화하고 서버와 통신하기 위한 소켓을 만들어준다.
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);
if (connect(clientSocket, (SOCKADDR*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR)
return 0;
- 연결할 서버에 대한 정보를 SOCKADDR_IN에 기재하고, connect 함수를 통해 서버에 접속을 시도한다.
- connect은 해당 서버와 연결되지 않으면 SOCKET_ERROR를 반환하고 로직에 의해 종료된다.
while (true)
{
// clientSocket의 sendBuffer(커널단)에 sendBuffer(유저단)의 데이터를 sizeof(sendBuffer)만큼 전달하겠다.
char sendBuffer[100] = "Hello ! I am Client!";
int32 resultCode = send(clientSocket, sendBuffer, sizeof(sendBuffer), 0);
if (resultCode == SOCKET_ERROR)
return 0; // 커널단의 sendBuffer에 잘 전달되었는지를 확인
/*-----------------------------------------------------------------------------------------------------*/
// clientSocket의 recvBuffer(커널단)으로부터 recvBuffer(유저단)에 sizeof(recvBuffer)만큼의 데이터를 받겠다
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);
}
- 위 내용은 클라이언트가 서버와 연결되었을 때의 이루어지는 코드이며, while문을 통해 서버와 지속적으로 통신한다.
서버와 클라이언트의 소통 방법
각 소켓은 커널단에 sendBuffer, recvBuffer를 할당받는데 이 버퍼들은 소켓 개별적으로 존재한다.
send 함수는 커널단의 sendBuffer에 데이터를 잘 밀어넣었는지 여부를 반환한다.
그렇다면 실제로 이 sendBuffer가 상대방의 recvBuffer에 전달되는 시점은 언제인가가 궁금해지는데
이 시점은 프로그래머 시점에서 확인이 불가능하며, 운영 체제의 네트워크 스택에 의해 처리된다.
네트워크 스택은 데이터를 네트워크로 전송하기 위해 내부적인 처리를 수행하며, 이 처리는 운영체제와 네트워크 장비에 의해 이루어진다.
따라서 데이터가 실제로 전송되는 시점은 다양한 요소의 영향을 받는데 네트워크 상태, 대역폭, 네트워크 지연 등 여러가지 요인이 있다.
recv 함수는 이 네트워크 스택으로부터 처리되어 해당 소켓에 할당된 커널단의 recvBuffer로부터 데이터를 긁어온다
만약 커널단의 recvBuffer의 데이터가 우리가 인자로 넘긴 유저단의 sizeof(recvBuffer)보다 작다면 있는 만큼 긁어온다.
그리고 긁어온 만큼의 데이터 길이를 반환한다.
/* Server.cpp */
#include "pch.h"
#include "ThreadManager.h"
int main()
{
/*소켓 초기화*/
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData))
return 0;
/*Handle의 개념*/
SOCKET listenSocket = socket(AF_INET/*IPv4*/, SOCK_STREAM/*TCP 방식 사용*/, 0);
if (listenSocket == INVALID_SOCKET)
return 0;
/*-----------직원 고용------------*/
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);
// 서버 소켓에 ip 주소와 포트 번호를 할당
if (::bind(listenSocket, (SOCKADDR*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR)
return 0;
// 서버 소켓을 대기 상태로 전환 -> 이 과정을 거쳐야지만 accept에서 대기할 수 있다
if (::listen(listenSocket, SOMAXCONN) == SOCKET_ERROR)
return 0;
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 connected! IP = " << ip << endl;
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;
int32 resultCode = ::send(clientSocket, recvBuffer, recvLen, 0);
if (resultCode == SOCKET_ERROR)
return 0;
}
}
closesocket(listenSocket);
WSACleanup();
}
/* DummyClient.cpp */
#include "pch.h"
// 클라
// 1. 소켓 생성
// 2. 서버에 연결 요청
// 3. 통신
int main()
{
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
return 0;
SOCKET clientSocket = ::socket(AF_INET, SOCK_STREAM, 0);
if (clientSocket == INVALID_SOCKET)
return 0;
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);
if (connect(clientSocket, (SOCKADDR*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR)
return 0;
cout << "Connected To Server!" << endl;
while (true)
{
// clientSocket의 sendBuffer(커널단)에 sendBuffer(유저단)의 데이터를 sizeof(sendBuffer)만큼 전달하겠다.
char sendBuffer[100] = "Hello ! I am Client!";
int32 resultCode = send(clientSocket, sendBuffer, sizeof(sendBuffer), 0);
if (resultCode == SOCKET_ERROR)
return 0; // 커널단의 sendBuffer에 잘 전달되었는지를 확인
/*-----------------------------------------------------------------------------------------------------*/
// clientSocket의 recvBuffer(커널단)으로부터 recvBuffer(유저단)에 sizeof(recvBuffer)만큼의 데이터를 받겠다
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);
}
closesocket(clientSocket);
WSACleanup();
}
'게임 서버 > [Inflearn_rookiss]올인원_클라&서버 연동' 카테고리의 다른 글
11. 논 블로킹 소켓 (1) | 2023.12.11 |
---|---|
10. TCP vs UDP (0) | 2023.12.11 |
8. 스마트 포인터 (2) | 2023.12.11 |
7. 이벤트와 조건변수 (0) | 2023.12.11 |
6. 데드 락 (0) | 2023.12.11 |
댓글