리눅스 기반 서버프로그램
#include <stdio.h> |
서버 프로그램의 전체적인 실행 순서는 다음과 같다.
1. 소켓 디스크립터(또는 핸들) 생성 : int socket(int protocolFamily, int type, int protocol);
socket() 함수는 소켓의 번호(디스크립터)를 반환해준다. 이는 read() write()함수의 파일 핸들과 같은 형식을 가지므로, 소켓의 핸들을 파일 핸들자리에 사용할 수 있다.
int protocolFamily - 사용할 프로토콜의 계열을 명시한다.
AF_INET, PF_INET : IPv4 형식 프로토콜 사용
PF_INET6 : IPv6 형식 프로토콜 사용
int type - 소켓의 형식을 지정한다.
SOCK_STREAM : TCP 소켓 사용
SOCK_DGRAM : UDP 소켓 사용
int protocol - end-to-end protocol을 나타낸다. 앞의 int type 파라미터와 연관되어 있으므로, 0으로
지정해도 상관없다.
IPPROTO_TCP : TCP 소켓
IPPROTO_UDP : UDP 소켓
2. 소켓 구조체 설정 : sockaddr_in 구조체 멤버에 초기값 설정
* memset(&echoServAddr, 0, sizeof(echoServAddr));
memset() 은 해당하는 변수가 할당된 메모리를 (&echoServAddr) 지정한 바이트 크기 만큼
(sizeof(echoServAddr)) 해당하는 값(0)으로 초기화 해주는 함수이다.
* sockaddr_in 구조체의 원형 - sockaddr_in 구조체는 sockaddr 구조체를 TCP/IP 프로토콜에 사용하는 구조로 변형시킨 것이다. 두 구조체는 같은 크기를 가지므로(16 byte) 캐스팅하여 사용할 수 있다.
struct sockaddr_in
{
unsigned short sin_family;
unsigned short sin_port;
struct in_addr sin_addr;
char sin_zero[8];
};
* sin_family : 프로토콜을 지정하는 구조체 멤버 변수다. sockaddr_in 구조체를 사용하는 경우 반드시 AF_INET을 사용해야 한다.
* sin_port : 포트번호를 지정하는 변수다. 2bytes의 크기를 가지는 short형이므로 0~65535까지의 포트번호를 지정할 수 있다.
echoServAddr.sin_port = htons(echoServPort);
htons() 함수는 Host TO Network Short, host 컴퓨터의 데이터 저장방식에서 네트워크 표준인 Big Endian
형식의 short 형 변수로 바꿔주는 함수이다.
* sin_addr : IP 주소를 저장하는 변수다.
echoServAddr.sin_addr.s_addr = htonl(INADDR_ANY);
hotonl() 함수 역시 네트워크 표준인 Big Endian 형식으로 데이터의 저장순서를 바꿔주는 함수이다.
s_addr은 아래 in_addr 구조체의 멤버로 long 형식의 데이터이므로 htonl()함수로 바꿔준다.
(Host To Network Long)
INADDR_ANY는 0으로 define 되어 있어 현재 컴퓨터의 IP주소를 big endian 형식의 long형 데이터로
변환해준다.
* sin_zero : 사용하지 않는 부분이다. sockaddr 구조체와 크기를 맞추기 위해 선언된 변수.
sockaddr_in 구조체의 멤버 변수 struct in_addr sin_addr 는 다음과 같이 정의되어 있다.
struct in_addr
{
unsigned long s_addr
};
sockaddr_in 구조체는 bind()나 accept() 함수의 인수로 대입될 때, sockaddr 구조체로 캐스팅
(struct sockaddr *) 되는데, sockaddr 구조체의 원형은 다음과 같다.
struct sockaddr
{
unsigned short sa_family;
char sa_data[14];
};
3. 소켓에 주소 할당 : int bind(int sockfd, struct sockaddr * myaddr, int addrlen);
bind()는 socket에 앞서 초기화 시켜준 sockaddr_in 구조체의 주소를 할당하는 함수이다.
bind()의 리턴값은 소켓할당에 실패했을 시 -1을 반환한다.
iRet = bind(servSock, (struct sockaddr *) &echoServAddr, sizeof(echoServAddr));
위의 코드에서는 서버 소켓 sockaddr 형태로 캐스팅한 sockaddr_in 구조체의 주소, sockaddr_in의 길이를
인수로 전달한다.
4. 서버 소켓에게 접속을 대기시키는 함수 : int listen(int socket, int queueLimit)
listen()은 첫 번째 인수의 소켓이 클라이언트의 연결 요청을 대기하도록 하고, 두 번째 인수는 연결 요청을
받아들일 수 있는 최대 개수이다.
listen()으로 서버 소켓을 등록하면 연결 요청 대기 큐가 생성되어 서버 소켓은 접속요청을 대기하는 역할을
한다. 소켓 대기 큐 생성에 실패했을 경우 -1을 반환한다.
클라이언트 측에서 접속을 요청할 경우 먼저 listen()에 등록된 서버 소켓(servSock)이 접속을 받아들이고,
이후의 accept()에서 클라이언트 소켓(clntSock)으로 클라이언트 측과 데이터를 송수신한다.
이 때 서버소켓은 랑데뷰 소켓이라 하여 최초 접속만 처리하고, 실제로 통신하는 클라이언트 소켓은 커뮤니케이션
소켓이라 한다.
5. 클라이언트 소켓의 연결 요청을 수락하는 함수 :
int accept(int socket, struct sockaddr *clientAddress, unsigend int *addressLength);
accept()의 반환값은 int형으로 소켓의 번호를 받아온다.(= file descriptor) 여기서 반환되는 소켓번호는
listen()의 서버 소켓(servSock)을 통과한 클라이언트 측의 소켓 번호이다.
accept()에 넘겨주는 첫번째 인수인 int socket에는 서버소켓(servSock)을 넘겨주고, 나머지 두 인수는
클라이언트 소켓의 ip주소가 담긴 sockaddr 구조체(&echoClntAddr)와 해당 구조체의 크기(&clntLen)이다.
clntLen은 listen()에서 연결 요청이 들어 온 후
clntLen = sizeof(echoClntAddr); 로 클라이언트 소켓어드레스 구조체의 크기를 미리 받아둔다.
6. 소켓을 이용하여 클라이언트 측과 통신
이제 클라이언트 측과 연결 되었으므로 소켓(clntSock)을 이용하여 클라이언트 측에서 정보를 받아올 수 있다.
while(1)
{
// clntSock으로 데이터를 읽어옴 : client가 write할 때까지 대기
iRet = read(clntSock, ucBuffer, 499);
ucBuffer[iRet] = 0; // insert Null character on the last position of the array
write(1, ucBuffer, iRet); // print on monitor
write(clntSock, ucBuffer, iRet); // echo to the client
if ('q' == *(ucBuffer+0))
break;
}
read()로 버퍼에 소켓의 데이터를 읽어 온 후,
iRet = read(clntSock, ucBuffer, 499);
화면과 클라이언트 측에 해당 데이터를 다시 재전송해준다.
write(1, ucBuffer, iRet); // print on monitorwrite(clntSock, ucBuffer, iRet); // echo to the client
여기서 read()와 write()는 저수준 파일 입출력 함수이지만,
두 함수의 첫번째 인수인 int fd는 리눅스에서 키보드, 모니터를 비롯한 파일과 소켓을 모두 파일로 간주하므로
통신에서도 사용할 수 있다.