1. 소켓 기초, 윈도우 소켓 튜토리얼

인공능지연구소·2025년 10월 7일

Socket Programming

목록 보기
1/1

🧠 소켓 프로그래밍 총정리 (개념·원리 중심)


Ⅰ. 소켓 프로그래밍의 본질

  • 소켓(socket) 은 네트워크 통신을 위한 가상의 연결점(endpoint) 이다.
  • 하나의 소켓은 IP 주소 + 포트 번호 + 프로토콜(TCP/UDP) 의 조합으로 식별된다.
  • 프로그램은 소켓을 통해 데이터를 송수신하며,
    실제 패킷 전송은 OS의 네트워크 스택이 담당한다.

즉, 소켓은 “운영체제 안에서 만들어지는 네트워크용 파일 핸들”이다.


Ⅱ. 네트워크 통신의 3요소

요소역할예시
IP 주소어떤 컴퓨터인가192.168.0.10
포트 번호어떤 프로그램인가80(HTTP), 22(SSH), 7777(사용자 지정)
프로토콜어떤 규칙으로 대화하는가TCP, UDP

이 세 가지가 합쳐져서 “어디로 연결할지”를 완전히 정의한다.


Ⅲ. TCP 통신의 특징

  • 연결형(Connected): 3-way handshake로 신뢰성 확보
  • 순서 보장(Ordering): 패킷 순서가 바뀌면 재조합
  • 손실 복구(Reliability): 누락 시 재전송
  • 흐름 제어: 송신·수신 속도 조절

TCP는 신뢰성과 순서를 보장하지만 속도는 UDP보다 느리다.


Ⅳ. TCP 연결 과정 (3-Way Handshake)

1️⃣ 클라이언트 → 서버 : SYN (연결 요청)
2️⃣ 서버 → 클라이언트 : SYN + ACK (요청 수락)
3️⃣ 클라이언트 → 서버 : ACK (수락 확인)
→ 연결 성립, 데이터 교환 가능

이 절차는 모든 TCP 연결에서 반드시 거친다.


Ⅴ. 서버와 클라이언트의 역할

역할동작의미
서버(Server)포트를 열고 bind(), listen()으로 대기“문을 열어둔 쪽”
클라이언트(Client)서버의 IP/포트로 connect() 호출“찾아가는 쪽”

연결이 성립된 뒤에는 완전히 대등한 관계지만,
연결을 누가 먼저 시작했는가(initiator)라는 역할은 끝까지 유지된다.


Ⅵ. 양방향 통신의 구조

  • TCP 연결이 수립되면,
    양쪽 모두 자유롭게 send()recv() 를 호출할 수 있다.
  • 즉, 데이터 흐름은 양방향이지만
    서버/클라이언트 역할은 변하지 않는다.
  • 서버는 계속 “연결을 받는 입장”,
    클라이언트는 “연결을 여는 입장”으로 세션이 유지된다.

Ⅶ. OS 내부의 실제 동작

1️⃣ 프로그램이 send() 호출 → OS 커널의 송신 버퍼에 데이터 복사
2️⃣ 커널이 네트워크 카드로 패킷 전송
3️⃣ 상대방 커널의 수신 버퍼에 데이터 적재
4️⃣ 상대방 프로그램이 recv()로 버퍼의 데이터를 읽음

즉, 우리가 직접 패킷을 다루는 게 아니라
OS의 버퍼를 통해 간접적으로 통신하는 구조다.


Ⅷ. 엔디언(Endian) 변환

  • 리틀엔디언(Little Endian): 하위 바이트 먼저 저장 (Intel CPU 대부분)
  • 빅엔디언(Big Endian): 상위 바이트 먼저 (네트워크 규약)
  • 네트워크 통신은 항상 빅엔디언으로 전송하므로
    htons, htonl, ntohs, ntohl 함수로 변환해야 한다.

Ⅸ. 블로킹 / 논블로킹 통신

모드특징예시
블로킹결과가 나올 때까지 멈춤accept()는 클라이언트 연결 전까지 대기
논블로킹바로 반환, 준비 안 되면 오류 코드실시간 서버에 자주 사용

논블로킹 소켓은 보통 select(), poll(), epoll() 같은 이벤트 감시 함수와 함께 쓴다.


Ⅹ. TCP vs UDP 비교

항목TCPUDP
연결 방식연결형비연결형
신뢰성손실·순서 보장보장 없음
속도느림빠름
용도웹, DB, 파일 전송게임, 스트리밍, 센서 데이터

Ⅺ. 루프백(Loopback) 주소의 의미

  • 127.0.0.1자기 자신을 가리키는 가상 네트워크 인터페이스
  • 외부 네트워크를 거치지 않고 OS 내부에서 처리
  • 같은 컴퓨터 안의 서버와 클라이언트 통신에만 사용 가능
  • 다른 기기에서는 반드시 실제 LAN IP(192.168.x.x)를 사용해야 함

Ⅻ. Winsock API의 핵심 이해

  • Windows는 소켓 함수를 직접 노출하지 않고
    Winsock(Windows Sockets) 라이브러리를 통해 제공한다.
  • 모든 소켓 프로그램은 반드시 WSAStartup()으로 Winsock 초기화 후 시작해야 한다.
  • 종료 시에는 반드시 WSACleanup()으로 해제해야 한다.
  • 링크 시 ws2_32.lib가 필요하다 (#pragma comment(lib, "ws2_32.lib") 또는 링커 설정에 추가).

XIII. inet_addr() → InetPton() 변화의 이유

  • 이전: inet_addr() (IPv4만, 오류 구분 불명확)
  • 이후: InetPton() (IPv4/IPv6, 스레드 안전, 명확한 반환값)
  • Windows Vista(2006) 이후 표준 API로 대체됨
  • 단, 학습이나 단순 프로젝트 단계에서는 #define _WINSOCK_DEPRECATED_NO_WARNINGSinet_addr()도 사용 가능.

XIV. C와 C++의 관계

  • Winsock API는 C 기반으로 설계 → C++에서도 그대로 사용 가능.
  • C++에서는 RAII(소멸자 자동 자원 해제)와 예외 처리(try/catch)를 이용해
    더 안전하게 감싸는 것이 일반적.
  • 코드 자체는 거의 동일하지만, 문자열(std::string)과 헤더 순서(winsock2.hwindows.h)만 주의.

즉, C에서 배운 소켓 코드는 C++에서도 그대로 돌아간다.


XV. 서버-클라이언트 관계의 지속성

  • TCP는 양방향 통신 가능이지만,
    “누가 연결을 시작했는가(initiator)”는 세션이 끝날 때까지 유지된다.
  • 서버는 계속 연결을 수락하는 쪽,
    클라이언트는 연결을 여는 쪽으로 인식된다.
  • 데이터는 쌍방향이지만,
    역할(Role) 은 세션 단위로 고정된다.

즉, “말은 서로 주고받지만, 문은 여전히 서버가 열어둔 문이다.”


XVI. 실무적 개념 정리

개념설명
포트 충돌같은 포트 번호를 다른 프로세스가 bind하면 에러 발생
TIME_WAITTCP 연결 종료 후 일정 시간 포트 재사용 불가 상태
SO_REUSEADDRTIME_WAIT 중인 포트를 즉시 재사용 가능
Nagle 알고리즘작은 패킷을 합쳐 전송 효율 향상 (지연 가능성 있음)
방화벽 / ACL포트 접근 차단 여부 확인 필수
loopback / LAN IP / public IPIP의 범위와 통신 가능 범위 구분

XVII. 전체 구조 한눈에 보기

[서버]                        [클라이언트]
socket()                      socket()
bind()                        connect()
listen()                      (3-way handshake)
accept()  ⇄  connect()  → 연결 성립
recv() ⇄ send()        ← 양방향 데이터 교환
closesocket()                 closesocket()

이 구조는 OS, 언어와 상관없이 동일하다.
(Windows, Linux, macOS, C, C++, Python, Java 전부 같은 개념)


XVIII. 학습 단계 진도 요약

단계학습 내용상태
Winsock API 기초WSAStartup, socket, bind, listen
서버 대기 / 클라이언트 연결accept, connect
루프백 통신127.0.0.1 실습
Winsock 버전 / pragma링크 원리
inet_addr → InetPton신식 API 이해
소켓 구조 및 원리TCP / 버퍼 / 엔디언 / 블로킹
서버·클라이언트 역할 지속성개념 완전 정리

XIX. 핵심 문장 요약

소켓은 운영체제가 제공하는 네트워크용 파일 핸들이며,
TCP는 그 위에서 신뢰성과 순서를 보장하는 프로토콜이다.
서버와 클라이언트는 연결을 맺은 순간부터
대등하게 데이터를 주고받을 수 있지만,
연결을 시작한 쪽과 대기한 쪽의 역할은 세션이 끝날 때까지 유지된다.


이로써 당신은

  • API 사용법 (Winsock의 실제 코드 흐름)
  • 통신의 개념적 구조 (IP, 포트, TCP, 엔디언, 블로킹 등)
  • 서버/클라이언트의 역할과 세션 개념
    까지 전부 학습한 상태입니다.

📘 다음 추천 학습 경로:

  1. send() / recv() 로 실제 문자열 송수신 (에코 통신)
  2. 다중 클라이언트 처리 (select, poll, epoll)
  3. C++로 소켓 클래스화 및 RAII 적용
  4. Linux/ARM 이식 → 이종 플랫폼 간 통신 실습
  5. UDP, 비동기 소켓, 프로토콜 설계로 확장

윈도우 소켓 튜토리얼

/*
* 1단계 - 헤더 포함하기
*/
#include <stdio.h>  // 표준 입출력
#include <winsock2.h>  // 윈도우에서는 소켓 프로그래밍을 위하여 이 헤더 파일을 포함해야합니다.

#pragma comment(lib, "ws2_32.lib")  // 소켓 함수는 winsock2.h 헤더 파일에 선언되어 있지만 실제 구현은 ws2_32.lib 라이브러리에 들어있습니다.
// 비주얼 스튜디오는 링커 설정에서 이 라이브러리를 추가해주어야 하지만, 위와 같이 소스 코드에 직접 명시할 수도 있습니다.
// 링커 설정에서 추가하는 방법은 "프로젝트 속성 -> 링커 -> 입력 -> 추가 종속성"에 "ws2_32.lib"를 추가하면 됩니다.

int main()
{
	/*
	* 2단계 - Winsock 초기화하기
	* 모든 소켓 함수를 사용하기 전에 Windows에서는 반드시 Winsock 라이브러리를 초기화해야합니다.
	* 이것은 리눅스에는 없는 Windows 특유의 절차입니다.
	*/
	WSADATA wsaData;
	int result = WSAStartup(MAKEWORD(2, 2), &wsaData);  // MAKEWORK(2, 2)는 Winsock 2.2 버전을 사용하겠다는 의미입니다.
	if (result != 0)
	{
		printf("WSAStartup() 오류: %d\n", result);
		return 1;
	}
	// 시스템에 설치된 Winsock 버전이 2.2보다 낮으면 오류가 발생합니다.

	/*
	* 3단계 - 소켓 생성(TCP 스트림 지정)
	*/
	SOCKET listen_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);  // TCP 소켓을 생성합니다.
	if (listen_sock == INVALID_SOCKET)
	{
		printf("socket() 오류: %d\n", WSAGetLastError());
		WSACleanup();  // Winsock 종료
		return 1;
	}

	/*
	* 4단계 - (선택사항) 옵션 설정
	*/
	BOOL yes = 1;
	setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, (const char*)&yes, sizeof(yes));
	// 서버가 종료된 후 바로 다시 실행할 때 이전에 사용하던 포트가 TIME_WAIT 상태로 남아있으면
	// bind()에서 오류가 발생할 수 있습니다. 
	// SO_REUSEADDR 옵션을 설정하면 이런 문제를 방지할 수 있습니다(즉시 재사용이 가능).
	// 리눅스에서는 기본적으로 이 옵션이 설정되어 있지만, 윈도우에서는 기본값이 FALSE이므로
	// 반드시 위와 같이 설정해주어야 합니다.
	// 서버를 빠르게 재사용할 수 있습니다.

	/*
	* 5단계 - 주소 구조체 설정
	*/
	struct sockaddr_in servaddr;
	//ZeroMemory(&servaddr, sizeof(servaddr));  // 구조체 인스턴스를 0으로 초기화(윈도우 전용)
	memset(&servaddr, 0, sizeof(servaddr));  // 구조체 인스턴스를 0으로 초기화(C/C++ 표준)
	servaddr.sin_family = AF_INET;  // 이거는 통신으로 전달되는 데이터가 아니므로 빅엔디안 이런거 필요 없음
	servaddr.sin_port = htons(7777); // 빅엔디언 바이트 순서로(u short)
	servaddr.sin_addr.s_addr = htonl(INADDR_ANY);  // 빅엔디안 바이트 순서로(u long)

	/*
	* 6단계 - 소켓과 주소를 바인딩
	*/
	if (bind(listen_sock, (struct sockaddr*)&servaddr, sizeof(servaddr)) == SOCKET_ERROR)
	{
		printf("bind() 오류: %d\n", WSAGetLastError());
		closesocket(listen_sock);
		WSACleanup();  // Winsock 종료
		return 1;
	}

	/*
	* 7단계 - 클라이언트 접속 대기
	*/
	if (listen(listen_sock, SOMAXCONN) == SOCKET_ERROR)
	{
		printf("listen() 오류: %d\n", WSAGetLastError());
		closesocket(listen_sock);
		WSACleanup();  // Winsock 종료
		return 1;
	}
	printf("서버가 클라이언트 접속을 대기 중입니다...\n");

	/*
	* 8단계 - 클라이언트 접속 허용
	*/
	struct sockaddr_in cliaddr;
	int clilen = sizeof(cliaddr);
	SOCKET client_sock = accept(listen_sock, (struct sockaddr*)&cliaddr, &clilen);
	if (client_sock == INVALID_SOCKET)
	{
		printf("accept() 오류: %d\n", WSAGetLastError());
		closesocket(listen_sock);
		WSACleanup();  // Winsock 종료
		return 1;
	}

	/*
	* 9단계 - 접속한 클라이언트의 주소/포트 출력
	*/
	char isptr[16];
	DWORD ip = ntohl(cliaddr.sin_addr.s_addr);
	snprintf(isptr, sizeof(isptr), "%d.%d.%d.%d",
		(ip >> 24) & 0xff, (ip >> 16) & 0xff, (ip >> 8) & 0xff, ip & 0xff);
	printf("클라이언트 접속 허용: %s:%d\n", isptr, ntohs(cliaddr.sin_port));

	// 명시적으로 소켓과 Winsock 자원 해제
	closesocket(client_sock);
	closesocket(listen_sock);
	WSACleanup();

	return 0;
}
#define _WINSOCK_DEPRECATED_NO_WARNINGS  // inet_addr이 deprecated됨을 안내하는 경고를 무시합니다.
// 이것은 IPv6 도입되고 안정성을 개선하기 위해 InetPton()으로 바뀌었습니다. 어쨌든...
#include <stdio.h>
#include <winsock2.h>
#pragma comment(lib, "ws2_32.lib")

int main()
{
	WSADATA wsa;
	if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0)
	{
		printf("WSAStartup failed; %d\n", WSAGetLastError());
		return 1;
	}

	SOCKET s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (s == INVALID_SOCKET)
	{
		printf("socket failed: %d\n", WSAGetLastError());
		WSACleanup();
		return 1;
	}

	struct sockaddr_in srv;
	srv.sin_family = AF_INET;
	srv.sin_port = htons(7777);
	srv.sin_addr.s_addr = inet_addr("127.0.0.1");

	printf("Connecting to server...\n");
	if (connect(s, (struct sockaddr*)&srv, sizeof(srv)) == SOCKET_ERROR)
	{
		printf("connect failed: %d\n", WSAGetLastError());
		closesocket(s);
		WSACleanup();
		return 1;
	}

	printf("Connected!\n");

	// 명시적으로 소켓과 Winsock 자원 해제
	closesocket(s);
	WSACleanup();

	return 0;
}
profile
내가 뭘 해봤는지는 시리즈 탭에 다 정리해뒀슈

0개의 댓글