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

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

네트워크 프로그래밍 = 네트워크로 연결된 서로 다른 두 컴퓨터가 데이터를 주고받을 수 있도록 하는 것

  • 대부분의 컴퓨터는 인터넷으로 물리적 연결되어있음
  • 소프트웨어적인 데이터의 송수신은 운영체제에서 제공하는 소켓(Socket)이라는 장치를 사용
  • 소켓은 전화기로 비유할 수 있음


연결을 요청받는 소켓 구현

  1. 소켓 생성 - 전화기 장만
    #include <sys/socket.h>
    int socket(int domain, int type, int protocol);
    // 성공시 fd, 실패시 -1 반환
    
  2. bind() 호출 - 전화번호 부여
    생성한 소켓에 IP와 포트 번호 할당
    #include <sys/socket.h>
    int bind(int sockfd, struct sockaddr *myaddr, socklen_t addrlen);
    // 성공시 0, 실패시 -1 반환
    
  3. listen() 호출 - 전화 케이블에 연결
    소켓을 연결요청이 가능한 상태로 변경
    #include <sys/socket.h>
    int listen(int sockfd, int backlog);
    // 성공시 0, 실패시 -1 반환
    
  4. accept() 호출 - 수화기 들기
    데이터 송수신을 위한 연결요청에 대한 수락
    #include <sys/socket.h>
    int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
    // 성공시 fd, 실패시 -1 반환
    

연결요청을 수락하는 프로그램 (= 서버) 프로그램 예시

  • 서버에서 생성한 소켓은 서버 소켓 또는 리스닝 소켓이라고 부름
    ```cpp // hello_server.cpp

#include #include #include #include <arpa/inet.h> #include <sys/socket.h>

using namespace std;

void error_handling(char *message);

int main(int argc, char *argv[]) { int serv_sock; int clnt_sock;

struct sockaddr_in	serv_addr;
struct sockaddr_in	clnt_addr;

socklen_t	clnt_addr_size;
string		message = "Hello World!";
if(argc != 2)
{
	cout << "Usage : " << argv[0] << " <port>\n";
	exit(1);
}

serv_sock = socket(PF_INET, SOCK_STREAM, 0);
if (serv_sock == -1)
	error_handling("socket() error");

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

if (::bind(serv_sock, (sockaddr*)&serv_addr, sizeof(serv_addr)) == -1)
	error_handling("bind() error");

if (::listen(serv_sock, 5) == -1)
	error_handling("listen() error");

clnt_addr_size = sizeof(clnt_addr);
clnt_sock = accept(serv_sock, (sockaddr*)&clnt_addr, &clnt_addr_size);
if (clnt_sock == -1)
	error_handling("accept() error");

write(clnt_sock, message.c_str(), sizeof(message));
close(clnt_sock);
close(serv_sock);
return 0; }

void error_handling(char *message) { fputs(message, stderr); fputc(‘\n’, stderr); exit(1); }


<br/>

연결을 요청하는 소켓 구현
1. 소켓 생성 - 전화기 장만
2. `connect()` 호출 - 전화 걸기
```cpp
#include <sys/socket.h>
int connect(int sockfd, struct sockaddr *serv_addr, socklen_t addrlen);
// 성공시 0, 실패시 -1 반환

연결을 요청하는 프로그램 (= 클라이언트) 프로그램 예시

  • 클라이언트에서 생성한 소켓은 클라이언트 소켓이라고 부름
    ```cpp // hello_client.cpp

#include #include #include #include <arpa/inet.h> #include <sys/socket.h>

using namespace std;

void error_handling(char *message);

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

struct sockaddr_in	serv_addr;

char	message[30];
int		str_len;

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

sock = socket(PF_INET, SOCK_STREAM, 0);
if (sock == -1)
	error_handling("socket() error");

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

if (::connect(sock, (sockaddr*)&serv_addr, sizeof(serv_addr)) == -1)
	error_handling("connect() error");

str_len = read(sock, message, sizeof(message) - 1);
if (str_len == -1)
	error_handling("read() error");

cout << "Message from server : " << message << endl;
close(sock);
return 0; }

void error_handling(char *message) { fputs(message, stderr); fputc(‘\n’, stderr); exit(1); }


***

## 01-2 : 리눅스 기반 파일 조작하기

리눅스에서의 소켓은 파일의 일종으로 취급    
따라서 파일 입출력 함수를 소켓 입출력에 사용할 수 있음    

저수준(Low-level) : 표준에 상관없이 운영체제가 독립적으로 제공    
파일 디스크립터(File Descriptor) : 운영체제가 만든 파일을 편리하게 지칭하기 위해 부여한 숫자    
* 리눅스에서는 표준입력을 0번, 표준출력을 1번, 표준에러를 2번에 할당함    

<br/>

1. 파일 열기    
```cpp
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int open(const char *path, int flag);
// 성공시 fd, 실패시 -1 반환

매개변수 flag에 전달되는 비트값에 따라 동작이 조금씩 달라짐

  • O_CREAT / O_TRUNC / O_APPEND / O_RDONLY / O_WRONLY / O_RDWR
    1. 파일 닫기
      ```cpp #include

int close(int fd); // 성공시 0, 실패시 -1 반환

3. 파일에 데이터 쓰기
```cpp
#include <unistd.h>

ssize_t write(int fd, const void *buf, size_t nbytes);
// 성공시 전달한 바이트 수, 실패시 -1 반환

size_t : 시스템에서 unsigned int로 정의되어있음
ssize_t : 시스템에서 signed int로 정의되어있음

  1. 파일에 저장된 데이터 읽기 ```cpp #include

ssize_t read(int fd, void *buf, size_t nbytes);


파일 디스크립터를 다루는 예제    
```cpp
// fd_seri.cpp

#include <iostream>
#include <unistd.h>
#include <fcntl.h>
#include <sys/socket.h>

using namespace std;

int main()
{
	int fd1, fd2, fd3;

	fd1 = socket(PF_INET, SOCK_STREAM, 0);
	fd2 = open("test.dat", O_CREAT | O_WRONLY | O_TRUNC);
	fd3 = socket(PF_INET, SOCK_DGRAM, 0);

	cout << "file descriptor 1 : " << fd1 << endl;
	cout << "file descriptor 2 : " << fd2 << endl;
	cout << "file descriptor 3 : " << fd3 << endl;

	close(fd1);
	close(fd2);
	close(fd3);
	return 0;
}

01-3 : 윈도우 기반으로 구현하기

윈도우 소켓인 윈속(winsock)을 기반으로 한 프로그램을 개발하기 위해서는

  • winsock2.h 헤더파일을 포함시켜야함
  • ws2_32.lib 라이브러리를 링크시켜야함
  1. 윈속을 사용할시 반드시 WSAStartup() 함수의 호출이 선행적으로 요구됨
    • 프로그램에서 요구하는 윈도우 소켓의 버전을 알림
    • 해당 버전을 지원하는 라이브러리의 초기화 작업을 진행 ```cpp #include

int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData); // 성공시 0, 실패시 에러코드 값 반환

사용할 소켓의 버전정보를 `WORD` 형으로 전달해야하는데, 상위 8비트에 주 버전 정보/하위 8비트에 부 버전 정보를 표시함    
* 이를 간단하게 처리하기 위해 매크로 함수인 `MAKEWORD()`를 사용    

`LPWSADDATA`는 `WSADATA` 구조체의 포인터형    
`WSAStartup()` 함수 호출시 매개변수로 받은 `WSADATA` 구조체에 초기화된 라이브러리의 정보가 채워짐    

2. 초기화된 라이브러리의 해제는 `WSACleanup()` 함수를 사용    
```cpp
#include <winsock2.h>
int WSACleanup(void);
// 성공시 0, 실패시 SOCKET_ERROR 반환

라이브러리 해제시 할당된 윈속 라이브러리가 운영체제에 반환되므로 윈속 관련 함수의 호출이 불가능해짐


01-4 : 윈도우 기반의 소켓관련 함수와 예제

  1. 소켓 생성 ```cpp #include

SOCKET socket(int af, int type, int protocol); // 성공시 소켓 핸들, 실패시 INVALID_SOCKET 반환

2. 소켓 bind
```cpp
#include <winsock2.h>

int bind(SOCKET s, const struct sockaddr *name, int namelen);
// 성공시 0, 실패시 SOCKET_ERROR 반환
  1. 소켓 listen ```cpp #include

int listen(SOCKET s, int backlog); // 성공시 0, 실패시 SOCKET_ERROR 반환

4. 소켓 accept
```cpp
#include <winsock2.h>

SOCKET accept(SOCKET s, struct sockaddr *addr, int *addrlen);
// 성공시 소켓 핸들, 실패시 INVALID_SOCKET 반환


윈도우에서는 시스템 함수 호출을 통해 파일 생성시 파일 디스크립터 대신 핸들(handle)을 반환
단, 리눅스와는 달리 파일과 소켓의 핸들을 구분하여 사용

윈도우 기반 서버 프로그램 예시

// hello_server_win.cpp

#include <iostream>
#include <winsock2.h>

using namespace std;

void	ErrorHandling(char* message);

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

	int		szClntAddr;
	string	message = "Hello World!";
	if (argc != 2)
	{
		cout << "Usage : " << argv[0] << " <port>\n";
		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.c_str(), sizeof(message), 0);
	closesocket(hClntSock);
	closesocket(hServSock);
	WSACleanup();
	return 0;
}

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

윈도우 기반 클라이언트 프로그램 예시

// hello_client_win.cpp

#include <iostream>
#include <winsock2.h>

using namespace std;

void	ErrorHandling(char *message);

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

	char	message[30];
	int		strLen;
	if(argc != 3)
	{
		cout << "Usage : " << argv[0] << " <IP> <port>\n";
		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)
		error_handling("connect() error");

	strLen = recv(hSocket, message, sizeof(message) - 1, 0);
	if (strLen == -1)
		error_handling("read() error");

	cout << "Message from server : " << message << endl;
	closesocket(hSocket);
	WSACleanup();
	return 0;
}

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


윈도우 기반 입력 함수

#include <winsock2.h>

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

윈도우 기반 출력 함수

#include <winsock2.h>

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


내용 확인문제

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

    소켓은 물리적으로 연결된 네트워크상에서의 데이터 송수신에 사용할 수 있는 소프트웨어적인 장치를 의미하며, 따라서 네트워크 망을 연결하는 역할을 함
    소켓은 전력망으로부터 전기를 공급받기 위한 도구인 가전기기의 소켓과 비슷하게 네트워크 망의 연결에 사용되는 도구이기 때문에 소켓이라는 이름이 붙음

  2. 서버 프로그램에서는 소켓생성 이후에 listen 함수와 accept 함수를 차례대로 호출한다. 그렇다면 이들의 역할은 각각 무엇인지 비교해서 설명해보자.

    listen 함수는 소켓을 연결요청을 받아들일 수 있는 상태로 전환하는 역할
    반면 accept 함수는 실제로 들어온 연결요청을 수락하는 역할

  3. 리눅스의 경우 파일 입출력 함수를 소켓 기반의 데이터 입출력에 사용할 수 있다. 반면 윈도우에서는 불가능하다. 그렇다면 리눅스에서는 가능하고, 윈도우에서는 불가능한 이유가 무엇인가?

    리눅스는 소켓을 파일의 일종으로 취급하여 동일하게 다루지만, 윈도우에서는 소켓과 파일을 별개로 분리하여 다루기 때문

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

    각각의 소켓들을 구별하기 위해서는 해당 소켓의 주소정보가 필요하며, 따라서 소켓에 주소를 할당하기 위해 bind 함수를 호출함

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

    소켓의 구분 및 지칭을 편하게 하기 위해 운영체제가 부여한 정수값

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

    저수준 파일 입출력 함수는 표준이 아니므로 운영체제별로 함수가 다름
    반면 ANSI 표준은 C에서 공통적으로 사용되는 표준이므로 운영체제에 관계없이 동일한 함수임

  7. 본문에서 보인 예제 low_open.clow_read.c를 참조하여 파일 복사 프로그램을 작성하되, 저수준 파일 입출력 함수를 기반으로, 그리고 ANSI 표준 입출력 함수를 기반으로 각각 한번씩 작성해보자. 그리고 복사 프로그램의 사용방법은 여러분이 임의로 결정하기 바란다.

    저수준 파일 입출력 함수 기반

     #include <iostream>
     #include <fcntl.h>
     #include <unistd.h>
     #define BUF_SIZE 100
    
     using namespace std;
    
     void	error_handling(char* message);
    
     int		main()
     {
         int		fd;
         char	buf[] = "HELOOOOOOO";
         char	buf2[BUF_SIZE];
    
         fd = open("data.txt", O_CREAT|O_WRONLY|O_TRUNC, 0644);
         if (fd == -1)
             error_handling("open() error");
         cout << "file descriptor : " << fd << endl;
    
         if (write(fd, buf, sizeof(buf)) == -1)
             error_handling("write() error");
    
         close(fd);
    
         fd = open("data.txt", O_RDONLY);
    
         if (read(fd, buf2, sizeof(buf)) == -1)
             error_handling("read() error");
         cout << "file data : " << buf2 << endl;
    
         close(fd);
    
         return 0;
     }
    
     void	error_handling(char* message)
     {
         fputs(message, stderr);
         fputc('\n', stderr);
         exit(1);
     }
    

    ANSI 표준 입출력 함수 기반

     #include <iostream>
     #include <fcntl.h>
     #include <unistd.h>
     #define BUF_SIZE 100
    
     using namespace std;
    
     void	error_handling(char* message);
    
     int		main()
     {
         FILE *fp;
         char	buf[] = "HELOOOOOOO";
         char	buf2[BUF_SIZE];
    
         fp = fopen("data.txt", "w");
         if (fp == NULL)
             error_handling("open() error");
    
         if (fwrite(buf, sizeof(char), sizeof(buf), fp) == -1)
             error_handling("write() error");
    
         fclose(fp);
    
         fp = fopen("data.txt", "r");
    
         if (fread(buf2, sizeof(char), sizeof(buf), fp) == -1)
             error_handling("read() error");
         cout << "file data : " << buf2 << endl;
    
         fclose(fp);
    
         return 0;
     }
    
     void	error_handling(char* message)
     {
         fputs(message, stderr);
         fputc('\n', stderr);
         exit(1);
     }