IT 그리고 정보보안/Knowledge base

네트워크 소켓(Socket)은 무엇인가

plummmm 2021. 4. 11. 13:45
반응형

자 이제 소켓 프로그래밍을 하기 위해 기초지식을 알아보자.

먼저 소켓이 뭔지 알아야 할 것이다.

 

소켓이란?

인터넷 소켓(Internet socket, socket' 혹은 network socket 라고 부르기도 한다)은 네트워크로 연결되어 있는 컴퓨터의 통신의 접점에 위치한 통신 객체다. 네트워크 통신을 위한 프로그램들은 소켓을 생성하고, 이 소켓을 통해서 서로 데이터를 교환한다.소켓은 RFC 147에 기술사항이 정의 되어 있다.

 

인터넷 소켓은 다음과 같은 요소들로 구성되어 있다.

  • 인터넷 프로토콜 (TCP, UDP, raw IP)
  • 로컬 IP 주소
  • 로컬 포트
  • 원격 IP 주소
  • 원격 포트

라고 위키피디아에 나와 있다.

 

네트워크 프로그래밍은 소켓을 이용하는데, 뭐 소위 소켓 프로그래밍이라고 부른다.

요 소캣 프로그래밍을 하기 위해서 몇가지 소캣 함수들을 사용한다.

각 함수마다 각자의 기능이 있다.

대충 네트워크 구성도는 아래 그림과 같이 구성된다.

* 서버 측

1. 수신 소캣을 생성한다. (socket)

2. ip주소와 포트번호를 결정하여 소캣에 적용시킨다. (bind)

3. 클라이언트 요청을 대기하는 상태(listen 상태)로 변경한다. (listen)

4. 서버에 접속한 클라이언트와 통신할 수 있는 새로운 소캣을 생성한다. (accept)

5. 데이터 송수신을 한다. (recv, send)

6. 끝. (close)

 

* 클라이언트 측

1. 송신 소캣을 생성한다. (socket)

2. 서버에 접속한다. (connect)

3. 데이터를 송수신한다. (recv, send)

4. 끝 (close)

 

소켓 주소 구조체

소켓에서 사용하는 주소들에 대한 얘기를 해볼까한다.

소켓에서는 클라이언트나 서버의 주소를 좀더 상세하게 표현하기 위해 소켓 주소 구조체 를 사용한다.

 

보통 이 구조체들의 이름은 sockaddr_ 로 시작하여 사용하는 프로토콜에 따른 접미사로 끝난다.

먼저 일반적인 소켓 주소 구조체인 sockaddr 를 보자.

 

#include <sys/socket.h>

 

sa_family : 소켓의 주소체계를 말한다. (AF_INET 이라면 IPv4  주소체계 )

sa_data[14] : 해당 프로토콜의 상세 주소 정보를 말한다. IP주소와 포트번호가 담긴다.

 

나중에 다시 언급하겠지만 bind() 함수의 인자로 sockaddr 구조체 포인터를 사용한다.

 

근데 data[14]에 ip주소와 포트번호를 넣으면 구분하기 불편하고 

우리가 네트워크 통신할 때 거의 IP주소와 포트를 사용하므로 인터넷 주소 구조체가 따로 정의되었다.

sockaddr_in 이라는 구조체다.

 

#include <netinet/in.h>

 

위에 in_addr 구조체는 32비트의 IP주소를 담고있는 구조체이다.

자료형이 좀 생소한데, POSIX에서 정의한 자료형을 표로 함 보자.

 

sockaddr_in 에서 in_addr 구조체에 저장된 IP주소를 가져오고 그외 다른 정보를 담는다. 내용을보자.

sin_family : 주소체계를 말한다. AF_INET(인터넷 주소 체계)를 주로 쓴다.

sin_port : 16비트 포트번호이다. 빅 엔디안으로 저장해야 한다. 

sin_addr : in_addr 구조체에 저장된 IP주소

sin_zero[8] : sockaddr 구조체와 같은 크기(16byte)를 유지하기 위해 필요한 쓰레기값. 항상 0이다.

                 다양한 주소체계를 표현하고자 구현되었는데 잘 안씀.

 

아래는 모두 소개하지는 않았지만, 여러가지 소켓 주소 구조체들의 구조이다.

 

소켓 주소 구조체들의 정보를 넘겨줄 때는, 당연한 말이지만 구조체 포인터를 이용하여 넘겨준다.

그리고 구조의 길이도 전달하는데, 전달 방향에 따라 전달 방법이 달라진다.

 

1. 왼쪽 그림을 보면 프로세스에서 커널로 보내는 방향이다. bind(), connect(), sendto() 함수에 해당하며,

구조체 포인터와 길이 정수값(int형)을 전달한다.

커널은 포인터와 포인터가 가르키는 것의 길이를 전달받기 때문에 정확한 data의 정보를 알고 있다.

 

2. 오른쪽 그림을 보면 커널에서 프로세스로 보내는 방향이다. accept(), recvfrom(), getsockname(), getpeername()

함수에 해당하며, 구조체 포인터와 길이에 대한 정수형 포인터값을 전달한다. 

 

왜 바꼈지?? 이 인자를 보고 "값-결과 인수(value-result argument)" 라고 하는데, 

결과적으로 말하자면, 길이가 가변적이기 때문이다. 함수 호출 시점(value) 함수 리턴 시점(result)에서 값이 다를 수 있기 때문.

value는 구조체의 크기로 커널에서 프로세스로 전달할 때 크기를 벗어나지 않도록 참조하고 result는 커널에서 실제로 프로세스로 전달한 크기가 된다. 이런 인자를 값-결과 인수라고 한다.

 

이런 값-결과 인수를 사용하는 경우는 소켓 주소 구조의 길이가 일정한 IPv4(sockaddr_in = 16), IPv6(sockaddr_in6 = 24)은 관련없는데 유닉스 영역의 주소 구조(sockaddr_un) 같은 경우 크기가 고정적이지 않아 값-결과 인수가 필요하다. 

 

 

바이트 순서 (Byte ordering)

시스템마다 메모리에 저장하는 바이트 순서가 다르다는 것을 알고 있을 것이다. (리틀 엔디안, 빅 엔디안 두가지)

우리가 사용하는 시스템은 대부분 리틀 엔디안 이다.

 

이 얘기를 갑자기 왜 꺼내느냐. 네트워크에서는 빅-엔디안을 쓰기로 규칙이 정해져 있다.

그래서 리틀-엔디안을 사용하는 시스템과 통신하려면 바이트 순서 변환이 필요하다 이말.

 

이렇게 바이트를 변환해야 하니, 그에 따른 함수들도 존재하기 마련이다. 한번알아보자.

 

host(h) 에서(to) network(n)로 변환하는 함수들이다. htons, htonl 두가지인데,

s는 short형 즉 2바이트. 포트 번호를 변환하는 함수이고

l은 long형 즉 4바이트, IP 주소를 변환하는 함수이다. (리눅스 환경에서 long 형은 4바이트 이다)

 

그럼 반대로 가는 함수들에 대해서도 알아보자.

network(n) 에서(to) host(h) 로 변환하는 함수들이다. ntohs, ntohl 

마찬가지로, s는 short형 즉 2바이트. 포트 번호를 변환하는 함수이고

l은 long형 즉 4바이트, IP 주소를 변환하는 함수이다. (리눅스에서 long 형은 4바이트 이다)

 

근데 한가지 의문점.. 내 시스템이 빅-엔디안 이라면 바이트 오더 변환이 필요없는가?

아니다. 코드 작성하고 니혼자만 쓸거냐?? 그것도 소켓 프로그램 코드를...

빅-엔디안, 리틀-엔디안에 상관없이 동작하는 코드를 작성해야 할 것 아닌가.

 

 

소켓 주소 변환

자.. 바이트 순서 변환에 대해 알아봣으니 이제 주소를 변환하는 것에 대해 알아보자.

 

먼저, 일단 IP주소는 점 찍힌 10진수 문자열 표현방식(Dotted-Decimal Notation)이 우리가 익숙한 방식이다.

192.168.0.32 뭐 이런식으로 된 문자열. 근데 이걸 컴퓨터가 알아먹겠냐.

기존에는 inet_addr() 라는 함수를 사용하여 변환하였다. 이걸 쓰면 자동으로 빅에디안으로 변환도 해준다.

 

 

inet_addr 함수는 주소를 변환해줄 뿐만 아니라 유효하지 않은 IP주소에 대해 오류검출 기능도 있다.

근데, 이렇게 문자열을 넣어서 변환을 하고 나면 변환한 IP주소의 정보를

sockaddr_in 구조체에 선언된 in_addr 구조체로 집어넣어야 한다. 주소를 여기서 관리하니까.

(그리고 하나더, 실패했을 시에 INADDR_NONE 이라는 32비트 모두 1이 셋팅된 (255.255.255.255) 값을

반환하는데, 이것 때문에 255.255.255.255 의 주소 변환을 처리하지 못한다.)

 

이 귀찮을 짓을 일일이 하겠는가. 당연히 함수가 구현되어 있다.

inet_aton() 과 반대 과정의 inet_ntoa() 함수이다. (address to network 인가??)

inet_aton()  부터 알아보자.

 

 

inet_aton을 사용하면 따로 in_addr 구조체에 저장할 필요가 없다. 바로 꽂아줌.

보통은 inet_addr 가 아닌 inet_aton 함수를 사용한다. 자 그리고 inet_aton의 반대 기능을 가진 

inet_ntoa()를 알아보자.

 

 

 

이 친구는 그냥 반대기능을 하는 것이다. 다만 주의해야될 것이 있다.

반환형이 char형 포인터라는 것인데, 문자열을 반환하면 보통은 메모리에 공간이 할당 되었다는 말인데,

이건 함수 내부적으로 메모리 공간을 할당하여 변환된 문자열 정보를 저장하는 것이다.

그래서 이놈으로 주소를 변환하고 나면 별도로 저장해주는게 좋다. (재호출 했을 때 이전에 변환한게 사라질수 있다.)

 

위 3가지 함수는 모두 IPv4 에만 사용가능한 함수들이다. 그럼 IPv6에 대한 구현이 또 따로 있겠지. 

한번 알아보자. inet_pton() 과 inet_ntop() 이다. 얘들은 IPv4, IPv6 모두 핸들링 가능하다.

 

 

크게 언급할 것이 보이진 않는다. 그냥 inet_aton, inet_ntoa 와 다를게 없음. 

IPv6 사용 가능하단거 말고

 

 

readn(), writen()

TCP 소켓 같은 Stream 소켓은 일반적인 파일 입출력 (read/write를 이용한) 과 좀 다르다.

스트림 소켓에서는 read/write의 인자에 있는 바이트수 보다 적은 수의 바이트를 입출력 한다.

 

이런 동작의 이유는 커널에 있는 소켓에 대한 버퍼가 미리 한계에 도달할 수 있기 때문이다.

그래서 read나 write를 또 호출해야한다.

 

이게 귀찮았던 스티븐스 아저씨는 readn 과 writen 이라는 것을 구현해주심.

 

readn() 함수의 구현

 

EINTR 시그널 즉, 인터럽트가 걸리면, 상위 if문으로 돌아가 read를 처음부터 다시 실행한다.

read가 양수를 반환하여 읽기 작업이 성공했다면 nleft(남은 바이트) = nleft - nread 를 통해

남은 바이트를 계산하고 읽은 만큼 ptr 포인터를 옮긴다.

nleft <= 0 이 되면 즉, 더이상 읽을 바이트가 남지 않으면 return한다.

 

writen() 함수의 구현

 writen도 readn 과 마찬가지로 돌아간다.

 

위에서도 말했듯이 그냥 반복해서 read/write를 수행해야만 입출력이 완료되기 때문에

이런걸 구현해놓은 듯.

 

반응형

'IT 그리고 정보보안 > Knowledge base' 카테고리의 다른 글

TCP echo Server/Client  (1) 2021.04.11
소켓 프로그래밍  (0) 2021.04.11
UDP (User Datagram Protocol)  (0) 2021.04.11
TCP (Transmission Control Protocol)  (0) 2021.04.11
소프트웨어 공학 - 테스팅 프로세스  (0) 2021.04.11