소켓 시작하기
개념부터 빌드와 실행까지
Windows와 Linux에서 소켓을 만들고 닫는 코드를 읽습니다. 각 환경의 초기화와 오류 처리 방식을 확인한 뒤 빌드하고 실행합니다.
이번 실습은 소켓을 만들고 닫는 프로그램입니다. 서버 접속과 송수신은 이후에 다룹니다. 포인터·구조체가 낯설다면 2-1 · C 기초를 먼저 읽습니다.
소켓은 프로그램이 사용하는 통신 끝점입니다
프로그램은 운영체제에 요청해 데이터를 보내고 받습니다. 이때 통신의 끝점으로 사용하는 자원이 소켓(socket)입니다. 소켓을 만들고 사용하는 작업을 요청하는 함수들을 소켓 API라고 합니다.
운영체제가 돌려준 식별자를 변수에 보관합니다.
통신에 필요한 상태와 자원을 관리합니다.
socket()은 소켓을 만들고 식별자를 반환합니다. Windows에서는 이를 SOCKET sock에, Linux에서는 int fd에 저장합니다. 이 식별자는 IP 주소, 포트 번호, 메모리 주소와는 다른 값입니다.
| 구분 | 역할 | 예 |
|---|---|---|
| 소켓 식별자 | 프로그램 안에서 특정 소켓을 지정 | Windows의 sock, Linux의 fd |
| IP 주소 | 통신할 네트워크 인터페이스의 주소를 지정 | 루프백 주소 127.0.0.1 |
| 포트 번호 | TCP/UDP에서 통신 끝점을 구분하는 데 사용 | 예: TCP 포트 9000 |
TCP 통신에서는 소켓 생성 뒤에 주소 설정, 연결 또는 연결 수락, 송수신이 이어집니다. 이번 프로그램은 이 중 생성과 정리만 실행하므로 상대 주소와 포트, 별도의 서버가 필요하지 않습니다.
소켓 생성과 정리의 실행 순서
두 운영체제 모두 소켓을 생성하고 사용이 끝나면 닫습니다. Windows에서는 그 앞뒤에 Winsock 사용을 시작하고 끝내는 단계가 추가됩니다. 아래는 모든 호출이 성공했을 때의 실행 순서입니다.
WSAStartup()Winsock 사용 준비socket()소켓 생성closesocket()소켓 닫기WSACleanup()Winsock 사용 종료
socket()소켓 생성close()소켓 닫기
초기화가 실패하면 소켓 생성으로 넘어가지 않고, 생성이 실패하면 소켓 닫기를 건너뜁니다. 따라서 반환값을 확인한 뒤 다음 단계로 이동해야 합니다.
프로그램의 실행 순서
main()에서 준비 → 생성 → 정리를 수행합니다. 실패한 경우에는 오류를 출력하고, 확보한 자원을 정리한 뒤 종료합니다.
소스 파일의 작성 순서
소스는 헤더 포함 → 보조 함수 정의 → main() 순서입니다. 보조 함수는 호출될 때 실행됩니다.
Windows에서는 Winsock을 먼저 초기화합니다
Winsock은 Windows의 소켓 API입니다. WSAStartup()은 사용할 버전을 요청하고 초기화 결과를 구조체에 기록합니다. Linux의 기본 소켓 API에는 이 초기화 단계가 없습니다.
WSADATA wsa;
int result = WSAStartup(MAKEWORD(2, 2), &wsa);
if (result != 0) {
fprintf(stderr, "WSAStartup failed: %d\n", result);
return 1;
}MAKEWORD(2, 2)Winsock 2.2 사용 요청
WSAStartup호출result반환값: 성공 0 / 실패 시 오류 코드
wsa&wsa로 전달한 공간에 초기화 정보 기록
wsa의 자료형은 WSADATA이며, 구조체 변수입니다. &wsa는 그 변수의 주소입니다. 이 주소를 받은 함수가 구조체에 초기화 결과를 기록합니다. MAKEWORD(2, 2)는 하위 바이트에 주 버전, 상위 바이트에 부 버전을 넣습니다.
초기화 성공 후 LOBYTE(wsa.wVersion)과 HIBYTE(wsa.wVersion)으로 합의된 버전의 두 부분을 읽습니다. 아래 전체 코드는 두 값이 2인지 확인한 뒤 소켓을 만듭니다. 버전이 맞지 않아 종료할 때도 초기화에 성공했다면 Winsock 사용을 정리합니다.
WSAStartup() 실패의 원인은 반환값 자체에 있습니다. 이 경우 WSAGetLastError()로 다시 조회하지 않습니다. 성공한 초기화 한 번에는 WSACleanup() 한 번을 대응시킵니다.
socket()의 세 인자로 통신 방식을 지정합니다
사용할 주소 체계와 데이터 전달 방식을 정하면 운영체제가 소켓을 생성합니다. 이번 실습은 IPv4의 TCP 소켓을 만듭니다.
SOCKET sock = socket(AF_INET, SOCK_STREAM, 0);AF_INETIPv4 주소를 사용
SOCK_STREAM연속된 바이트 흐름을 전달
0앞의 조합에 맞는 기본값 선택
| 만들 소켓 | 주소 체계 | 소켓 타입 | 프로토콜 |
|---|---|---|---|
| IPv4 TCP | AF_INET | SOCK_STREAM | 0 또는 IPPROTO_TCP |
| IPv6 TCP | AF_INET6 | SOCK_STREAM | 0 또는 IPPROTO_TCP |
| IPv4 UDP | AF_INET | SOCK_DGRAM | 0 또는 IPPROTO_UDP |
| IPv6 UDP | AF_INET6 | SOCK_DGRAM | 0 또는 IPPROTO_UDP |
TCP는 연결을 맺은 뒤 순서 있는 바이트 스트림을 전달합니다. UDP는 데이터그램 단위를 유지하지만 도착·순서·중복 방지를 보장하지 않습니다. IPv4와 IPv6는 주소 체계이고, TCP와 UDP는 전송 프로토콜이므로 서로 다른 선택입니다.
소켓 호출 조합해 보기
보충 체험 · 선택한 조합의 코드를 표시합니다. 실제 소켓은 생성하지 않습니다.
반환값은 운영체제에 맞는 타입에 저장합니다
| 환경 | 저장할 변수 | 성공 | 실패 |
|---|---|---|---|
| Windows | SOCKET sock | 유효한 소켓 핸들 | INVALID_SOCKET |
| Linux | int fd | 0 이상의 파일 디스크립터 | -1 |
Windows의 SOCKET을 임의로 int에 넣으면 값이 잘릴 수 있습니다. Linux에서는 0도 유효한 fd이므로 if (!fd) 대신 if (fd == -1)로 실패를 검사합니다.
반환값 확인과 오류 처리 순서
오류 처리는 함수가 실패했는지 판단하고, 원인을 보관한 뒤, 다음 동작을 결정하는 과정입니다. 실패했을 때는 다음 작업 대신 종료나 복구 경로로 진행합니다.
유효한 결과를 사용해
다음 작업을 수행합니다.
오류 코드 저장 → 메시지 출력
→ 확보한 자원 정리 → 종료 또는 복구
| 함수 | 성공 | 실패 | 실패 원인을 읽는 곳 |
|---|---|---|---|
Windows WSAStartup() | 0 | 0이 아닌 값 | 반환값 자체 |
Windows socket() | 유효한 SOCKET | INVALID_SOCKET | WSAGetLastError() |
Windows closesocket() / WSACleanup() | 0 | SOCKET_ERROR | WSAGetLastError() |
Linux socket() | 0 이상의 fd | -1 | errno |
Linux close() | 0 | -1 | errno |
원본은 오류 확인 방식에 따라 함수를 세 가지로 분류합니다: 오류 확인이 필요 없는 함수, 반환값 자체가 오류 코드인 함수, 반환값과 별도 오류 코드를 함께 확인하는 함수. 위 표는 이 장에서 사용하는 함수의 확인 방법입니다.
Windows: 오류 코드를 저장한 뒤 메시지로 바꿉니다
socket()이 INVALID_SOCKET을 반환하면 WSAGetLastError()로 현재 스레드의 Winsock 오류 코드를 읽습니다. 다른 API가 오류 상태를 바꿀 수 있으므로 실패를 확인한 직후 변수에 저장합니다.
SOCKET sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock == INVALID_SOCKET) {
int error = WSAGetLastError();
report_error("socket", error);
/* 호출한 쪽에서 Winsock 정리와 종료를 이어서 처리한다. */
}여기서 report_error()는 아래 전체 코드의 오류 출력 보조 함수입니다. "socket"은 실패한 작업의 이름, error는 원인을 나타내는 숫자입니다. 이 함수는 FormatMessageA()로 숫자에 해당하는 시스템 메시지를 찾아 출력합니다.
- 코드 보관error = WSAGetLastError()
- 문자열 요청FormatMessageA()
- 오류 출력fprintf(stderr, …)
- 메시지 해제LocalFree(message)
stderr는 오류 출력 스트림입니다. 일반 결과와 구분해 오류를 기록할 때 사용합니다. 메시지 변환에 실패하더라도 저장한 숫자 코드는 출력할 수 있습니다.
원본의 err_quit()과 err_display()도 보조 함수입니다. 두 함수는 오류를 출력한 뒤의 동작이 다릅니다.
| 오류 출력 함수 | 하는 일 | 호출 후 흐름 |
|---|---|---|
| 원본 err_quit() | 오류 메시지 출력 | exit(1)로 프로세스 종료 |
| 원본 err_display() | 오류 메시지 출력 | 호출한 곳으로 복귀 |
| 보충 report_error() | 전달받은 오류 코드와 설명 출력 | 호출한 곳에서 자원 정리와 종료 결정 |
FormatMessageA의 인자와 포인터 읽기
| 설정 또는 값 | 이 코드에서의 역할 |
|---|---|
| FORMAT_MESSAGE_FROM_SYSTEM | Windows가 제공하는 메시지에서 오류 설명을 찾습니다. |
| FORMAT_MESSAGE_ALLOCATE_BUFFER | 문자열을 담을 공간을 API가 할당하게 합니다. |
| FORMAT_MESSAGE_IGNORE_INSERTS | 별도 삽입 인자 없이 메시지를 읽습니다. |
(LPSTR)&message | 새 문자열의 주소를 받을 포인터 변수의 주소입니다. ALLOCATE_BUFFER 모드에서 요구하는 전달 방식입니다. |
반환값 length | 0이면 메시지 변환 실패입니다. 이때는 숫자 오류 코드를 출력합니다. |
LocalFree(message) | 이 옵션으로 할당받은 문자열을 사용 후 해제합니다. |
char *message = NULL;은 문자열의 주소를 저장할 포인터 변수를 선언합니다. 이 선언만으로 문자열을 담을 메모리가 할당되지는 않습니다.
&message를 전달하면 API가 message 변수에 새 공간의 주소를 기록할 수 있습니다. 이 공간은 malloc()으로 만들지 않았으므로 free() 대신 LocalFree()로 해제합니다.
Linux: errno를 저장하고 strerror()로 설명을 읽습니다
Linux에서는 소켓 함수가 실패하면 errno에 원인이 기록됩니다. errno는 함수가 아니며, 이 코드에서는 그 값을 정수 변수에 복사합니다. strerror(error)는 저장한 숫자에 해당하는 설명 문자열을 반환합니다.
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd == -1) {
int error = errno;
fprintf(stderr, "socket: %d (%s)\n", error, strerror(error));
return 1;
}함수 호출이 성공해도 이전 오류 값이 남아 있을 수 있습니다. errno만 보고 실패를 판단하지 말고, 먼저 fd == -1인지 확인합니다.
Windows의 메시지
FormatMessageA()에 버퍼 할당을 요청했으므로 출력 후 LocalFree()로 해제합니다.
Linux의 메시지
strerror()가 돌려준 문자열은 직접 할당한 공간이 아닙니다. 내용을 수정하거나 free()하지 않습니다.
성공한 단계까지만 정리합니다
Winsock 초기화와 소켓 생성은 서로 다른 단계입니다. 종료할 때는 실제로 확보한 자원을 확인하고 정리합니다. Windows에서는 소켓을 먼저 닫고 Winsock 사용을 마칩니다.
| 어디까지 진행했나요? | 확보한 자원 | 정리 순서 |
|---|---|---|
| WSAStartup 실패 | 이 호출로 시작된 Winsock 사용 없음 | 오류를 알리고 종료 |
| WSAStartup 성공 → socket 실패 | Winsock 사용만 시작됨 | WSACleanup() |
| WSAStartup 성공 → socket 성공 | Winsock 사용과 소켓 | closesocket(sock) → WSACleanup() |
| Linux socket 성공 | 소켓 | close(fd) |
closesocket()은 소켓 하나를 닫습니다. WSACleanup()은 Winsock 사용을 종료합니다.
예를 들어 초기화에 한 번 성공한 뒤 소켓을 세 개 만들었다면, 세 소켓을 각각 닫고 그 초기화에 대응하는 WSACleanup()을 한 번 호출합니다.
실패 위치에 따른 흐름
보충 체험 · 버튼을 누르면 선택한 상황의 실행 순서가 표시됩니다. 실제 API는 호출하지 않습니다.
아래 Windows 전체 코드에서는 status에 프로그램의 최종 성공·실패를 기록합니다. 초기화가 성공한 뒤 오류가 생기면 status = 1로 바꾸고 공통 정리 지점까지 진행합니다. 오류가 생겨도 정리 코드를 거친 뒤 종료하는 구조입니다.
이번 예제는 기본 블로킹 소켓을 생성한 뒤 바로 닫습니다. 연결된 소켓의 남은 데이터, 논블로킹 동작, linger 설정은 이후 통신 단계에서 다룹니다. 닫은 식별자는 이전 소켓을 가리키는 값으로 다시 사용하지 않습니다.
소스 파일을 구성 단위로 나누어 읽습니다
두 파일은 각각 Windows와 Linux에서 실행할 수 있는 예제입니다. 사용할 운영체제에 맞는 파일 하나를 선택합니다. 오류 메시지 처리와 정리 결과 확인을 포함한 보충 예제입니다.
Windows 파일: 헤더 → 보조 함수 → main
- 1헤더와 링크 지시
#include … / #ifdef _MSC_VER …사용할 타입·함수 선언을 읽습니다. MSVC일 때만 pragma 링크 지시를 적용합니다.
- 2오류 출력 보조 함수
static void report_error(…)저장된 오류를 출력하고 문자열을 해제합니다. 소켓 정리나 프로그램 종료는 호출한 쪽이 결정합니다.
- 3실행 흐름
int main(void)Winsock 초기화 → 버전 확인 → 소켓 생성 → 닫기 → Winsock 종료를 수행합니다.
| 구성 요소 | 왜 필요한가요? |
|---|---|
winsock2.h | SOCKET, WSADATA, 소켓 API 선언을 제공합니다. 이 파일에서는 windows.h보다 먼저 포함합니다. |
windows.h | FormatMessageA, LocalFree 등 Windows API를 사용합니다. |
stdio.h | printf, puts, fprintf와 stderr를 사용합니다. |
static void report_error | 이 파일 안에서만 사용하는 보조 함수입니다. 출력 후 호출한 곳으로 돌아갑니다. |
result와 status | result는 초기화 함수의 반환값이고, status는 프로그램이 마지막에 반환할 종료 코드입니다. |
main()의 초기화 실패는 즉시 return 1;로 끝납니다. 초기화 성공 뒤의 버전 불일치나 소켓 오류는 status = 1;로 기록하고 마지막 WSACleanup()을 거칩니다. 정상 경로에서는 0을 반환합니다.
Windows 전체 코드 · winsock_lifecycle.c
#include <winsock2.h>
#include <windows.h>
#include <stdio.h>
/* MSVC는 pragma, MinGW/Dev-C++는 빌드 명령의 -lws2_32로 링크한다. */
#ifdef _MSC_VER
#pragma comment(lib, "ws2_32.lib")
#endif
/* 오류 코드를 먼저 저장한 뒤 전달한다. 이 함수는 종료하지 않는다. */
static void report_error(const char *where, int error)
{
char *message = NULL;
DWORD length = FormatMessageA(
FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM
| FORMAT_MESSAGE_IGNORE_INSERTS,
NULL, (DWORD)error, 0, (LPSTR)&message, 0, NULL);
if (length != 0 && message != NULL) {
fprintf(stderr, "%s: %d (%s)\n", where, error, message);
} else {
fprintf(stderr, "%s: %d (message unavailable)\n", where, error);
}
if (message != NULL) {
LocalFree(message);
}
}
int main(void)
{
WSADATA wsa;
int status = 0;
/* 실패 코드는 WSAStartup의 반환값 자체에 있다. */
int result = WSAStartup(MAKEWORD(2, 2), &wsa);
if (result != 0) {
report_error("WSAStartup", result);
return 1;
}
printf("Winsock %u.%u initialized\n",
(unsigned)LOBYTE(wsa.wVersion), (unsigned)HIBYTE(wsa.wVersion));
/* 이 예제는 Winsock 2.2를 사용한다. 성공한 초기화는 정리한다. */
if (LOBYTE(wsa.wVersion) != 2 || HIBYTE(wsa.wVersion) != 2) {
fprintf(stderr, "Winsock 2.2 is required\n");
status = 1;
} else {
/* 소켓 자원만 생성한다. 연결이나 데이터 송수신은 하지 않는다. */
SOCKET sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock == INVALID_SOCKET) {
int error = WSAGetLastError();
report_error("socket", error);
status = 1;
} else {
puts("TCP socket created (not connected)");
/* 생성에 성공한 소켓만 닫는다. 오류는 즉시 저장한다. */
if (closesocket(sock) == SOCKET_ERROR) {
int error = WSAGetLastError();
report_error("closesocket", error);
status = 1;
} else {
puts("Socket closed");
}
}
}
/* 초기화가 성공한 모든 경로는 이곳에서 한 번 정리한다. */
if (WSACleanup() == SOCKET_ERROR) {
int error = WSAGetLastError();
report_error("WSACleanup", error);
status = 1;
} else {
puts("Winsock cleaned up");
}
return status;
}
Linux 파일: 헤더 → 생성과 닫기를 수행하는 main
Linux 예제에는 Winsock 초기화·버전 확인·종료 단계가 없습니다. 오류 출력은 main() 안에 있습니다. 소켓 식별자는 SOCKET 별칭 대신 int fd를 사용합니다.
| 헤더 | 사용하는 기능 |
|---|---|
sys/types.h, sys/socket.h | 소켓 관련 타입, socket과 주소 체계·타입 상수 |
unistd.h | close |
errno.h, string.h | errno와 strerror |
stdio.h | printf, puts, fprintf |
Linux 전체 코드 · posix_lifecycle.c
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <errno.h>
#include <stdio.h>
#include <string.h>
int main(void)
{
/* Linux에서는 Winsock 초기화가 필요 없다. */
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd == -1) {
int error = errno;
fprintf(stderr, "socket: %d (%s)\n", error, strerror(error));
return 1;
}
printf("TCP socket created: fd=%d (not connected)\n", fd);
/* fd의 값은 실행마다 다를 수 있다. 한 번 닫은 번호는 재사용하지 않는다. */
if (close(fd) == -1) {
int error = errno;
fprintf(stderr, "close: %d (%s)\n", error, strerror(error));
return 1;
}
puts("Socket closed");
return 0;
}
두 파일 모두 별도의 common.h 없이 빌드됩니다. 여러 파일로 나누려면 헤더에 선언을, 함수 구현은 .c에 둡니다. 실행 프로그램 하나에는 main()도 하나만 포함해야 합니다.
실행 파일을 만든 뒤, 프로그램을 실행합니다
C 소스를 컴파일하고 필요한 라이브러리와 링크해 실행 파일을 만드는 과정을 빌드라고 합니다. 그 파일을 운영체제에서 시작하는 것이 실행입니다.
- 소스 .c사람이 작성한 C 코드
- 오브젝트 .o컴파일 결과, 실행 파일은 아님
- 실행 파일링크로 외부 함수 참조 연결
- 프로그램 실행main의 흐름과 결과 확인
-c는 전처리·컴파일·어셈블을 거쳐 오브젝트를 만들고 링크 전에 멈춥니다. 다음 링크 명령에서 실행 파일을 만듭니다.컴파일할 때: #include <winsock2.h>로 함수 선언과 타입을 읽습니다. 컴파일러가 함수의 인자와 반환값을 확인하는 데 필요한 정보입니다.
링크할 때: MinGW의 -lws2_32로 Winsock 라이브러리를 지정합니다. 헤더를 포함하는 것과 라이브러리를 연결하는 것은 별도 단계입니다.
실행할 때: 이 프로그램은 Windows가 제공하는 Winsock DLL을 사용합니다.
Windows / Dev-C++·MinGW
winsock_lifecycle.c를 선택합니다. Dev-C++의 내장 GCC는 C11로 빌드하고 Winsock 라이브러리를 링크합니다.
Ubuntu / GCC
posix_lifecycle.c를 선택합니다. Ubuntu 터미널에서 C17로 빌드합니다.
Windows: Dev-C++에서 빌드하고 실행하기
winsock_lifecycle.c를 별도 실습 폴더에 내려받습니다. 이 파일은 UTF-8입니다. 한글 주석이 깨지면 편집기의 파일 인코딩 설정을 확인합니다.- Dev-C++에서 새 C 콘솔 프로젝트를 만들고 이름을
winsock_lifecycle로 지정합니다. 자동 생성된 빈 소스 대신 내려받은 파일을 프로젝트에 추가합니다. 빌드 대상의main()은 하나여야 합니다. - 프로젝트 → 프로젝트 옵션 → 매개변수에서 C 컴파일러 옵션과 링커 옵션을 아래와 같이 설정합니다. 설치 버전에 따라 메뉴 표기는 다를 수 있습니다.
- 컴파일(F9)을 선택하고 오류·경고를 확인합니다. 여기서 IDE의 컴파일 명령은 일반적으로 링크까지 수행해 실행 파일을 만듭니다.
- 빌드가 성공하면 실행(F10)을 선택합니다. Dev-C++ 5.11의 기본 단축키 기준으로 F11은 컴파일 후 실행입니다. 결과를 아래 예상 출력과 비교합니다.
| 설정 위치 | 입력할 값 |
|---|---|
| C 컴파일러 | -std=c11 -Wall -Wextra -Wpedantic -g -O0 |
| 링커 | -lws2_32 |
이 예제는 새 프로젝트에서 실행합니다. Windows 파일 하나만 추가하고 Linux 예제는 함께 링크하지 않습니다.
Windows PowerShell: 컴파일과 링크를 나누어 확인하기
MinGW GCC가 설치돼 있고 gcc --version이 동작하는 터미널을 사용합니다. winsock_lifecycle.c가 있는 폴더에서 아래 명령을 한 줄씩 실행합니다. 각 단계의 오류를 확인한 뒤 다음 단계로 넘어갑니다.
gcc -std=c11 -Wall -Wextra -Wpedantic -g -O0 -c winsock_lifecycle.c -o winsock_lifecycle.o-c 때문에 아직 실행 파일은 만들어지지 않습니다. 성공하면 winsock_lifecycle.o가 생성됩니다.
gcc winsock_lifecycle.o -o winsock_lifecycle.exe -lws2_32-o 뒤는 출력 파일 이름입니다. -lws2_32는 오브젝트 뒤에 둡니다. 성공하면 winsock_lifecycle.exe가 생성됩니다.
.\winsock_lifecycle.exe
$LASTEXITCODE현재 폴더의 실행 파일은 .\를 붙여 실행합니다. $LASTEXITCODE로 방금 실행한 프로그램의 종료 코드를 확인합니다.
Winsock 2.2 initialized
TCP socket created (not connected)
Socket closed
Winsock cleaned up
0앞의 네 줄은 프로그램이 출력한 내용이고 마지막 0은 PowerShell에서 확인한 종료 코드입니다. not connected는 이 예제가 소켓만 만들었다는 뜻입니다. 창이 바로 닫혀 내용을 읽기 어렵다면 위와 같이 열린 터미널에서 실행하세요.
컴파일·링크를 한 명령으로 실행하기 / MSVC 사용 시
gcc -std=c11 -Wall -Wextra -Wpedantic -g -O0 winsock_lifecycle.c -o winsock_lifecycle.exe -lws2_32이 명령에는 -c가 없으므로 컴파일과 링크를 이어서 수행합니다. 실행은 별도로 .\winsock_lifecycle.exe를 입력합니다.
MSVC가 설정된 개발자 명령 프롬프트에서는 cl /W4 /TC winsock_lifecycle.c로 빌드합니다.
소스의 #ifdef _MSC_VER 안에 있는 #pragma comment(lib, "ws2_32.lib")는 MSVC에서만 적용됩니다. MinGW에서는 링크 옵션을 사용합니다.
Ubuntu: GCC로 빌드하고 실행하기
posix_lifecycle.c를 Ubuntu의 실습 폴더에 준비합니다. 그 파일이 있는 폴더의 Ubuntu 터미널에서 다음 명령을 실행합니다.
Windows PowerShell의 GCC와 혼동하지 않도록 gcc --version으로 사용할 컴파일러를 먼저 확인합니다.
gcc -std=c17 -Wall -Wextra -Wpedantic -g -O0 -c posix_lifecycle.c -o posix_lifecycle.o
gcc posix_lifecycle.o -o posix_lifecycle각 명령이 성공했는지 확인하며 한 줄씩 실행합니다. 첫 명령은 .o를 만들고, 두 번째 명령은 확장자 없는 실행 파일 posix_lifecycle을 만듭니다. 이 예제는 Winsock 라이브러리 옵션을 사용하지 않습니다.
./posix_lifecycle
echo $?TCP socket created: fd=3 (not connected)
Socket closed
0fd=3은 예상 출력의 예이며 실제 fd 값은 실행 환경에 따라 다를 수 있습니다. 마지막 0은 echo $?로 확인한 종료 코드입니다. 실행 파일 이름 앞의 ./는 현재 폴더를 뜻합니다.
Ubuntu에서 컴파일·링크를 한 명령으로 실행하기
gcc -std=c17 -Wall -Wextra -Wpedantic -g -O0 posix_lifecycle.c -o posix_lifecycle명령 옵션과 결과 확인
| 옵션 또는 결과 | 의미 |
|---|---|
-std=c11 / -std=c17 | 각 환경에서 사용할 C 언어 표준 |
-Wall -Wextra -Wpedantic | 코드의 의심스러운 부분과 표준 관련 경고 확인 |
-g -O0 | 디버그 정보 포함, 최적화를 끄고 코드 흐름 관찰 |
-c / -o 이름 | 링크 전에 멈춤 / 출력 파일 이름 지정 |
| 빌드 성공 | 소스의 컴파일과 링크가 끝나 실행 파일이 생성됨 |
| 실행 종료 코드 0 / 1 | 이 예제에서 정상 완료 / 오류 경로로 종료 |
빌드에 성공한 프로그램도 실행 중 초기화나 소켓 생성에 실패할 수 있습니다. 소켓 생성 성공은 TCP 연결이나 송수신 성공을 뜻하지 않습니다. 빌드 결과, 실행 출력, 종료 코드를 각각 확인합니다.
오류가 발생한 단계를 먼저 구분합니다
실행 파일이 만들어지지 않는 문제와 실행 중 소켓 함수가 실패하는 문제는 원인이 다릅니다. 메시지가 나온 단계를 먼저 찾고 그 단계의 입력과 설정을 확인합니다.
| 단계 | 증상 | 확인할 내용 |
|---|---|---|
| 명령 실행 전 | gcc를 찾을 수 없음 | GCC가 설치돼 있고 현재 터미널에서 찾을 수 있는지 확인합니다. Dev-C++ 설치와 PowerShell의 PATH 등록은 별개입니다. |
| 컴파일 | winsock2.h를 찾을 수 없음 | Windows 소스를 Linux용 GCC로 빌드하고 있지 않은지 확인합니다. Windows에서는 MinGW 또는 MSVC 환경을 사용합니다. |
| 컴파일 | socket 또는 close의 선언을 찾을 수 없음 | Linux 소스에 sys/socket.h와 unistd.h가 포함됐는지 확인합니다. |
| 링크 | undefined reference to WSAStartup / __imp_socket | MinGW 링커에 -lws2_32를 넣었는지, 오브젝트 뒤에 배치했는지 확인합니다. |
| 링크 | multiple definition of main | 한 프로젝트에 main이 있는 C 파일 여러 개를 넣었는지 확인합니다. |
| 실행 | 오류 코드 10093 | WSAStartup을 먼저 호출했고 그 호출이 성공했는지 확인합니다. |
| 실행 후 | 코드를 바꿨는데 출력이 그대로임 | 다시 빌드해 성공했는지, 현재 폴더의 새 실행 파일을 실행했는지 확인합니다. |
| 실행 후 | 소켓 생성 성공만 나오고 종료됨 | 이번 예제의 정상 동작입니다. 생성·닫기만 수행하고 스스로 종료합니다. |
Linux에서 실패 경로 관찰하기
내려받은 파일을 복사해 실험용 사본 posix_failure.c를 만듭니다.
사본에서 socket(AF_INET, SOCK_STREAM, 0)을 socket(-1, SOCK_STREAM, 0)으로 바꿉니다. 지원하지 않는 주소 체계를 주어 소켓 생성 실패를 관찰하는 실험입니다.
gcc -std=c17 -Wall -Wextra -Wpedantic -g -O0 posix_failure.c -o posix_failure
./posix_failure
echo $?빌드에 성공한 뒤 실행하면 socket:으로 시작하는 오류 설명과 종료 코드 1을 예상할 수 있습니다. 오류 번호와 문구는 환경마다 다를 수 있습니다. 생성에 실패했으므로 성공 메시지와 Socket closed는 출력되지 않는지 확인합니다.
개념과 실행 흐름 확인
WSAStartup이 0이 아닌 값을 반환하면 무엇을 출력해야 하나요?
그 반환값 자체가 오류 코드입니다. 다른 Winsock 함수처럼 WSAGetLastError로 다시 조회하지 않습니다.
Windows socket()이 실패했을 때 첫 번째로 할 일은?
INVALID_SOCKET인지 확인한 뒤 WSAGetLastError()를 호출해 코드부터 저장합니다. 이후 출력과 정리를 수행합니다.
err_display()를 호출하면 오류가 해결되나요?
아닙니다. 메시지를 출력하고 돌아올 뿐입니다. 계속 실행할지, 재시도할지, 자원을 정리하고 끝낼지는 호출한 코드가 결정합니다.
FormatMessage가 할당한 문자열과 strerror의 문자열을 같은 방법으로 해제하나요?
아닙니다. ALLOCATE_BUFFER로 받은 문자열은 LocalFree로 해제합니다. strerror가 돌려준 문자열은 직접 해제하지 않습니다.
초기화에는 성공했지만 socket()에 실패하면 무엇을 닫나요?
소켓은 생성되지 않았으므로 closesocket을 호출하지 않습니다. 성공한 초기화에 대응해 WSACleanup을 호출합니다.
SOCKET sock을 int에 저장해도 되나요?
Windows에서는 SOCKET 타입을 유지합니다. Linux의 파일 디스크립터 int와 같은 크기라고 가정하지 않습니다.
socket(AF_INET6, SOCK_DGRAM, 0)은 무엇을 만들까요?
IPv6 주소 체계의 기본 UDP 소켓을 만듭니다. protocol의 0은 기본 프로토콜 선택입니다.
Linux socket()이 0을 반환하면 실패인가요?
성공입니다. 0도 유효한 파일 디스크립터일 수 있습니다. 실패값은 -1입니다.
TCP 소켓을 만들면 상대와 연결된 건가요?
아닙니다. 통신에 쓸 자원을 만든 상태이며 주소 지정·연결 등의 후속 단계가 필요합니다.
-lws2_32는 소스 코드 어디에 쓰나요?
C 코드가 아니라 MinGW의 링크 옵션에 넣습니다. MSVC의 pragma comment와 도구를 혼동하지 않습니다.
.o 파일이 생겼다면 빌드와 실행이 모두 끝난 건가요?
아직입니다. 오브젝트를 라이브러리와 링크해 실행 파일을 만들어야 합니다. 그 실행 파일을 시작하면 프로그램이 실행됩니다.
헤더에 함수 선언이 있는데도 링크 오류가 나는 이유는 무엇인가요?
헤더는 컴파일에 필요한 선언을 제공합니다. 호출할 함수의 구현과 연결하려면 링크 설정도 필요합니다. MinGW의 Winsock 예제에서는 -lws2_32를 지정합니다.
운영체제에 맞는 소스를 선택합니다
Windows에서는 Winsock 예제를, Ubuntu에서는 POSIX 예제를 내려받습니다. 페이지의 전체 코드와 같은 파일이며, 두 파일 모두 UTF-8입니다. 별도로 작성할 헤더 파일은 없습니다.
먼저 전체 소스의 구성을 읽고, 컴파일·빌드·실행의 해당 환경 안내를 따릅니다. 출력은 그 절의 예상 결과와 비교합니다. 실패가 발생하면 문제 해결에서 단계를 구분해 확인합니다.
원본 범위와 참고 문서
2-2장 강의 자료: ch02_TCP_소켓시작하기_2.pdf 29쪽. 한 PDF 쪽에 슬라이드 두 장이 있어 PDF 쪽 번호와 인쇄된 슬라이드 번호는 다릅니다.
원본의 “실습 2-1”은 교재 실습 번호이며 이 사이트의 강의 구분 “2-1”과 별개입니다.
| 원본 PDF 쪽 | 반영 내용 | 본문 |
|---|---|---|
| 1–3 | 학습 목표·범위·통신 구조 | 학습 지도 |
| 4–12 | 오류 유형, Windows/Linux 메시지와 보조 함수 | 오류 처리 |
| 13–20 | Winsock 준비, 초기화·종료, common.h 실습 | 초기화·종료 |
| 21–25 | 소켓, 주소 체계·타입·프로토콜, 실패 확인 | 소켓 생성 |
| 26–28 | 닫기, Windows/Linux 전체 코드 | 전체 실습 |
| 29 | Dev-C++ 링크 설정 | 빌드·실행 |
전체 코드, 오류 코드 보관, 실패 경로 도식·체험, 정리 결과 확인은 보충 자료입니다.
원본의 표기와 코드에서 보완한 부분
- PDF 10쪽: 비교표에는 MessageBox로 표시되어 있지만 err_display의 구현은 printf를 사용합니다. 오류 출력 후 복귀해도 오류가 복구된 것은 아닙니다.
- PDF 19쪽: 제목의 commom.h와 include의 common.h가 다릅니다. 파일을 나눌 때는 실제 파일 이름과 include의 철자를 맞춥니다.
- PDF 22–24쪽: TCP와 UDP 모두 IPv4 또는 IPv6를 쓸 수 있습니다. AF_INET6의 숫자는 운영체제에 따라 달라질 수 있으므로 상수 이름을 사용합니다. AF_BTH는 Windows Bluetooth 주소 체계이며 이번 실습에서는 사용하지 않습니다.
- PDF 24쪽: TCP 예제의 SOCK_????? 빈칸은 SOCK_STREAM입니다.
- PDF 27–28쪽: Linux의 typedef int SOCKET은 타입 별칭이며 Winsock API를 제공하지 않습니다. 보충 예제에는 socket과 close에 필요한 헤더가 포함되어 있습니다.
- Microsoft · WSAStartup — 반환값, 버전 협상, 성공한 초기화와 종료의 대응.
- Microsoft · WSAGetLastError — 스레드별 오류 상태.
- Microsoft · FormatMessageA — 버퍼 할당·반환값·메모리 해제.
- Microsoft · socket, closesocket, WSACleanup — 생성과 정리.
- Linux man-pages · socket(2), close(2), errno(3), strerror(3) — Linux의 반환값·오류·문자열.
- GCC · 전체 처리 단계와 -c/-o, 링크 옵션 — 오브젝트 생성과 라이브러리 지정.