Joinc 팀블로그 리눅스 메뉴얼 정리 Joinc 위키
댓글

Recent Comments

Powered by Disqus
팀블로그 카테고리
  전체 (1105)
   공지사항 (1)
   검색엔진 (21)
   기술동향 (58)
   게임 (2)
   독서 (6)
   리눅스 (12)
   보안 (1)
   사회문제 (22)
   어셈블리 (43)
   영화 (3)
   오픈소스 (10)
   음악 (9)
   인물 (1)
   포인터 (4)
   프로그래머 (23)
   팀블로그 (20)
   테터툴즈 (29)
   C/C++ (152)
   FireFox (11)
   Gimp (2)
   Google (98)
   Java (13)
   Perl (2)
   Pthread (11)
   STL (13)
   TCP/IP (8)
   Tools (31)
   Web2.0 (42)
   Wiki (1)
«   2010/07   »
        1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
2008/08/31 09:35

TIME_WAIT 제거

TIME_WAIT 에 대해서

close.png

만들어진 TCP연결이 종료되기 위해서는 FIN패 킷 교환이 이루어진다. 이때 우아한 종료가 이루어지기 위해서는 총 4번의 패킷교환이 필요하게 된다. 우아한 종료라는 것은 연결된 호스트 양쪽이 모두 연결이 종료되었음을 알게 되는 상태다. 만약 FIN 패킷을 보냈는데, 상대 호스트에서 ACK 패킷을 보내지 않고 종료해버리는 경우 FIN을 보낸측은 우아한 종료를 위해서 일정시간 ACK를 기다리게 된다. 리눅스의 경우 대략 90초 정도를 기다린다. netstat(1)로 확인해 보면 TIME_WAIT인 상태로 나타난다.

TIME_WAIT상태일 경우 해당 포트를 계속 점유하는데, 연결이 빈번한 네트워크 서비스일 경우 연결거부와 관련된 문제가 발생할 수 있다.

TIME_WAIT 문제 발생 상황

mysql의 성능중 처리량을 측정하기 위해서 수백개의 클라이언트로 동접상황을 시뮬레이션 하는 프로그램을 만들어서 테스트하던 중 다음과 같은 문제가 발생했다.
  1. open 파일 제한 갯수
    이문제는 /proc/sys/fs/file-max 의 값을 조절하는 걸로 간단히 해결했다.
  2. TIME_WAIT
    매우 바쁜 동접테스트 환경을 만들기를 원했다. 이 경우 connect와 close가 빈번하게 발생하는데, TIME_WAIT가 계속 늘어나게 되고 결국에는 할당가능한 PORT를 모두 소비해서 더이상 연결을 할 수 없는 상황이 발생했다.

소프트웨어적인 해결

소켓옵션 변경 : linger

직접 네트워크 프로그램을 제작한다면 가장 좋은 방법일 것이다. 그러나 애플리케이션에서 제공하는 API를 이용한 프로그래밍에는 적용할 수 없다.

예를들어 Mysql DB 성능측정을 위해서, Mysql API를 이용해서 측정 클라이언트를 개발할 경우에는 소켓옵션을 제어할 수가 없다.

다음은 socket옵션을 이용해서 TIME_WAIT이 발생하지 않도록 하는 코드다.
    ...  
int sock
struct linger ling;

ling.l_onoff = 1;
ling.l_linger = 0; /* 0 for abortive disconnect */

...
...

setsockopt(sock, SOL_SOCKET, SO_LINGER, &ling, sizeof(ling))

소켓옵션 변경 : SO_REUSEADDR

SO_REUSEADDR 옵션을 사용하면, TIME_WAIT 상태에 있는 PORT를 사용할 수 있게 된다. 서버프로그램이 비정상적으로 종료되건나 클라이언트를 정리하지 않고 종료되면 TIME_WAIT 상태로 넘어가게 된다. 이때 서버 프로그램을 실행시키면 bind() 에러가 발생하는데, 이 옵션을 이용해서 bind()문제를 해결할 수 있다.
setsockopt(sock_fd, SOL_SOCKET, SO_REUSEADDR, &option, sizeof(option)); 

커널레벨에서의 해결

client Port range 변경

매우 바쁜 클라이언트 프로그램일 경우에 대량의 TIME_WAIT가 발생해서 할당해야될 PORT가 부족할 수가 있다. 클라이언트에 할당될 port의 범위를 늘리는 것으로 어느정도 조정가능하다.
  • /proc/sys/net/ipv4/ip_local_port_range
보통 32768 61000로 지정되어 있을 것이다. 1024 ~ 65535 로 변경하자.
# echo "1024 ~ 65535" > /proc/sys/net/ipv4/ip_local_port_range 

이것은 어디까지나 임시적인 방법으로, 문제를 완전히 해결할 수는 없을 거다.

tcp_fin_timeout

소켓이 완전히 닫힐려면 마지막 FIN을 기다려야 하는데, 이 간격을 초단위로 설정할 수 있다. 아마 60초 정도로 설정되어 있을 것이다. 10초이하로 줄여보도록 하자.

timewait 설정 변경

커널레벨에서 TIME-WAIT자원에 대한 사용을 변경할 수도 있다.
  • tcp_tw_recycle
    기본값은 0인데, 1로 할경우 TIME-WAIT 상태를 빠르게 recycling 하도록 도와준다. 이에 대한 명확한 정보를 얻기 힘들었다.
  • tcp_tw_reuse
    기본값은 0인데, 1로 할경우 TIME-WAIT 상태의 소켓을 재사용할 수 있게 해준다. TIME-WAIT 문제를 피하기 위한 가장 확실한 방법이다. setsockopt()에 SO_REUSEADDR을 사용한 것과 같은 효과.

:::
2007/08/02 00:00

소켓 프로그래밍 : 인터넷 주소 다루기


internet address 변환

윤 상배

dreamyun@yahoo.co.kr



1절. 소개

이번 글은 도메인주소 그리고 점박이 3형제(xxx.xxx.xxx.xxx) 주소, 32 bit 주소간의 변환이 어떻게 이루어지는지에 대한 내용을 담고 있다.

이들에 대한 이해는 인터넷 관련 어플리케이션을 제작하는데 많은 도움을 줄것이다.


2절. 인터넷 도메인 주소에 대해서

firefox, ncftp 등의 인터넷 프로그램을 사용할경우 원하는 서버에 접근하기 위해서 보통 숫자로 이루어진 인터넷 주소 대신에 www.joinc.co.kr 과 같은 도메인 주소를 활용하게 된다. 이유는 간단하다. 211.234.96.147 과 같이 숫자로 이루어진 인터넷주소보다 훨씬 이해하기 쉽고, 기억해 내기가 쉽기 때문이다. 게다가 인터넷주소는 그때그때 사정에 따라서 바뀔수 있는데, 도메인이름의 경우 인터넷주소가 바뀐다고 하더라도 바뀔필요가 없기 때문이다(네임서버만 이 변경된 정보를 알고 있으면된다.) 1

그렇다면 우리가 어플리케이션의 주소입력창등에 www.joinc.co.kr 이라고 도메인이름을 입력했을경우 www.joinc.co.kr 의 인터넷 주소가 211.234.96.147 이라고 어플리케이션에 알려줄수 있는 어떤 장치를 필요로 할것이다.

이러한 장치는 도메인 서버(네임서버)라고 불리우는 서버/클라이언트 모델에 의해서 만들어져 있다. 즉 어플리케이션에서 www.joinc.co.kr 의 도메인 주소에 대한 인터넷주소를 필요로 한다면 운영체제에 이것을 요청하고 운영체제는 미리 설정된 네임서버에 연결해서 www.joinc.co.kr 의 인터넷 주소를 요청하고, 운영체제는 이 값을 해당 어플리케이션에 전달해 주게 된다.

사실 네임서버가 클라이언트의 요청을 받아서 도메인에 대한 인터넷주소를 넘겨주는 것은 좀더 복잡한 과정을 거치지만, 이 글에서는 이러한 과정을 설명하진 않을것이다. 이 내용에 대해서는 Powered by DNS의 문서를 참고하기 바란다.

이 문서에서는 프로그래머의 입장에서 위의 과정이 어떻게 이루어지는지만 확인해 볼것이다.


3절. 도메인 주소를 인터넷 주소로 변환

도메인 주소에 대한 인터넷주소의 변환은 2가지 방법을 따른다. 즉 /etc/resolv.conf 를 이용해서 외부 도메인주소에 대한 인터넷주소를 가져오는 것과, /etc/hosts 를 이용해서 인터넷주소를 가져오는 것이다.

엄밀히 말하자면 2가지 방법이 있는게 아니고, 우선 /etc/hosts 의 호스트 정보를 참조하고 없을경우 /etc/resolv.conf 를 참조하는 순서를 따른다.

이번장에서는 이 순서에 따른 인터넷주소를 가져오는 방법에 대한 설명을 할것이다.


3.1절. resolv.conf 참조

어플리케이션에서 도메인 주소를 받았다면 이 도메인주소를 이용해서 인터넷주소를 얻어와야 한다. 이러한 주소 변환작업을 위해서 gethostbyname() 이라는 함수를 제공한다.

#include <netdb.h>

struct hostent *gethostbyname(const char *name);
위 함수를 실행시키면 hostent 라는 구조체를 되돌려 준다. 이 구조체는 다음과 같은 정보들을 가지고 있으며, 이 정보들을 이용해서 주어진 도메인 주소 name 의 인터넷주소 목록을 얻어올수 있다.
struct hostent
{
char *h_name; /* 호스트의 공식 이름 */
char **h_aliases; /* 별칭 리스트 */
int h_addrtype; /* 호스트 주소 타입 */
int h_length; /* 주소의 길이 */
char **h_addr_list; /* 주소 리스트 */
}

gethostbyname() 을 실행할경우 도메인이름에 대한 인터넷주소를 얻어오기 위한 네임서버로의 쿼리를 수행해야 하는데, 이일은 운영체제가 알아서 수행한다. Linux의 경우 위 함수를 이용하면 운영체제는 /etc/resolv.conf 에 등록되어 있는 네임서버로 쿼리를 던져서 그 정보를 얻어와서 리턴시켜준다. resolv.conf 를 보면 다음과 같이 설정되어 있을것이다.

search localhost
nameserver 211.62.36.242
nameserver 164.124.101.2
만약에 인터넷상에 등록되지 않은 도메인이름에 대한 정보를 요청할경우 NULL 을 돌려주게 될것이다. h_addr_list 를 보면 포인터의 포인터로 되어 있다. 그 이유는 하나의 도메인주소에 대하여 여러개의 인터넷 주소를 할당해 줄수 있기 때문이다.

다음은 gethostbyname() 을 이용해서 인터넷 주소를 얻어오는 간단한 예제 프로그램이다.

gethostbyname.c

#include <netdb.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>

int main(int argc, char **argv)
{
struct hostent *myent;
struct in_addr myen;
long int *add;

myent = gethostbyname(argv[1]);
if (myent == NULL)
{
perror("ERROR : ");
exit(0);
}

printf("%s\n", myent->h_name);

while(*myent->h_addr_list != NULL)
{
add = (long int *)*myent->h_addr_list;
myen.s_addr = *add;
printf("%s\n", inet_ntoa(myen));
myent->h_addr_list++;
}
}
다음은 위의 프로그램을 통해서 www.joinc.co.kr 과 www.yahoo.com 의 인터넷 주소를 가져오기 테스트를 한 결과이다.
[root@localhost test]# ./gethostbyname www.joinc.co.kr
www.joinc.co.kr
211.234.96.147
[root@localhost test]# ./gethostbyname www.yahoo.com
www.yahoo.akadns.net
66.218.71.83
66.218.71.84
66.218.71.86
66.218.71.87
66.218.71.89
66.218.71.80
66.218.71.81
각 도메인에 할당된 주소정보를 제대로 가져오고 있음을 알수 있다.


3.1.1절. /etc/hosts 참조

위에서 말했듯이 gethostbyname() 을 호출할경우 우선적으로 /etc/hosts 의 호스트 정보를 찾게 되고 정보가 없을경우 /etc/reslov.conf 를 참조하게 된다. 다음은 테스트를 위한 /etc/hosts 정보이다.

192.168.1.102	gateway	
192.168.1.103 test.web.co.kr
위의 프로그램을 실행시켜 보면 다음과 같은 결과를 보여줄것이다.
[root@coco test]# ./gethostbyname gateway       
gateway
192.168.1.102
[root@coco test]# ./gethostbyname test.web.co.kr
test.web.co.kr
192.168.1.103



4절. 인터넷 주소를 이진 데이터로 변환

위의 방법을 통해서 xxx.xxx.xxx.xxx (점박이 3형제) 스타일의 인터넷 아이피를 얻는데에 까지 성공했다. 그러나 IP 헤더에 대한 지식을 가지고 있다면, 실제 인터넷상에서는 점박이 3형제 스타일의 IP 정보가 전송되는게 아닌 long int 형의 32bit 바이너리(이진)데이타 형식으로 전송된다는것을 알고 있을것이다. (IP 자세히보기 를 참조하라.)

그러므로 우리는 위의 인터넷 주소를 다시 32bit 바이너리 데이타 형태로 수정을 해야만 한다.

C 는 이러한 변환작업을 위해서 inet_addr(3)이라는 함수를 제공한다.

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

unsigned long int inet_addr(const char *cp);
이 함수를 사용하면 인자로 주어진 점박이 3형제 스타일의 인터넷 주소를 인터넷바이트 오더를 따르는 32bit 이진 데이타 형태로 변경 시켜준다. 이 정보는 IP 헤더의 source address 와 destination address 에 쓰여져서 전달되게 된다.

다음은 간단한 예제이다.

 
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h<
#include <unistd.h>

int main(int argc, char **argv)
{
unsigned long int bin_addr;
int dot_addr[4];

bin_addr = inet_addr(argv[1]);

printf("%u\n", bin_addr);
}
이 프로그램을 컴파일한다음에 아규먼트로 192.168.100.130 을 입력한다음에 결과를 확인해 보면 다음과 같이 출력될것이다.
[root@localhost test]# ./inet_addr 192.168.100.130
2187634880
이것을 이진코드로 변경하면 아래와 같이 계산된다.
		10000010 01100100 10101000 11000000
130 100 168 192
기본적으로 inet_addr 을 했을경우 네트웍오더 바이가 적용되므로 리눅스의 리틀엔디안과는 반대의 바이트 오더를 보여준다. 그러므로 위의 값은 192.168.100.130 으로 표현되게 된다.


5절. 이진 데이타를 인터넷주소로 변환

어플리케이션 측에서 도메인주소를 인터넷주소로 인터넷 주소를 다시 이진 데이타로 변경했다면 이제 데이타를 인터넷을 통해서 목적지까지 보내기 위한 모든 준비가 완료된 셈이다.

이제 이데이타는 게이트 웨이를 거치고 라우터를 거치면서 최종목적지를 향하여 전송될것이고 목적지 인터넷주소가 인터넷에 물려있는 호스트라면 호스트까지 보내어질 것이고, 이것은 다시 해당 포트에 대기하고 있는 서버 어플리케이션으로 보내어질 것이다.

이제 데이타를 받은 서버 어플리케이션 측에서는 이 데이타가 어느 인터넷 주소로부터 도착했는지 알아야 할것이다(보통의 경우 굳이 알필요 없긴하지만 여러가지 이유로 - 이를테면 디버깅 이라든지 인터넷 데이타 통계를 위해서 - 상대방의 인터넷주소정보를 가져와야 한다.) 이럴경우 이진데이타를 다시 인터넷주소로 변경해야 하는데 이때 사용되는 함수가 inet_ntoa(3) 이다.

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

char *inet_ntoa(struct in_addr in);
약간 주의해야할 점은 인자로 들어가는 데이타가 unsigned long int 형이 아닌 struct in_addr 타입이라는 점이다. struct in_addr 의 멤버변수 값을 채워준다음 입력시켜주면 점박이 3형제 스타일의 인터넷주소 정보를 넘겨준다.
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int main(int argc, char **argv)
{
char *addr;
struct in_addr in;
unsigned long int bin_addr;

bin_addr = inet_addr(argv[1]);

in.s_addr = bin_addr;
printf("%u\n", in.s_addr);
addr = inet_ntoa(in);

printf("address is %s\n", addr);
}
위의 예제는 예제를 위한 예제이다. 아규먼트로 점박이 3형테 인터넷 주소정보를 입력하면 이것을 이진 데이타로 변경하고 다시 inet_ntoa() 을 이용해서 다시 점박이 3형제 인터넷 주소로 변경했다.

예제를 위한 예제가 아닌 좀더 실질적인 예를 알고 싶다면 getpeername(2)에 나와 있는 예제를 참고하기 바란다.


6절. 결론(총정리)

마지막으로 지금까지 다루었던 내용을 그림을 통하여 총정리 하도록 하겠다.

                                            gethostbyname() 
|
+----------------------------------------------+
| |
+-------------+ Domain name +------------+ 그렇지 않다면 +------------------+
| Application | -------------> | /etc/hosts | ---------------> | /etc/resolv.conf |
| Client | +------------+ +------------------+
| | | 만약 존재한다면 |
| +---|----------<------------+ IP 주소를 리턴 | IP 주소를 리턴
| +---|----------<-----------------------------------------------+
| | |
| +---|----- > 32BIT 이진 데이타 주소로 변경 ---------------------------> 인터넷
+-------------+ inet_addr(IP 주소)


+-------------+ inet_ntoa(32BIT 이진 데이타)
| Application | <--------- 32BIT 이진 데이타를 IP 주소로 변경 <--------- 인터넷
| Server |
+-------------+

이 문서는 수정될 수 있습니다. 최신문서는 Joinc Wiki에서.

:::
2007/07/20 00:35

linux man page : setsockopt - 소켓옵션을 변경한다.



소켓옵션

네트워크 환경은 매우 다양하며, 예측하기 힘든 경우도 많이 발생한다. 때문에 네트워크프로그램의 종류에 따라서 소켓의 세부사항을 조절해야 하는 경우가 발생한다. 이러한 소켓옵션 설정을 위해서 소켓은 getsockopt()와 setsockopt()두개의 함수를 제공한다. 이름에서 알 수 있듯이 getsockopt는 현재의 소켓옵션값을 가져오기 위해서, setsockopt는 소켓옵션값을 변경하기 위해서 사용한다.

예를 들자면 동일한 네트워 프로그램이라고 하더라도 ATM망에서 작동하는 것과 인터넷망 PPP에서 작동하는 것은 환경에 있어서 차이가 생길 수 밖에 없을 것이다. 소켓버퍼의 크기를 예로 들자면, 일반적으로 (대역폭 * 지연율) * 2의 공식에 따를 경우 최적의 효과를 보여준다고 한다. 다음은 이들 함수의 사용방법이다.
#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_SOCKETIPPROTO_TCP 중 하나를 사용할 수 있다.
  • optname : 설정을 위한 소켓옵션의 번호
  • optval : 설정값을 저장하기 위한 버퍼의 포인터
  • optlen : optval 버퍼의 크기

설정값을 void * 로 넘기는 이유는 설정하고자 하는 소켓옵션에 따라서, boolean, interger, 구조체등 다양한 크기를 가지는 데이터형이 사용되기 때문이다. 만약 변경하고자 하는 소켓옵션이 boolean을 따른다면, 0혹은 1이 사용될 것이다.

SOL_SOCKET레벨에서 사용할 수 있는 옵션과 데이타형은 다음과 같다.
옵션값 데이터형 설명
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정보를 유지하고 있음으로 발생하는 문제다. 보통 1-2분 정도 지나만 커널이 알아서 정리를 하긴 하지만, 그 시간동안 기달려야 한다는 것은 상당히 번거로운 일이 될 것이다. 이 경우 다음과 같은 코드를 삽입함으로써 문제를 해결할 수 있다.
int sock = socket(...);
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (char *)&bf, (int)sizeof(bf));
이렇게 하면 커널은 기존에 bind로 할당된 소켓자원을 프로세스가 재 사용할 수 있도록 허락하게 된다.

다음은 소켓버퍼의 크기를 가져오고 설정하는 완전한 코드다.
#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

이 옵션을 이해하려면 Nagle알고리즘에 대해서 이해를 해야 한다. Nagle 알고리즘이 적용되면, 운영체제는 패킷을 ACK가 오기를 기다렸다가 도착하면, 그 동안 쌓여있던 데이터를 한꺼번에 보내게 된다. 이러한 방식을 사용하게 되면, 대역폭이 낮은 WAN에서 빈번한 전송을 줄이게 됨으로 효과적인 대역폭활용이 가능해진다.

대부분의 경우에 있어서 Nagle 알고리즘은 효율적으로 작동하긴 하지만, 빈번한 응답이 중요한 서비스의 경우에는 적당하지 않은 경우가 발생한다. 예를 들어 X-Terminal을 이용할 경우 마우스 이벤트는 즉시 전달될 필요가 있는데, Nagle알고리즘을 사용하면 아무래도 반응시간이 떨어지게 될 것이다. 실시간적인 반응이 중요한 온라인 게임역시 Nagle 알고리즘을 제거하는게 좋을 것이다.

아래의 이미지는 nagle이 적용되었을 때와 그렇지 않을 때, 어떻게 데이터 전송이 일어나는지를 보여주고 있다.

nagle.png

SO_LINGER

SO_LINGER은 소켓이 close()되었을 때, 소켓버퍼에 남아있는 데이터를 어떻게 할 것이지를 결정하기 위해서 사용한다. 다음은 SO_LINGER 옵션에 사용되는 데이터구조체이다.
struct linger
{
int l_onoff;
int l_linger;
}
  • l_onoff : linger 옵션을 끌것인지 킬 것인지 결정
  • l_linger : 기다리는 시간의 결정

위의 두개의 멤버변수의 값을 어떻게 하느냐에 따라 3가지 close방식을 결정되어 진다.
  1. l_onoff == 0 : 이 경우 l_linger의 영향을 받지 않는다. 소켓의 기본설정으로 소켓버퍼에 남아 있는 모든 데이터를 보낸다. 이때 close()는 바로 리턴을 하게 되므로 백그라운드에서 이러한 일이 일어나게 된다. 우아한 연결 종료를 보장한다.
  2. l_onoff > 0 이고 l_linger == 0 : close()는 바로 리턴을 하며, 소켓버퍼에 아직 남아있는 데이터는 버려 버린다. TCP 연결상태일 경우에는 상대편 호스트에 리셋을 위한 RST 패킷을 보낸다. hard 혹은 abortive 종료라고 부른다.
  3. l_onoff > 0 이고 l_linger > 0 : 버퍼에 남아있는 데이터를 모두 보내는 우아한 연결 종료를 행한다. 이때 close()에서는 l_linger에 지정된 시간만큼 블럭상태에서 대기한다. 만약 지정된 시간내에 데이터를 모두 보냈다면 리턴이 되고, 시간이 초과되었다면 에러와 함께 리턴이 된다.

참고문헌

이 문서는 수정될 수 있습니다. 최신문서는 Joinc Wiki에서.
:::
2007/07/19 08:51

소켓 네트워크 프로그래밍 - inetd와 stand alone 방식의 서버 구현


1절. inetd 와 stand alone 방식의 차이

telnet, ftp 와 같은 인터넷 서비스 프로그램을 만들경우 2가지 방식이 사용된다. 하나는 자체적으로 port 를 만들어서 클라이언트의 연결을 기다리는 stand alone 방식이고, 다른 하나가 inetd 라고 불리우는 "인터넷 서비스 데몬" 을 이용하는 방식이다.

이번 장에서는 이러한 2가지 인터넷 서비스 방식의 차이점과 inetd 프로그래밍 방식에 대해서 알아 볼것이다.


1.1절. stand alone 방식

우리말로 번역하자면 "홀로서기" 정도가 될것이다. 실제로 stand alone 방식으로 제작된 프로그램은 단독으로 인터넷 서비스가 가능하다. 즉 스스로 서비스 port를 할당하고 서비스 port 에 대한 클라이언트 연결을 받아들여서 서비스를 하게 된다. stand alone 방식의 서버프로그램은 다중의 클라이언트 를 받아들 이기 위해서 fork(2), select(2), poll(2), thread 등을 프로그래밍 기법을 사용 한다.

                            +---------------------------------------+
| Server |
+---------+ | +-+ +--------+ +--------+ |
| client1 | <------------> | |P|<--->| accept |<------->| 처리 1 | |
+---------+ | |O| | | | +--------+ |
+---------+ | |R| | | | +--------+ |
| client2 | <------------> | |T| | | +--->| 처리 2 | |
+---------+ | +-+ +--------+ +--------+ |
+---------------------------------------+
위의 그림은 Stand alone 방식의 서버 프로그램이 client1 의 요청을 처리하는 전형적인 방식이다. 다중의 클라이언트를 받아들이기 위해서 Server 은 accept(2) 한후 fork(2) 등의 기술을 사용하여서 각 클라이언트의 요청을 처리한다.

지금까지 이 사이트에서 예로 들었던 모든 인터넷 서버 프로그램들은 바로 stand alone 방식을 이용해서 만들어졌다.


1.2절. inetd 방식

반면 inetd 방식을 사용하는 인터넷 서비스 프로그램의 경우는 모든것을 처리하는 방식이 아니다. 해당 포트에 대기 해서 클라이언트로 부터 연결을 받아들이는 일을 외부 데몬프로그램인 inetd 에 맡기고 실제 서비스 프로그램은 연결된 클라이언트의 요청을 처리하는 일만 한다.

inetd 데몬은 시스템이 시작될때 시작되어서, 종료될때까지 계속적으로 시스템에 떠있게 된다. 이 프로그램은 /etc/services 파일과 서비스 설정파일인 /etc/inetd.conf (레뎃 7.x 의 경우에는 /etc/xinetd.d) 를 참고하여서, 해당 포트에 연결을 대기 하고 있다가 클라이언트로 부터 연결이 들어오면 자식프로세스를 fork 시킨다음에 inetd.conf 에 설정된 서버 프로그램을 exec 시키게 되고, exec 된 서버 프로그램이 클라이언트의 요청을 처리하게 된다.

 -->> : fork & exec
+---------+ +-------+ +----------+
| client1 | ---------> | inetd | ----->> | Server 1 |
+---------+ | | +----------+
+---------+ | | +----------+
| client1 | ---------> | | ----->> | Server 2 |
+---------+ +-------+ +----------+
fork시 보통 부모 프로세스의 열린 파일 지시자들도 자식 프로세스로 복사된다. 또한 exec 할때도 열린 파일 지시자들은 새로운 프로세스로 복사된다. 그러므로 inetd 에서 새로운 클라이언트를 받아서 fork 할때는 소켓 연결 지시자를 넘겨줘야 하므로, 소켓 연결 지시자를 표준입력(0)으로 복사한다. 최종적으로 exec 된 Server는 표준입력을 통해서 client 와 통신하게 된다.


2절. inetd 프로그래밍 하기

2.1절. inetd 방식 프로그래밍 예제

그러면 간단한 예를 통해서 inetd 방식의 서버 프로그램을 하나 만들어 보도록 하겠다.(stand alone 방식은 지금까지 자주 사용되어 왔음으로 생략)

예제 프로그램은 지금까지 자주 사용했던 fork 버젼의 zipcode_multi.c 를 inetd 방식으로 변경된 버젼이다. zipcode_multi.c 의 예제 파일은 다중연결서버 만들기 (1)을 참고하기 바란다.

예제: zipcode_inetd.c

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

int main(int argc, char **argv)
{
struct sockaddr *clientaddr;
char buf[255];
char line[255];
char myline[255];
FILE *fp;
int length;

clientaddr = (struct sockaddr *)malloc(128);
length = 128;

if (getpeername(0, clientaddr, &length) < 0)
{
perror("socket failure: ");
exit(0);
}

if ((fp=fopen("/etc/zipcode.txt", "r")) == NULL)
{
perror("file open error : ");
exit(0);
}
while(1)
{
if (read(0, buf, 255) <= 0)
break;
if (strncmp(buf, "quit", 4) == 0)
{
write(0, "byebye\n", 8);
break;
}

while(fgets(line,255,fp) != NULL)
{
if (strstr(line, buf) != NULL)
{
write(0, line, 255);
}
}
write(0, "end\n", 4);
rewind(fp);
}

close(0);
fclose(fp);
exit(0);
}
쏘쓰를 보면 알겠지만, 예전의 zipcode_multi.c 에 비해서 매우 간단해 졌음을 알수 있다. 이는 socket, bind, listen, accetp 로 이어지는 일련의 통신 초기화 과정을 inetd 데몬에서 담당하기 때문에 zipcode_inetd 에서는 실제 통신 부분만을 코딩에 신경쓰면 되기 때문이다.

위의 코드에서 신경쓸 부분은 getpeername(3) 정도일 것이다. 이 함수를 호출해서 첫번째 인자로 주어진 값이 사용가능한 소켓인지 알아낼수 있다.


2.1.1절. inetd(xinetd) 데몬 재가동하기

zipcode_inetd 프로그램을 작성하고 컴파일 완료 했다면, inetd 설정파일인 /etc/inetd.conf 를 수정하고 inetd 데몬을 다시 실행시켜줘야 한다. 그러나 앞에서 말했다 시피, 레드헷 7.x 이상에서부터는 inetd 대신에 xinetd 를 사용한다. 필자가 테스트한 Linux 가 레드헷 7.2 이므로 xinetd 를 기준으로 설명할것이다. inetd 설정에 대한 내용은 The inetd Supper-Server을 참고하기 바란다.

xinetd 데몬은 /etc/xinetd.d 밑에 각 서비스 별로 설정파일을 둔다. xinetd.d 디렉토리 아래에 다음과 같은 내용을 가지는 zipcode 라는 파일을 만들도록 하자.

service zipcode
{
socket_type = stream
protocol = tcp
wait = no
user = root
server = /usr/local/bin/zipcode
port = 12345
}
tcp 프로토콜을 사용하며 12345 포트를 사용한다. 그리고 연결을 받아들였을경우 /usr/local/bin/zipcode 라는 프로그램을 실행시키도록 설정하였다. 위의 컴파일된 zipcode_inetd 실행파일은 /usr/local/bin 에 zipcode 라는 파일이름으로 복사하도록 하자.

이제 모든 준비는 끝났다. 바뀐 내용을 적용시키기 위해서 xinetd 데몬을 다시 실행시키도록 하자. xinetd 데몬의 pid 를 알아내서 SIGHUP 신호를 보내는 방법과 xinetd 시동 스크립트를 가동시키는 방법이 있다.

[root@localhost root]$ /etc/rc.d/init.d/xinetd restart
xinetd 를 정지함: [ 확인 ]
xinetd (을)를 시작합니다: [ 확인 ]
[root@localhost root]#


2.1.2절. 테스트 하기

이제 클라이언트를 이용해서 테스트 하기만 하면 된다. 클라이언트는 셈플로 알아보는 소켓프로그래밍에 있는 zipcode_cli.c 를 그대로 사용하면 된다.


2.2절. inetd 방식과 stand alone 방식중 어느걸 선택해야 하나

결론 부터 말하자면 서비스할 서버의 환경에 따라서 적당한 방식을 채택하면 된다.(너무 뻔한 답변인것 같지만 --;)


2.2.1절. inetd

각각 적당한 용도가 있다. 일단 inetd 방식의 경우 위에서 보았겠지만 코딩량이 많이 줄어든다. 또한 에러발생확률도 줄어든다. inetd 데몬 자체가 검증된 프로그램이기 때문이다. 또한 다중의 클라이언트를 받아들이기 위해서 사용하는 fork, select, poll 등에 대해서 신경쓸필요가 없다. 보통 이들 fork, select, poll 은 꽤 까다로운 프로그래밍 작업을 요구하며, 이는 많은 오류를 발생시키기도 한다.

inetd 방식은 telnet, ftp, pop 서비스와 같은 빈번한 요구가 발생하지 않거나, 일단 요청을 받아들이면 꽤 오랜시간 작업을 수행하는 서비스에 적당하다. 반면 http 서비스와 같이 초기 반응속도가 중요시하거나 매우 빈번한 요청이 일어나는 서비스에는 적당한 방법이 아니다.

그리고 에러를 표준출력시킬수 없으므로 약간 디버깅이 까다로와 질수 있다는 단점을 가지고 있다. 디버깅을 원한다면 에러로그를 파일로 저장하는 방법을 사용해야 할것이다.


2.2.2절. inetd

stand alone 방식은 http와 같은 반응속도가 중요하거나, 매우 빈번한 요청이 일어나는 서비스에 적당하다.

예를들어 서버가 oracle 에 연결해서 데이타를 가져오는 일을한다고 했을때 inetd 방식을 사용하면 클라이언트의 연결이 들어올때 마다 oracle DB로의 연결을 수행해야 할것이다. 이는 엄청난 자원의 비효율성을 증가 시킬수 있다. 그러나 stand alone 방식으로 했을때는 최초에 한번만 oracle DB로 연결하고, 새로운 클라이언트가 연결되더라도 이미 연결된 oracle DB로 작업을 할수 있을것이다.

그러나 코드량이 많아지고, 신경써야할 부분들이 매우 많아진다는 단점을 가지고 있다.


이 문서는 수정될 수 있습니다. 최신문서는 Joinc Wiki에서..
:::
2007/05/26 15:58

소켓 프로그래밍 - socket API 레퍼런스

최신문서는 Joinc Wiki 를 통해서 확인하세요.

소켓옵션

완성이 되면 docbook에 추가될 것이다.

네트워크 환경은 매우 다양하며, 예측하기 힘든경우도 많이 발생한다. 때문에 네트워크프로그램의 종류에 따라서 소켓의 세부사항을 조절해야 하는 경우가 발생한다. 이러한 소켓옵션 설정을 위해서 소켓은 getsockopt()와 setsockopt()두개의 함수를 제공한다. 이름에서 알 수 있듯이 getsockopt는 현재의 소켓옵션값을 가져오기 위해서, setsockopt는 소켓옵션값을 변경하기 위해서 사용한다.

예를 들자면 동일한 네트워 프로그램이라고 하더라도 ATM망에서 작동하는 것과 인터넷망 PPP에서 작동하는 것은 환경에 있어서 차이가 생길 수 밖에 없을 것이다. 소켓버퍼의 크기를 예로 들자면, 일반적으로 (대역폭 * 지연율) * 2의 공식에 따를 경우 최적의 효과를 보여준다고 한다. 다음은 이들 함수의 사용방법이다.
#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_SOCKETIPPROTO_TCP 중 하나를 사용할 수 있다.
  • optname : 설정을 위한 소켓옵션의 번호
  • optval : 설정값을 저장하기 위한 버퍼의 포인터
  • optlen : optval 버퍼의 크기

설정값을 void * 로 넘기는 이유는 설정하고자 하는 소켓옵션에 따라서, boolean, interger, 구조체등 다양한 크기를 가지는 데이터형이 사용되기 때문이다. 만약 변경하고자 하는 소켓옵션이 boolean을 따른다면, 0혹은 1이 사용될 것이다.

SOL_SOCKET레벨에서 사용할 수 있는 옵션과 데이타형은 다음과 같다.
옵션값 데이터형 설명
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정보를 유지하고 있음으로 발생하는 문제다. 보통 1-2분 정도 지나만 커널이 알아서 정리를 하긴 하지만, 그 시간동안 기달려야 한다는 것은 상당히 번거로운 일이 될 것이다. 이 경우 다음과 같은 코드를 삽입함으로써 문제를 해결할 수 있다.
int sock = socket(...);
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (char *)&bf, (int)sizeof(bf));
이렇게 하면 커널은 기존에 bind로 할당된 소켓자원을 프로세스가 재 사용할 수 있도록 허락하게 된다.

다음은 소켓버퍼의 크기를 가져오고 설정하는 완전한 코드다.
#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

이 옵션을 이해하려면 Nagle알고리즘에 대해서 이해를 해야 한다. Nagle 알고리즘이 적용되면, 운영체제는 패킷을 ACK가 오기를 기다렸다가 도착하면, 그 동안 쌓여있던 데이터를 한꺼번에 보내게 된다. 이러한 방식을 사용하게 되면, 대역폭이 낮은 WAN에서 빈번한 전송을 줄이게 됨으로 효과적인 대역폭활용이 가능해진다.

대부분의 경우에 있어서 Nagle 알고리즘은 효율적으로 작동하긴 하지만, 빈번한 응답이 중요한 서비스의 경우에는 적당하지 않은 경우가 발생한다. 예를 들어 X-Terminal을 이용할 경우 마우스 이벤트는 즉시 전달될 필요가 있는데, Nagle알고리즘을 사용하면 아무래도 반응시간이 떨어지게 될 것이다. 실시간적인 반응이 중요한 온라인 게임역시 Nagle 알고리즘을 제거하는게 좋을 것이다.

아래의 이미지는 nagle이 적용되었을 때와 그렇지 않을 때, 어떻게 데이터 전송이 일어나는지를 보여주고 있다.

nagle.png

SO_LINGER

SO_LINGER은 소켓이 close()되었을 때, 소켓버퍼에 남아있는 데이터를 어떻게 할 것이지를 결정하기 위해서 사용한다. 다음은 SO_LINGER 옵션에 사용되는 데이터구조체이다.
struct linger
{
    int l_onoff;
    int l_linger;
}
  • l_onoff : linger 옵션을 끌것인지 킬 것인지 결정
  • l_linger : 기다리는 시간의 결정

위의 두개의 멤버변수의 값을 어떻게 하느냐에 따라 3가지 close방식을 결정되어 진다.
  1. l_onoff == 0 : 이 경우 l_linger의 영향을 받지 않는다. 소켓의 기본설정으로 소켓버퍼에 남아 있는 모든 데이터를 보낸다. 이때 close()는 바로 리턴을 하게 되므로 백그라운드에서 이러한 일이 일어나게 된다. 우아한 연결 종료를 보장한다.
  2. l_onoff > 0 이고 l_linger == 0 : close()는 바로 리턴을 하며, 소켓버퍼에 아직 남아있는 데이터는 버려 버린다. TCP 연결상태일 경우에는 상대편 호스트에 리셋을 위한 RST 패킷을 보낸다. hard 혹은 abortive 종료라고 부른다.
  3. l_onoff > 0 이고 l_linger > 0 : 버퍼에 남아있는 데이터를 모두 보내는 우아한 연결 종료를 행한다. 이때 close()에서는 l_linger에 지정된 시간만큼 블럭상태에서 대기한다. 만약 지정된 시간내에 데이터를 모두 보냈다면 리턴이 되고, 시간이 초과되었다면 에러와 함께 리턴이 된다.



Socket

윤 상배

고친 과정
고침 0.9 2004년 8월 19일
입출력함수/도메인 이름변환/바이트오더 관련 함수 추가

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

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 들의 사용예는 이 사이트에서 충분히 찾아볼수 있을것이다.

:::