책정리/열혈 TCP,IP

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

GONII 2015. 8. 5. 16:07
  • 네트워크 프로그래밍과 소켓의 이해

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

    네트워크로 연결되어 있는 두 컴퓨터간에 데이터를 주고받기 위해 필요한 것

    물리적인 연결

    물리적인 연결을 기반으로 하는 소프트웨어적인 데이터의 송수신 방법 ( 운영체제에서 '소켓(Socket)'이라는 것을 제공 )

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

    • 전화기의 장만에 비유되는 socket 함수호출의 이해

      전화기에 해당하는 소켓을 생성하는 함수

#include <sys/socket.h>

int socket(int domain, int type, int protocol);

// 성공 : 파일 디스크립터

// 실패 : -1 반환

  • 전화번호의 부여에 비유되는 bind 함수호출의 이해

    전화기에 전화번호가 할당되는 것처럼 다음 함수를 이용해서 앞서 생성한 소켓에 IP와 포트번호라는, 소켓의 주소정보에 해당하는 것을 할당해야 한다

#include <sys/socket.h>
int bind(int sockfd, struct sockaddr* myaddr, socklen_t addrlen);
// 성공 : 0
// 실패 : -1

  • 전화기의 케이블에 연결에 비유되는 listen 함수호출의 이해

    전화기가 전화 케이블에 연결되는 순간 전화를 받을 수 있는 상태가 된다. 이는 누군가 전화를 걸어서 연결요청을 할 수 있는 상태에 놓임을 뜻한다. 마찬가지로 소켓도 연결요청이 가능한 상태가 되어야 한다 다음 함수는 소켓을 연결요청이 가능한 상태가 되게 한다

#include <sys/socket.h>

int listen(int sockfd, int backlog);

// 성공 : 0

// 실패 -1

  • 수화기를 드는 것에 비유되는 accept 함수호출의 이해

    수화기를 들었다는 것은 연결요청에 대한 수락을 의미한다. 소켓도 마찬가지이다. 누군가 데이터의 송수신을 위해 연결요청을 해오면, 다음 함수호출을 통해 그 요청을 수락해야 한다

#include <sys/socket.h>

int accept(int sockfd, struct sockaddr* addr, socklen_t* addrlen);

// 성공 : 파일 디스크립터

// 실패 : -1

  • 소켓의 생성과정

소켓 생성

socket 함수 호출

IP주소와 PORT번호 할당

bind 함수 호출

연결요청 가능상태로 변경

listen 함수 호출

연결요청에 대한 수락

accept 함수 호출

  • Hello world 서버프로그램 구현

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

#include <unistd.h>

#include <arpha/inet.h>

#include <sys/socket.h>

   

void error_handling(char* message);

   

int main(int argc, char* argv[])

{

int serv_sock;

int clnt_sock;

   

struct sockaddr_in serv_addr;

struct sockaddr_int clnt_addr;

socklent_t clnt_addr_size;

   

char message[] = "hello world!";

   

if( argc != 2 )

{

printf("Usage : %s <port>\n", argv[0]);

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, (struct 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, (struct sockaddr*)&clnt_addr, &clnt_addr_size);

if( clnt_sock == -1 )

error_handling("accept() error");

   

write(clnt_sock, message, sizeof(message));

close(clnt_sock);

close(serv_sock);

   

return 0;

}

   

void error_handling(char* message)

{

fputs(message, stderr);

fputc('\n', stderr);

exit(1);

}

  • 전화 거는 소켓의 구현

    앞서 보인 서버 프로그램에서 생성한 소켓을 가리켜 '서버 소켓' 또는 '리스닝(listening) 소켓'이라 한다. 연결요청을 진행하는 소켓은 '클라이언트 소켓'이다

#include <sys/socket.h>

int connect(int sockfd, struct sockaddr* serv_addr, socklent_t addrlen);

// 성공 : 0

// 실패 : -1

  • hello world 클라이언트 프로그램

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

#include <unistd.h>

#include <arpha/inet.h>

#include <sys/socket.h>

   

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 )

{

printf("Usage : %s <IP> <port>\n", argv[0]);

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, (struct 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!");

   

printf("Message from server : %s\n", message);

close(sock);

   

return 0;

}

   

void error_handling(char* message)

{

fputs(message, stderr);

fputc('\n', stderr);

exit(1);

}

  • 리눅스 기반 파일 조작하기

    리눅스는 소켓을 파일의 일종으로 구분한다. 따라서 파일 입출력 함수를 소켓 입출력에, 다시 말해서 네트워크상에서의 데이터 송수신에 사용할 수 있다.

    • 저 수준 파일 입출력(Low-level File Access)과 파일 디스크립터(File Descriptor)

      여기서 말하는 저수준이란 "표준에 상관없이 운영체제가 독립적으로 제공하는~"의 으미로 받아들이면 된다. 리눅스에서 제공하는 파일 입출력 함수를 사용하려면 파일 디스크립터에 대한 개념을 먼저 세워야 한다.

      여기서 말하는 파일 디스크립터란 시스템으로부터 할당받은 파일 또는 소켓에 부여된 정수를 의미한다. 표준 입출력 및 표준 에러에도 리눅스에서는 다음과 같이 파일 디스크립터를 할당하고 있다

파일 디스크립터

대상

0

표준입력 : Standard Input

1

표준출력 : Standard Output

2

표준에러 : Standard Error

일반적으로 파일과 소켓은 생성의 과정을 거쳐야 파일 디스크립터가 할당된다. 반면 위에서 보이는 세 가지 입출력 대상은 별도의 생성 과정을 거치지 않아도 프로그램이 실행되면 자동으로 할당되는 파일 디스크립터들이다.

  • 파일 열기

    데이터를 읽거나 쓰기 위해서 파일을 열 때 사용하는 함수.

    첫 번째 인자로는 대상이 되는 파일의 이름 및 경로 정보를, 두 번째 인자로는 파일의 오픈 모드 정보(파일의 특성 정보)를 전달한다

#include <sys/types.h>

#include <sys/stat.h>

#include <fcntl.h>

int open(const char* path, int flag);

// 성공 : 파일 디스크립터

// 실패 : -1

두 번째 매개변수 flag에 전달할 수 있는 값과 그 의미는 다음과 같으며, 하나 이상의 정보를 비트 OR 연산자로 묶어 전달 가능하다

오픈 모드

의미

O_CREAT

필요한 파일을 생성

O_TRUNC

기존 데이터 전부 삭제

O_APPEND

기존 데이터 보존하고, 뒤에 이어서 저장

O_RDONLY

읽기 전용으로 파일 오픈

O_WRONLY

쓰기 전용으로 파일 오픈

O_RDWR

읽기, 쓰기 겸용으로 파일 오픈

  • 파일 닫기

    파일을 사용 후 반드시 닫아줘야 한다 때문에 파일을 닫을 때 호출하는 함수를 다음과 같다

#include <unistd.h>

int close(int fd);

// 성공 : 0

// 실패 : -1

위 함수는 파일 뿐만 아니라, 소켓을 닫을 때에도 사용된다. 이는 파일과 소켓을 구분하지 않는다는 리눅스 운영체제의 특성을 확인 할 수 있다.

  • 파일에 데이터 쓰기

    write함수는 파일에 데이터를 출력(전송)하는 함수이다. 리눅스에서는 파일과 소켓을 동일하게 취급하므로, 소켓을 통해서 다른 컴퓨터에 데이터를 전송할 때에도 이 함수를 사용할 수 있다.

#include <unistd.h>

   

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

// 성공 : 전달한 바이트 수

// 실패 : -1

  • 예제 low_open.c

#include <stdio.h>

#include <stdlib.h>

#include <fcntl.h>

#include <unistd.h>

   

void error_handling(char* message);

   

int main(void)

{

int fd;

char buf[] = "Let's go!\n";

fd = open("data.txt", OCREAT | O_WRONLY | O_TRUNC);

if( fd == -1 )

error_handling("open() error!");

printf("file descriptor : %d\n", fd);

   

if( write(fd, buf, sizeof(buf)) == -1 )

error_handling("write() error");

return 0;

}

   

void error_handling(char* message)

{

fputs(message, stderr);

fputc('\n', stderr);

exit(1);

}

  • 파일에 저장된 데이터 읽기

    read함수는 데이터를 입력(수신)하는 기능의 함수이다

#include <unistd.h>

   

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

// 성공 : 수신한 바이트 수(파일의 끝을 만나면 0)

// 실패 : -1

  • 예제 low_read.c

#include <stdio.h>

#include <stdlib.h>

#include <fcntl.h>

#include <unistd.h>

#define BUF_SIZE 100

void error_handling(char* message);

   

int main(void)

{

int fd;

char buf[BUF_SIZE];

   

fd = open("data.txt", O_RDONLY);

if( fd == -1 )

error_handling("open() error!");

printf("file descriptor : %d\n", fd);

   

if( read(fd, buf, sizeof(buf)) == -1 )

error_handling("read() error!");

printf("file data : %s", buf);

close(fd);

   

return 0;

}

   

void error_handling(char* message)

{

fputs(message, stderr);

fputc('\n', stderr);

exit(1);

}

  • 파일 디스크립터와 소켓

    이번 예제에서는 파일도 생성해보고, 소켓도 생성해본다. 그리고 반환되는 파일 디스크립터의 값을 정수형태로 비교해 보겠다

#include <stdio.h>

#include <fcntl.h>

#include <unistd.h>

#include <sys/socket.h>

   

int main(void)

{

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);

   

printf("file descriptor 1: %d\n", fd1);

printf("file descriptor 2: %d\n", fd2);

printf("file descriptor 3: %d\n", fd3);

   

close(fd1);

close(fd2);

close(fd3);

   

return 0;

}

출력된 디스크립터의 정수 값을 비교해보면, 일련의 순서대로 넘버링(numbering)이 되는 것을 알 수 있다. 참고로 파일 디스크립터가 3에서부터 시작하는 이유는 0, 1 그리고 2는 표준 입출력에 이미 할당되었기 때문이다.

  • 윈도우 기반으로 구현하기
    • 윈도우 소켓을 위한 헤더와 라이브러리 설정

      윈속(윈도우 소켓)을 기반으로 프로그램을 개발하기 위해서는 기본적으로 다음 두 가지를 진행해야 한다.

    • 헤더 파일 winsock2.h를 포함
    • ws2_32.lib 라이브러를 링크
    • 윈속(winsock)의 초기화

      윈속 프로그래밍을 할 때에는 반드시 WSAStartup함수를 호출해서 프로그램에서 요구하는 왼두오 소켓의 버전을 알리고, 해당 버전을 지원하는 라이브러리의 초기화 작업을 진행해야 한다.

#include <winsock2.h>

int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);

// 성공 : 0

// 실패 : 0이 아닌 에러 코드 값 반환

윈도우 소켓에는 몇몇 버전이 존재한다. 따라서 사용할 소켓의 버전정보를 WORD형으로 구성해서, 위 함수의 첫 번째 매개변수 wVersionRequested로 전달한다. 만약 사용할 소켓의 버전이 1.2라면, 1이 주 버전이고 2가 부 버전이므로 0x0201을 인자로 전달해야 한다.

이렇듯 상위 8비트에는 부 버전 정보를, 하위 8비트에는 주 버전 정보를 표시해서 인자로 전달하게 되는데, 우리는 버전 2.2를 기반으로 프로그래밍을 하기 때문에 0x0202를 인자로 전달하면 된다. 그런데 바이트 단위로 쪼개서 버전정볼르 설정하는 것이 조금 번거롭게 느껴질 것이다. 그래서 매크로 함수인 MAKEWORD가 제공되고 있다. 이 함수를 사용하면 간단히 WORD형 버전정보를 구성할 수 있다.

MAKEWORD(1,2); // 주 버전1, 부 버전2, 0x0201 반환

MAKEWORD(2,2); // 주 버전2, 부 버전2, 0x0202 반환

두 번째 매개변수는 WSADATA 구조체 변수의 주소값을 인자로 전달해야 한다.그러면 함수호출이 완료되고 난 다음에 해당 변수에는 초기화된 라이브러리의 정보가 채워진다. 특별히 큰 의미를 지니지는 않지만, 함수호출을 위해서는 반드시 WSADATA 구조체 변수의 주소 값을 전달해야 한다.

다음 함수는 윈속 라이브러리의 해제에 사용되는 함수이다.

#include <winsock2.h>

int WSACleanup(void);

// 성공 : 0

// 실패 : SOCKET_ERROR

이 함수를 호출하면, 할당된 윈속 라이브러리는 윈도우 운영체제에 반환이 되면서, 윈속관련 함수의 호출이 불가능해진다. 따라서 더 이상 윈속관련 함수의 호출이 불필요할 때, 위 함수를 호출하는 것이 원칙이나 프로그램이 종료되기 직전에 호출하는 것이 보통이다.

  • 윈도우 기반의 소켓관련 함수와 예제
    • 윈도우 기반 소켓관련 함수들

      다음 함수는 리눅스의 socket함수와 동일한 기능을 제공한다.

#include <winsock2.h>

SOCKET socket(int af, int type, int protocol);

// 성공 : 소켓 핸들

// 실패 : INVALID_SOCKET 반환

다음 함수는 리눅스의 bind 함수와 동일한 기능을 제공한다. 즉, IP주소와 PORT번호의 할당을 목적으로 호출되는 함수이다.

#include <winsock2.h>

int bind(SOCKET s, const struct sockaddr* name, int namelen);

// 성공 : 소켓 핸들

// 실패 : SOCKET_ERROR 반환

다음 함수는 리눅스의 listen 함수와 동일한 기능을 제공한다. 즉, 소켓이 클라이언트 프로그램의 연결 요청을 받아들일 수 있는 상태가 되게 하는 것을 목적으로 호출되는 함수이다.

#include <winsock2.h>

int listen(SOCKET s, int backlog);

// 성공 : 0

// 실패 : SOCKET_ERROR

다음 함수는 리눅스의 accept 함수와 동일한 기능을 제공한다. 즉, 클라이언트 프로그램에서의 연결요청을 수락할 때 호출하는 함수이다.

#include <winsock2.h>

SOCKET accept(SOCKET s, struct sockaddr* addr, int* addrlen);

// 성공 : 소켓 핸들

// 실패 : INVALID_SOCKET 반환

다음 함수는 클라이언트 프로그램에서 소켓을 기반으로 연결요청을 할 때 호출하는 함수로써, 리눅스의 connect 함수와 동일한 기능을 제공한다.

#include <winsock2.h>

int connect(SOCKET s, const struct sockaddr* name, int namelen);

// 성공 : 0

// 실패 : SOCKET_ERROR 반환

다음 함수는 소켓을 닫을 때 호출하는 함수이다. 리눅스에서는 파일을 닫을 때에도 소켓을 닫을 때에도 close 함수를 호출하지만, 윈도우에서는 소켓을 닫을 때 호출하는 다음 함수가 별도로 마련되어 있다.

#include <winsock2.h>

int closesocket(SOCKET s);

// 성공 : 0

// 실패 : SOCKET_ERROR 반환

  • 윈도우에서의 파일 핸들과 소켓 핸들

    리눅스는 내부적으로 소켓도 파일로 취급하기 때문에, 파일을 생성하건 소켓을 생성하건 파일 디스크립터가 반환된다고 설명했다. 윈도우에서도 시스템 함수의 호출을 통해서 파일을 생성할 때 '핸들(handle)'이라는 것을 반환한다. 즉, 윈도우에서의 핸들은 리눅스에서의 파일 디스크립터에 비교될 수 있다. 그런데 윈도우는 리눅스와 달리 파일 핸들과 소켓 핸들을 구분하고 있다. 물론 핸들이라는 관점에서 바라볼 때는 동일하다고 판단할 수 있지만 리눅스에서처럼 완벽히 동일하게 취급되지 않는다. 때문에 파일 핸들 기반의 함수와 소켓 핸들 기반의 함수에 차이가 있다. 이점이 리눅스의 파일 디스크립터와 다른 점이다.

  • 예제 hello_server.c

#include <stdio.h>

#include <stdlib.h>

#include <winsock2.h>

#pragma comment(lib, "ws2_32.lib")

void errorHandling(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(char* message)

{

fputs(message, stderr);

fputc('\n', stderr);

exit(1);

}

  • 예제 hello_client

#include <stdio.h>

#include <stdlib.h>

#include <winsock2.h>

#pragma comment(lib, "ws2_32.lib")

   

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 )

{

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("recv() error");

printf("Message form server : %s\n", message);

   

closesocket(hSocket);

WSACleanup();

   

return 0;

}

   

void errorHandling(char* message)

{

fputs(message, stderr);

fputc('\n', stderr);

exit(1);

}

  • 윈도우 기반 입출력 함수

    리눅스는 소켓도 파일로 간주하기 때문에, 파일 입출력 함수인 read와 write를 이용해서 데이터를 송수신할 수 있다. 그러나 윈도우는 파일 입출력 함수와 소켓 입출력 함수가 엄연히 구분된다.

#include <winsock2.h>

int send(SOCKET s, const char* buf, int len, int flags);

// 성공 : 전송된 바이트 수

// 실패 : SOCKET_ERROR 반환

위 함수를 리눅스의 write 함수와 비교해보면, 마지막 매개변수 flags가 존재하는 것 이외에는 차이가 없음을 알 수 있다.

#include <winsock2.h>

int recv(SOCKET s, const char* buf, int len, int flags);

// 성공 : 수신한 바이트 수(EOF 전송 시 0)

// 실패 : SOCKET_ERROR 반환

 

반응형

'책정리 > 열혈 TCP,IP' 카테고리의 다른 글

5장 TCP 기반 서버/클라이언트2  (0) 2015.08.05
4장 TCP 기반 서버/클라이언트1  (0) 2015.08.05
3장 주소체계와 데이터 정렬  (0) 2015.08.05
2장 소켓의 타입과 프로토콜의 설정  (0) 2015.08.05
목차  (0) 2015.08.05