본문 바로가기
Network/HTTP

Socket Programming

by 수픽 2020. 3. 2.

Socket


  • 컴퓨터 네트워크를 경유하는 프로세스 간 통신의 종착점
  • 대부분의 네트워크 소켓은 인터넷 소켓
  • 인터넷 프로토콜 (TCP, UDP, raw IP), 로컬 IP 주소, 로컬 포트, 원격 IP 주소, 원격 포트로 구성
  • UDP 프로토콜을 사용하는 경우와 TCP 프로토콜을 사용하는 경우로 분류
  • 클라이언트와 서버가 통신하기 위해 필요함
  • HTTP도 TCP/UDP 위 계층에서 돌아가기 때문에 소켓 프로그래밍을 해야 하므로 Socket과 HTTP는 이분법적인 개념이 아니다.

동작 과정은 다음과 같다.

소켓 통신 동작 과정

통신을 초기화하는 측을 클라이언트, 세션을 시작하기 위해 접속을 기다리는 측을 서버라고 한다.

서버와 클라이언트 모두 socket단계에서 소켓을 생성한다. 서버는 bind 단계에서 소켓을 서버의 로컬 IP 주소, 포트와 연결한다. 클라이언트에게서 요청을 받을 통로를 여는 과정이다. listen 단계에서 클라이언트의 요청을 기다리고 accept 부분에서 요청을 받아들인다. 클라이언트는 connect 부분에서 서버에 연결을 요청한다. send/recv 단계에서 데이터를 송수신하고 close로 닫아준다.

 

  • socket( ) : 소켓 생성
  • bind( ) : 소켓에 이름 지정
  • listen( ) : 클라이언트 연결 기다리기
  • accept( ) : 연결 요청 수락하기
  • connect( ) : 서버와 연결하기
  • send( ) : 데이터 보내기
  • recv( ) : 데이터 받기
  • close( ) : 소켓 종료

HTTP request - response


1. 헤더 파일, 전처리기 선언

소켓 프로그래밍을 하기 위해 winsock2.h 헤더 파일이 필요하다.

 

pragma comment 전처리기는 콘솔실행파일을 빌드하게 되면 해당 파일을 포함하여 처리하라는 명령이다. include <header.h>와 비슷한 기능을 한다. 

#pragma comment(lib, "libname")

 

2. WinSock 초기화

winsock2.h 안에 있는 winsock을 사용하기 전에 WSADATA 구조체와 관련 함수를 사용하여 초기화를 먼저 해준다. winsock을 사용하겠다는 것을 운영체제에게 알려주고 동시에 관련 리소스를 받아야 하기 때문에 필수적으로 해줘야 하는 과정이다.

 

WSAData 구조체는 윈도우 소켓 초기화 정보 구조체이다. 

struct WSAData {
	WORD wVersion;
    WORD wHighVersion;
    char szDescription[WSADESCRIPTION_LEN+1];
    char szSystemStatus[WSASYSSTATUS_LEN+1];
    unsigned short iMaxSockets;
    unsinged short iMaxUdpDg;
    char FAR* lpVendorInfo;
};

 

소켓을 시작하여 소켓 버전을 비교해준다. 버전이 맞지 않으면 시스템이 정지된다.

 

WSAStartup( ) 함수는 WSACleanup( ) 함수와 함께 소켓 프로그램의 시작과 끝을 알려준다. 윈속 동적 연결 라이브러리를 초기화하고 윈속 구현이 애플리케이션 요구사항을 충족하는지 확인한다.  

int WSAStartup(
	_In_ WORD wVersionRequested;
    _Out_LPWSADATA lpwSAData
};

이 함수의 첫 번째 인수는 윈속의 버전을 지정하고 두 번째 인수는 함수가 초기화된 상태를 저장하는 변수를 넘겨준다.

윈속을 초기화한 상태를 WSADATA구조체에 저장한다.

 

MAKEWORD 매크로는 소켓의 버전을 나타내준다.

#define MAKEWORD(주버전, 보조버전)

 

3. Socket 생성

socket( ) 함수는 소켓을 생성하여 반환한다. 

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

domain은 인터넷을 통해 통신할지 같은 시스템 내에서 프로세스끼리 통신할 지의 여부를 설정한다. type은 데이터의 전송 형태를 지정하고 protocol은 특정 프로토콜을 사용하는 부분이다. 보통 0을 사용한다. PF_INET은 IPv4 프로토콜을 뜻하고, SOCK_STREAM은 연결 지향을 말한다.

 

SOCKADDR_IN 구조체는 리눅스/유닉스 시스템에서 소켓의 통신 대상을 지정하기 위해 주소를 사용하는데, 주소를 저장하거나 표현하는 데 사용된다. SOCKADDR 구조체에서 sa_family가 AF_INET인 경우에 해당되는 구조체이다.

struct sockaddr_in {
		short sin_family; //주소 패밀리. 무조건 af_inet
		unsigned short sin_port; //포트 번호
		struct in_addr sin_addr; //호스트 ip 주소
		char sin_zero[8]; //zero padding
};

sin_port는 0~65535의 범위를 갖는 포트번호이다. 이 값은 네트워크 바이트 순서여야 한다. sin_addr은 호스트 IP 주소이고, sin_zero는 8 bytes dummy data이다. 반드시 0으로 채워져있어야 하는데, SOCKADDR 구조체와 크기를 일치시키기 위함이다.

 

소켓 생성이 실패했을 때 INVALID_SOCKET이라는 -1 반환 값을 반환한다. 오류 확인을 위한 코드이다.

 

4. host 설정

host는 2번에서 발견할 수 있다. (struct hostent *host)

hostent 구조체는 윈도우즈 소켓 시스템에 할당된다. 

struct hostent {
		char *h_name; //공식 도메인 이름
		char **h_aliases; //공식 도메인 이름외의 다른 이름들
		int h_addrtype; //주소체계
		int h_length; //ip주소의 길이(4bytes)
		char **h_addr_list; //바이트 정렬된 ip주소
		#define h_addr h_addr_list[0]; //여러 ip중, 첫번째 ip 주소
};

gethostbyname은 host 도메인 이름을 IP 주소로 변환해주는 함수이다.

 

5. SOCKET 상세 주소 지정

SOCKETADDR_IN 구조체를 이용한 부분이다. SOCKETADDR_IN 구조체이므로 AF_INET을 지정해주고 htons( ) 함수를 이용해 포트를 지정하고 순서를 바꿔준다. 그리고 호스트의 IP 주소를 SOCKETADDR_IN 구조체의 IP 주소로 지정한다.

 

htons( ) 함수는 short 메모리 값을 호스트 바이트 순서에서 네트워크 바이트 순서로 변경해주는 함수이다. 

  • 호스트 바이트 순서 : 2바이트 이상의 큰 숫자 변수에 대해 바이트를 메모리 상에 어떻게 배치하는지의 순서 (PC)

  • 네트워크 바이트 순서 : 2 바이트 이상의 큰 숫자에 대해 어떤 바이트로부터 전송할지에 대한 순서 (네트워크)

2바이트 이사의 큰 단위를 먼저 전송할지 작은 단위를 먼저 전송할지 정하지 않으면 문제가 발생한다.

  • Big-Endian 방식 : 메모리에 저장할 때 작은 단위의 값부터 저장 (0x34->0x12)

  • Little-Endian 방식 : 메모리에 저장할 때 큰 단위의 값부터 저장 (0x12->0x34)

방식이 같지 않아 바이트 배열 순서가 다르다면 어느 바이트가 큰 단위의 값인지 혼란이 발생할 것이다. 그래서 전송하는 방식을 맞추어야 하는데, PC에서는 리틀 엔디안 방식을, 네트워크에서는 빅 엔디안 방식을 사용한다. htons( ) 함수는 우리가 저장한 리틀 엔디안 방식을 빅 엔디안 방식으로 바꿔준다.

 

inet_addr( ) 함수는 문자열 형태의 IP주소 값을 unsinged long 타입의 값으로 변환한다. SOCKADDR_IN 구조체의 주소 데이터 타입은 unsigned long이기 때문에 IP 주소를 할당하기 위해 사용된다.

unsigned long inet_addr(const char *string)

 

6. connect

서버와 연결이 되지 않을 때 시스템을 중지한다.

 

connect( ) 함수는 클라이언트 소켓을 생성하고 서버로 연결을 요청한다. 

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

sockfd는 클라이언트 소켓의 파일 디스크립터이고, addr은 연결 요청을 보낼 서버 주소 정보를 지닌 구조체 변수의 포인터이다. addrlen은 serv_addr 포인터가 가리키는 주소 정보 구조체 변수의 크기이다.

클라이언트의 주소 할당은 connect( ) 함수를 호출할 때 bind 과정 없이 커널이 자동으로 해준다.

 

6. send/receive

send( ) 함수는 소켓 s를 통해 크기가 len인 메시지 msg를 flags에 지정한 방법으로 전송한다. 실제로 전송한 데이터의 바이트 수를 리턴한다. s는 소켓 번호, buf는 전송할 데이터가 저장된 버퍼, length는 buf 버퍼의 크기, flags는 보통 0을 뜻한다.

int send(int s, const void *msg, size_t len, int flags);

 

recv( ) 함수는 소켓 s를 통해 전송받은 메시지를 크기가 len인 버퍼 buf에 저장한다. 실제로 수신한 데이터의 바이트 수를 리턴한다. s는 소켓번호, buf는 수신 데이터를 저장할 버퍼, length는 buf 버퍼의 크기, flags는 보통 0을 뜻한다.

int recv(int s, void *buf, size_t len, int flags);

 

7. 출력 및 종료

서버로부터 받은 데이터를 출력한다.

closesocket( ) 함수와 WSACleanup( ) 함수를 사용하여 소켓과 ws2_32.dll을 종료시킨 후에 시스템을 종료한다.

 

8. 결과

구글로부터 받아온 응답 패킷이다.

 

 

 

'Network > HTTP' 카테고리의 다른 글

DDOS Generator 2  (0) 2020.03.30
Thread Pool  (0) 2020.03.22
멀티 스레딩  (0) 2020.03.16
HTTP 서버  (0) 2020.03.09
DDOS KISA  (0) 2020.02.24