1. TCP/IP 프로토콜 스택을 4개의 계층으로 구분해 보자. 그리고 TCP 소켓이 거치는 계층구조와 UDP 소켓이 거치는 계층구조의 차이점을 설명해보자.
>TCP는 LINK 계층<->IP 계층<->TCP 계층<->Application 계층의 구조를 갖는다
UDP는 LINK계층<->IP 계층<->UDP 계층<->Application 계층의 구조를 갖는다
2. TCP/IP 프로토콜 스택 중에서 LINK 계층과 IP 계층이 담당하는 역할이 무엇인지 설명해보자. 그리고 이 둘의 관계도 함께 설명해보자.
>LINK 계층은 LAN, WAN, MAN 같은 네트워크 표준과 관련된 프로토콜을 정의하는 영역으로, 물리적인 성격의 표준을 정의하고 있는 계층으로 정리할 수 있다. 반면 IP 계층은 인터넷을 통한 데이터 전송의 표준을 정의하고 있는 계층이다.
즉, IP 계층은 LINK 계층을 기반으로 구성되는 물리적인 네트워크를 기반으로 하는 데이터 전송의 표준을 담당한다.
3. TCP/IP 프로토콜 스택을 4개의 계층(또는 7개의 계층)으로 나누는 이유는 무엇인가? 이를 개방형 시스템에 대한 설명과 함께 답해보자.
>복잡한 TCP/IP 프로토콜을 계층화하면 문제해결을 전문화할 수 있으며 계층화된 영역을 표준화해서 개방형 시스템으로 발전 시킬 수 있다. 실제로 TCP/IP는 개방형 시스템이기 때문에 각 계층별로 표준화 되어있고, 이 표준을 근거로 인터넷망이 구성되어 있다. 따라서 계층별 표준을 따르는 하드웨어 및 소프트웨어의 상호대체가 가능하며, 이러한 표준화는 TCP/IP가 발전할 수 있는 근거가 되었다.
4. 클라이언트는 connect 함수호출을 통해서 서버로의 연결을 요청한다. 그렇다면 클라이언트는 서버가 어떠한 함수를 호출한 이후부터 connect 함수를 호출할 수 있는가?
>서버가 서버소켓(리스닝 소켓)을 대상으로 listen 함수를 호출한 이후부터 연결요청이 가능하다.
5. 연결요청 대기 큐라는 것이 생성되는 순간은 언제이며, 이것이 어떠한 역할을 하는지 설명해보자. 그리고 accept 함수와의 관계도 함께 설명해보자
>연결요청 대기 큐가 생성되는 시점은 listen 함수가 호출되는 시점이다. 그리고 이때 연결요청 대기큐는 클라이언트의 연결요청 정보가 저장되는 공간이다. accept 함수가 호출되면, 바로 이 공간에 저장된 연결요청 정보를 참조하여 해당 클라이언트와의 연결을 구성한다.
6. 클라이언트 프로그램에서 소켓에 주소정보를 할당하는 bind 함수호출이 불필요한 이유는 무엇인가? 그리고 bind 함수를 호출하지 않았을 경우, 언제 어떠한 방식으로 IP주소와 PORT번호가 할당되는가?
>클라이언트는 연결을 요청하는 프로그램이다.(즉, 연결을 요청받는 입장이 아니다) 따라서 서버의 주소정보가 더 중요한 요소가 된다. 이 때문에 bind 함수호출을 통해서 명시적으로 주소정보를 할당할 필요는 없다. 그러나 서버와의 통신을 위해서는 소켓에 자신의 주소정보가 할당되어야 하기 때문에 서버로의 연결요청을 시도하는 connect 함수가 호출되었을 때, 인자로 전달된 소켓에 자동으로 IP와 PORT번호가 할당된다.
7. 앞서 선보였던 리눅스, 윈도우 기반 서버, 클라이언트 예제 프로그램을 iterative 모델로 변경하고, 제대로 변경이 되었는지 클라이언트와 함께 테스트해보자.
>
리눅스 기반 서버 프로그램(앞서 선보였던 프로그램과 거의 같으나 while문이 추가되었다)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <srpa/inet.h>
#include <sys/socket.h>
void error_handling(char *message);
int main(int argc, char *argv[])
{
int serv_sock;
int clnt_sock;
struct sockaddr_in serv_addr;
struct sockaddr_in clnt_addr;
socklen_t clnt_addr_size;
char message[] = "Hello World!";
if (argc != 2)
{
printf("Usage:%s <port>\n", argv[0]);
exit(1);
}
serv_sock = socket(PF_INET, SOCK_STREAM, 0); //socket 함수호출을 통해서 소켓을 생성하고 있다
if (serv_sock == -1)
error_handling("socket() error");
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(atoi(argv[1]));
if (bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1) //bind 함수호출을 통해서 IP주소와 PORT번호를 할당하고 있다
error_handling("bind() error");
if (listen(serv_sock, 5) == -1) //listen 함수를 호출하고 있다. 이로써 소켓은 연결요청을 받아들일 수 있는 상태가 된다
error_handling("listen() error");
clnt_addr_size = sizeof(clnt_addr);
while (1) //이 부분이 앞서 보였던 서버 프로그램과 다른 부분이다
{
clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_addr_size); //연결요청의 수락을 위한 accept 함수를 호출하고 있다
//연결요청이 없는 상태에서 이 함수가 호출되면, 연결요청이 있을 때까지 함수는 반환하지 않는다
if (clnt_sock == -1)
error_handling("accept() error");
write(clnt_sock, message, sizeof(message)); //write 함수는 데이터를 전송하는 기능의 함수인데, 이 문장이 실행됬다는 것은 연결요청이 있었다는 뜻
close(clnt_sock);
}
close(serv_sock);
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
정상적으로 실행된다면 클라이언트 프로그램과 연결될 때까지 커서가 깜빡거리며 대기한다
또한 iterative 모델이므로 기존과 다르게 클라이언트와 연결되도 계속 연결요청을 받을 준비를 하기 때문에 클라이언트 프로그램과 연결되더라도 꾸준히 커서가 깜빡거리며 대기한다.
리눅스 클라이언트 프로그램(앞서 선보인 프로그램과 동일하다)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
void error_handling(char *message);
int main(int argc, char *argv[])
{
int sock;
struct sockaddr_in serv_addr;
char message[30];
int str_len;
if (argc != 3)
{
printf("Usage: %s <IP> <port>\n", argv[0]);
exit(1);
}
sock = socket(PF_INET, SOCK_STREAM, 0); //소켓을 생성하고 있다.
//소켓을 생성하는 순간에는 서버 소켓과 클라이언트 소켓으로 나뉘지 않는다.
//bind listen함수의 호출이 이어지면 서버 소켓, connect 함수의 호출로 이어지면 클라이언트 소켓
if (sock == -1)
error_handling("socket() error");
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
serv_addr.sin_port = htons(atoi(argv[2]));
if (connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1) //connect 함수호출을 통해서 서버 프로그램에 연결 요청
error_handling("connect() error");
str_len = read(sock, message, sizeof(message) - 1);
if (str_len == -1)
error_handling("read() error");
printf("Message from server:%s\n", message);
close(socket);
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
iterative 모델답게 반복 호출이 가능한 것을 볼 수 있다.
윈도우 기반 서버 프로그램(앞서 선보였던 프로그램과 거의 같으나 while문이 추가되었다)
#include <stdio.h>
#include <stdlib.h>
#include <WinSock2.h>
void ErrorHandling(char *message);
int main(int argc, char *argv[])
{
WSADATA wsaData;
SOCKET hServSock, hClntSock;
SOCKADDR_IN servAddr, clntAddr;
int szClntAddr;
char message[] = "Hello World!";
if (argc != 2)
{
printf("Usage:%s <port>\n", argv[0]);
exit(1);
}
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) //소켓 라이브러리 초기화
ErrorHandling("WSAStartup() error!");
hServSock = socket(PF_INET, SOCK_STREAM, 0); //소켓생성
if (hServSock == INVALID_SOCKET)
ErrorHandling("socket() error");
memset(&servAddr, 0, sizeof(servAddr));
servAddr.sin_family = AF_INET;
servAddr.sin_addr.s_addr = htonl(INADDR_ANY);
servAddr.sin_port = htons(atoi(argv[1]));
if (bind(hServSock, (SOCKADDR*)&servAddr, sizeof(servAddr)) == SOCKET_ERROR) //소켓에 IP주소와 PORT 번호 할당
ErrorHandling("bind() error");
if (listen(hServSock, 5) == SOCKET_ERROR) //listen 함수호출을 통해서 생성한 소켓을 서버 소켓으로 완성
ErrorHandling("listen() error");
szClntAddr = sizeof(clntAddr);
while (1) //앞서 선보인 예제와 다른 부분
{
hClntSock = accept(hServSock, (SOCKADDR*)&clntAddr, &szClntAddr); //클라이언트 연결요청 수락하기 위해 accept함수 호출
if (hClntSock == INVALID_SOCKET)
ErrorHandling("accept() error");
send(hClntSock, message, sizeof(message), 0); //send함수 호출을 통해서 연결된 클라이언트에 데이터를 전송
closesocket(hClntSock);
}
closesocket(hServSock);
WSACleanup(); //프로그램 종료 전에 초기화한 소켓 라이브러리 해제
return 0;
}
void ErrorHandling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
정상적으로 실행된다면 클라이언트 프로그램과 연결될 때까지 커서가 깜빡거리며 대기한다
또한 iterative 모델이므로 기존과 다르게 클라이언트와 연결되도 계속 연결요청을 받을 준비를 하기 때문에 클라이언트 프로그램과 연결되더라도 꾸준히 커서가 깜빡거리며 대기한다.
윈도우 기반 클라이언트 프로그램(앞서 선보인 프로그램과 동일하다)
#include <stdio.h>
#include <stdlib.h>
#include <WinSock2.h>
void ErrorHandling(char *message);
int main(int argc, char *argv[])
{
WSADATA wsaData;
SOCKET hSocket;
SOCKADDR_IN servAddr;
char message[30];
int strLen;
if (argc != 3)
{
printf("Usage:%s <IP> <port>\n", argv[0]);
exit(1);
}
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) //소켓 라이브러리를 초기화하고 있다
ErrorHandling("WSAStartup() error!");
hSocket = socket(PF_INET, SOCK_STREAM, 0); //소켓을 생성하고
if (hSocket == INVALID_SOCKET)
ErrorHandling("socket() error");
memset(&servAddr, 0, sizeof(servAddr));
servAddr.sin_family = AF_INET;
servAddr.sin_addr.s_addr = inet_addr(argv[1]);
servAddr.sin_port = htons(atoi(argv[2]));
if (connect(hSocket, (SOCKADDR*)&servAddr, sizeof(servAddr)) == SOCKET_ERROR) //생성된 소켓을 바탕으로 서버에 연결요청을 하고 있다
ErrorHandling("connect() error!");
strLen = recv(hSocket, message, sizeof(message) - 1, 0); //recv 함수 호출을 통해서 서버로부터 전송되는 데이터를 수신하고 있다.
if (strLen == -1)
ErrorHandling("read() error");
printf("Message from server:%s\n", message);
closesocket(hSocket); //소켓 라이브러리 해제
WSACleanup();
return 0;
}
void ErrorHandling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
iterative 모델답게 반복 호출이 가능한 것을 볼 수 있다.
[참고] 윤성우 저 TCP/IP 소켓 프로그래밍
'C > TCPIP 소켓 프로그래밍(윤성우 저)' 카테고리의 다른 글
TCP/IP 소켓 프로그래밍 6장 내용 확인문제 (0) | 2017.05.29 |
---|---|
TCP/IP 소켓 프로그래밍 5장 내용 확인문제 (2) | 2017.05.25 |
TCP/IP 소켓 프로그래밍 3장 내용 확인문제 (0) | 2017.05.18 |
TCP/IP 소켓 프로그래밍 2장 내용 확인문제 (0) | 2017.05.16 |
TCP/IP 소켓 프로그래밍 1장 내용확인 문제 (0) | 2017.05.13 |