Simple Daytime Program (Client-Side)
간단한 시각알림 프로그램 (클라이언트 측)
- 클라이언트가 서버와 TCP로 연결되어, 서버에게 시각과 날짜를 요청하는 프로그램이다. (Client-Side)
* TCP/IP Connection Function Process
+ OS Kernel을 구성하는 코드의 대부분은 TCP 기반의 네트워크를 운용하는 부분에 대한 코드이다.
Source Code for Client_Side (C Language)
// Header Files ---------------------------------------------
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <strings.h>
#include <string.h>
#include <arpa/inet.h>
#include <inttypes.h>
// ----------------------------------------------------------
#define MAXLINE 4096
int main(int argc, char **argv)
{
int sockfd;
int n;
char recvline[MAXLINE + 1];
struct sockaddr_in servaddr;
if (argc != 2)
perror("usage: a.out <IPaddress>");
if ( (sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
perror("socket error");
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(13);
if (inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0)
perror(strcat("inet_pton error for", argv[1]));
if (connect(sockfd, (struct sockaddr *) &servaddr, sizeof(servaddr)) < 0)
perror("connect error");
while ( (n = read(sockfd, recvline, MAXLINE)) > 0) {
recvline[n] = 0;
if (fputs(recvline, stdout) == EOF)
perror("fputs error");
}
if (n < 0)
perror("read error");
exit(0);
}
※ IPv4와 IPv6를 모두 지원하는 Host를 "IPv4/IPv6 Host" 또는 "Dual-Stack Host"라 부른다.
Execution Example (실행 예시)
[anonymous@localhost NP]$ gcc daytime.c
[anonymous@localhost NP]$ ls
a.out daytime.c
[anonymous@localhost NP]$ a.out 129.6.15.28
59107 20-09-15 07:41:25 50 0 0 416.0 UTC(NIST) *
* NIST ITS(Internet Time Service) (URL)
- 미국 국립표준기술연구소(NIST)에서 제공하는 인터넷 시간 서비스를 이용할 때 필요한 IP주소를 제공하는 사이트이다.
Code Description (코드 설명)
Header Files
#include <stdio.h>
// fputs() Function
// perror() Function
#include <stdlib.h>
// exit() Function
#include <unistd.h>
// read() Function
#include <sys/types.h>
#include <sys/socket.h>
// socket() Function
// connect() Function
#include <netinet/in.h>
// sockaddr_in Structure
#include <strings.h>
// bzero() Function
#include <string.h>
// strcat() Function
#include <arpa/inet.h>
// inet_pton() Function
// htons() Function
#include <inttypes.h>
// htons() Function
Constants
#define MAXLINE 4096
// read()로 읽어올 데이터 크기의 최댓값
\(\texttt{main()}\) Function
int main(int argc, char **argv)
{
....
}
- Command-Line Arguments와 함께 \(\texttt{main()}\) 함수를 정의하는 부분이다.
\(\texttt{int argc}\)
- main() 함수에 전달되는 매개변수의 개수를 전달한다.
- 해당 프로그램의 실행경로 또한 \(\texttt{argv}\)에 포함되므로, \(\texttt{argc}\)는 항상 1 이상의 값을 갖는다.
\(\texttt{char **argv}\) (또는, \(\texttt{char *argv[]}\))
- \(\texttt{main()}\) 함수에 전달되는 실질적인 데이터를 전달한다.
- 첫 번째 String(\(\texttt{argv[0]}\))은 프로그램의 실행경로로 고정되어 있다.
Example. \(\texttt{test.c}\) 파일의 실행파일 \(\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 sockfd;
- \(\texttt{sockfd}\) : Socket File Descriptor
* Descriptor (= Handle)
- File Object에 대응되는 정숫값이다.
- 보통, File Object의 이름이 길이가 길기 때문에, OS Kernel은 그 긴 이름을 모두 명시하지 않고,
이름을 간단한 정숫값에 대응시켜 User Process에게 전달한다.
int n;
- \(\texttt{read()}\) System Call을 통해, 서버로부터 읽어들인 String의 크기를 저장할 \(\texttt{int}\)형 변수이다.
char recvline[MAXLINE + 1];
- \(\texttt{read()}\) System Call을 통해, 서버로부터 읽어들인 내용(String)을 저장할 \(\texttt{char}\)형 Array이다.
struct sockaddr_in servaddr;
// (IPv6의 경우, sockaddr_in -> sockaddr_in6)
- 인터넷상에서 사용할 소켓 주소를 저장할 구조체 변수이다.
- 주소를 구성하는 값으로는 IP 주솟값, 포트번호값, 본 구조체의 크기, 주소군 구분값 등이 있다.
* \(\texttt{sockaddr_in}\) : Socket Address of Internet Structure (URL)
- 인터넷에서 사용할 소켓 주소를 의미한다.
- \(\texttt{<netinet/in.h>}\) 헤더파일에 정의되어 있다.
Program Input Check
if (argc != 2)
Err_exit("usage: a.out <IPaddress>");
- 프로그램 Interface에 맞지 않는 입력이 들어오는 경우를 처리하는 부분이다.
- 파일이름과 IP주소를 함께 입력해야 함을 알린다. (Usage(사용법)을 출력한다.)
\(\texttt{socket()}\) Function Call (\(\texttt{socket()}\) 함수 호출)
if ( (sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) // IPv6의 경우, AF_INET -> AF_INET6
perror("socket error");
- OS Kernel이 Socket Object를 만들고, 그에 해당되는 정수 번호를 리턴하여 유저 프로그램의 \(\texttt{sockfd}\) 변수에 할당한다.
- 뒤이은 \(\texttt{connect}\)와 \(\texttt{read}\) 호출에서 소켓을 지정하는데 Descriptor를 사용할 수 있다.
(즉, 소켓 정보를 굳이 긴 이름으로 표현하지 않고, 간단한 정숫값으로 표현할 수 있다.)
- \(\texttt{AF_INET}\)는 Internet 주소 체계를 의미하는 상수이고,
\(\texttt{SOCK_STREAM}\)은 Stream을 의미하는 상수이다.
(Stream Delivery Service를 보장하는 프로토콜은 TCP이다. 즉, TCP 소켓으로 설정함을 의미한다.)
* \(\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;
// IPv6의 경우, sin_family = AF_INET -> sin6_family = AF_INET6;
- \(\texttt{servaddr}\)의 Address Family(주소 군)를 \(\texttt{AF_INET}\)으로 설정하는 부분이다.
- AF_INET은 IPv4 인터넷 프로토콜을 의미하는 상수이다.
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으로만 통용된다.)
- 일반적으로, short 타입은 포트번호 변환에, long 타입은 IP주소 변환에 사용한다.
- 즉, 포트 번호를 13으로 설정하는 부분이다.
* Port Number: 13
- Daytime 서비스를 지원하는 TCP/IP 호스트를 위한 서버의 Well-Known Port Number이다.
* \(\texttt{htons()}\) Function and Other Byte Ordering Functions (URL)
IP Address Conversion (IP주소 변환)
if (inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0)
perror(strcat("inet_pton error for", argv[1]));
// IPv6의 경우, AF_INET, sin._addr -> AF_INET6, sin6_addr
- \(\texttt{inet_pton}\) : Internet Presentation to Numeric
- \(\texttt{argv[1]}\) 문자열(사용자가 입력한 IP주솟값)을 \(\texttt{AF_INET}\) 주소군에 해당되는 네트워크 주소로 변환하여
\(\texttt{servaddr.sin_addr}\)에 저장한다.
- 즉, 사람이 식별할 수 있는 IP주소를 기계가 식별할 수 있는 형태로 변환한다.
* IPv4 and IPv6 Conversion Functions (URL)
\(\texttt{connect()}\) Function Call (\(\texttt{connect()}\) 함수 호출)
if (connect(sockfd, (struct sockaddr *) &servaddr, sizeof(servaddr)) < 0)
perror("connect error");
- \(\texttt{servaddr}\)에 저장된 IP주소와 포트번호에 \(\texttt{sockfd}\) 소켓을 연결한다.
- 이 경우, \(\texttt{sockfd}\)는 TCP 소켓 Descriptor이므로, 서버와 TCP로 연결된다.
- Argument들을 토대로, Kernel은 요청 메시지를 TCP Packet으로 만들어 전송하고 현재 프로그램을 Block시킨다.
- \(\texttt{connect()}\) 요청에 대한 응답을 서버로부터 받을 때까지,
본 프로세스(Daytime Client)가 CPU 점유를 잠시 포기하고(해당 프로그램을 Block 시키고),
다른 프로세스가 CPU를 사용할 수 있도록 Context Switch을 수행한다.
(멀티태스킹의 구현 방법이다.)
- 요청한 데이터가 서버로부터 도착하면, Kernel은 해당 프로세스에게 다시 CPU를 점유시켜서 Wake-Up시킨다.
(Block 상태를 해제한다.)
* OS가 Process Scheduling하는 시점
- H/W Interrupt가 발생했을 때
- S/W Trap이 발생했을 때
- System Call을 호출했을 때
(User Process가 스스로 CPU 점유를 포기하는 경우도, \(\texttt{pause()}\) System Call을 호출하여 구현한 것이다.)
* \(\texttt{connect()}\) Function (URL)
Reading Data from Server (서버로부터 데이터 읽기)
while ( (n = read(sockfd, recvline, MAXLINE)) > 0) {
recvline[n] = 0; // null terminate (for string)
if (fputs(recvline, stdout) == EOF)
perror("fputs error");
}
if (n < 0)
perror("read error");
- 서버는 소켓을 통해 \(\texttt{write()}\) 하고, 클라이언트는 소켓을 통해 데이터를 \(\texttt{read()}\) 하는 구조이다.
\(\texttt{recvline[n] = 0;}\)
- \(\texttt{fputs()}\)은 string을 출력하는 함수이기 때문에, string 형태로 만들기 위해 0(NULL)으로 마무리해 놓아야 한다.
※ 서버로부터 읽어올 데이터의 크기가 크면, 한 번의 \(\texttt{read()}\) 호출로 부족할 수 있기 때문에,
\(\texttt{read()}\)를 Loop내에 위치시키고, \(\texttt{read()}\)의 리턴값이 0이하이면 Loop를 종료시키는 것이 바람직하다.
- 서버로부터 읽어들인 결과를 표준 I/O \(\texttt{fputs()}\) 함수를 사용하여 콘솔에 출력한다.
- 비정상적인 출력이 감지된 경우, \(\texttt{perror()}\)를 통해 에러 메세지를 출력한다.
- \(\texttt{read()}\)가 음수를 리턴할 경우, \(\texttt{perror()}\)를 통해 에러 메시지를 출력한다.
* \(\texttt{read()}\) Function (URL)
Exit Program (프로그램 종료)
exit(0);
- 프로그램을 종료한다.
Reference: UNIX Network Programming Volume 1, 3E: The Sockets Networking API (W. Richard Stevens, Bill Fenner, Andrew M. Rudoff 저, Addison-Wesley, 2004)