본문 바로가기

Embedded Programming/Linux System Programming

리눅스 기반 서버프로그램


#include <stdio.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

int main()
{
  int servSock;
  int clntSock;
  int iRet;
  struct sockaddr_in echoServAddr;
  struct sockaddr_in echoClntAddr;
  unsigned short echoServPort;  // server app port address
  unsigned int clntLen;
  unsigned char ucBuffer[500];

  echoServPort = 9999;  // 서버의 포트주소는 대부분 고정시킴

  // 1. 소켓 생성 
  servSock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
  if (servSock < 0)
  {
    printf("socket creation failure\n");
    return 1;
  }
        
  // 2. 소켓 구조체 설정 
  memset(&echoServAddr, 0sizeof(echoServAddr));
  echoServAddr.sin_family = AF_INET;
  echoServAddr.sin_addr.s_addr = htonl(INADDR_ANY);  // Big Endian으로 변환 
  echoServAddr.sin_port = htons(echoServPort);    // 9999 넣어줘도 됨 
  
  // 3. Bind : 소켓과 sockaddr_in 구조체의 정보를 한데 묶어 준다
  iRet = bind(servSock, (struct sockaddr *) &echoServAddr, sizeof(echoServAddr));
  if (iRet < 0)
  {
    fprintf(stderr, "Binding Error!\n");
    close(servSock);
    return -1;
  }
  
  // 4. Listen : 소켓 받을 준비 함수 
  iRet = listen(servSock, 5);
  if (iRet < 0)
  {
    fprintf(stderr, "listen() failed!\n");
    close(servSock);
    return -1;
  }
  
   clntLen = sizeof(echoClntAddr);

  // 5. accept : client 소켓 생성 함수
  // servSock : 랑데뷰 소켓 (최초 접속을 받는 소켓)
  // clntSock : communication socket (실제로 통신하는 소켓)
  clntSock = accept(servSock, (struct sockaddr *) &echoClntAddr, &clntLen);
  if (clntSock < 0)
  {
    fprintf(stderr, "accept() failed!\n");
    close(servSock);
    return -1;
  }

  // 손님(client) 정보 출력. 포트번호는 안쓰는 번호 랜덤으로 들어옴.
  printf("Handling client IP   : %s\n", inet_ntoa(echoClntAddr.sin_addr));
  // host 에 맞는 endian으로 바꿔서 출력. 
  printf("Handling client PORT : %d\n", ntohs(echoClntAddr.sin_port));
  

  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;
  }

  close(servSock);
  close(clntSock);

  return 0;
}

서버 프로그램의 전체적인 실행 순서는 다음과 같다.

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, 0sizeof(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 sockaddrmyaddr, 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 monitor

    write(clntSock, ucBuffer, iRet);  // echo to the client 

여기서 read()와 write()는 저수준 파일 입출력 함수이지만, 

두 함수의 첫번째 인수인 int fd는 리눅스에서 키보드, 모니터를 비롯한 파일과 소켓을 모두 파일로 간주하므로

통신에서도 사용할 수 있다.