https://sundaland.tistory.com/213
[ ▶ Socket ]
네트워크 소켓 (Network socket)은 컴퓨터 네트워크를 경유하는 프로세스 간 통신의 종착점이다. 오늘날 컴퓨터 간 통신의 대부분은 인터넷 프로토콜을 기반으로 하고 있으므로, 대부분의 네트워크 소켓은 인터넷 소켓이다. 네트워크 통신을 위한 프로그램들은 소켓을 생성하고, 이 소켓을 통해서 서로 데이터를 교환한다.
RFC 147 텍스트는 1971년 5월 7일에 작성된 네트워크 소켓에 대한 초기 정의를 설명하는 문서이다. J.M. Winett가 MIT 링컨 연구소에서 네트워크 소켓 위원회와 네트워크 커뮤니티에 보낸 것이다.
△ 소켓의 정의
- 소켓은 네트워크에서 정보가 전송되는 고유한 식별자로 정의된다.
- 32비트 숫자로 지정되며, 짝수 소켓은 수신 소켓, 홀수 소켓은 송신 소켓으르 구분된다.
- 소켓은 송수인 프로세스가 위치한 호스트에 의해 식별된다.
△ 포트와의 관계
- 이전 문서에서는 호스트 운영 체제 하에서 실행되는 프로세스가 여러 포트에 접근할 것이라고 가정했다.
- 포트는 물리적 또는 논리적 I/O 장치일 수 있으며, 시스템 호출을 통해 운영 체제에 의해 지원된다.
△ 소켓의 사용
- 소켓은 ARPA 네트워크를 통한 기계 간 통신을 위한 포트의 식별자로 정의됩니다.
- 각 호스트에 할당된 소켓은 알려진 프로세스와 고유하게 연결되어야 하며, 일부 소켓의 이름은 전 세계적으로 알려져 있어야 합니다.
△ 소켓 명명 및 사용자 투명성
- 네트워크 프로그램 사용자는 소켓 이름을 알 필요가 없을 수 있으며, 소켓 명세는 사용자게 투명할 수 있다.
- 프로세스가 동일한 목적으로 나중에 사용하는 소켓은 이전에 사용했던 것과 동일해야 할 수 있다.
△ 소켓과 네트워크 사용 계정
- 네트워크 제어 프로그램 (NCP)은 각 연결을 기록하고, 연결된 시간, 전송된 메시지 및 비트 수, 송수신 호스트, 그리고 연결에 참여한 송수신 호스트의 소켓을 기록해야 한다.
△ 32비트 소켓의 구조
- 32비트 소켓은 8비트 "홈" 필드, 16비트 "사용자" 필드, 8비트 "태그" 필드로 나뉩니다.
- 태그는 7비트 "플러그"와 1비트 "편향"으로 구성되며, "0" 편향은 수신 소켓, "1" 편향은 송신 소켓을 나타냅니다.
[ ▷ TCP Server Socket Programming ]
▼ 원도우즈 OS에서 TCP 기반 소켓 프로그래밍의 서버측 코드
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winsock2.h>
#pragma comment(lib, "ws2_32.lib")
#define MAX_BUFFER_SIZE 1024
int main() {
WSADATA wsaData;
SOCKET serverSocket, clientSocket;
struct sockaddr_in serverAddr, clientAddr;
char buffer[MAX_BUFFER_SIZE];
int clientAddrSize;
// Winsock 초기화
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
printf("Failed to initialize winsock.\n");
return -1;
}
// 소켓 생성
if ((serverSocket = socket(AF_INET, SOCK_STREAM, 0)) == INVALID_SOCKET) {
printf("Failed to create socket.\n");
return -1;
}
// 서버 설정
serverAddr.sin_family = AF_INET;
serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);
serverAddr.sin_port = htons(5001);
// 소켓 바인딩
if (bind(serverSocket, (struct sockaddr*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR) {
printf("Failed to bind socket.\n");
return -1;
}
// 클라이언트의 연결 대기
if (listen(serverSocket, 1) == SOCKET_ERROR) {
printf("Failed to listen on socket.\n");
return -1;
}
printf("서버가 실행 중입니다...\n");
// 클라이언트 연결 수락
clientAddrSize = sizeof(clientAddr);
if ((clientSocket = accept(serverSocket, (struct sockaddr*)&clientAddr,
&clientAddrSize)) == INVALID_SOCKET) {
printf("Failed to accept client connection.\n");
return -1;
}
printf("클라이언트가 연결되었습니다.\n");
while (1) {
// 클라이언트로부터 메시지 수신
memset(buffer, 0, MAX_BUFFER_SIZE);
int bytesRead = recv(clientSocket, buffer, MAX_BUFFER_SIZE, 0);
if (bytesRead == SOCKET_ERROR || bytesRead == 0) {
break;
}
printf("수신한 메시지: %s\n", buffer);
// 클라이언트에게 응답 전송
char response[MAX_BUFFER_SIZE];
snprintf(response, MAX_BUFFER_SIZE, "서버가 메시지를 수신했습니다: %s", buffer);
send(clientSocket, response, strlen(response), 0);
}
// 연결 종료
closesocket(clientSocket);
closesocket(serverSocket);
WSACleanup();
return 0;
}
△ WSAData
WSADATA wsaData;
WSADATA는 Windows Sockets API에서 사용하는 구조체이다.
이 API는 네트워크 통신을 위해 원도우 운영 체젱에서 제공하는 표준 인터페이스이다.
WASDATA 구조체는 WASStartup 함수를 호출할 때 Windows Sockets 구현에 대한 정보를 변환하는데 사용된다.
이 구조체는 원도우 소켓 구현의 버전과 다양한 기능적 특성을 설명한다.
△ Winsock 초기화
▼ Windows Sockcet API를 초기화하는 과정
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
printf("Failed to initialize winsock.\n");
return -1;
}
여기에서 WSAStartup 함수는 Windows Sockets 네트워크 통신을 사용하기 위한 필수적인 초기화 단계이다.
이 함수는 프로그램이 Window Sockets API를 사용하기 전에 반드시 호출해야 한다.
- WASStartup 함수: 이 함수는 Windows Sockets 라이브러리를 초기화하고 사용할 준비를 한다. 함수의 첫 번째 파라미터는 프로그램에서 요구하는 Windows Sockets 버전을 지정한다. 두 번째 파라미터는 WASDATA 구조체에 대한 포인터이다. 이구조체는 초기화 후 시스템의 Windows Sockets 구현에 대한 정보를 담게 된다.
- MAKEWORD(2, 2): 이 매크로는 원하는 Windows Sockets 버전을 지정한다. 2, 2는 버전으로 널리 사용되는 버전으로 많은 시스템에서 지원된다.
- &wasData: WASDATA 구조체에 대한 포인터이다. WSAStartup 함수는 이 구조체를 통해 시스템에서 사용 가능한 소켓의 상세 정보를 제공한다.
WASStartup 함수는 성공적으로 완료되면 0을 반환한다.
반환 값이 0이 아닌 경우, 즉 != 0일 떄, 이는 초기화에 실패했음을 의미한다.
이 조건문은 WSAStartup의 반환 값을 검사하여 초기화에 실패했을 떄 오류 메시지를 출력하고, 프로그램을 종료하는 로직을 구현한다.
이러한 초기화 단계는 Windows 환경에서 네트워크 프로그래밍을 할 때 필수적이다.
초기화가 성공적으로 완료되면 소켓을 생성하고, 데이터를 송수신하는 등의 네트워크 관련 작업을 진행할 수 있다.
△ SOCKET 데이터 타입
SOCKET이라는 용어는 원도우 네트워크 프로그래밍, 특히 Windows Sockets API (Winsock)에서 사용되는 개념이다. 그러나 SOCKET는 실제로 구조체 (Structure)가 아니라, 소켓 핸들을 나타내는 데이터 타입이다. 이는 네트워크 소켓을 참조하기 위한 정수형 값 (핸들)으로, 네트워크 연결을 관리하는데 사용된다.
- 정의: SOCKET은 원도우 플랫폼에서 정의된 특별한 데이터 타입이다. 이는 기본적으로 unsigned int 형이나 그와 유사한 다른 정수형 타입으로 정의된다. 특정한 구저체를 나타내는 것이 아니라, 소캣의 핸들 (식별자)을 위한 타입이다.
typedef unsigned __int64 UINT_PTR
typedef UINT_PTR SOCKET;
- 용도: SOCKET 타입은 네트워크 프로그래밍에서 소켓의 인스턴스를 식별하기 위해 사용된다. 이 핸들은 소켓을 생성, 관리, 송수신 및 닫는데 필요한 연산에 사용된다.
- 값: 유효한 SOCKET 값은 일반적으로 0 이상의 값을 갖는다. INVALID_SOCKET은 오류 상황을 나타내는 특별한 값으로, 보통 -1 또는 다른 특정한 음수 값을 가진다.
- 생성과 소멸: SOCKET은 socket 함수를 사용하여 생성되며, closesocket 함수를 사용하여 닫힌다. 소켓이 더 이상 필요하지 않을 때는 반드시 닫아야 한다.
- 사용 예시: SOCKET 타입은 TCP 또는 UDP 소켓 생성, 클라이언트의 연결 요청 수락, 데이터 송수신 등 다양한 네트워크 연산에서 사용된다.
- 플랫폼 의존성: SOCKET 타입은 주로 원도우 플랫폼의 Winsock 라이브러리에 특화되어 있으며, UNIX나 Linux 같은 다른 시스템에서는 다른 방식 (파일 디스크립터 등)을 사용할 수 있다.
요약하자면 SOCKET은 구조체가 아니라 네트워크 소멧의 인스턴스를 나타내느 핸들로 사용되는 데이터 타입이다. 이는 원도우 기반의 네트워크 프로그래밍에서 중요한 역할을 하며, 소켓 관련 연산을 수행하는데 필수적이다.
△ Socket 생성
아래의 코드 조각은 소켓을 생성하는 과정을 보여준다. 여기사 socket 함수는 네트워크 통신을 위한 소켓을 생성한다. 이 함수는 성공적으로 소켓을 생성하면 소켓에 대한 핸들 (또는 소켓 식별자)을 반환하고, 실패하면 INVALID_SOCKET을 반환한다.
if ((serverSocket = socket(AF_INET, SOCK_STREAM, 0)) == INVALID_SOCKET) {
printf("Failed to create socket.\n");
return -1;
}
1. socket 함수
이 함수는 네터워크 통신을 위한 새로운 소켓을 생성한다.
- AF_INET (Address Family): 이 파라미터는 소켓이 IPv4 인터넷 프로토콜을 사용할 것임을 나타낸다.
- SOCK_STREAM: 이 파라미터는 스트림 소켓을 생성할 것임을 지정한다. 스트림 소켓은 연결 지향적으며, 주로 TCP/IP 프로토콜을 사용한다.
- 0: 이 파라미터는 프로토콜을 지정한다. 대부분의 경우, 특정 소켓 타입에 대해 단일 프로토콜만 사용할 수 있으므로, 이 값을 0으로 설정하면 기본 프로토콜을 사용한다. SOCK_STREAM의 경우, 이는 TCP를 의미한다.
2. 변수 할당과 비교
serverSocket = socket(AF_INET, SOCK_STREAM, 0)는 socket 함수를 호출하여 반환된 소켓 핸들을 serverSocket 변수에 할당한다.
이후, == INVALID_SOCKET을 사용해서 생성된 소켓이 유효한지 확인한다. INVALID_SOCKET은 소켓 생성이 실패했음을 나타내는 특별한 값이다.
3. 오류 처리
만약 socket 함수가 INVALID_SOCKET을 반환하면, 이는 소켓 생성에 실패했음을 의미한다. 이 경우, Failed to create socket.\n 이라는 오류 메시지를 출력하고, 함수는 -1을 반환하여 오류 상태를 나타낸다.
이 코드는 네트워크 프로그래밍의 초기 단계에서 매우 중요하다. 소켓 생성은 클라이언트와 서버 간의 통신을 위한 기본적인 조건이며, 이후의 모든 네트워크 작업 (연결 수립, 데이터 송수신 등)의 기반이 된다. 소켓 생성에 실패하면 이러한 네트워크 작업을 진행할 수 없기 때문에, 적절한 오류 처리가 필수적이다.
△ struct sockaddr_in 데이터 타입
sockaddr_in은 C 언어에서 인터넷 소켓 주소를 정의하기 위해 사용되는 구조체이다. 이 구조체는 인터넷 (IPv4) 주소를 나타내며, 네트워크 프로그래밍에서 주로 사용된다. sockaddr_in은 sockaddr 구조체의 특화된 버전으로, 인터넷 주소를 다루기 위해 설계되었다.
- sin_family: 이 필드는 주소 체계 (Address Family)를 지정한다. 인터넷 주소의 경우, 이 값은 AF_INET으로 설정된다. AF_INET은 IPv4 네트워크 주소 체계를 나타낸다.
- sin_port: 이 필드는 포트 번호를 저장한다. 네트워크 바이트 순서 (big-endian)로 표현되어야 한다. htons 함수를 사용하여 호스트 바이트 순서에서 네트워크 바이트 순서로 변환하는 것이 일반적이다. 이 포트 번호는 소켓이 통신할 때 사용하는 포트를 지정한다.
- sin_addr: 이 구조체는 실제 인터넷 주소를 저장한다. sin_addr은 struct_in_addr 타입으로, IPv4 주소를 나타낸다. 이 필드는 주로 inet_addr 함수나 inet_pton 함수를 사용하여 설정된다.
- sin_zero: 이 필드는 sockaddr_in 구조체의 크기를 struct sockaddr와 동일하게 맞추기 위해 사용된다. 일반적으로 이 필드는 0으로 설정되어야 하며, 구조체의 크기를 16바이트로 맞추는데 사용된다.
- htons 함수는 "Host to Network Short"의 약어이다. 이는 호스트 바이트 순서를 네트워크 바이트 순서로 변환하는 변환하는 함수이다. 네트워크 통신에서 사용되는 데이터는 네트워크 바이트 순서 (Big Endian)로 전송되는데, 대부분의 컴퓨터는 호스트 바이트 순서 (Little Endian)을 사용한다. htons는 16비트 (2바이트) 정수 값, 즉 short 값을 호스트 바이트 순서에서 네트워크 바이트 순서로 변환할 때 사용된다. 예를 들어, 포트 번호를 네트워크 바이트 순서로 변환할 때 htons를 사용할 수 있다.
- inet_addr 함수는 문자열로 표현된 IPv4 주소를 32비트의 네트워크 바이트 순서로 변환하는 함수이다. 즉 사람이 읽을 수 있는 점으로 구분된 4개의 십진수 형태 (192.168.1.1 등)의 IP 주소를 소켓 프로그래밍에서 사용하는 이진수 형태로 변환해 준다.
- inet_pton 함수는 "Presentation to Numeric"의 역어로, 사람이 읽을 수 있는 텍스트 형태의 IP 주소 (프레젠테이션 형태)를 네트워크가 사용하는 이진수 (숫자) 형태로 변환하는 함수이다. 이 함수는 IPv4 뿐만 아니라 IPv6 주소 변환도 지원하므로 inet_addr 함수보다 더 범용적으로 사용할 수 있다.
struct sockaddr_in 구조체는 주로 소켓을 생성하고, 바인딩하며, 연결을 수립하는데 사용된다. 예를 들어, 서버는 이 구조체를 사용하여 어떤 IP 주소와 포트 번호에서 수신 대기할지를 결정하고, 클라이언트는 서버에 연결할 때 서버의 IP 주소와 포트 번호를 지정하는데 사용한다.
sockaddr_in 구조체는 소켓 프로그래밍에서 IPv4 주소 정보를 저장하는데 사용되는 구조체이다. 소켓을 통해 데이터를 송수신할 때, IP 주소와 포트 번호를 저장하여 네트워크 연결을 설정하는데 사용된다.
△ sockaddr_in 구조체의 정의
struct sockaddr_in {
sa_family_t sin_family; // 주소 체계(AF_INET)
in_port_t sin_port; // 16비트 포트 번호 (네트워크 바이트 순서)
struct in_addr sin_addr; // 32비트 IPv4 주소
char sin_zero[8]; // 구조체 크기를 맞추기 위한 패딩 (사용되지 않음)
};
- sin_family: 주소 체계를 지정합니다. IPv4를 사용할 때는 항상 AF_INET으로 설정됩니다.
- sin_port: 16비트의 포트 번호로, 네트워크 바이트 순서(빅엔디언)로 저장됩니다. 보통 htons 함수를 사용하여 호스트 바이트 순서를 네트워크 바이트 순서로 변환합니다.
- sin_addr: 32비트의 IPv4 주소로, struct in_addr 구조체를 사용하여 저장됩니다. IP 주소는 문자열에서 숫자로 변환하기 위해 inet_pton이나 inet_addr 같은 함수를 사용합니다.
- sin_zero: 구조체 크기를 맞추기 위한 패딩 필드로, 특별한 용도로 사용되지 않으며 항상 0으로 채워집니다.
▼ 간단한 예시
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(80); // HTTP 포트 (80)
addr.sin_addr.s_addr = inet_addr("192.168.0.1");
memset(&(addr.sin_zero), 0, 8); // sin_zero 필드를 0으로 초기화
위 예시는 sockaddr_in 구조체를 사용하여 IP 주소 192.168.0.1 및 포트 번호 80으로 통신을 설정하고 있다.
[ ▷ Server 설정 ]
serverAddr.sin_family = AF_INET;
serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);
serverAddr.sin_port = htons(5001);
위 코드는 네트워크 프로그래밍에서 사용되며, 소켓 주소 구조체인 serverAddr를 설정하는 과정을 보여준다. 이 구조체는 일반적으로 sockaddr_in 타입으로 선언되며, 인터넷 프로토콜 (IP) 주소와 포트 번호를 지정하는데 사용된다.. 여거시 설정된 값들을 서버 소켓이 특정 네트워크 인터페이스와 포트 번호에 바인딩되는데 필요하다.
- serverAddr.sin_family = AF_INET: 이 줄은 serverAddr 구조체의 sin_family 멤버를 AF_INET으로 설정한다. AF_INET은 이 주소가 IPv4 인터넷 프로토콜을 사용한다는 것을 의미한다. sin_family 필드는 소켓이 사용할 주소 쳬게를 지정하는데 필요하다. ※ sin_family는 Socket INterent Family를 의미한다.
- serverAddr.sin_addr.s_addr = htonl(INADDR_ANY): 여기서는 serverAddr 구조체의 sin_addr 필드를 설정한다. sin_addr는 in_addr 구조체를 사용하여 IP 주소를 저장한다.
- INADDR_ANY는 모든 사용 가능한 인터페이스에 서버를 바인딩하겠다는 것을 의미한다. 즉, 서버는 호스트의 모든 네트워크 인터페이스를 통해 들어오는 연결 요청을 수락할 준비가 되어 있다.
- htonl 함수는 호스트 바이트 순서에서 네트워크 바이트 순서로 32비트 숫자를 변환한다. IP 주소는 네트워크 바이트 순서로 저장되어야 하기 때문이 이 변환이 필수적이다.
- serverAddr.sin_port = htons(5001): 이 줄은 serverAdrr 구조체의 sin_port 필드에 포트 번호를 할당한다.
- 5001은 서버가 사용할 TCP 포트 번호이다.
- htons 함수는 호스트 바이트 순서에서 네트워크 바이트 순서로 16비트 숫자를 변환한다. 포트 번호는 네트워크 바이트 순서로 저장되어야 하므로, 이 변환도 필수적이다.
이 코드는 네트워크 프로그래밍에서 소켓을 설정할 때 필수적인 단게이다. 서버 소켓이 올바르게 설정되면, 해당 소켓을 특정 포트에 바인딩하고, 클라이언트의 연결 요청을 수신 대기할 수 있다.
[ ▷ Socket Binding ]
// 소켓 바인딩
if (bind(serverSocket, (struct sockaddr*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR) {
printf("Failed to bind socket.\n");
return -1;
}
위 코드는 네트워크 프로그래밍에서 소켓을 특정 주소 (일반적으로 IP 주소와 포트 번호)에 바인딩하는 과정을 보여준다.
bind 함수는 소켓에는 로컬 주소를 할당하는데 사용한다. 이 작업은 서버 소켓을 특정 포드에 연결하는데 필요하며, 클라이언트가 해당 서버에 연결할 수 있게 만든다.
- bind 함수: bind 함수는 serverSocket이라는 소켓을 serverAddr이라는 주소 구조체에 바인딩한다. 이 함수는 소켓, 바인딩할 주소, 그리고 주소 구조체의 크기를 매개변수로 받는다.
- serverSocket: 바인딩할 소켓의 식별자이다. 이전에 socket 함수를 사용하여 생성된 소켓이 이 자리에 온다,
- (struct sockaddr*)&serverAddr: struct sockaddr_in 타입이지만, bind 함수는 더 일반적인 struct sockaddr 타입을 요구한다.
- sizeof(serverAddr): 바인딩할 주소 구조체의 크기이다. 이 값은 bind 함수에 주소의 크기를 알려준다.
- 조건문: bind 함수는 성공적으로 완료되면 0을 반환하고, 실패하면 SOCKET_ERROR를 반환한다. 이 조건문은 bind 함수가 SOCKET_ERROR를 반환하는지 확인한다. 만약 그렇다면, 바인딩 과정에서 오류가 발생했음을 의미한다.
- 오류 메시지와 반환: 만약 bind 함수가 실패하면 Failed to bind socket.\n 이라는 메시지가 출력되고, 프로그램은 -1을 반환하여 오류 상태를 나타낸다.
이 코드는 서버 소켓을 설정하고 네트워크상에서 해당 서버를 식별할 수 있도록 준비하는 핵심 단계이다. 소켓이 성공적으로 바인딩되면, 서버는 해당 주소에서 들어오는 클라이언트의 연결 요청을 수신 대기할 수 있다.
[ ▷ Listen 대기 ]
if (listen(serverSocket, 1) == SOCKET_ERROR) {
printf("Failed to listen on socket.\n");
return -1;
}
위 코드는 네트워크 서버 프로그래밍의 두 중요한 단계 중, 클라이언트의 연결 요청에 대한 수신 대기를 보여준다. 클라이언트의 연결 요청을 수신 대기하는 과정 (Listen)과 실제로 클라이언트의 연결을 수락하는 과정 (Asccept).
△ Listen
- listen 함수는 소켓을 TCP Connection Request를 수신할 수 있게 만드는 명령이다. 이 함수는 두 파라미터를 가진다.
- serverSocket: 클라이언트의 연결 요청을 수신 대기할 서버의 소켓 핸들이다.
- 1: 이 매개변수는 소켓의 연결 대기 큐의 최대 길이를 나타낸다. 여기서 1은 한 번에 하나의 연결 요청만 수신 대기하겠다는 의미이다.
- SOCKET_ERROR 체크: listen 함수가 SOCKET_ERROR를 반환하면, 연결 대기를 시작하는데 실패했음을 의미한다. 이 경우 오류 메시지를 출력하고 -1을 반환한다.
[ ▷ Client와 Connection Establishment ]
clientAddrSize = sizeof(clientAddr);
if ((clientSocket = accept(serverSocket, (struct sockaddr*)&clientAddr, &clientAddrSize))
== INVALID_SOCKET) {
printf("Failed to accept client connection.\n");
return -1;
}
위 코드는 네트워크 서버 프로그래밍의 두 중요한 단계 중, 클라이언틔 연결 요청을 수락하는 과정 (accept)를 보여준다. 클라이언트의 연결 요청을 수신 대기하는 과정 (listen)과 실제로 클라이언트의 연결 요청을 수락하는 과정 (accept).
△ 클라이언트 연결 수락 (Accept)
- accept 함수는 연결 대기 큐에서 첫 번째 연결 요청을 받아들이고, 새로운 소켓을 생성한다. 이 새로운 소켓은 서버와 클라이언트 간의 통신에 사용된다.
- serverSocket: 클라이언트의 연결 요청을 수신하는 서버의 소켓 핸들이다.
- (struct sockaddr*)&clientAddr: 클라이언트의 주소 정보를 저장할 구조체이다. accept 함수는 이 구조체를 통해 연결한 클라이언트의 주소 정보를 반환한다.
- &clientAddrSize: clientAddr 구조체의 크기이다. accept 함수가 호출되기 전에 이 값을 설정해야 한다.
- INVALID_SOCKED 체크: accept 함수가 INVALID_SOCKET을 반환하면, 클라이언트 연결을 수락하는데 실패했음을 의미한다. 이 경우 오류 메시지를 출력하고 -1을 반환한다.
위 코드들은 서버가 클라이언의 연결 요청을 기다리고, 실제로 연결을 수락하는 전형적인 흐름을 나타낸다. 이 과정은 네트워크 기반 서버 애플리케이션의 기본적인 동작이다.
[ ▷ 클라이언트와 데이터 송수신 ]
while (1) {
// 클라이언트로부터 메시지 수신
memset(buffer, 0, MAX_BUFFER_SIZE);
int bytesRead = recv(clientSocket, buffer, MAX_BUFFER_SIZE, 0);
if (bytesRead == SOCKET_ERROR || bytesRead == 0) {
break;
}
printf("수신한 메시지: %s\n", buffer);
// 클라이언트에게 응답 전송
char response[MAX_BUFFER_SIZE];
snprintf(response, MAX_BUFFER_SIZE, "서버가 메시지를 수신했습니다: %s", buffer);
send(clientSocket, response, strlen(response), 0);
}
위 코드는 서버가 클라이언트로부터 메시지를 수신하고, 수신된 메시지에 대한 응답을 보내는 반복적인 과정을 구현하고 있다. 네트워크 프로그래밍에서 이러한 패턴은 클라이언트-서버간의 통신을 처리하는 표준적인 방법 중 하나이다.
- 무한 루프 while(1): 서버는 클라이언트와의 연결이 유지되는 동안 계속해서 메시지를 수신하고 응답을 보내는 작업을 반복한다.
- 클라이언트로부터 메시지 수신:
- memset(buffer, 0,MAX_BUFFER_SIZE): 수신 버퍼 (buffer)를 0으로 초기화합니다. 이는 이전 메시지의 데이터가 남아 있지 않도록 하기 위함이다.
- recv 함수 : 클라이언트로부터 데이터를 수신한다. clientSocket은 클라이언트와의 통신에 사용되는 소켓, buffer는 수신된 데이터를 저장할 버퍼, MAX_BUFFER_SIZE는 버퍼의 최대 크기를 나타낸다.
- bytesRead : recv 함수가 반환하는 값으로, 실제로 수신된 바이트 수이다. SOCKET_ERROR 또는 0 (연결히 닫힘)을 반환하면 루프를 빠져나온다.
- 수신 메시지 출력: print("수신한 메시지: %s\n", buffer): 수신된 메시지를 콘솔에 출력한다.
- 클라이언트에게 응답 전송:
- char response[MAX_Buffer_SIZE]: 응답 메시지를 저장할 버퍼를 선언합니다.
- snprintf(response, MAX_BUFFER_SIZE, "서버가 메시지를 수신했습니다: %s", buffer): 수신된 메시지에 대한 응답 메시지를 생성합니다.
- send 함수: 생성된 응답 메시지를 클라이언트에게 전송합니다. clientSocket은 목적지 클라이언트의 소켓, response는 전송할 데이터, strlen(response)는 전송할 데이터의 크기를 나타냅니다.
이 코드는 네트워크 프로그래밍에서 클라이언트와 서버 간의 기본적인 대화형 통신을 구현하는 방법을 보여준다. 서버는 클라이언트로부터 메시지를 수신하고, 그에 대해 응답을 보내는 과정을 계속 반복한다.
[ ▷ Connection Close ]
// 연결 종료
closesocket(clientSocket);
closesocket(serverSocket);
WSACleanup();
이 코드 조각은 Windows Sockets API를 사용하는 네트워크 프로그램에서 연결을 종료하고, 리소스를 정리하는 과정을 보여준다. 각 함수는 다음과 아래와 같은 역할을 한다.
△ closesocket(clientSocket);
- 이 함수는 클라이언트와의 연결을 담감하는 소켓 (clientSocket)을 닫는다. clientSocket은 클라이언트와의 통신에 사용된 소켓으로, accept 함수를 통해 생성되었다.
- 소켓을 닫는 것은 연결을 종료하고,. 해당 소켓에 할당된 모든 리소스를 해제하는 것을 의미한다. 이는 네트워크 프로그래밍에서 매우 중요한 단계로, 사용하지 않는 소켓을 열어 두면 리소스 누수가 발생할 수 있다.
△ closecoket(serverSocket);
- 이 함수는 서버의 메인 소켓 (serverSocket)를 닫는다. serverSocket은 서버가 클라이언트의 연결 요청을 수신 대기하는데 사용된 소켓이다.
- serverSocket도 사용이 끝난 후에는 반드시 닫아야 하며, 이를 통해 해당 소켓에 대한 리소스가 해제된다.
△ WSACleanup();
- WSACleanup 함수는 Windows Sockets API의 사용을 종료한다, 이 함수는 WSAStartup 함수에 의해 초기화된 Winsock 라이브러리의 리소스를 해제한다.
- 프로그램이 Winisock API의 사용을 완전히 마쳤을 때 한 번 호출해야 한다. 모든 소켓 작업이 완료된 후에 WSACleanup을 호출하느 것이 일반적이다.
- WSACleanup 호출 없이 프로그램이 종료되면 시스템 리소스가 제대로 해제되지 않을 수 있으며, 이는 잠재적인 메모리 누수가 다른 네트워크 문제를 일으킬 수 있다.
이 코드는 네트워크 서버 프로그램을 정상적으로 종료하고 모든 네트워크 리소스를 정리하는 데 필수적인 단계이다. 이러한 종료 및 정리 과정은 프로그램의 안전성과 시스템 리소스의 효율적 관리를 위해 중요하다.
[ ▷ TCP Client Socket Programming ]
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winsock2.h>
#include <ws2tcpip.h>
#pragma comment(lib, "ws2_32.lib")
#define MAX_BUFFER_SIZE 1024
int main() {
WSADATA wsaData;
SOCKET clientSocket;
struct sockaddr_in serverAddr;
struct in_addr ipAddr;
char buffer[MAX_BUFFER_SIZE];
char message[MAX_BUFFER_SIZE];
// Winsock 초기화
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
printf("Failed to initialize winsock.\n");
return -1;
}
// 소켓 생성
if ((clientSocket = socket(AF_INET, SOCK_STREAM, 0)) == INVALID_SOCKET) {
printf("Failed to create socket.\n");
return -1;
}
// 서버 정보 설정
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(5001);
if (inet_pton(AF_INET, "127.0.0.1", &serverAddr.sin_addr) <= 0) {
printf("Invalid address/ Address not supported.\n");
return -1;
}
// 서버에 연결
if (connect(clientSocket, (struct sockaddr*)&serverAddr, sizeof(serverAddr)) < 0) {
printf("Failed to connect to server.\n");
return -1;
}
while (1) {
// 사용자로부터 메시지 입력
printf("메시지를 입력하세요 (종료하려면 q 또는 Q): ");
fgets(message, MAX_BUFFER_SIZE, stdin);
message[strcspn(message, "\n")] = '\0';
if (strcmp(message, "q") == 0 || strcmp(message, "Q") == 0) {
break;
}
// 서버로 메시지 전송
send(clientSocket, message, strlen(message), 0);
// 서버로부터 응답 수신
memset(buffer, 0, MAX_BUFFER_SIZE);
int bytesRead = recv(clientSocket, buffer, MAX_BUFFER_SIZE, 0);
if (bytesRead == SOCKET_ERROR || bytesRead == 0) {
break;
}
printf("서버로부터 받은 응답: %s\n", buffer);
}
// 연결 종료
closesocket(clientSocket);
WSACleanup();
return 0;
}
[ ▷ 서버 설정 ]
// 서버 정보 설정
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(5001);
if (inet_pton(AF_INET, "127.0.0.1", &serverAddr.sin_addr) <= 0) {
printf("Invalid address/ Address not supported.\n");
return -1;
}
위 코드는 네트워크 프로그래밍에서 서버 주소를 설정하는 과정을 보여준다. 구체적으로는 serverAddr, 즉 struct sockaddr_in 타입의 구조체를 설정하고 있다. 이 구조체는 인터넷 (IPv4) 주소를 다루는 데 사용된다.
inet_pton은 Internet Protocol string TO Number의 약자이다.
△ 서버 정보 설정
- serverAddr.sin_family = AF_INET: sin_family 필드를 AF_INET으로 설정합니다. 이는 주소 체계가 IPv4임을 나타낸다.
- serverAddr.sin_port = htons(5001): sin_port 필드에 서버의 포트 번호를 설정합니다. 여기서 htons 함수는 호스트 바이트 순서의 숫자(여기서는 5001)를 네트워크 바이트 순서로 변환한다.
△ IP 주소 설정
- inet_pton(AF_INET, "127.0.0.1", &serverAddr.sin_addr): inet_pton 함수는 문자열 형태의 IP 주소("127.0.0.1")를 네트워크 바이트 순서의 이진 형태로 변환하고 serverAddr.sin_addr에 저장한다.
- AF_INET은 IPv4 주소 체계를 나타내며, "127.0.0.1"은 로컬 호스트 주소(즉, 현재 컴퓨터)를 나타낸다
△ 오류 확인
- inet_pton 함수는 변환에 성공하면 1을, 실패하면 0 또는 음수를 반환합니다. 여기서 조건문은 inet_pton의 반환 값이 0 이하인지 확인하여 IP 주소 변환에 실패했는지 검사한다.
- IP 주소 변환에 실패하면, "Invalid address/ Address not supported."라는 오류 메시지를 출력하고 프로그램을 -1로 종료한다.
[ ▷ 서버 연결 ]
// 서버에 연결
if (connect(clientSocket, (struct sockaddr*)&serverAddr, sizeof(serverAddr)) < 0) {
printf("Failed to connect to server.\n");
return -1;
}
위 코드는 클라이언트 프로그램이 서버에 연결을 시도하는 과정을 보여준다. connect 함수는 클라이언트 소켓을 서버의 주소에 지정된 소켓에 연결한다. 이는 네트워크 기반 클라이언트 애플리케이션에서 서버에 연결하기 위한 핵심 단계이다.
△ connect 함수
- clientSocket: 이는 클라이언트 측에서 생성된 소켓의 핸들이다. 이전에 socket 함수를 호출하여 생성된 소켓을 사용한다.
- (struct sockaddr*)&serverAddr: 이 매개변수는 서버의 주소 정보를 담고 있는 serverAddr 구조체의 주소이다. 여기서 serverAddr은 struct sockaddr_in 타입이지만, connect 함수는 더 일반적인 struct sockaddr 타입을 요구하기 때문에 캐스팅이 필요하다.
- sizeof(serverAddr): 이는 serverAddr 구조체의 크기이다. connect 함수에 서버 주소의 정확한 크기를 전달하는 것이 중요하다.