TCP의 전송 특성에 맞게 클라이언트를 재구성해야한다.
앞선 에코 클라이언트에서는 1 write - 1 read 를 통해 데이터를 수신하려고 한다. 하지만 이는 데이터 전송 방법 상 옳지 않은 방법이다.
에코 클라이언트는 자신의 말을 서버로 보내고 그 말이 그대로 자신에게 돌아오는 시스템이다.
즉, 클라이언트는 자신이 보낸 데이터의 바이트를 알고 있다.
-> 자신이 보낸 바이트 모두 수신될때까지 반복문으로 read()
하여 문제를 해결 !
// op_server.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 1024
#define RLT_SIZE 4
#define OPSZ 4
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("not a correct usage\n");
exit(1);
}
serv_sock = socket(PF_INET, SOCK_STREAM, 0);
if (serv_sock == -1)
error_handling("socket function error\n");
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 function error\n");
if (listen(serv_sock, 5) == -1)
error_handling("listen function error\n");
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 = calc(opnd_cnt, (int*)opinfo, opinfo[recv_len - 1]);
write(clnt_sock, (char*)&result, sizeof(result));
close(clnt_sock);
}
close(serv_sock);
return 0;
}
위 코드에서 for문이 5 번의 연결요청을 accept()
한다.
하나의 클라이언트가 보낸 데이터에 대한 결과를 calc()
로 계산하여 송신한다.
결과를 한 번의 write()
로 다시 클라이언트에 전달해준다.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 1024
#define RLT_SIZE 4
#define OPSZ 4
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("not a correct usage\n");
exit(1);
}
sock = socket(PF_INET, SOCK_STREAM, 0);
if (sock == -1)
error_handling("socket function error\n");
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 function error\n");
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); // delete remained \n in buffer.
fputs("Operator: ", stdout);
scanf("%c", &opmsg[opnd_cnt * OPaSZ + 1]);
write(sock, opmsg, opnd_cnt * OPSZ + 2);
read(sock, &result, RLT_SIZE);
printf("Operation result: %d \n", result);
close(sock);
return 0;
}
데이터를 누적하여 송수신할 것이기 때문에 배열을 통해 연속적인 메모리공간으로 생성하는 것이 좋다.
또한 수신할 데이터 크기가 4 바이트이므로 한 번의 read()
로 충분히 받을 수 있다.
하나의 배열에 다양한 종류의 데이터를 저장해서 전송하려면, char형 배열을 선언하여야함. 플랫폼에 따라 데이터 타입 호환성이 좋고, 메모리 레이아웃이 8비트를 사용하기 때문에 일관되고 데이터 관리가 편하다.
슬라이딩 윈도우(sliding window) : 소켓 간 데이터 송수신에 대해 확인을 진행한다는 프로토콜.
TCP에는 슬라이딩 윈도우가 존재해서 버퍼가 차고 넘쳐서 데이터가 소멸되는 일이 없다. 정확히 받을 수 있는 데이터만큼을 수신한다.
write()
가 반환디는 시점
: 전송할 데이터가 출력버퍼로 이동이 완료되는 시점.
TCP 의 경우 출력버퍼로 이동된 데이터의 전송을 보장.
-> "write 함수는 데이터의 전송이 완료되어야 반환된다." 라고 표현함.
소켓은 전 이중(Full-duplex) 방식으로 동작하므로 양뱡향으로 데이터를 주고 받는다.
연결 과정에서 TCP 소켓은 3 번의 대화를 주고받음. (Three-way handshaking)
SYN : 데이터 전송을 위한 동기화 메시지 전송(A -> B)
SYN + ACK : 동기화 메시지 + 응답 메시지.
ACK : 응답 메시지 전달
이렇게 총 3 회의 주고받음을 통해 데이터 송수신의 준비완료를 확인함.
마찬가지로 SEQ와 ACK에 패킷 번호를 부여해서 올바르게 송수신이 이뤄지는지를 체크함
ACK num = SEQ num + sended bytes + 1
이렇게 하면 데이터가 보낸만큼 잘 받았는지 확인할 수 있다.
ACK 응답을 요구하는 패킷 전송 시, 타이머가 작동한다. 제한 시간 내 ACK가 오지 않으면 타임아웃이 발생하여 다시한번 패킷 재전송.
종료를 알리는 FIN
메시지를 주면 그에 대한 ACK를 답한다.
각 소켓은 서로에게 FIN
메시지를 전달하고 그에 대한 확인 패킷을 전달하므로 총 4회의 교류가 발생. (Four-way handshaking)