아래 내용들은 윤성우 저자의 “지피지기 TCP/IP소켓 프로그래밍” 책의 내용을 바탕으로 본인이 읽고 새로 정리한 내용들이니,
다소의 오류가 있을 수도 있습니다.
서버 소켓 기본 코드
[code cpp]
/*
* helloWorld_server.c
* Written by SW.YOON
* Edited by Michael YOON
*/
#include
#include
#include
#include
#include
#include
#include
#define default_PT 89123
void error_handling(char *message);
int main(int argc, char ** argv)
{
int serv_sock;
int clnt_sock;
struct sockaddr_in serv_addr;
struct sockaddr_in clnt_addr;
int clnt_addr_size;
char message[]=”Hello World!n”;
if(argc>1)
{
printf(“Usage : %s n” ,argv[0]); //사용법
printf(“Default port is 89123n”);
}
////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////기본 소켓 생성 설정/////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////
serv_sock=socket(PF_INET, SOCK_STREAM,0); //서버소켓 만들기
//PF_INET은
if(serv_sock == -1)
error_handling(“socket() error”); //서버소켓 만들기 실패 시
memset(&serv_addr,0,sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
if(argc==2)
{
serv_addr.sin_port = htons( atoi(argv[1]) );
}
else
{
serv_addr.sin_port = htons( atoi(default_PT) );
}
if( bind( serv_sock, (struct sockaddr* )&serv_addr, sizeof(serv_addr) ) == -1 )
{
error_handling(“bind() error”);
}
if( listen(serv_sock,5) == -1 )
{
error_handling(“listen() error”);
}
////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////
clnt_addr_size = sizeof(clnt_addr);
clnt_sock=accept( serv_sock, (struct sockaddr*) &clnt_addr, &clnt_addr_size );
/* 연결 수락 */
if(clnt_sock == -1)
error_handling(“accept() error”); write( clnt_sock, message, sizeof(message) ); /* 데이터 전송 */
close(clnt_sock); /* 연결 종료 */
return 0;
}
void error_handling(char *message)
{
fputs( message, stderr );
fputc( ‘n’, stderr);
exit(1);
}
[/code]
기본 연결포트는 89123포트로 설정해 놓았습니다.
우선 하나하나 코드를 살펴보면
[#M_17줄. 에러 핸들러를 선언합니다.|17줄. 에러 핸들러를 선언합니다.|에러메시지를 인자로 전달하면 그 메시지를 출력한 뒤 프로그램을 종료합니다._M#]
[#M_21줄. 서버소켓을 선언합니다.|21줄. 서버소켓을 선언합니다.|
int형인 이유는 디스크립터를 만든 뒤, 그 디스크립터의 순서 번호를 지정해야 하기 때문입니다.
앞으로는 이 순서 번호를 편의상 디스크립터라고 하겠습니다.
_M#]
22줄. 클라이언트 소켓을 선언합니다.
[#M_이를 간단히 표로 정리해보면 |이를 간단히 표로 정리해보면 |
struct sockaddr_in의 정의는 다음과 같다.
[code cpp]
struct sockaddr_in
{
sa_family_t sin_family; /* 주소 체계(address family) */
uint16_t sin_port; /* 16비트 TCP / UDP Port */
struct in_addr sin_addr; /* 32 비트 IPv4 주소 */
char sin_zero[8]; /* 사용안함 */
}
struct in_addr
{
uint32_t s_addr; /* 32비트 IPv4 인터넷 주소 */
}
[/code]
여기에 생소한 타입들이 있는데 이들을 POSIX 표준 인터페이스라고 한다.
[#M_이를 간단히 표로 정리해보면 |이를 간단히 표로 정리해보면 |
Data type(타입 명) |
Description(설명) |
Header(필요 헤더) |
int8_t |
8비트 부호 있는 정수 |
|
uint8_t |
8비트 부호 없는 정수 |
|
int16_t |
16비트 부호 있는 정수 |
|
uint16_t |
16비트 부호 없는 정수 |
|
int32_t |
32비트 부호 있는 정수 |
|
uint32_t |
32비트 부호 없는 정수 |
|
sa_family_t |
주소체계 |
|
socklen_t |
구조체의 길이 |
_M#]
24줄. 클라이언트의 IPv4주소체계를 기록하는 구조체이다.
25줄. 클라이언트 IPv4 주소 구조체의 크기를 기록하는 구조체이다.
26줄. 간단히 Hello World! 문자열을 출력하기 위한 문자배열이다.
28~32줄. 인자의 개수를 파악하여 사용법을 알려주기도 한다.
[#M_36줄. 서버소켓을 생성한다.|36줄. 서버소켓을 생성한다.|
소켓함수의 원형은
[code cpp]int socket(int domain, int type, int protocol);[/code]
로 되어있는데, 처음 인자인 domain은 프로토콜의 체계이다.
프로토콜 체계는
프로토콜 체계 |
Description(설명) |
PF_INET |
IPv4 인터넷 프로토콜 |
PF_INET6 |
IPv6 인터넷 프로토콜 |
PF_LOCAL |
Local통신용 UNIX 프로토콜 |
PF_PACKET |
Low level socket interface |
PF_IPX |
IPX 노벨 프로토콜 |
두 번째 인자인 type 는 데이터의 전송 타입인데 대표적으로 SOCK_STREAM과 SOCK_DGRAM방식이 있다
SOCK_STREAM
방식은 연결지향성 방식으로, 컨베이어 벨트를 이용하듯이 에러나 데이터의 손실이 없이 순차적으로 데이터를 전달하며 데이터의
경계(Boundary)가 없다.(경계가 없다는 이야기는 한번의 전송을 2번에 나눠 받을 수도, 반대로 2번의 전송을 한번에 받을
수도 있다는 것이다.)
SOCK_DGRAM방식은 비 연결지향성 방식으로, 택배와 같이 먼저 보낸 순서와 상관없이 가장 빠른 경로로 도착하여 속도가 빠르다.
(예
를 들어 0ms에 전달에 필요한 시간이 A루트는 10ms, B루트는 20ms였는데, 2ms후 전달에 필요한 시간이 A루트는
7ms, B루트는 5ms라면 0ms에 보낸 데이터가 2ms에 보낸 데이터보다 늦게 도착한다.) 또한 데이터의 경계가 존재하여
2번의 전송을 했다면 수신 측에서는 2번의 수신을 해야 한다. 하지만 데이터가 손실되거나 에러가 발생할 수 있고, 한번에
전달하는 데이터 크기는 제한되어 있다.
세 번째 인자인 protocol은 호스트 대 호스트 에서 사용될 프로토콜을 설정하는 것이다.
우선 프로토콜체계(1 번째 인자)가 PF_INET일 때에는 2가지 값이 될 수 있다.
IPPROTO_TCP : TCP기반 소켓(연결지향성)
IPPROTO_UDP : UDP기반 소켓(비 연결지향성)
하지만 이미 2번째 인자에서 연결지향성인지 비 연결지향성인지 구분을 했기 때문에 0을 넣어도 잘 작동한다.
(책
에는 두 값 모두 0이라고 되어있지만, socket.h파일을 열어본 결과 TCP는 6, UDP는 17의 값을 갖고 있다. 아마
0을 넣으면 2번째 인자의 값에 맞도록 default값이 설정 되어있거나, 2번째 인자의 값에 따라 3번째 인자를 무시하는
경우가 있을지도 모르겠다.)
_M#]
[#M_42~52줄에서는 서버주소체계를 설정하고 있다.|42~52줄에서는 서버주소체계를 설정하고 있다.|
주소체계 구조체에는 네트워크 바이트 순서(BIG-Endian방식)으로 채워져야 한다.
이를 변환해 주는 함수는
htons, ntohs, htonl, ntohl이 있는데
h는 호스트변수, n은 네트워크변수,
s는 short변수용, l은 long변수용로 생각하면 된다.
(ex. 네트워크 바이트로 short int형 변수를 변환할때
[code cpp]short int si_test=1234;
si_test = htons( si_test ); //host – to – network – short[/code])
memset함수는 서버주소체계 구조체를 초기화 하고 있다.
serv_addr.sin_family
는 주소체계를 나타내며 프로토콜 체계와 유사하다.(실제로 PF타입을 그냥 넣어도 같은 결과를 얻을 수 있다. PF와 AF방식의
상수 값이 같기 때문이다. 미래에 프로토콜과 주소체계에 변화가 있을 것을 대비한 것 이라는데,,,,)
주소 체계 |
Description(설명) |
AF_INET |
IPv4 인터넷 프로토콜 |
AF_INET6 |
IPv6 인터넷 프로토콜 |
AF_LOCAL |
Local통신용 UNIX 프로토콜 |
serv_addr.sin_addr.s_addr은 IP주소를 기억하는 변수다. INADDR_ANY상수는 시스템의IP를 자동으로 찾아주지만, 클라이언트는 수동으로 할당해야 한다. 이때 IP주소를 long형으로 변환하는 방법은
[code cpp] unsigned long inet_addr(const char *string);[/code]
에 집어 넣는 것이다.
주소체계변수.sin_addr.saddr=inet_addr(“222.122.195.5”);
또 다른 방법은
[code cpp] int inet_aton(const char *string, struct in_addr *addr);[/code]
에 넘기는 것이다.
inet_aton(“222.122.195.5”, &주소체계변수.sin_addr);
serv_addr.sin_port는 port번호를 지정하는 변수이다.
네트워크형식으로 넣기위해 htons 를 사용하고, 문자열을 int로 바꾸기 위해 atoi를 사용한다. _M#]
[#M_54줄. bind함수로 소켓과 인터넷 주소를 이어준다..|54줄. bind함수로 소켓과 인터넷 주소를 이어준다.|[code cpp]int bind(int sockfd, struct sockaddr *myaddr, int addrlen);[/code]
이 함수에서 sockfd는 소켓의 파일 디스크립터이다. 위에서 socket함수로 받은 인자를 넘기면 된다.
myaddr은 서버의 주소체계의 포인터인 &serv_addr을 넘겨주면 된다. 단 serv_addr은 sockaddr_in형이므로 sockaddr형 포인터로 바꿔줘야 한다
3번째 addrlen은 serv_addr의 크기를 넘겨줘야 하니 sizeof(serv_addr)을 넘겨줘야 한다. _M#]
[#M_59줄, listen함수.|59줄, listen함수.|대망의 연결 대기이다. 이 함수가 호출되면 프로그램은 연결 요청을 기다리는 상태가 된다.
[code cpp]int listen(int s, int backlog);[/code]
이 함수의 s는 서버소켓의 파일 디스크립터를 줘서 어떤 소켓으로 연결요청을 받을 것인가를 결정하고,
backlog에는 몇 개의 연결요청을 큐에 넣어 대기할 것인지(몇 개의 클라이언트를 줄 세워 놓을 것인지)를 알리는 인자이다. _M#]
[#M_66줄.|66줄.|[code cpp]clnt_addr_size = sizeof(clnt_addr);[/code]클라이언트 소켓 주소체계 구조체(,,,)의 크기를 매번 쓰기 불편하니까 변수에 저장 _M#]
[#M_67줄. accept함수!!|67줄. accept함수!!|드디어!! 연결을 한다!! 위에 까지는 연결이 목적이 아니라 연결의 준비과정이었다.
이 함수에 이르러 드디어 연결을 시도하게 된다.
사용되는 함수는
[code cpp]int accept(int s, struct sockaddr* addr, int *addrlen);[/code]
인자 목록이 이제 눈에 익을 것이다. bind함수와 인자 목록이 똑같다.
다만 bind함수는 서버소켓과 서버 주소체계를 이어주어 서버소켓의 파일디스크립터를 반환했다면
accept함수는listen 함수의 대기 큐의 첫 번째 클라이언트의 주소체계와 클라이언트 소켓을 이어주며 클라이언트의 파일 디스크립터를 반환한다.
즉, s자리에는 serv_sock(연결이 준비된 서버소켓),
addr자리에는 클라이언트의 정보를 기록할 클라이언트 주소체계 변수의 포인터 &clnt_addr을 넣어준다.
(단 struct sockaddr* 형으로 형 변환을 한다)
addrlen에는 위 66줄에서 기억해놓은 주소체계의 크기, clnt_addr_size가 들어간다._M#]
[#M_69~70줄. 여기는 간단히 에러처리 부분이다.|69~70줄. 여기는 간단히 에러처리 부분이다.|accept함수가 반환한 clnt_sock가 -1이라는 것은 연결에 실패했다는 의미이다. _M#]
[#M_71. 여기서는 write 함수를 이용, message를 전송한다.|71. 여기서는 write 함수를 이용, message를 전송한다.|[code cpp]ssize_t write(int fildes, const void * buf, size_t nbytes);[/code]
fildes는 전송의 목적지가 되는 파일 디스크립터.
buf는 전송할 버퍼의 포인터. nbytes는 전송할 데이터의 크기이다.
여기서 ssize_t나 size_t는 primitive자료 형이라고 하여 sys/types.h(BSD기반에서)에 정의되어 있다.
이들은 typedef로 재정의된 자료형인데 이를 쓰는 이유는 네트워크 프로그래밍 같이 항상 일정한 크기로 시스템 종속적이지 않은 자료형이 필요할 때 사용된다.
예를 들어 32비트 시스템에서는 int가 32비트이지만 int가 16비트이던 시절도 있었다. 이런 경우 항상 4byte인 자료형을 쓰기 위해서는 int가 아닌 size_t로 선언을 한 뒤 typedef로 각 시스템에 맞는 4byte자료형을 붙여주면된다.
Data type(타입 명) |
Description(설명) |
ssize_t |
signed 32bit integer |
size_t |
unsigned 32bit integer |
_M#]
[#M_72줄. 클라이언트 소켓을 닫는다|72줄. 클라이언트 소켓을 닫는다|[code cpp]int close(int fildes);[/code]
이 함수를 이용하여 닫고자 하는 소켓의 파일 디스크립터(clnt_sock)를 닫을 수 있다._M#]