모든 내용은 [윤성우 저, "열혈강의 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 소켓 프로그래밍", 오렌지미디어
'Programming > 열혈 TCP, IP 소켓 프로그래밍(저자 윤성우)' 카테고리의 다른 글
Ch 02. 내용 확인문제 (0) | 2020.08.04 |
---|---|
Ch 02. 소켓의 타입과 프로토콜의 설정 (0) | 2020.08.04 |
표준 파일 입출력 함수(고수준 파일 입출력 함수) (0) | 2020.07.30 |
Ch 01. 내용 확인문제 (0) | 2020.07.28 |
TCP/IP 소켓 프로그래밍 (0) | 2020.07.28 |