소켓옵션 ¶
#include <sys/types.h> #include <sys/socket.h> int getsockopt(int s, int level, int optname, void *optval, socklen_t *optlen); int setsockopt(int s, int level, int optname, const void *optval, socklen_t optlen);
- s : 소켓지정번호
- level : 소켓의 레벨로 어떤 레벨의 소켓정보를 가져오거나 변경할 것인지를 명시하며, SOL_SOCKET와 IPPROTO_TCP 중 하나를 사용할 수 있다.
- optname : 설정을 위한 소켓옵션의 번호
- optval : 설정값을 저장하기 위한 버퍼의 포인터
- optlen : optval 버퍼의 크기
옵션값 | 데이터형 | 설명 |
SO_BROADCAST | BOOL | 브로드캐스트 메시지 전달이 가능하도록 한다. |
SO_DEBUG | BOOL | 디버깅 정보를 레코딩 한다. |
SO_DONTLINGER | BOOL | 소켓을 닫을때 보내지 않은 데이터를 보내기 위해서 블럭되지 않도록 한다. |
SO_DONTROUTE | BOOL | 라우팅 하지 않고 직접 인터페이스로 보낸다. |
SO_GROUP_PRIORITY | int | 사용하지 않음 |
SO_KEEPALIVE | BOOL | Keepalives를 전달한다. |
SO_LINGER | struct LINGER | 소켓을 닫을 때 전송되지 않은 데이터의 처리 규칙 |
SO_RCVBUF | int | 데이터를 수신하기 위한 버퍼공간의 명시 |
SO_REUSEADDR | BOOL | 이미 사용된 주소를 재사용 (bind) 하도록 한다. |
SO_SNDBUF | int | 데이터 전송을 위한 버퍼공간 명시 |
IPPROTO_TCP레벨에서 사용할 수 있는 옵션과 데이터형이다.
TCP_NODELAY | BOOL | Nagle 알고리즘 제어 |
SO_REUSEADDR ¶
bind error : Address already in use이는 기존 프로그램이 종료되었지만, 비정상종료된 상태로 아직 커널이 bind(2)정보를 유지하고 있음으로 발생하는 문제다. 보통 1-2분 정도 지나만 커널이 알아서 정리를 하긴 하지만, 그 시간동안 기달려야 한다는 것은 상당히 번거로운 일이 될 것이다. 이 경우 다음과 같은 코드를 삽입함으로써 문제를 해결할 수 있다.
int sock = socket(...); setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (char *)&bf, (int)sizeof(bf));이렇게 하면 커널은 기존에 bind로 할당된 소켓자원을 프로세스(:12)가 재 사용할 수 있도록 허락하게 된다.
#include <sys/types.h> #include <sys/stat.h> #include <sys/socket.h> #include <unistd.h> #include <stdio.h> #include <string.h> int main(int argc, char **argv) { int sockfd; int bufsize; int rn; if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) { perror("Error"); return 1; } rn = sizeof(int); // 현재 RCVBUF 값을 얻어온다. if (getsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &bufsize, (socklen_t *)&rn) < 0) { perror("Set Error"); return 1; } printf("Socket RCV Buf Size is %d\n", bufsize); // 버퍼의 크기를 100000 으로 만든다. bufsize = 100000; if (setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, (void *)&bufsize, (socklen_t)rn) < 0) { perror("Set Error"); return 1; } return 0; }
TCP_NODELAY ¶
SO_LINGER ¶
struct linger { int l_onoff; int l_linger; }
- l_onoff : linger 옵션을 끌것인지 킬 것인지 결정
- l_linger : 기다리는 시간의 결정
- l_onoff == 0 : 이 경우 l_linger의 영향을 받지 않는다. 소켓의 기본설정으로 소켓버퍼에 남아 있는 모든 데이터를 보낸다. 이때 close()는 바로 리턴을 하게 되므로 백그라운드에서 이러한 일이 일어나게 된다. 우아한 연결 종료를 보장한다.
- l_onoff > 0 이고 l_linger == 0 : close()는 바로 리턴을 하며, 소켓버퍼에 아직 남아있는 데이터는 버려 버린다. TCP 연결상태일 경우에는 상대편 호스트에 리셋을 위한 RST 패킷을 보낸다. hard 혹은 abortive 종료라고 부른다.
- l_onoff > 0 이고 l_linger > 0 : 버퍼에 남아있는 데이터를 모두 보내는 우아한 연결 종료를 행한다. 이때 close()에서는 l_linger에 지정된 시간만큼 블럭상태에서 대기한다. 만약 지정된 시간내에 데이터를 모두 보냈다면 리턴이 되고, 시간이 초과되었다면 에러와 함께 리턴이 된다.
소켓 timeout ¶
- 차례
- 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) 중간에 존재하기 때문이다. 아래의 그림을 보라
위의 그림은 TCP/IP 개요에서 이미 본적이 있는 그림일 것이다. 그때의 그림과 달라진 점이 있다면, 응용계층과 전송계층에 Socket Layer 가 존재한다는 것이다. 이 Socket Layer 가 응용계층과 전송계층 사이에 존재하게 됨으로 우리 프로그래머들은 복잡하게 TCP 를 직접 제어할 필요없이, Socket Layer 에서 제공하는 다양한 함수(Socket API)를 이용해서 간단하게 인터넷 네트웍 프로그래밍 작업을 하게 되는것이다.
Socket Layer 은 응용계층에서 받은 메시지를 하부 Socket API 를 이용해서 전송계층으로 보낸다. 전송계층에는 2가지 대표적인 프로토콜 이 있는데 바로 TCP(:12) 와 UDP(:12) 이다. 그럼으로 우리 프로그래머들은 TCP프로토콜(:12)을 사용할것인지 UDP 프로토콜을 사용할것인지만 결정해주면된다.
1.2. 왜 Layer 구조를 가지는가
일상 생활에서 소켓레이어와 비슷한게, 전화기라고 볼수 있을것이다. 우리는 상대편에서 전화를 걸기 위해서 상대편전화의 지리적 위치가 어디인지, 어떤 전화국에서 관리하는지, 언어를 신호로 변환 시키기 위해서 어떠한 작업을 해야하는지, 어떻게 보내야 하는지 전혀 알필요가 없다. 그냥 수화기 들고 전화 번호만 누르면 그걸로 끝이다. 즉 전화기 라는게 있음으로 그 내부에서 일어나는 여러가지 복잡한 통신 프로세스를 모르고도 상대편과 전화통화를 할수 있게 된다.
Socket Layer 이 존재함으로써, 우리는 TCP/UDP 헤더를 어떻게 만들어야 하는지, 구조가 어떻게 되는지, 어떻게 커널에 전달해야 하는지 신경쓸필요 없이 네트웍 프로그램을 만들수 있게 된다.
1.3. Socket
"Socket 이라뇨 우리는 위에서 Socket Layer를 이미 다루었는데요 ?" 라고 의문을 가질수도 있을것이다. Socket Layer 과 Socket 는 엄연히 다르다. Socket Layer 는 계층을 나타내는 것이다. 즉 Socket 를 다루기 위한 계층이다. 이는 TCP가 전송계층(:12)이 아닌것과 마찬가지이다. 우리는 Socket Layer 에서 제공하는 다양한 API를 통해서 Socket 를 제어하게 된다.
그럼 Socket 이란 무엇인가. 소켓이란 유닉스 파일 지시자 를 이용하여 다른 프로그램과 정보교환을 하는 방법(혹은 도구) 이다. 일반적으로 유닉스 상에서 정보교환은 파일지시자를 통한다는걸 알고 있을것이다. 마찬가지로 Socket 를 이용한 지역 혹은 네트웍으로 연결된 프로그램 간의 정보교환 역시 파일지시자를 통해서 이루어진다.
다중연결서버 만들기(1) 의 zipcode_multi.c 를 이용해서 소켓이 어떻게 작동하는지 알아보도록 하겠다. 먼저의 위의 프로그램을 컴파일(:12) 시키고 작동을 시켜보자. 작동을 시켰다면 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 |
[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] |
여기에 새로운 클라이언트가 접근을하면 (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(:12) 4계층을 보면 Socket Layer 아래에는 최소한 2개 이상의 사용가능한 데이타 그램의 타입이 있음을 알수 있다. 이러한 데이타 그램의 타입에는 TCP, UDP, RAW(:12) 등이 있다. 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); |
즉 인터넷 프로토콜을 이용하는 TCP 소켓을 만들기 원한다면 socket(AF_INET, SOCK_STREAM, 0) 과 같이 사용하면 된다.
socket 함수가 성공적으로 수행되면, 사용가능한 소켓을 가르키는 파일 지시자를 되돌려주며, 이 파일지시자는 endpoint(연결 확인 통로) 로써 사용된다.
1.4.1.2. bind(2) 함수
socket 함수를 이용해서 만들어진 소켓에 이름을 부여한다.
라고 번역된 man(:12) 페이지혹은 관련된 번역서에서 설명을 하고 있지만, 소켓에 특성을 부여(소켓과 특성을 묶는다(bind))한다 라는게 좀더 적당한 설명이 아닐까 싶다.
int bind(int sockfd, struct sockaddr *my_addr, socklen_t addrlen); |
bind 함수는 보통 서버에서 사용된다. 그 이유는 대부분의 서비스(HTTP, FTP..)들이 지정된 포트(:12)번호를 통해서 서비스 되기 때문이다. 반면 클라이언트의 경우 커널에서 할당한 임의의 포트번호를 이용해서 서버와 연결하기 때문에 bind 를 사용할 필요가 없다.
1.4.1.3. connect(2) 함수
int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen); |
1.4.1.4. listen(2) 함수
int listen(int sockfd, int backlog); |
1.4.1.5. accept(2) 함수
int accept(int s, struct sockaddr *addr, socklen_t *addrlen); |
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); |
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); |
1.4.3. 인터넷 주소변환
인터넷 주소 자체가 인간이 인지하기 어려운 수로 되어 있다 보니 이것을 관리하기 쉽도록 점박이 3형제 스타일의 인터넷 주소체계를 만들어서 관리하고 여기에 또 도메인(:12) 이름을 줘서 쉽게 기억할 수 있도록 하고 있다. 프로그래머나 사용자는 보통 도메인 이름이나 점박이 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)의 맨페이지(:12)를 참고 하기바란다.
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(:12)의 바이트 오더 방식을 알려 주는 간단한 함수다.
int endian(void) { int i = 0x00000001; if ( ((char *)&i)[0] ) return LITTLE_ENDIAN; else return BIG_ENDIAN; } |
2. 소켓 프로그래밍 일반
2.1. 서버측 socket 생성 순서
다음은 서버측의 소켓 생성 순서를 나열한 것이다.
-
서버측의 소켓 생성순서는 최초 socket 함수를 이용해서 endpoint 소켓, 즉 클라이언트의 연결을 듣기 위한 소켓을 생성하게 된다. 이 소켓은 서버가 종료될때까지 남아있게 된다.
-
bind 함수를 호출하여 소켓특성을 묶어준다. 이 함수를 이용하여 port 번호를 지정해주며, 받아들일 IP주소에 대한 설정을 한다.
-
listen 함수를 이용하여 듣기 소켓(socket 함수를 통해서 만들어진) 에 연결이 있는지 기다린다. 만약 연결이 있다면, 연결 대기열(queue)에 쌓아놓는다.
-
accept 함수를 이용하여 연결 대기열에 대기중인 연결이 있다면 해당 연결에 대하여 새로운 소켓을 만들고 만들어진 소켓에 대한 파일 지시자를 되돌려준다. 이 소켓은 읽기/쓰기로 만들어진다. 만약 연결 대기열에 대기중인 연결이 없다면 (기본적으로) 해당 영역에서 봉쇄(block)된다.
-
read, write 등의 함수를 이용해서 통신을 한다.
2.2. 클라이언트 측 socket 생성순서
다음은 클라이언트측의 소켓 생성 순서를 나열한 것이다. 서버측에 비하여서 훨신 간단하게 이루어짐을 알수 있다.
-
최초 socket 를 이용하여 endpoint 소켓을 생성한다. 클라이언트 이므로 이것은 듣기 소켓이 아니고, 연결 소켓이 될것이다. (이름만 다를뿐 사실 듣기 소켓과 연결 소켓의 구분은 없다)
-
connect 를 이용하여 서버에 연결한다.
-
read, write 등의 함수를 이용해서 서버와 통신한다.
3. 결론
이상 Socket Layer 의 개념과 Socket Layer 에서 제공하는 Socket API 에 대한 간단히 알아 보았다. 여기에 있는 API 들은 가장 기본적인(통신을 위해서 필요한) 함수들이다. 나머지 좀더 세밀한 함수들에 대해서는 Unix NetWork Programming 등의 서적을 참고하기 바란다.
여기에 있는 소켓 API 들의 사용예는 이 사이트에서 충분히 찾아볼수 있을것이다.
'Skills > Interface' 카테고리의 다른 글
QA Testing — What is DEV, SIT, UAT & PROD? (0) | 2020.12.21 |
---|---|
프로세스간 통신(IPC ; Inter-Process Communication) (1) | 2014.04.02 |
MAXIM 스마트 카드 I/F 원리 (0) | 2014.02.26 |
댓글