에코 클라이언트의 완벽구현
에코 서버는 문제가 없고, 에코 클라이언트만 문제가 있나?
문제는 에코서버가 아니라, 에코 클라이언트에 있다. 코드만 놓고 비교하면, 이부분이 이해되지 않을 수가 있다. 입출력에 사용된 함수의 호출문이 동일하기 때문.
//에코 서버의 입출력 문장
while((str_len=read(clnt_sock, message, BUF_SIZE))!=0)
write(clnt_sock, message, str_len);
//에코 클라이언트의 입출력 문장
write(sock, message, strlen(message));
str_len=read(sock, meassage, BUF_SIZE-1);
while(1)
{
fputs("Input message,(Q to quit):", stdout);
fgets(message, BUF_SIZE, stdin);
write(sock, message, stlen(message));
str_len=read(sock, message, BUF_SIZE-1);
message[str_len]=0;
printf("Message from server: %s", message);
에코 클라이언트는 문자열을 전송한다. write 함수 호출을 통해서 한방에 전송하는 것이 문제.
에코 클라이언트의 해결책
클라이엍느는 수신해야 할 데이터의 크기를 미리 알고 있기 때문에 20바이트인 문자열을 전송했다면, 20바이트를 수신할 때까지 반복해서 read함수를 호출하면 된다.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 1024
void error_handling(char *message);
int main(int argc, char *argv[])
{
int sock;
char message[BUF_SIZE];
int str_len, recv_len, recv_cnt;
struct sockaddr_in serv_adr;
if(argc!=3) {
printf("Usage : %s <IP> <port>\n", argv[0]);
exit(1);
}
sock=socket(PF_INET, SOCK_STREAM, 0);
if(sock==-1)
error_handling("socket() error");
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family=AF_INET;
serv_adr.sin_addr.s_addr=inet_addr(argv[1]);
serv_adr.sin_port=htons(atoi(argv[2]));
if(connect(sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr))==-1)
error_handling("connect() error!");
else
puts("Connected...........");
while(1)
{
fputs("Input message(Q to quit): ", stdout);
fgets(message, BUF_SIZE, stdin);
if(!strcmp(message,"q\n") || !strcmp(message,"Q\n"))
break;
str_len=write(sock, message, strlen(message));
recv_len=0;
while(recv_len<str_len)
{
recv_cnt=read(sock, &message[recv_len], BUF_SIZE-1);
if(recv_cnt==-1)
error_handling("read() error!");
recv_len+=recv_cnt;
}
message[recv_len]=0;
printf("Message from server: %s", message);
}
close(sock);
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
위 예제에서 변경 및 추가된 부분 이전예제에서는 단순히 read함수를 한번 호출하고 말았던 것을 이 예제에서는 전송한 데이터의 크기만큼 데이터를 수신하기 위해서 read함수를 반복호출 함. 따라서 정확히 전송한 바이트 크기만큼 데이터를 수신할 수 있게 됨.
str_len=write(sock, message, strlen(message));
recv_len=0;
while(recv_len<str_len)
{
recv_cnt=read(sock, &message[recv_len], BUF_SIZE-1);
if(recv_cnt==-1)
error_handling("read() error!");
recv_len+=recv_cnt;
}
message[recv_len]=0;
에코 클라이언트 이외의 경우에는? 어플리케이션 프로토콜의 정의
에코 클라이언트의 경우에는 수신할 데이터의 크기를 이전에 파악 할 수는 있지만, 이것이 불가능한 경우가 훨씬 많음.
이러한 경우게 필요한 것이 바로 어플리케이션 프로토콜의 정의.
데이터의 송수신 과정에서도 데이터의 끝을 표현하거나, 송수신될 데이터의 크기를 미리 알려줘서 그에 따른 대비가 가능해아 한다.
서버, 클러이언트의 구현과정에서 하나, 둘씩 만들어지는 약속을 모아서 '어플리케이션 프로토콜'이라고 함.
실행결과 : op_server.c
실행결과 : op_client.c one
계산기 서버, 클라이언트의 예
계산기 예제를 구현하기 전에 최소한의 프로토콜 정의.
- 클라이언트는 서버에 접속하자마자 피연산자의 개수정보를 1바이트 정수형태로 전달한다.
- 클라이언트가 서버에 전달하는 정수 하나는 4바이트로 표현한다.
- 정수를 전달한 당므에는 연산의 종류를 전달한다. 연산정보는 1바이트로 전달한다.
- 문자 +, -, * 중 하나를 선택해서 전달한다.
- 서버는 연산결과를 4바이트 정수의 현태로 클라이언트에게 전달한다.
- 연산결과를 얻은 클라이언트는 서버와의 연결을 종료한다.
#include <stdio.h>
#define BUF_SIZE 1024
#define RLT_SIZE 4
#define OPSZ 4
void error_handling(char *message);
int main(int argc, char *argv[])
{
int sock;
char opmsg[BUF_SIZE];
int result, opnd_cnt, i;
struct sockaddr_in serv_adr;
if(argc!=3) {
printf("Usage : %s <IP> <port>\n", argv[0]);
exit(1);
}
sock=socket(PF_INET, SOCK_STREAM, 0);
if(sock==-1)
error_handling("socket() error");
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family=AF_INET;
serv_adr.sin_addr.s_addr=inet_addr(argv[1]);
serv_adr.sin_port=htons(atoi(argv[2]));
if(connect(sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr))==-1)
error_handling("connect() error!");
else
puts("Connected...........");
fputs("Operand count: ", stdout);
scanf("%d", &opnd_cnt);
opmsg[0]=(char)opnd_cnt;
for(i=0; i<opnd_cnt; i++)
{
printf("Operand %d: ", i+1);
scanf("%d", (int*)&opmsg[i*OPSZ+1]);
}
fgetc(stdin);
fputs("Operator: ", stdout);
scanf("%c", &opmsg[opnd_cnt*OPSZ+1]);
write(sock, opmsg, opnd_cnt*OPSZ+2);
read(sock, &result, RLT_SIZE);
printf("Operation result: %d \n", result);
close(sock);
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
해설
#define RLT_SIZE 4
#define OPSZ 4
피연산자의 바이트 수와 연산결과의 바이트 수를 상수화 하였다.
char opmsg[BUF_SIZE];
데이터의 송수신을 위한 메모리 공간은 이렇듯 배열을 기반으로 생성하는 것이 좋다. 데이터를 누적해서 송수신 해야하기 때문이다.
scanf("%d", &opnd_cnt);
opmsg[0]=(char)opnd_cnt;
프로그램 사용자로부터 피연산자의 개수정보를 입력 받은 후, 이를 배열 opmsg에 저장하고 있다.
for(i=0; i<opnd_cnt; i++)
{
printf("Operand %d: ", i+1);
scanf("%d", (int*)&opmsg[i*OPSZ+1]);
}
프로그램 사용자로부터 정수를 입력받아서 배열 opmsg에 이어서 저장하고 있다.
fgetc(stdin);
다음 행에서 문자를 입력받아야 하는데, 이에 앞서 버퍼에 남아있는 \n문자의 삭제를 위함.
scanf("%c", &opmsg[opnd_cnt*OPSZ+1]);
마지막으로 연산자 정보를 입력받아서 배열 opmsg에 저장하고 있다.
write(sock, opmsg, opnd_cnt*OPSZ+2);
드디어 write 함수호출을 통해서 opmsg에 저장되어 있는 연산과 관련된 정보를 한방에 전송하고 있다. write 함수 호출을 통해서 묶어서 보내도 되고, 여러번의 write 함수호출을 통해서 나눠서 보내도 된다.
read(sock, &result, RLT_SIZE);
서버가 전송해주는 연산결과의 저장과정을 보이고 있다. 수신할 데이터의 크기가 4바이트이기 때문에 이렇게 한번에 read함수 호출로 충분히 수신이 가능하다.
#include <stdio.h>
#define BUF_SIZE 1024
#define OPSZ 4
void error_handling(char *message);
int calculate(int opnum, int opnds[], char oprator);
int main(int argc, char *argv[])
{
int serv_sock, clnt_sock;
char opinfo[BUF_SIZE];
int result, opnd_cnt, i;
int recv_cnt, recv_len;
struct sockaddr_in serv_adr, clnt_adr;
socklen_t clnt_adr_sz;
if(argc!=2) {
printf("Usage : %s <port>\n", argv[0]);
exit(1);
}
serv_sock=socket(PF_INET, SOCK_STREAM, 0);
if(serv_sock==-1)
error_handling("socket() error");
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family=AF_INET;
serv_adr.sin_addr.s_addr=htonl(INADDR_ANY);
serv_adr.sin_port=htons(atoi(argv[1]));
if(bind(serv_sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr))==-1)
error_handling("bind() error");
if(listen(serv_sock, 5)==-1)
error_handling("listen() error");
clnt_adr_sz=sizeof(clnt_adr);
for(i=0; i<5; i++)
{
opnd_cnt=0;
clnt_sock=accept(serv_sock, (struct sockaddr*)&clnt_adr, &clnt_adr_sz);
read(clnt_sock, &opnd_cnt, 1);
recv_len=0;
while((opnd_cnt*OPSZ+1)>recv_len)
{
recv_cnt=read(clnt_sock, &opinfo[recv_len], BUF_SIZE-1);
recv_len+=recv_cnt;
}
result=calculate(opnd_cnt, (int*)opinfo, opinfo[recv_len-1]);
write(clnt_sock, (char*)&result, sizeof(result));
close(clnt_sock);
}
close(serv_sock);
return 0;
}
int calculate(int opnum, int opnds[], char op)
{
int result=opnds[0], i;
switch(op)
{
case '+':
for(i=1; i<opnum; i++) result+=opnds[i];
break;
case '-':
for(i=1; i<opnum; i++) result-=opnds[i];
break;
case '*':
for(i=1; i<opnum; i++) result*=opnds[i];
break;
}
return result;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
해설
for(i=0; i<5; i++)
총 5개의 클라이언트 연결요청을 수용하기 위해서 for문이 구성되었다.
read(clnt_sock, &opnd_cnt, 1);
제일먼저 피연산자의 개수정보를 수신하고 있다.
while((opnd_cnt*OPSZ+1)>recv_len)
{
recv_cnt=read(clnt_sock, &opinfo[recv_len], BUF_SIZE-1);
recv_len+=recv_cnt;
}
이 전 행을 통해서 확인한 피연산자의 개수정보를 바탕으로 피연산자 정보를 수신하고 있다.
result=calculate(opnd_cnt, (int*)opinfo, opinfo[recv_len-1]);
calculate 함수를 호출하면서, 피연산자의 정보와 연산자 정보를 인자로 전달하고 있다.
write(clnt_sock, (char*)&result, sizeof(result));
calculate 함수가 반환한 연산결과를 클라이언트에게 전송하고 있다.
TCP의 이론적인 이야기
TCP 소켓에 존재하는 입출력 버퍼
TCP 소켓의 데이터 송수신에는 경계가 없음. 따라서 서버가 한번의 write함수 호출을 통해서 40바이트를 전송해도 클라이언트는 네번의 read함수호출을 통해서 10바이트씩 데이터를 수신해야함.
read함수가 10바이트씩 가져올 때, 남아있는 30바이트는 버퍼에 저장되어 있음.
• 입출력 버퍼는 TCP 소켓 각각에 대해 별도로 존재한다.
• 입출력 버퍼는 소켓생성시 자동으로 생성된다.
• 소켓을 닫아도 출력버퍼에 남아있는 데이터는 계속해서 전송이 이뤄진다.
• 소켓을 닫으면 입력버퍼에 남아있는 데이터는 소멸되어버린다.
TCP의 내부 동작원리1 : 상대 소켓과의 연결
TCP 소켓의 생성에서 소멸까지 거치게 되는 일을 크게 나누면, 다음 세가지로 구분할 수 있다.
- 상대 소켓과의 연결
- 상대 소켓과의 데이터 송수신
- 상대 소켓과의 연결종료
TCP의 내부 동작원리2 : 상대 소켓과의 데이터 송수신
호스트 A가 호스트 B에게 200바이트를 두번에 나눠서 전송하는 과정.
ACK번호가 1201이 아니라 1301인 이유는 ACK번호를 전송된 바이트 크기만큼 추가로 증가시켰기 때문.
ACK번호 →SEQ번호 + 전송된 바이트 크기 +1
TCP의 내부 동작원리3 : 상대 소켓과의 연결종료
위 그림에서 패킷 안에 삽입되어 있는 FIN은 종료를 알리는 메시지이다. 이 과정은 상호간에 한번씩 주고 받아서 연결이 종료되는데, 이 과정이 네 단계에 걸쳐서 진행되기 때문에 이를 가리켜 Four-way handsahking이라고 한다.
'TCP&IP' 카테고리의 다른 글
TCP/IP 9장 소켓의 다양한 옵션 (0) | 2024.09.05 |
---|---|
TCP/IP 8장 도메인 이름과 인터넷 주소 (0) | 2024.09.05 |
TCP/IP 4장 기반서버/클라이언트1 (0) | 2024.09.03 |
TCP/IP 3장 주소체계와 데이터 정렬 (0) | 2024.09.03 |
TCP/IP 2.장 소켓의 프로토콜과 그에 따른 데이터 전송 특성 (0) | 2024.09.03 |
댓글