본문 바로가기
기타/아카이브

[Network exercise] 2. TCP 소켓을 이용하여 HTTP 서버 만들기

by Riverandeye 2020. 5. 27.

TCP 소켓을 이용하여 서버의 현재 시간을 알려주는 HTTP 서버를 만들어보자. 

우선 만든 결과 프로그램은 다음과 같다. 

 

#if defined(_WIN32) // windows
#ifndef _WIN32_WINNT
#define _WIN32_WINNT 0x0600
#endif

#include <winsock2.h>
#include <ws2tcpip.h>
#pragma comment(lib, "ws2_32.lib")

#else // linux
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <unistd.h>
#include <errno.h>
#endif

#if defined(_WIN32)
#define ISVALIDSOCKET(s) ((s) != INVALID_SOCKET)
#define CLOSESOCKET(s) closesocket(s)
#define GETSOCKETERRNO() (WSAGetLastError())
#else
#define ISVALIDSOCKET(s) ((s) >= 0)
#define CLOSESOCKET(s) close(s)
#define SOCKET int
#define GETSOCKETERRNO() (errno)
#endif

#include <stdio.h>
#include <string.h>
#include <time.h>

#include <stdio.h>
int main() {
  #if defined(_WIN32)
    WSADATA d;
    if (WSAStartup(MAKEWORD(2, 2), &d)) {
      fprintf(stderr, "Failed to initialize.\n");
      return 1;
    }
  #endif

  printf("Configuring local address...\n");
  struct addrinfo hints;
  memset(&hints, 0, sizeof(hints));

  hints.ai_family = AF_INET; // use IPv4 addr
  hints.ai_socktype = SOCK_STREAM; // TCP
  hints.ai_flags = AI_PASSIVE; 

  struct addrinfo *bind_address;
  getaddrinfo(0, "8080", &hints, &bind_address); // bind_address에 주소 저장

  printf("Creating socket...\n");

  SOCKET socket_listen;
  socket_listen = socket(bind_address->ai_family, bind_address->ai_socktype, bind_address->ai_protocol);

  if (!ISVALIDSOCKET(socket_listen)) {
    fprintf(stderr, "socket() failed. (%d)\n", GETSOCKETERRNO());
    return 1;
  }

  printf("Binding socket to local address...\n");
  
  if (bind(socket_listen, bind_address->ai_addr, bind_address->ai_addrlen)) {
    fprintf(stderr, "bind() failed. (%d)\n", GETSOCKETERRNO());
    return 1;
  }

  freeaddrinfo(bind_address);

  printf("Listening...\n");
  if (listen(socket_listen, 10) < 0) {
    fprintf(stderr, "listen() failed. (%d)\n", GETSOCKETERRNO());
    return 1;
  }

  printf("Waiting for connection...\n");
  struct sockaddr_storage client_address;
  socklen_t client_len = sizeof(client_address);
  
  while(1){
    SOCKET socket_client = accept(socket_listen, (struct sockaddr*) &client_address, &client_len);
    
    if (!ISVALIDSOCKET(socket_client)) {
      fprintf(stderr, "accept() failed. (%d)\n", GETSOCKETERRNO());
      return 1;
    }

    printf("Client is connected... ");
    char address_buffer[100];

    getnameinfo((struct sockaddr*)&client_address, client_len, address_buffer, sizeof(address_buffer), 0, 0, NI_NUMERICHOST);
    printf("%s\n", address_buffer);

    printf("Reading request...\n");
    char request[1024];
    int bytes_received = recv(socket_client, request, 1024, 0);
    
    printf("Received %d bytes.\n", bytes_received);
    printf("%.*s", bytes_received, request);

    printf("Sending response...\n");
    const char *response =
      "HTTP/1.1 200 OK\r\n"
      "Connection: close\r\n"
      "Content-Type: text/plain\r\n\r\n"
      "Local time is: ";
    int bytes_sent = send(socket_client, response, strlen(response), 0);
    printf("Sent %d of %d bytes.\n", bytes_sent, (int)strlen(response));


    time_t timer;
    time(&timer);
    char *time_msg = ctime(&timer);
    bytes_sent = send(socket_client, time_msg, strlen(time_msg), 0);
    printf("Sent %d of %d bytes.\n", bytes_sent, (int)strlen(time_msg));

    printf("Closing connection...\n");
    CLOSESOCKET(socket_client);
  }

  printf("Closing listening socket...\n");
  CLOSESOCKET(socket_listen);
  
  #if defined(_WIN32)
    WSACleanup();
  #endif
  
  return 0;
}

 이 거대한 괴물을 어떻게 상대해야 할지.. 차근차근 살펴보자. 

 

0 ~ 29

헤더를 보면, 윈도우에서는 윈도우 헤더가 적용되게, 리눅스에서는 리눅스 헤더가 적용되게 설정하였다. 

 

37 ~ 43, 129 ~ 131

윈도우의 경우 메인 함수에서 WSAStartup과 WSACleanup을 통해 웹 소켓을 초기화해야 한다. 

 

45 ~ 64

getaddrinfo를 통해 호스트와 서비스에 대해 사용할 수 있는 주소와 포트 번호, 프로토콜 정보를 가져온다.

그 후 해당 주소정보를 이용하여 소켓을 생성하고, 소켓을 포트에 바인드한다.

 

SOCKET 타입은 윈도우에서나 가능한거고, unix에선 socket descriptor의 타입이 int로 지정되어 있다. 

윈도우가 아닐때 #define SOCKET int 가 지정되어있는 이유이다. 

 

ISVALIDSOCKET 은 매크로로 지정한 함수이다. listening socket이 valid하지 않으면 프로그램을 종료시킨다. 

 

68 ~ 73

소켓을 바인드 한 후, bind_address에 할당된 메모리를 free 해준다 (더이상 필요 없음). bind에 실패한 경우 bind 함수는 0이 아닌 값을 리턴하게 되어 if문에 걸린다.

 

75 ~ 83

소켓을 listen 상태로 변경하고, listen의 두번쨰 인자로 응답을 기다리는 connection의 갯수를 10개로 제한한다.

클라이언트가 많아지면, 들어오는 커넥션을 queue 하고, 응답 못받고 기다리는 커넥션이 10개 이상으로 넘어가면 reject 한다. 

 

86 ~ 93

클라이언트의 커넥션을 accept() 를 통해 wait 한다. 이때 요청이 들어올 때 까지 프로그램이 Block 되어있다. 

 

94 ~ 104

1024 사이즈의 버퍼로 데이터를 받는다. recv에서 입력이 오지 않으면 block 된다. 만약 client에서 connection이 제거되면 recv는 0이나 -1을 반환하기 때문에, production 레벨에서는 저걸 꼭 체크해주어야 한다. 

 

107 ~ 113

HTTP response를 전송한다. client_socket에 response 메세지를 전송한다.

 

116 ~ 123

ctime 메소드를 이용하여 현재 시간에 대한 time message를 생성하고, 전송한 후 클라이언트의 커넥션을 종료한다.

 

 

Reference

hands-on network programming with c

댓글