Simple Daytime Program (Server-Side)
간단한 시각알림 프로그램 (서버 측)
- 서버는 클라이언트로부터 시각과 날짜를 요청받아 응답을 해주는 프로그램이다.
* TCP/IP Connection Function Process
+ OS Kernel을 구성하는 코드의 대부분은 TCP 기반의 네트워크를 운용하는 부분에 대한 코드이다.
Source Code for Server Side (C Language)
// Header Files ---------------------------------------------
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <strings.h>
#include <netinet/in.h>
#include <time.h>
#include <unistd.h>
#include <string.h>
// ----------------------------------------------------------
#define MAXLINE 4096
#define LISTENQ 1024
int main(int argc, char **argv)
{
int listenfd;
int connfd;
socklen_t len;
struct sockaddr_in servaddr;
struct sockaddr_in cliaddr;
char buff[MAXLINE];
time_t ticks;
if ( (listenfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
perror("socket error");
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(40000);
if (bind(listenfd, (struct sockaddr *) &servaddr, sizeof(servaddr)) < 0)
perror("bind error");
if (listen(listenfd, LISTENQ) < 0)
perror("listen error");
for ( ; ; ) {
if ( (connfd = accept(listenfd, (struct sockaddr *) &cliaddr, &len)) < 0)
perror("accept error");
printf("connection from %s, port %d\n", inet_pton(AF_INET, &cliaddr.sin_addr, buff, sizeof(buff)), ntohs(cliaddr.sin_port));
ticks = time(NULL);
snprintf(buff, sizeof(buff), "%.24s\r\n", ctime(&ticks));
if (write(connfd, buff, strlen(buff)) != strlen(buff))
perror("write error");
if (close(connfd) == -1)
perror("close error");
}
}
※ IPv4와 IPv6를 모두 지원하는 Host를 "IPv4/IPv6 Host" 또는 "Dual-Stack Host"라 부른다.
Code Description (코드 설명)
Header Files
#include <stdio.h>
// printf() Function
// snprintf() Function
// perror() Function
#include <stdlib.h>
// exit() Function
#include <sys/types.h>
#include <sys/socket.h>
// socket() Function
// accept() Function
// bind() Function
#include <strings.h>
// bzero() Function
#include <netinet/in.h>
// sockaddr_in Structure
#include <time.h>
// time_t Type
#include <unistd.h>
// write() Function
// close() Function
#include <string.h>
// strlen() Function
Constants
#define MAXLINE 4096
// read()로 읽어올 데이터 크기의 최댓값
#define LISTENQ 1024
// Kernel이 Listening Descriptor에 할당할 수 있는 클라이언트 수의 최댓값
\(\texttt{main()}\) Function
int main(int argc, char **argv)
{
...
}
- Command-Line Arguments와 함께 \(\texttt{main()}\) 함수를 정의하는 부분이다.
\(\texttt{int argc}\)
- main() 함수에 전달되는 매개변수의 개수를 전달한다.
\(\texttt{char **argv}\) (또는, \(\texttt{char *argv[]}\))
- \(\texttt{main()}\) 함수에 전달되는 실질적인 데이터를 전달한다.
- 첫 번째 String(\(\texttt{argv[0]}\))은 프로그램의 실행경로로 고정되어 있다.
Example. \(\texttt{test.c}\) 파일의 Execution파일 \(\texttt{a.out}\)을 아래와 같이 실행할 경우
solaris % a.out A 123 x
\(\texttt{A}\)는 \(\texttt{argv[1]}\)에 저장되고,
\(\texttt{123}\)은 \(\texttt{argv[2]}\)에 저장되며,
\(\texttt{x}\)는 \(\texttt{argv[3]}\)에 저장된다.
Declarations
int listenfd;
- \(\texttt{socket()}\)이 리턴하는 Descriptor를 저장할 변수이다.
- 즉, Listen Socket의 Descriptor이다.
int connfd;
- \(\texttt{accept()}\)가 리턴하는 Descriptor를 저장할 변수이다.
- 즉, Connected Socket의 Descriptor이다.
* Descriptor (= Handle)
- File Object에 대응되는 정숫값이다.
- 보통, File Object의 이름이 길이가 길기 때문에, OS Kernel은 그 긴 이름을 모두 명시하지 않고,
이름을 간단한 정숫값에 대응시켜 User Process에게 전달한다.
socklen_t len;
- \(\texttt{accept()}\) 함수를 통해 얻어온, 연결된 클라이언트의 소켓 주소 구조체의 크기를 저장할 변수이다.
struct sockaddr_in servaddr;
- \(\texttt{sockaddr_in}\) : Socket Address of Internet
- 즉, 인터넷에서 사용할 소켓 주소를 의미한다.
- \(\texttt{<netinet/in.h>}\) 헤더파일에 정의되어 있다.
* \(\texttt{sockaddr_in}\) Structure (URL)
struct sockaddr_in cliaddr;
- \(\texttt{accept()}\) 함수를 통해 얻어온, 연결된 클라이언트의 주솟값 정보를 저장할 구조체 변수이다.
- 서버와 연결된 클라이언트의 IP주소와 포트번호를 서버의 Console에 출력하기 위한 구조체 변수이다.
char buff[MAXLINE];
- 클라이언트의 IP 주솟값, 클라이언트에게 알려줄 시간 정보값들을 저장할 Array이다.
time_t ticks;
- 서버가 클라이언트에게 알려줄 시간값을 저장할 변수이다.
* \(\texttt{time_t}\)
- \(\texttt{<time.h>}\) 헤더파일에 정의되어 있는 Alias이다. (일반적으로, 32bits)
\(\texttt{socket()}\)Function Call(\(\texttt{socket()}\)함수 호출)
if ( (listenfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) // IPv6의 경우, AF_INET6
perror("socket error");
- TCP 프로토콜(\(\texttt{SOCK_STREAM}\))과 IPv4 인터넷 프로토콜(\(\texttt{AF_INET}\)) 기반의 소켓을 할당받고,
할당받은 소켓을 지칭하는 Descriptor를 \(\texttt{listenfd}\)에 할당받는다.
* \(\texttt{socket()}\) Function (URL)
* TCP 프로토콜 (URL)
\(\texttt{perror("socket error");}\)
- \(\texttt{socket()}\) 함수가 음수를 리턴하여, 호출에 실패했음을 알리면,
오류 메세지를 출력하고, 프로그램을 종료한다.
* \(\texttt{perror()}\) Function (URL)
\(\texttt{servaddr}\) Structure Initializing (\(\texttt{servaddr}\) 구조체 초기화)
bzero(&servaddr, sizeof(servaddr));
- \(\texttt{servaddr}\) 구조체 변수의 모든 Attribute에 0을 채워넣는 부분이다.
- 있을지도 모르는 쓰레기값을 초기화하기 위한 작업이다.
- ANSI C에 정의된 함수인 \(\texttt{memset()}\) 함수 대신, \(\texttt{bzero()}\) 함수를 사용하는 이유는,
\(\texttt{bzero()}\)가 \(\texttt{memset()}\)보다 요구하는 매개변수의 개수가 적어 사용하기에 용이하기 때문이다.
* \(\texttt{bzero()}\) Function (URL)
* \(\texttt{memset()}\) Function (URL)
Address Family Setting (주소군 설정)
servaddr.sin_family = AF_INET;
- \(\texttt{servaddr}\)의 Address Family(주소 군)를 \(\texttt{AF_INET}\)으로 설정하는 부분이다.
- AF_INET은 IPv4 인터넷 프로토콜을 의미하는 상수이다.
IP Address Setting (IP 주소 설정)
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
- 어떤 인터페이스에서도, 클라이언트가 서버에 접근할 수 있도록 IP주소를 \(\texttt{INADDR_ANY}\)로 설정한다.
(처음 연결이 설정될 때, 서버는 클라이언트의 IP 주소를 모르기 때문에 완전 개방하는 것이다.)
- \(\texttt{htons()}\) 함수와 마찬가지로, Host Byte-Order를 Network Byte-Order로 변환한다.
(\(\texttt{unit32_t}\) Type은 주로 IP주소를 변환할 때 사용한다.)
* \(\texttt{htonl()}\) Function and Other Byte Ordering Functions (URL)
Port Number Setting (포트번호 설정)
servaddr.sin_port = htons(13);
- \(\texttt{htons}\)는 Host to Network Short의 약어이다.
- \(\texttt{htons()}\) 함수는 16 bits의 Host Byte-Order를 Network Byte-Order로 변환한다.
(네트워크에서의 Byte-Order는 무조건 Big Endian으로만 통용된다.)
- 즉, 포트 번호를 13으로 설정하는 부분이다.
* Port Number: 13
- Daytime 서비스를 지원하는 TCP/IP 호스트를 위한 서버의 Well-Known Port Number이다.
* \(\texttt{htons()}\) Function and Other Byte Ordering Functions (URL)
Socket Binding (소켓 바인딩)
if (bind(listenfd, (struct sockaddr *) &servaddr, sizeof(servaddr)) < 0)
perror("bind error");
- \(\texttt{servaddr}\)에 저장된 IP주소와 포트번호를 Listen Socket(\(\texttt{listenfd}\))에 바인딩한다.
- Client-Side 프로그램에서는 이 과정을 Kernel이 수행하기 때문에,
Server_Side 프로그램에서만 bind()를 수행한다.
* \(\texttt{bind()}\) Function (URL)
Data Listening (데이터 수신대기)
if (listen(listenfd, LISTENQ) < 0)
perror("listen error");
- \(\texttt{listen()}\) 함수는 서버가 \(\texttt{listenfd}\) 소켓을 통해 Client의 접속 요청을 기다리도록 설정한다.
(즉, \(\texttt{listenfd}\) 소켓을 듣는 소켓으로 전환한다.)
- \(\texttt{LISTENQ}\) : Kernel이 Listening Descriptor에 할당가능한 최대 클라이언트 수이다.
※ TCP Server가 Listening Descriptor를 준비하는 Processs는 아래와 같다. (TCP Passive Open Process)
- \(\texttt{socket()} \to \texttt{bind()} \to \texttt{listen()}\)
* \(\texttt{listen()}\) Function (URL)
Loop to Process Client (클라이언트 처리 루프)
for ( ; ; ) {
if ( (connfd = accept(listenfd, (struct sockaddr *) &cliaddr, &len)) < 0)
perror("accept error");
printf("connection from %s, port %d\n", inet_pton(AF_INET, &cliaddr.sin_addr, buff, sizeof(buff)), ntohs(cliaddr.sin_port));
ticks = time(NULL);
snprintf(buff, sizeof(buff), "%.24s\r\n", ctime(&ticks));
if (write(connfd, buff, strlen(buff)) != strlen(buff))
perror("write error");
if (close(connfd) == -1)
perror("close error");
// connfd와의 연결을 해제한다.
// TCP 3-Way Handshaking에서 FIN signal을 클라이언트에게 보내는 과정이다.
}
- 본 Daytime 프로그램은, accept()를 통해 서버가 클라이언트에 연결되자마자
서버의 Console에는 연결된 클라이언트의 주소 정보를 출력하고,
클라이언트의 Console에는 시간값을 출력시키는 구조이다.
(클라이언트의 별다른 시간 요청 과정이 필요없다. 서버와 연결되면 그 즉시 시간값을 전달받는다.)
* \(\texttt{accept()}\) Function (URL)
- \(\texttt{listen()}\)으로 접속을 요청한 소켓과 연결한다.
- \(\texttt{accept()}\)는 TCP 3-Way Handshaking이 정상적으로 종료되어야 해당 소켓의 File Descriptor를 리턴한다.
* \(\texttt{snprintf()}\) Function (URL)
- \(\texttt{buff}\)에 현재 시간값을 형식("%.24s\r\n")에 맞춰서 저장한다.
- \(\texttt{snprintf()}\)는 \(\texttt{sizeof(buff)}\) 인자를 통해 상대 호스트의 Buffer Overflow를 방지할 수 있다.
* \(\texttt{char *ctime(const time_t *clock)}\) Function
#include <time.h>
char *ctime(const time_t *clock);
- 1970년 1월 1일 0시 (UTC; Coordinated Universal Time) 부터 해당 시점까지의 시간값을
초 단위로 환산한 Integer 값(\(\texttt{clock}\) Argument)을 읽기 쉬운 시간 값을 나타내는 문자열로 리턴한다.
time() : 1600493274 // time_t Type
ctime() : Sat Sep 19 14:27:54 2020 // string Type
* \(\texttt{ssize_t write(int fd, const void *buf, size_t count)}\) Function (URL)
- \(\texttt{buf}\)로 부터 \(\texttt{count}\) 만큼의 Bytes를 \(\texttt{fd}\)에 출력한다.
- 본 Daytime 프로그램은 서버는 소켓을 통해 \(\texttt{write()}\) 하고,
클라이언트는 소켓을 통해 데이터를 \(\texttt{read()}\) 하는 구조이다.
* \(\texttt{int close(int fd)}\) Function (URL)
- 본 Daytime 프로그램은 연결된 클라이언트에게 시간을 한 번 알려주고, 연결을 끊는다.
(시간값을 전송한 후, TCP 3-Way Handshaking에서의 FIN Signal을 클라이언트에게 보낸다.)
- 연결을 끊고, 다음 Loop를 진행하여 다른 클라이언트의 요청을 받을 준비를 한다.
* Iterative Server - Concurrent Server
- 본 프로그램은 클라이언트의 요청을 병렬이 아닌, 순차적으로 처리하는, Iterative Server의 형태를 취하고 있다.
- Iterative Server의 반댓말은 Concurrent Server이다.
- Concurrent Server의 구현 방법은 다양히 존재하며, 가장 간단한 방법으로는
UNIX \(\texttt{fork()}\) 함수를 이용하여 클라이언트마다 Child Process를 생성하는 방법,
Multi Threading을 이용하는 방법,
서버 시작 시 정해진 수만큼의 Child Process를 미리 생성해 놓는 방법 등이 있다.
Reference: UNIX Network Programming Volume 1, 3E: The Sockets Networking API (W. Richard Stevens, Bill Fenner, Andrew M. Rudoff 저, Addison-Wesley, 2004)