모든 내용은 [윤성우 저, "열혈강의 TCP/IP 소켓 프로그래밍", 오렌지미디어] 를 기반으로 제 나름대로 이해하여 정리한 것입니다. 다소 부정확한 내용이 있을수도 있으니 이를 유념하고 봐주세요!
01. 프로토콜이란 무엇을 의미하는가? 그리고 데이터의 송수신에 있어서 프로토콜을 정의한다는 것은 어떠한 의미가 있는가?
프로토콜이란 컴퓨터 상호간의 대화에 필요한 통신규약을 의미한다. 즉, 프로토콜을 정의한다는 것은 서로 데이터를 주고받기 위해 약속을 정의해놓는다는 것을 의미한다.
02. 연결지향형 소켓인 TCP 소켓의 특성 세가지를 나열하라.
- 중간에 데이터가 소멸되지 않고 목적지로 전송됨
- 전송 순서대로 데이터가 수신됨
- 전송되는 데이터의 경계가 존재하지 않음 -> read, write 함수 호출횟수가 큰 의미를 가지지 않음
+
- 자신과 연결된 상대 소켓의 상태를 파악해가며 데이터를 전송함
(수신소켓의 입력버퍼가 꽉찬 경우에, 송신소켓은 데이터 전송을 멈춤)
(데이터가 제대로 전송되지 않으면, 송신소켓은 데이터를 재전송함)
- 소켓에 존재하는 버퍼가 꽉차는 경우에도 데이터는 소멸하지 않음
- 소켓 대 소켓의 연결은 반드시 1:1
03. 다음 중 비연결지향형 소켓의 특성에 해당하는 것을 모두 고르면?
* 옳은 내용
a. 전송된 데이터는 손실될 수 있다
c. 가장 빠른 전송을 목표로 한다.
e. 연결지향형 소켓과 달리 연결이라는 개념이 존재하지 않는다
* 틀린 내용
b. 데이터의 경계(Boundary)가 존재하지 않는다 - 연결지향형 소켓의 특성임
d. 한번에 전송할 수 있는 데이터의 크기가 제한되어있지 않다 - 제한되어 있음
04. 다음 유형의 데이터 송수신에 적합한 타입의 소켓은 무엇인지 결정하고, 그러한 결정을 하게 된 이유를 설명해보자.
a. 서태지와 아이들의 실시간 라이브 방송 멀티미디어 데이터
- 약간의 데이터의 손실에 민감하지 않고, 전송 속도가 훨씬 더 중요함 => 비연결지향형 소켓
b. 철수가 압축한 텍스트 파일의 전송
- 압축파일은 특성상 조금이라도 데이터가 손실되면 안됨 => 연결지향형 소켓
c. 인터넷 뱅킹을 이용하는 고객과 은행 사이에서의 데이터 송수신
- 고객과 은행 간의 데이터 송수신 속도는 크게 중요하지 않다. 오히려 송수신 데이터가 손실되거나 수정되는 경우가 훨씬 치명적이기에 이를 방지할 필요가 있다. => 연결지향형 소켓
05. 데이터의 경계(Boundary)가 존재하지 않는 소켓은 어떠한 타입의 소켓인가? 그리고 이러한 소켓은 데이터를 수신할 때 무엇을 주의해야 하는지 서술해보자.
연결지향형 소켓(e.g. TCP)은 송수신 데이터의 경계가 존재하지 않으므로, 입출력 함수(read, write)의 호출횟수가 큰 의미를 갖지 않는다. 따라서 입출력 함수 호출횟수에 신경쓸 것이 아니라, 송신한 데이터가 모두 제대로 수신되었는지를 확인할 필요가 있다.
06. 이번에는 서버가 여러차례의 write 함수호출을 통해서 전송한 문자열을 클라이언트에서 한번의 read 함수호출을 통해서 읽어들이는 형태로 예제를 작성해보자.(단, 이를 위해서 클라이언트는 read 함수의 호출 시기를 다소 늦출 필요가 있다.)
# 서버 코드
#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 serv_sock;
int clnt_sock;
int slice; // 문자열을 나눠서 보낼때, 그 데이터 크기(바이트)
struct sockaddr_in serv_addr;
struct sockaddr_in clnt_addr;
socklen_t clnt_addr_size;
char message[]="Hello World!";
if(argc!=2) // 인자로 실행파일명/Port번호 를 입력받아야 하므로
{
printf("Usage : %s <port>\n",argv[0]);
exit(EXIT_FAILURE);
}
serv_sock=socket(PF_INET,SOCK_STREAM,0); // TCP 서버소켓 생성
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)
error_handling("bind() error");
// 연결대기큐를 생성함(이제서야, 진정한 서버소켓의 역할을 하게됨)
// 이 함수 호출 이후부터, 클라이언트의 연결요청(connect)가 허용됨
if(listen(serv_sock,5)==-1)
error_handling("listen() error");
clnt_addr_size=sizeof(clnt_addr);
// 연결요청한 클라이언트에 대해 그 요청을 수락
// 연결대기큐에 있는 순서대로 요청을 수락
// 아직 연결요청한 클라이언트가 없다면 블로킹
// 이때, 연결요청한 클라이언트소켓과 1:1로 연결되는 소켓이 생성됨(서버 소켓과는 다른 소켓임)
clnt_sock=accept(serv_sock,(struct sockaddr*)&clnt_addr, &clnt_addr_size);
if(clnt_sock==-1)
error_handling("accept() error");
// accept 함수 호출로 생성된 소켓을 통해 데이터를 전송함
slice=4; // 임의로 설정한 slice 길이
// 총 4번의 write 함수로 나눠서 데이터를 전송
write(clnt_sock,message,slice);
write(clnt_sock,message+slice,slice);
write(clnt_sock,message+2*slice,slice);
write(clnt_sock,message+3*slice,sizeof(message)-3*slice); // message의 총 바이트수가 13이므로
close(clnt_sock); // 클라이언트 소켓과 연결될때 생성된 소켓을 없앰
close(serv_sock); // 서버소켓을 없앰
return 0;
}
void error_handling(char *message)
{
fputs(message,stderr);
fputc('\n',stderr);
exit(EXIT_FAILURE);
}
# 클라이언트 코드
#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 idx=0,read_len=0;
int i;
if(argc!=3) // 인자로 실행파일명/(서버의)IP/(서버의)Port번호 를 전달받아야 함
{
printf("Usage : %s <IP> <port>\n",argv[0]);
exit(1);
}
sock=socket(PF_INET,SOCK_STREAM,0); // TCP 클라이언트소켓 생성
// 아직 명확하게 "클라이언트 소켓"은 아님, 그냥 소켓임
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]));
// 클라이언트소켓이 주소정보를 토대로 서버소켓에게 연결요청(이때, 이 소켓이 클라이언트소켓이 됨)
// 이때, 클라이언트소켓의 주소정보가 할당됨
// IP는 호스트IP로, Port번호는 임의로.
if(connect(sock,(struct sockaddr*)&serv_addr, sizeof(serv_addr))==-1)
error_handling("connect() error!");
// 서버가 데이터를 모두 전송할 때까지
// 클라이언트가 read 함수의 호출 시기를 늦추기 위함임
for (i = 0; i<10000; ++i); // 이러한 방법을 "바쁜 대기(busy waitng)이라고 함"
// 서버에서 나누어 보낸 데이터를
// 단 한번의 read 함수로 클라이언트가 모두 제대로 수신했음을 확인해보자
read_len=read(sock,message,sizeof(message));
if(read_len==-1) // read 함수가 -1을 반환하면 오류
error_handling("read() error!");
printf("Message from server: %s \n",message);
printf("read_len: %d \n",read_len);
close(sock);
return 0;
}
void error_handling(char *message)
{
fputs(message,stderr);
fputc('\n',stderr);
exit(EXIT_FAILURE);
}
# 서버
# 클라이언트
서버 소켓에서 4번의 write 함수를 호출하여 데이터를 나누어 송신했지만,
클라이언트 소켓에서 단 1번의 read 함수로 모든 데이터를 제대로 수신했음을 확인할 수 있습니다.
이것으로 연결지향형 소켓에서 "데이터의 경계가 존재하지 않음"을 확인할 수 있다.
[출처] : 윤성우 저, "열혈강의 TCP/IP 소켓 프로그래밍", 오렌지미디어
'Programming > 열혈 TCP, IP 소켓 프로그래밍(저자 윤성우)' 카테고리의 다른 글
Ch 03. 내용 확인문제 (0) | 2020.08.05 |
---|---|
Ch 03. 주소체계와 데이터 정렬 (0) | 2020.08.05 |
Ch 02. 소켓의 타입과 프로토콜의 설정 (0) | 2020.08.04 |
표준 파일 입출력 함수(고수준 파일 입출력 함수) (0) | 2020.07.30 |
Ch 01. 내용 확인문제 (0) | 2020.07.28 |