Socket 에 대한 기본지식

출처 : http://blog.naver.com/hongjig/140007926146

차례
1. Socket 에 대한 기본지식
1.1. Socket Layer
1.2. 왜 Layer 구조를 가지는가
1.3. Socket
1.4. socket API
1.4.1. 소켓 생성 및 연결
1.4.1.1. socket(2) 함수
1.4.1.2. bind(2) 함수
1.4.1.3. connect(2) 함수
1.4.1.4. listen(2) 함수
1.4.1.5. accept(2) 함수
1.4.2. 입출력 함수
1.4.2.1. 입력함수 – recvfrom/recvmsg
1.4.2.2. 출력함수 – sendto/sendmsg
1.4.3. 인터넷 주소변환
1.4.4. 인터넷 주소 <-> 32bit 주소
1.4.5. 도메인 이름 -> 32bit 주소
1.4.5.1. gethostbyname/gethostbyaddr
1.4.6. 네트워크 바이트 오더
1.4.6.1. 호스트 바이트 오더 -> 네트워크 바이트 오더
1.4.6.2. 네트워크 바이트 오더 -> 호스트 바이트 오더
1.4.6.3. 엔디안 검사 함수
1.4.7. 소켓 옵션
1.4.7.1. 소켓 옵션 설정 – setsockopt(2)
1.4.7.2. 소켓 옵션 가져오기 – getsockopt(2)
2. 소켓 프로그래밍 일반
2.1. 서버측 socket 생성 순서
2.2. 클라이언트 측 socket 생성순서
3. 결론


1. Socket 에 대한 기본지식



1.1. Socket Layer


Socket 은 유닉스의 파일 기술자를 통해서 다른 프로그램간의 정보교환을 가능하도록 해주는 방법으로, 같은 시스템에 있는 프로그램들간의 정보교환을 위한목적, 혹은 다른 시스템(네트웍 상으로 멀리떨어져있는) 들간의 정보교환을 위한 목적으로 사용된다.


그런데 왜 Layer 라고 부르는가 그 이유는 TCP/IP 4계층의 응용계층(applicaton layer)과 전송계층(transmission layer) 중간에 존재하기 때문이다. 아래의 그림을 보라


그림 1. 소켓 계층


위의 그림은 TCP/IP 개요에서 이미 본적이 있는 그림일 것이다. 그때의 그림과 달라진 점이 있다면, 응용계층과 전송계층에 Socket Layer 가 존재한다는 것이다. 이 Socket Layer 가 응용계층과 전송계층 사이에 존재하게 됨으로 우리 프로그래머들은 복잡하게 TCP 를 직접 제어할 필요없이, Socket Layer 에서 제공하는 다양한 함수(Socket API)를 이용해서 간단하게 인터넷 네트웍 프로그래밍 작업을 하게 되는것이다.


Socket Layer 은 응용계층에서 받은 메시지를 하부 Socket API 를 이용해서 전송계층으로 보낸다. 전송계층에는 2가지 대표적인 프로토콜 이 있는데 바로 TCP 와 UDP 이다. 그럼으로 우리 프로그래머들은 TCP 프로토콜을 사용할것인지 UDP 프로토콜을 사용할것인지만 결정해주면된다.




1.2. 왜 Layer 구조를 가지는가


일상 생활에서 소켓레이어와 비슷한게, 전화기라고 볼수 있을것이다. 우리는 상대편에서 전화를 걸기 위해서 상대편전화의 지리적 위치가 어디인지, 어떤 전화국에서 관리하는지, 언어를 신호로 변환 시키기 위해서 어떠한 작업을 해야하는지, 어떻게 보내야 하는지 전혀 알필요가 없다. 그냥 수화기 들고 전화 번호만 누르면 그걸로 끝이다. 즉 전화기 라는게 있음으로 그 내부에서 일어나는 여러가지 복잡한 통신 프로세스를 모르고도 상대편과 전화통화를 할수 있게 된다.


Socket Layer 이 존재함으로써, 우리는 TCP/UDP 헤더를 어떻게 만들어야 하는지, 구조가 어떻게 되는지, 어떻게 커널에 전달해야 하는지 신경쓸필요 없이 네트웍 프로그램을 만들수 있게 된다.




1.3. Socket


“Socket 이라뇨 우리는 위에서 Socket Layer를 이미 다루었는데요 ?” 라고 의문을 가질수도 있을것이다. Socket Layer 과 Socket 는 엄연히 다르다. Socket Layer 는 계층을 나타내는 것이다. 즉 Socket 를 다루기 위한 계층이다. 이는 TCP가 전송계층이 아닌것과 마찬가지이다. 우리는 Socket Layer 에서 제공하는 다양한 API를 통해서 Socket 를 제어하게 된다.


그럼 Socket 이란 무엇인가. 소켓이란 유닉스 파일 지시자 를 이용하여 다른 프로그램과 정보교환을 하는 방법(혹은 도구) 이다. 일반적으로 유닉스 상에서 정보교환은 파일지시자를 통한다는걸 알고 있을것이다. 마찬가지로 Socket 를 이용한 지역 혹은 네트웍으로 연결된 프로그램 간의 정보교환 역시 파일지시자를 통해서 이루어진다.


다중연결서버 만들기(1) 의 zipcode_multi.c 를 이용해서 소켓이 어떻게 작동하는지 알아보도록 하겠다. 먼저의 위의 프로그램을 컴파일 시키고 작동을 시켜보자. 작동을 시켰다면 ps 로 zipcode_multi 프로그램의 pid 를 확인해보고 /proc/pid/fd 디렉토리로 이동해서 어떠한 파일 지시자를 가지고 있는지 확인해보도록 하자.




[yundream@localhost test]# ./zipcode_multi 4444

[yundream@localhost test]# ps -ax | grep zipcode
2473 ttyp1 S 0:00 ./zipcode_multi 4444
pid가 2473 이므로 이 프로그램의 /proc/2473/fd 로 이동해서 ls해보면 프로그램에서 사용하고있는 파일지시자들에 대해서 알수 있다.



[yundream@localhost test]# ls -al /proc/2473/fd
합계 0
dr-x—— 2 root root 0 5월 28 16:07 .
dr-xr-xr-x 3 root root 0 5월 28 16:07 ..
lrwx—— 1 root root 64 5월 28 16:14 0 -> /dev/ttyp1
lrwx—— 1 root root 64 5월 28 16:14 1 -> /dev/ttyp1
lrwx—— 1 root root 64 5월 28 16:14 2 -> /dev/ttyp1
lr-x—— 1 root root 64 5월 28 16:14 3 -> /home/mycvs/test/zipcode.txt
lrwx—— 1 root root 64 5월 28 16:14 4 -> socket:[171434]
0, 1, 2 는 각각 표준입력, 표준출력, 표준에러를 가리키는 파일지시자 라는것은 이미 알고 있을것이다. 3 은 프로그램이 연 파일을 가리킨다. 마지막 4가 바로 socket 통신을 위해 만들어진 파일 지시자이다. 다른 것들이 터미널이나 파일을 가리키는것과는 달리 socket 를 가리 키고 있음을 알수 있다.


여기에 새로운 클라이언트가 접근을하면 (telnet 이나 전용클라이언트 를 이용해서) 다음과 같은 파일 지시자가 하나 추가 될것이다.




lrwx——    1 root     root           64  5월 28 16:14 5 -> socket:[171435]




1.4. socket API


이번에는 socket 레이어에서 제공하는 소켓 관련 함수들을 설명하도록 하겠다.




1.4.1. 소켓 생성 및 연결



1.4.1.1. socket(2) 함수

이러한 소켓 은 socket(2) 함수를 이용해서 만들어진다. 최초 socket 함수를 이용해서 소켓을 생성하면 커널은 통신을 위한 종점(end point,즉 통신연결상황을 체크하는)을 생성하고, 여기에 대한 파일 지시자를 되돌려준다. 프로그램은 socket 함수를 이용해서 생성한 파일 지시자에 새로운 연결이 들어오는 지를 확인하게 된다.


위에 있는 TCP/IP 4계층을 보면 Socket Layer 아래에는 최소한 2개 이상의 사용가능한 데이타 그램의 타입이 있음을 알수 있다. 이러한 데이타 그램의 타입에는 TCP, UDP, RAW 등이 있다. TCP 소켓, UDP 소켓, RAW 소켓이라고 부르기도 한다. 또한 다양한 소켓 주소패밀리(군)를 제공한다.


표 1. 소켓주소 패밀리





















UNIX 유닉스 도메인 소켓, IPC 용으로 많이 사용한다.
INET TCP/IP 프로토콜을 이용한 인터넷주소 패밀리, 보통의 네트웍프로그래밍시 주로 사용
IPX 노벨의 IPX 프로토콜, 게임을 좋아한다면 많이 들어봤음직한
AX25 아마추어 라디오 X.25
X25 X.25 프로토콜


그러므로 socket 함수는 위의 소켓 주소 패밀리와 소켓 타입 지정이 가능해야 한다.




int socket(int domain, int type, int protocol);
첫번째 아규먼트가 소켓주소 패밀리 지정을 위해서 사용되며, 두번째 아규먼트가 소켓 타입지정을 위해서 사용된다. 소켓주소 패밀리는 주로 INET(AF_INET), UNIX(AF_UNIX) 가 사용되며, 소켓타입은 TCP(SOCK_STREAM), UDP(SOCK_DGRAM), RAW(SOCK_RAW) 가 사용된다.


즉 인터넷 프로토콜을 이용하는 TCP 소켓을 만들기 원한다면 socket(AF_INET, SOCK_STREAM, 0) 과 같이 사용하면 된다.


socket 함수가 성공적으로 수행되면, 사용가능한 소켓을 가르키는 파일 지시자를 되돌려주며, 이 파일지시자는 endpoint(연결 확인 통로) 로써 사용된다.




1.4.1.2. bind(2) 함수

socket 함수를 이용해서 만들어진 소켓에 이름을 부여한다.


라고 번역된 man 페이지혹은 관련된 번역서에서 설명을 하고 있지만, 소켓에 특성을 부여(소켓과 특성을 묶는다(bind))한다 라는게 좀더 적당한 설명이 아닐까 싶다.




int bind(int  sockfd, struct sockaddr *my_addr, socklen_t addrlen);
인자로 주어진 sockfd 에대해서 sockaddr 을 이용해서 특성을 묶어준다. bind 함수를 통해서 우리는 sockfd 가 사용할 포트번호(port), 그리고 연결을 받아들일 IP 주소 특성등을 묶어줄수 있다. IP 주소는 IPv4, IPv6 등이 사용될수 있을것이다.


bind 함수는 보통 서버에서 사용된다. 그 이유는 대부분의 서비스(HTTP, FTP..)들이 지정된 포트번호를 통해서 서비스 되기 때문이다. 반면 클라이언트의 경우 커널에서 할당한 임의의 포트번호를 이용해서 서버와 연결하기 때문에 bind 를 사용할 필요가 없다.




1.4.1.3. connect(2) 함수





int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen);
이것은 클라이언트측 에서 사용되며, struct sockaddr 구조체에 세팅된 내용대로 서버측에 연결한다. sockaddr 구조체에는 연결될 서버에 대한 정보들, 즉 주소 패밀리 IP 번호와 PORT 번호 등이 들어가 있으며, connect 함수는 sockaddr 정보를 이용해서 서버측에 연결을 하며 서버와의 통신을 위한 endpoint 와 sockfd 를 연결시킨다. sockfd 는 socket 함수를 이용해서 만들어진 소켓 지정 번호이다.




1.4.1.4. listen(2) 함수





int listen(int sockfd, int backlog);
서버측에서 사용되며 socket 함수를 이용해서 만들어진 sockfd 에 대해서, 들어 오는 연결을 기다린다. backlog 는 아직 완전히 연결되지 않은 연결들이 대기할 queue 의 길이를 명시하기 위해서 사용된다.




1.4.1.5. accept(2) 함수





int accept(int s,  struct  sockaddr  *addr,  socklen_t *addrlen);
listen 을 통해서 만들어진 미연결의 대기열에서 가장 앞에 있는 연결의 내용을 가져와서 새로운 연결 소켓을 만들어준다. 새로만들어진 연결소켓은 파일 지시자를 할당하여서 리턴해주게 되며, 우리는 리턴된 파일 지시자를 이용해서 새로만들어진 소켓과 통신을 할수 있게 된다.




1.4.2. 입출력 함수


유닉스에서 소켓은 파일과 동일하게 취급 되기 때문에 read(), write()와같은 시스템 함수를 이용해도 대부분의 입출력을 다룰 수 있다. 그러나 이들 시스템 함수들은 네트워크의 특성을 고려하지 않고 만들었기 때문에 네트워크 정보를 필요로 하는 작업을 하기에는 적당하지 않은 점이 있다.


예를들어 UDP를 이용해서 통신을 할경우 읽기는 문제없지만 쓰기에는 문제가 생길 수 있다. UDP는 연결 소켓을 만들지 않기 때문에 쓸때 연결된 호스트의 정보를 알 수가 없기 때문에 write()함수로는 데이터를 전송할 수 없게 된다. 이럴경우에는 소켓 API를 사용해서 통신을 해주어야 한다.




1.4.2.1. 입력함수 – recvfrom/recvmsg

소켓으로 부터 데이터를 받기 위해서 사용한다.





#include <sys/types.h>
#include <sys/socket.h>

ssize_t recvfrom(int s, void *buf, size_t len, int flags, struct sock-
addr *from, socklen_t *fromlen);

ssize_t recvmsg(int s, struct msghdr *msg, int flags);

소켓 지정자 s로 부터 데이터를 읽는 일을 한다. 둘다 연결지향 소켓과 비연결지향 소켓 모두에 사용할 수 있다. 보통 recvfrom()함수가 사용하기에 직관적인 관계로 쉽게 사용할 수 잇다. 소켓으로 부터 len 만큼 데이터를 읽어와서 buf에 저장한다. 또한 5번째 인자인 from를 통해서 데이터를 보낸 호스트의 인터넷 정보를 얻어 올 수 있다. 그러므로 비연결 지향 소켓을 사용하더라도 이 인터넷 정보를 통해서 데이터를 수신할 목적지 호스트를 결정할 수 있게 된다. fromlen는 sockaddr 구조체의 길이다. 나머지 자세한 내용은 recvform(2)의 맨페이지를 참고하기 바란다.




1.4.2.2. 출력함수 – sendto/sendmsg

소켓으로 데이터를 보내기 위해서 사용한다.




#include <sys/types.h>
#include <sys/socket.h>

ssize_t sendto(int s, const void *buf, size_t len, int flags, const
struct sockaddr *to, socklen_t tolen);
ssize_t sendmsg(int s, const struct msghdr *msg, int flags);

역시 직관적인 sendto를 널리 사용한다. 소켓 지정자 slen크기만큼 buf의 내용을 보낸다. to를 이용해서 데이터를 받을 호스트를 명시할 수 있다. sendto()와 recvfrom()함수의 사용예는 UDP 소켓 프로그래밍 을 참고하기 바란다.




1.4.3. 인터넷 주소변환


인터넷 주소 자체가 인간이 인지하기 어려운 수로 되어 있다 보니 이것을 관리하기 쉽도록 점박이 3형제 스타일의 인터넷 주소체계를 만들어서 관리하고 여기에 또 도메인 이름을 줘서 쉽게 기억할 수 있도록 하고 있다. 프로그래머나 사용자는 보통 도메인 이름이나 점박이 3형제 스타일의 인터넷 주소를 사용하게 되는데, 실제 네트워크 프로그램에서는 32bit 주소 형태로 변환 시켜줘야할 필요가 있다.


여기에서는 이들 주소간 변환과 관련된 함수를 소개한다.




1.4.4. 인터넷 주소 <-> 32bit 주소


inet_addr(3), inet_aton(3), inet_network(3), inet_ntoa(3) 의 함수를 이용해서 인터넷 주소와 32bit 주소간 변환을 할 수 있다. inet_addr(3)과 inet_network(3)함수는 점박이 3형재 스타일 인터넷 주소로 부터 32bit 주소를 얻기 위해서, inet_aton(3)과 inet_ntoa(3)그 반대의 변환 값을 얻기 위해서 사용한다. 자세한 내용은 man 페이지를 참고하기 바란다(그냥 함수 링크를 클릭하면 된다).




1.4.5. 도메인 이름 -> 32bit 주소


점박이 3형제 스타일의 인터넷 주소는 확실히 관리하기 좋고 외우기에 좀더 편하긴 하지만 숫자로 되어 있다는 것 때문에 인터넷 서비스를 위한 호스트 주소로 사용하기엔 적당하지 않다. 그래서 인터넷 주소에 이름을 주는 서비스가 만들어지게 되었는데 도메인 서비스이다. 도메인 서비스는 도메인 이름에 대한 인터넷 주소를 되돌려 주는 일을 한다. 자세한 내용은 인터넷 주소 변환문서를 참고하기 바란다.




1.4.5.1. gethostbyname/gethostbyaddr

도메인 이름에서 인터넷 주소를 얻어오는 일을 한다. 자세한 내용은 gethostbyname(3)과 getbyaddr(3)의 맨페이지를 참고 하기바란다.




1.4.6. 네트워크 바이트 오더


네트워크 통신을 하다보면 CPU의 바이트 오더가 다른 이유로 이를 표준 바이트 오더인 네트워크 바이트 오더로 변환해서 보내고, 받아들인 데이터는 호스트의 바이트 오더에 맞게 다시 변경시켜주는 작업이 필요하다. 이러한 작업을 위해서 소켓은 몇 개의 함수들을 제공한다. 바이트 오더에 대한 자세한 내용은 endian에 대해서 를 참고하기 바란다.




1.4.6.1. 호스트 바이트 오더 -> 네트워크 바이트 오더

htonl(3), htons(3) 함수를 사용한다. 전자는 4byte 데이터, 후자는 2byte 데이터를 네트워크 바이트 오더를 따르도록 변환한다.




1.4.6.2. 네트워크 바이트 오더 -> 호스트 바이트 오더

ntohl(3), ntohs(3) 함수를 사용한다. 전자는 4byte데이터, 후자는 2byte데이터를 호스트 바이트 오더를 따르도록 변환한다.




1.4.6.3. 엔디안 검사 함수

이건 보너스다. 현재 CPU의 바이트 오더 방식을 알려 주는 간단한 함수다.




int endian(void)
{
int i = 0x00000001;
if ( ((char *)&i)[0] )
return LITTLE_ENDIAN;
else
return BIG_ENDIAN;
}





2. 소켓 프로그래밍 일반



2.1. 서버측 socket 생성 순서


다음은 서버측의 소켓 생성 순서를 나열한 것이다.




  1. 서버측의 소켓 생성순서는 최초 socket 함수를 이용해서 endpoint 소켓, 즉 클라이언트의 연결을 듣기 위한 소켓을 생성하게 된다. 이 소켓은 서버가 종료될때까지 남아있게 된다.



  2. bind 함수를 호출하여 소켓특성을 묶어준다. 이 함수를 이용하여 port 번호를 지정해주며, 받아들일 IP주소에 대한 설정을 한다.



  3. listen 함수를 이용하여 듣기 소켓(socket 함수를 통해서 만들어진) 에 연결이 있는지 기다린다. 만약 연결이 있다면, 연결 대기열(queue)에 쌓아놓는다.



  4. accept 함수를 이용하여 연결 대기열에 대기중인 연결이 있다면 해당 연결에 대하여 새로운 소켓을 만들고 만들어진 소켓에 대한 파일 지시자를 되돌려준다. 이 소켓은 읽기/쓰기로 만들어진다. 만약 연결 대기열에 대기중인 연결이 없다면 (기본적으로) 해당 영역에서 봉쇄(block)된다.



  5. read, write 등의 함수를 이용해서 통신을 한다.





2.2. 클라이언트 측 socket 생성순서


다음은 클라이언트측의 소켓 생성 순서를 나열한 것이다. 서버측에 비하여서 훨신 간단하게 이루어짐을 알수 있다.




  1. 최초 socket 를 이용하여 endpoint 소켓을 생성한다. 클라이언트 이므로 이것은 듣기 소켓이 아니고, 연결 소켓이 될것이다. (이름만 다를뿐 사실 듣기 소켓과 연결 소켓의 구분은 없다)



  2. connect 를 이용하여 서버에 연결한다.



  3. read, write 등의 함수를 이용해서 서버와 통신한다.





3. 결론


이상 Socket Layer 의 개념과 Socket Layer 에서 제공하는 Socket API 에 대한 간단히 알아 보았다. 여기에 있는 API 들은 가장 기본적인(통신을 위해서 필요한) 함수들이다. 나머지 좀더 세밀한 함수들에 대해서는 Unix NetWork Programming 등의 서적을 참고하기 바란다.


여기에 있는 소켓 API 들의 사용예는 이 사이트에서 충분히 찾아볼수 있을것이다.


 


http://www.joinc.co.kr/modules/moniwiki/wiki.php/article_socket_layer#AEN14


Leave a Reply

Your email address will not be published. Required fields are marked *