본문 바로가기
Programming/열혈 TCP, IP 소켓 프로그래밍(저자 윤성우)

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

by minjunkim.dev 2020. 7. 28.

    모든 내용은 [윤성우 저, "열혈강의 TCP/IP 소켓 프로그래밍", 오렌지미디어] 를 기반으로 제 나름대로 이해하여 정리한 것입니다. 다소 부정확한 내용이 있을수도 있으니 이를 유념하고 봐주세요!


# 네트워크(or 소켓) 프로그래밍이란?
- 네트워크로 연결되어 있는 서로 다른 두 컴퓨터가 데이터를 주고받을 수 있도록 하는 것
- 이를 위해 필요한 것들
1) 물리적인 연결(e.g. 인터넷)
2) 소프트웨어적인 데이터의 송수신 방법 : 이를 위해 운영체제에서 "소켓"을 제공


# 소켓이란?
- 물리적으로 연결된 네트워크상에서의 데이터 송수신에 사용할 수 있는 소프트웨어적인 장치
- 소켓은 네트워크 망의 연결에 사용되는 도구(망에 연결되어야만 망을 이용할 수 있음)

 


 

# 리눅스와 윈도우의 차이점 중 하나는?
리눅스 : 소켓조작과 파일조작을 동일하게 간주 -> 파일 입출력 함수를 소켓을 통한
입출력(데이터 송수신)에 사용 가능
윈도우 : 파일과 소켓을 구분 -> 별도의 데이터 송수신 함수를 참조해야 함

 

# 리눅스에서 독립적으로 제공하는 파일 입출력 함수를 통해서 소켓을 조작할 수 있으므로, 이를 통해 파일 입출력을 진행할수 있음(저수준 파일 입출력)

 

# 저수준 파일 입출력
- 표준에 상관없이 운영체제가 독립적으로 제공하는 파일 입출력 함수를 이용한 파일 입출력을 의미
- 즉, ANSI 표준에서 정의한 함수를 사용하지 않음(C언어를 배우면서 공부했던 fopen, fclose, fread, fwrite, fputs, fgets, fprintf, fscanf, freopen, fseek, ... 등이 여기에 해당)
- 리눅스에서 독립적으로 제공하는 파일 입출력 함수를 사용하려면 "파일 디스크립터" 개념을 알아야 함.
- ANSI 표준에서 정의한 함수를 사용한 파일 입출력은 "고수준 파일 입출력"이라고 하며, 이를 이용하기 위해서는 FILE 구조체 포인터(FILE *)가 필요

 

# 파일 디스크립터(윈도우에서의 파일 핸들)란?
- 시스템으로부터 할당받은 "파일 또는 소켓에 부여된 정수"
- 일련의 순서대로 "넘버링" 됨
파일 디스크립터 0 : 표준입력
파일 디스크립터 1 : 표준출력
파일 디스크립터 2 : 표준에러
위 3가지의 경우에만 자동으로 할당되며,
나머지는 파일과 소켓의 "생성 과정"을 거쳐야 파일 디스크립터가 할당됨
- 리눅스에서는 파일과 소켓을 동일하게 간주하므로 파일과 소켓의 파일 디스크립터는 "완전히 동일"

 


 

# 파일 열기

#include <sys/types.h>
#include <sys/stats.h>
#include <fcntl.h>

int open(const char *path, int flag);
// -> 성공시 파일 디스크립터, 실패시 -1 반환

- path : 파일 이름을 나타내는 문자열의 주소값
- flag : 파일의 오픈 모드 정보

 


# 파일의 오픈 모드 정보

오픈 모드 의 미
O_CREAT 필요하면 파일을 생성
O_TRUNC 기존 데이터 전부 삭제
O_APPEND 기존 데이터 보존하고, 뒤에 이어서 저장
O_RDONLY 읽기 전용으로 파일 오픈
O_WRONLY 쓰기 전용으로 파일 오픈
O_RDWR 읽기, 쓰기 겸용으로 파일 오픈

- 하나 이상의 정보를 전달하고 싶으면 비트 OR 연산자로 묶어서 전달 가능
e.g. OCREAT|O_WRONLY|O_TRUNC

 

 

# 파일 닫기 : 파일을 열었다면 반드시 닫아주어야 함을 기억

#include <unistd.h>

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

- fd : 닫고자 하는 파일 또는 소켓의 파일 디스크립터

 

 

# open/close 함수는 리눅스의 저수준 파일 입출력 함수

# 파일이든 소켓이든 동일한 사용법으로 열고 닫을 수 있음

 

 

# _t로 끝나는 자료형 : 시스템(운영체제)에서 정의하는 자료형, "고전적인(primitive) 자료형"
e.g.) typedef unsigned int size_t; typedef signed int ssize_t;
- 일반적으로 sys/type.h 헤더파일에 typedef 선언을 통해서 정의되어 있음
- 시스템의 차이나, 시간의 흐름에 따라서 자료형의 표현 방식은 언제든지 달라질 수 있어,
(보통 우리가 int 자료형을 4byte라고 배우지만, 이는 절대적인 것이 아님)
프로그램상에서 선택된 자료형의 변경이 요구될 수 있는데, 기본 자료형을 사용하면 많이 번거로움
- "고전적인(primitive) 자료형"들을 사용하면 typedef 선언만을 변경하여 컴파일 해주면 되기 때문에 코드변경의 최소화가 가능

 

 

# 파일에 데이터 쓰기

#include <unistd.h>

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

- fd : 데이터 전송대상을 나타내는 파일 디스크립터
- buf : 전송할 데이터가 저장된 버퍼의 주소값
- nbytes : 전송할 데이터의 바이트수

 

 

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

#include <unistd.h>

ssize_t read(int fd, void *buf, size_t nbytes);
// -> 성공시 수신한 바이트 수(단 파일의 끝(EOF)을 만나면 0), 실패시 -1 반환

- fd : 데이터 수신대상을 나타내는 파일 디스크립터
- buf : 수신한 데이터를 저장할 버퍼의 주소값
- nbytes : 수신할 최대 바이트수

 

 

# write/read 함수는 리눅스의 저수준 파일 입출력 함수

# 위의 함수들로 파일의 쓰기/읽기 뿐만 아니라 소켓을 통한 데이터 송/수신이 가능

 


# 계속해서 "리눅스에서는 파일과 소켓을 동일"하게 취급함을 강조!!

 

# 파일 디스크립터가 "일련의 순서로 넘버링" 위에서 언급했는데,
이를 좀 자세히 설명하자면 다음과 같다.
파일 디스크립터 0 - 2는 표준입력/출력/에러에 자동으로 할당되므로,
처음으로 만든 파일이나 소켓으로부터 얻은 파일 디스크립터는 "3"이 될 것이다.
이후로 얻은 파일 디스크립터는 4, 5, ... 이런 식으로 얻는다.

 


 

# 윈도우 소켓(윈속, winsock)을 위한 헤더와 라이브러리의 설정
1) 헤더파일 winsock2.h 를 포함해야 함
2) ws2_32.lib 라이브러리 링크를 링크시켜야 함

# 윈도우 소켓은 상당부분 BSD 계열 유닉스 소켓을 참고하여 설계됨 -> 리눅스 소켓과 유사하지만 다르다!

 

# 윈속의 초기화
- 윈속 프로그래밍 시에는 반드시 WSAStartup 함수를 호출해서,
프로그램에서 요구하는 윈속의 버전을 알리고, 해당 버전을 지원하는 라이브러리의 초기화 작업을 진행해야 함

#include <winsock2.h>

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

- wVersionRequested : 프로그래머가 사용할 윈속의 버전정보
- lpWSAData : WSADATA라는 구조체 변수의 주소값

 


- 윈속의 버전정보를 WORD형으로 구성해서 전달해야 함
e.g. 사용할 소켓의 버전이 1.2라면(주버전 1, 부버전 2)
-> 0x0201;(상위 8비트에 부버전 정보, 하위 8비트에 주버전 정보)
- 버전정보를 위처럼 바이트 단위로 쪼개서 설정해야하는 것이 번거로우므로
매크로 함수 MAKEWORD가 제공됨
e.g. MAKEWORD(1, 2) (윈속의 버전이 1.2)

 

 

# 윈속 기반의 프로그래밍에서의 거의 공식과 같은 코드 구조

#include <stdio.h>
#include <winsock2.h>

int main(int argc, char *argv[])
{
    WSADATA wsaData;
    ...
    if(WSAStartup(MAKEWORD(1,2),&wsaData) ! = 0) // 윈속관련 라이브러리의 초기화
	error_handling("...");
    ...
    WSACleanup(); // 초기화된 라이브러리 해제 -> 할당된 윈속 라이브러리가 윈도우 운영체제에 반환됨 -> 윈속관련 함수 호출이 불가능해짐
    return 0;
}

 

 

# 초기화된 윈속 라이브러리의 해제

#include <winsock2.h>

int WSACleanup(void);
// -> 성공시 0, 실패시 SOCKET_ERROR 반환

- 더이상 윈속관련 함수의 호출이 불필요할 때 위 함수를 호출하는 것이 원칙이나,
프로그램이 종료되기 직전에 호출하는 것이 일반적

 

 

# 윈도우는 리눅스와 달리 파일 핸들과 소켓 핸들을 구분
-> 파일핸들과 소켓핸들에 대한 함수에 차이가 있음(리눅스의 파일 디스크립터와의 차이점)

 


 

# 윈도우 기반 입출력 함수

#include <winsock2.h>

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

- s : 데이터 전송 대상과의 연결을 의미하는 소켓 핸들값
- buf : 전송할 데이터를 저장하고 있는 버퍼의 주소값
- len : 전송할 바이트수
- flags : 데이터 전송시 적용할 다양한 옵션정보

 

#include <winsock2.h>

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

- s : 데이터 수신 대상과의 연결을 의미하는 소켓 핸들값
- buf : 수신된 데이터를 저장할 버퍼의 주소값
- len : 수신할 수 있는 최대 바이트수
- flags : 데이터 수신시 적용할 다양한 옵션정보

 

 

# 리눅스 : read / write : 파일과 소켓을 동일시 하기에 소켓에 그대로 사용가능
- 그러나 리눅스에도 recv / send가 존재함
# 윈도우 : recv / send : "소켓" 입출력 함수

 

 

[출처] : 윤성우 저, "열혈강의 TCP/IP 소켓 프로그래밍", 오렌지미디어