본문 바로가기

도서 정리/서버 - 윤성우 열혈 TCP&IP 소켓 프로그래밍

윤성우 열혈 TCP / IP 소켓 프로그래밍 (1) - Chapter 01

https://www.yes24.com/Product/Goods/3630373

 

윤성우의 열혈 TCP/IP 소켓 프로그래밍 - 예스24

소켓을 이용하여 네트워크 프로그래밍을 시작하는 사람들을 위해 쓰여진 책으로, 「열혈강의 TCP/IP 소켓 프로그래밍」의 개정판이다. 운영체제와 시스템 프로그래밍 그리고 TCP/IP 프로토콜에 대

www.yes24.com

개요

클라이언트 개발자를 목표로 하고 있던중
제대로 된 서버 지식이 필요함을 느꼈습니다.

따라서 Win32 게임 개발 경험을 기반으로 C언어로 서버 공부를 시작했습니다.

 

도서 "윤성우의 열혈 TCP / IP 소켓 프로그래밍"은

먼저 리눅스 운영체제에서 C언어를 사용하는 소켓을 소개하고,

이후 윈도우 프로그래밍의 소켓을 소개합니다.

이는 리눅스와 윈도우의 서버 프로그래밍은 본질적으로 다르지 않음을 강조하기 위함이며

소켓 프로그래밍에서 리눅스 운영체제와 윈도우 운영체제 사이의 전환하는 능력을 키우는 의도라고 설명합니다.

 

Chapter 01 네트워크 프로그래밍과 소켓의 이해

 

-네트워크 프로그래밍이란

   -> 소켓 (Socket) : 물리적으로 연결된 네트워크상에서 데이터 송수신에 사용할 수 있는 소프트웨어적인 장치

   -> 네트워크 프로그래밍은 소켓을 사용하며, 소켓 프로그래밍이라고도 불린다.

 

리눅스에서 소켓의 구현

--- 서버 소켓 ---

- socket

///리눅스 운영체제에서 작동함///
#include <sys/socket.h>
int socket (int domain , int type , int protocol);
// 성공시 파일 디스크립터 반환. 실패시 -1 반환

   -> 소켓을 생성한다. 

 

- bind

///리눅스 운영체제에서 작동함///
#include <sys/socket.h>
int bind (int sockfd , struct sockaddr *myaddr , socklen_t addrlen)
// 성공시 0 반환. 실패시 -1 반환

   -> 소켓은 bind 함수를 통해 소켓의 주소정보(IP , 포트번호)를 할당해야한다. 

 

- listen

///리눅스 운영체제에서 작동함///
#include <sys/socket.h>
int listen (int sockfd , int backlog);
// 성공시 0 반환. 실패시 -1 반환

   -> listen 함수를 통해  연결이 가능한 상태로 전환한다. 연결이 준비가 되었음을 알린다.

 

 

- accept

///리눅스 운영체제에서 작동함///
#include <sys/socket.h>
int accept (int sockfd , struct sockaddr *addr , socklen_t *addrlen);
// 성공시 파일 디스크립터 반환. 실패시 -1 반환

   -> accept 함수를 통해  클라이언트가 데이터 송수신을 위해 연결 요청을 하면, 해당 요청을 수락한다.

 

--- 클라이언트 소켓 ---

-connect

///리눅스 운영체제에서 작동함///
#include <sys/socket.h>
int connect (int sockfd , struct sockaddr *serv_addr , socklen_t addrlen);
// 성공시 0 반환. 실패시 -1 반환

   -> 클라이언트 소켓이 서버의 소켓을 호출하는 함수이다.

 

-파일 디스크립터 (File Descriptor)

   ->  리눅스에선 소켓 조작과 파일 조작(fopen 등)이 동일하게 간주된다.

   ->  단 윈도우에선 소켓 조작과 파일 조작이 분리되어있다.

   ->  파일 디스크립터는 시스템으로부터 할당 받은 파일 또느 소켓에 부여된 정수를 의미한다.

   ->  파일 디스크립터는 생성과정을 거치지 않고, 파일과 소켓의 생성 과정에서 자동으로 할당 된다.

   ->  리눅스는 표준 입출력에도 파일 디스크립터가 할당된다.

   ->  파일 디스크립터는 개발자가 소켓과 파일의 지칭을 편하게 하기 위한 숫자이다. 속도와는 큰 관계가 없다. 윈도우에            선 핸들이라고 표현하고 이 또한 정수형으로 선언되어 파일 디스크립터와 동일한 목적으로 생성된다.

 

-_t로 끝나는 자료형

   ->  WINAPI에서 매우 자주 접하고, Unity에서도 가끔씩 보이는 _t로 끝나는 변수들이 있다. (ssize_t , size_t 등)

   -> 이들은 고전적인 자료형이라고 하며, typedef 선언을 통해서 정의된, 우리가 잘 알고 있는 기본 자료형들이다.

   ->  즉 size_t = unsigned int , ssize_t = signed int로 typedef로 선언되어 있다.

   ->  size_t는 컴퓨터가 발전하면서 자료형의 크기 변경이 필요할 때 typedef 정의만 바꾸게 하여 편리하게 사용하게 한다.

   ->  typedef로 정의되는 자료형은 기존 자료형 이름과의 구분을 위해 _t를 붙여야한다.

 

=== === === === === === === === === === === === === === === === === === === === === === === === === ===

 

윈도우에서 소켓 구현

 

-윈속(winsock)의 초기화

///윈도우 환경에서 작동///
#include <winsock2.h>
int WSAStartup(WORD wVersionRequested , LPWSADATA lpWSAData);
// 성공시 0 반환,  실패시 0이 아닌 에러코드 반환

 

-매개변수 wVersionRequested

   ->  WORD(typedef unsigned short)로 선언되어있다.

   ->  만약 사용할 소켓 버전이 1.2라면 1이 주버전 2가 주버전으로 0x0201을 인자로 전달해야한다.

   ->  위처럼 비트로 전달하는 것이 번거롭기 때문에 MAKEWORD를 사용하여 WORD형 버전 정보를 반환하여 사용한다.

int main(int argc, char* argv[])
{
    WSADATA wsaData;

    if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
        ErrorHandling("WSAStartup() error!");
    .
    .
    .
    return 0;
}

 

- 매개변수 lpWSAData

   ->  WSADATA 구조체 변수의 주소값이다.

   ->  WSAStartup이 호출 완료되면 해당 변수에 초기화된 라이브러리의 정보가 채워진다.

   ->  함수 호출을 위해선 반드시 WSADATA 구조체 변수의 주소 값을 전달해야 한다.

 

윈도우 기반 소켓 관련 함수들

-윈도우 소켓 구현은 형태만 다를 뿐 기본적인 구조는 Linux 운영체제와 거의 동일하다.

 

-socket 구현

///Window 환경에서 작동함///
#include <winsock2.h>
SOCKET socket (int af , int type , int protocol)
// 성공 시 소켓 핸들 , 실패 시 INVALID_SOCKET 반환

- bind

///Window 환경에서 작동함///
#include <winsock2.h>
int bind (SOCKET s , const struct sockaddr * name , int namelen);
// 성공 시 소켓 핸들 , 실패 시 INVALID_SOCKET 반환

- listen

///Window 환경에서 작동함///
#include <winsock2.h>
int listen (SOCKET s , int backlog)
// 성공 시 0 , 실패 시 INVALID_SOCKET 반환

- accept

///Window 환경에서 작동함///
#include <winsock2.h>
SOCKET accept (SOCKET s , struct sockaddr * addr , int * addrlen);
// 성공 시 소켓 핸들 , 실패 시 INVALID_SOCKET 반환

-contact

///Window 환경에서 작동함///
#include <winsock2.h>
int contact (SOCKET s , const struct sockaddr * name , int namelen);
// 성공 시 0 , 실패 시 SOCKET_ERROR

 

-closesocket

///Window 환경에서 작동함///
#include <winsock2.h>
int closesocket(SOCKET s);
// 성공 시 0, 실패 시 SOCKET_ERROR 반환

   -> 윈도우에선 소켓을 닫을 때 해당 함수를 호출해야한다. 

 

 

 

윈도우 기반 서버 , 클라이언트 예제의 작성

 

- hello_server_win.c

/// <summary>
/// hello_server_win.c
/// </summary>
#pragma comment (lib, "ws2_32.lib") 	// 이 라이브러리를 반드시 포함시켜야한다.
#include <stdio.h>
#include <stdlib.h>
#include <winsock2.h>

void ErrorHandling(const char* message);

int main(int argc, char* argv[])
{
	WSADATA	wsaData;
	SOCKET hServSock, hClntSock;
	SOCKADDR_IN servAddr, clntAddr;

	int szClntAddr;
	char message[] = "Hello World!";

	if (argc != 2)
	{
		printf("Usage : %s <port>\n", argv[0]);
		exit(1);
	}

	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
		ErrorHandling("WSAStartup() error!");

	hServSock = socket(PF_INET, SOCK_STREAM, 0);
	if (hServSock == INVALID_SOCKET)
		ErrorHandling("socket() error");

	memset(&servAddr, 0, sizeof(servAddr));
	servAddr.sin_family = AF_INET;
	servAddr.sin_addr.s_addr = htonl(INADDR_ANY);
	servAddr.sin_port = htons(atoi(argv[1]));

	if (bind(hServSock, (SOCKADDR*)&servAddr, sizeof(servAddr)) == SOCKET_ERROR)
		ErrorHandling("bind() error");

	if (listen(hServSock, 5) == SOCKET_ERROR)
		ErrorHandling("listen() error");

	szClntAddr = sizeof(clntAddr);
	hClntSock = accept(hServSock, (SOCKADDR*)&clntAddr, &szClntAddr);
	if (hClntSock == INVALID_SOCKET)
		ErrorHandling("accept() error");

	send(hClntSock, message, sizeof(message), 0);
	closesocket(hClntSock);
	closesocket(hServSock);
	WSACleanup();
	return 0;
}

void ErrorHandling(const char* message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

 

-hello_client_win.c

/// <summary>
/// hello_client_win.c
/// </summary>
#pragma comment (lib, "ws2_32.lib") 	// 해당 라이브러리를 반드시 포함시켜야한다.
#include <stdio.h>
#include <stdlib.h>
#include <winsock2.h>


void ErrorHandling(const char* message);

int main(int argc, char* argv[])
{
	WSADATA wsaData;
	SOCKET hSocket;
	SOCKADDR_IN servAddr;

	char message[30];
	int strLen;

	if (argc != 3)
	{
		printf("Usage : %s <IP> <port>\n", argv[0]);
		exit(1);
	}

	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
		ErrorHandling("WSAStartup() error!");

	hSocket = socket(PF_INET, SOCK_STREAM, 0);
	if (hSocket == INVALID_SOCKET)
		ErrorHandling("socket() error");

	memset(&servAddr, 0, sizeof(servAddr));
	servAddr.sin_family = AF_INET;
	servAddr.sin_addr.s_addr = inet_addr(argv[1]);
	servAddr.sin_port = htons(atoi(argv[2]));

	if (connect(hSocket, (SOCKADDR*)&servAddr, sizeof(servAddr)) == SOCKET_ERROR)
		ErrorHandling("connect() error!");

	strLen = recv(hSocket, message, sizeof(message) - 1, 0);
	if (strLen == -1)
		ErrorHandling("read() error!");
	printf("Message from server: %s \n", message);

	closesocket(hSocket);
	WSACleanup();
	return 0;
}

void ErrorHandling(const char* message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

 

cmd 실행 화면

서버 exe 실행 (Server는 Client가 호출하기 전까지 경직 상태이다.)
클라이언트 exe 실행 (Client가 Server의 IP와 포트번호를 올바르게 받아서 서버를 호출했다.)

 

윈도우 기반 입출력 함수

-send

/// <summary>
/// 윈도우 환경에서 작동
/// </summary>

#include <winsock2.h>

int send (SOCKET s , const char * buf , int len , int flags);
// 성공시 전송된 바이트 수 , 실패 시 SOCKET_ERROR 반환

   -> 데이터를 전송하는 함수이다. 

-recv

/// <summary>
/// 윈도우 환경에서 작동
/// </summary>

#include <winsock2.h>

int recv (SOCKET s , const char * buf , int len , int flags);
// 성공시 수신한 바이트 수(단 EOF 전송 시 0) , 실패 시 SOCKET_ERROR 반환

   -> 데이터를 받는 함수이다.

 

Chapter 01 확인 문제 풀이

 

1. 네트워크 프로그래밍에서 소켓이 담당하는 역할이 무엇인가 , 소켓이라는 이름이 붙은 이유는 어디에 있는가?

정답 : 소켓은 네트워크 망의 연결에 사용되는 도구이다. 이는 물리적으로 연결된 네트워크 상에서의 데이터 송수신에 사용할 수 있는 소프트웨어인 장치를 의미한다. 

2. 서버 프로그램에서는 소켓 생성 이후에 listen 함수와 accept 함수를 차례대로 호출한다.

정답 : listen : 연결이 준비가 되었음을 알림 연결이 가능한 상태로 전환

          accept : 어느 클라이언트가 데이터 송수신을 위해 연결 요청을 하면, 해당 요청을 수락하는 함수

3. 리눅스의 경우 파일 입출력 함수를 소켓 기반의 데이터 입출력에 사용할 수 있다. 반면 윈도우에서는 이것이 불가능하다.

정답 : 리눅스는 소켓도 파일로 간주한다. 즉, 이 둘을 구분 짓지 않기 때문에 파일 입출력 함수를 소켓 입출력에도 사용할 수 있다. 하지만 윈도우는 리눅스와 달리 파일과 소켓을 구분 짓기 때문에 파일 입출력 함수와 소켓 입출력 함수가 구분되어 있다.


4. 소켓을 생성한 다음에는 주소할당의 과정을 거친다. 그렇다면 주소할당이 필요한 이유는 무엇이며, 이를 목적으로 호출하는 함수는 또 무엇인가?

정답 : 인터넷상에서 소켓을 구분하기 위해서는 주소정보가 필요하다. 따라서 bind 함수를 이용하여 주소할당의 과정을 거친다.


5. 리눅스의 파일 디스크립터와 윈도우의 핸들이 의미하는 바는 사실상 같다. 그렇다면 이들이 의미하는 바가 무엇인지 소켓을 대상으로 설명해보자.

정답 : 소켓을 정의하면 파일 디스크립터가 자동으로 "할당"된다. 별도의 생성과정을 거치지 않는다. 이는 포인터와 비슷하게 행동하며, 개발자가 소켓과 파일의 지칭을 편하게 하기 위한 숫자이다. 속도와는 큰 관계가 없다. 윈도우의 핸들도 정수형으로 선언되어 파일 디스크립터와 동일한 목적으로 생성된다.

6. 저 수준 파일 입출력 함수와 ANSI 표준에서 정의하는 파일 입출력 함수는 어떠한 차이가 잇는가?

정답 : ANSI 표준에서 정의한 입출력 함수는 운영체제에 상관없이 C의 표준으로 제공되는 함수이다. 즉, 모든 운영체제에서 사용 가능하다. 반면, 저수준 파일 입출력 함수는 운영체제가 제공하는 입출력 함수이다. 따라서 운영체제 별로 정의하고 있는, 형태가 다른 입출력 함수이다.