일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- 10026번
- 스프링 핵심 원리
- Operating System.
- Operating System
- 2475번
- Spring
- inflearn
- 에러핸들링
- 열혈 TCP/IP 소켓 프로그래밍
- 열혈 tcp/ip 프로그래밍
- 윤성우 저자
- Window-Via-c/c++
- C#
- redis
- 우아한레디스
- 운영체제
- Four Squares
- 제프리리처
- 토마토
- 우아한 테크 세미나
- TCP/IP
- n타일링2
- OS
- 이펙티브코틀린
- FIFO paging
- HTTP
- C++
- BOJ
- 김영한
- 스프링 입문
- Today
- Total
나의 브을로오그으
#5. TCP 기반 서버/클라이언트 2 본문
#1. 에코 클라이언트의 완변 구현!
4장에서 했던 내용을 참고하면, 에코 서버보다는 클라이언트에 문제가 있다.]
[EchoServer]
/* Window */
while((strLen = recv(hSockClnt, message, BUF_SIZE, 0)) != 0) {
send(hSockClnt, message, strLen, 0);
}
[EchoClient]
send(hSocket, message, strLen(message), 0);
strLen = recv(hSocket, message, BUF_SIZE - 1, 0);
message[strLen] = 0;
클라이언트 쪽은 서버쪽에서 보낸 데이터를 100% 수신하지만, 단위에 문제가 있다.
바로 에코 클라이언트는 send()함수 호출을 통해 데이터를 한번에 보내고, recv()를 통해 한번에 받기를 원한다. 이것이 문제다! 그렇다면 이렇게 생각 할 수 있다.
"수신 데이터가 한 번 이상으로 올 수 있기 때문에 조금 기다렸다가 수신하면 한번에 수신 가능하지 않나?" 맞는 말이다.
다만 언제까지 기다려야 할까? 알 수 없다.
개선된 코드
[echo_client2_win.c]
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winsock2.h>
#pragma comment(lib, "ws2_32.lib")
#define BUF_SIZE 1024
void ErrorHandling(const char* message);
int main(int argc, char* argv[])
{
WSADATA wsaData;
SOCKET hSocket;
char message[BUF_SIZE] = { 0, };
int strLen = 0, recvLen = 0, recvCnt = 0;
SOCKADDR_IN servAddr;
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, IPPROTO_TCP);
if (hSocket == INVALID_SOCKET)
{
ErrorHandling("socket() error");
}
memset(&servAddr, 0, sizeof(servAddr));
servAddr.sin_family = AF_INET;
servAddr.sin_addr.S_un.S_addr = inet_addr(argv[1]);
servAddr.sin_port = htons(atoi(argv[2]));
if (connect(hSocket, (SOCKADDR*)&servAddr, sizeof(servAddr)) == SOCKET_ERROR)
{
ErrorHandling("connect()");
}
else
{
printf("Connected..........\n");
}
while (1)
{
fputs("Input message (Q to q): ", stdout);
fgets(message, BUF_SIZE, stdin);
if (!strcmp(message, "Q\n") || !strcmp(message, "q\n"))
{
break;
}
strLen = send(hSocket, message, strlen(message), 0);
recvLen = 0;
while (recvLen < strLen)
{
recvCnt = recv(hSocket, &message[recvLen], BUF_SIZE - 1, 0);
if (recvCnt == -1)
{
ErrorHandling("recv() error");
}
recvLen += recvCnt;
}
message[recvLen] = 0;
printf("Message from server : %s", message);
}
closesocket(hSocket);
WSACleanup();
return 0;
}
void ErrorHandling(const char* message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
recv() 함수를 반복적으로 호출하여 데이터를 받는다. while문을 빠져나가는 조건은 실제 send() 한 message 데이터의 크기 만큼을 다시 수신 할 때까지 계속해서 데이터를 recv() 한다.
애플리케이션 프로토콜의 정의
에코 클라이언트의 경우에는 수신할 데이터의 크기를 이전에 파악 가능했지만, 일반적으로는 불가능한 경우가 더 많다. 그렇다면 수신할 데이터 크기를 파악하지 못할 때에는 어떻게 데이터를 송수신 해야 할까? 이런 경우에 필요한 것이 에플리케이션 프로토콜의 정의이다.
(에코서버, 클라이언트에서는 다음의 프로토콜을 정의했다. (Q 또는 q)가 입력되면 종료)
이거와 마찬가지로 데이터 송수신 과정에서도 데이터의 끝을 파악할 수 있는 약속(프로토콜)을 별도로 정의해 데이터의 끝을 표현하거나, 송수신될 데이터의 크기를 미리 알려줘서 그에 따른 대비가 가능해야 한다.
(참고 : 서버 클라이언트 구현 과정에서 이렇게 하나, 둘 씩 만들어지는 약속을 모아서 '어플리케이션 프로토콜'이라고 한다. 이렇듯 어플리케이션 프로토콜은 대단한 것이 아닌 목적에 맞는 프로그램의 구현에 따라서 정의하게 되는 약속에 불과하다.)
ex) 간단한 계산 프로그램을 만들어 보자. (서버로 숫자와 연산 방식을 전달하면 연산 결과가 전달되게 해보자.)
[test_calc_server_win.c]
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <malloc.h>
#include <WinSock2.h>
#pragma comment(lib, "ws2_32.lib")
#define BUF_SIZE 1024
void ErrorHandling(const char* message);
int recvData(SOCKET hClntSock, char* message);
int msgToInt(const char* message, const int len);
int calculate(int* arr, int len, char op);
int main(int argc, char* argv[])
{
WSADATA wsaData;
SOCKET hServSock, hClntSock;
char message[BUF_SIZE] = { 0, };
SOCKADDR_IN servAdr, clntAdr;
int clntAdrSize = 0;
int* arr;
int result = 0;
int opCount = 0;
char optr;
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(&servAdr, 0, sizeof(servAdr));
servAdr.sin_family = AF_INET;
servAdr.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
servAdr.sin_port = htons(atoi(argv[1]));
if (bind(hServSock, (PSOCKADDR)&servAdr, sizeof(servAdr)) == SOCKET_ERROR)
{
ErrorHandling("bind() error");
}
if (listen(hServSock, 5) == SOCKET_ERROR)
{
ErrorHandling("listen() error");
}
clntAdrSize = sizeof(clntAdr);
memset(&clntAdr, 0, clntAdrSize);
hClntSock = accept(hServSock, (PSOCKADDR)&clntAdr, &clntAdrSize);
if (hClntSock == SOCKET_ERROR)
{
ErrorHandling("accept() error");
}
else
{
printf("Connected client %d \n", hClntSock);
}
opCount = recvData(hClntSock, message);
arr = (int*)malloc(opCount * sizeof(int));
if (arr == NULL)
{
ErrorHandling("malloc() error");
}
for (int i = 0; i < opCount; ++i)
{
arr[i] = recvData(hClntSock, message);
}
if (recv(hClntSock, &optr, 1, 0) == SOCKET_ERROR)
{
ErrorHandling("recv() operator error");
}
result = calculate(arr, opCount, optr);
sprintf(message, "%d", result);
send(hClntSock, message, strlen(message), 0);
free(arr);
closesocket(hClntSock);
closesocket(hServSock);
WSACleanup();
return 0;
}
void ErrorHandling(const char* message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
int recvData(SOCKET hClntSock, char* message)
{
int strLen = 0;
if ((strLen = recv(hClntSock, message, BUF_SIZE, 0)) == 0)
{
ErrorHandling("recv() error");
}
return msgToInt(message, strLen);
}
int msgToInt(const char* message, const int len)
{
int result = 0;
for (int i = 0; i < len; ++i)
{
result *= 10;
result += message[i] - '0';
}
return result;
}
int calculate(int* arr, int len, char op)
{
int result = 0;
switch (op)
{
case '+':
for (int i = 0; i < len; ++i)
{
result += arr[i];
}
break;
case '-':
result += arr[0];
for (int i = 1; i < len; ++i)
{
result -= arr[i];
}
break;
case '*':
result = 1;
for (int i = 0; i < len; ++i)
{
result *= arr[i];
}
break;
case '/':
result = arr[0];
for (int i = 1; i < len; ++i)
{
if (arr[i] == 0) {
return -1;
}
result /= arr[i];
}
break;
default:
/* Nothing */
break;
}
return result;
}
[test_calc_client_win.c]
#include <stdio.h>
#include <stdlib.h>
#include <WinSock2.h>
#include <string.h>
#pragma comment(lib, "ws2_32.lib")
#define BUF_SIZE 1024
void ErrorHandling(const char* message);
int main(int argc, char* argv[])
{
WSADATA wsaData;
char message[BUF_SIZE];
SOCKET hSocket;
SOCKADDR_IN servAdr;
int opCount = 0;
int operand = 0;
char optr;
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(&servAdr, 0, sizeof(servAdr));
servAdr.sin_family = AF_INET;
servAdr.sin_addr.S_un.S_addr = inet_addr(argv[1]);
servAdr.sin_port = htons(atoi(argv[2]));
if (connect(hSocket, (PSOCKADDR)&servAdr, sizeof(servAdr)) == SOCKET_ERROR)
{
ErrorHandling("connect() error");
}
else
{
printf("Connected...........\n");
}
printf("Operand count: ");
scanf_s("%d", &opCount);
sprintf(message, "%d", opCount);
send(hSocket, message, strlen(message), 0);
for (int i = 0; i < opCount; ++i)
{
printf("Operand %d : ", i + 1);
scanf_s("%d", &operand);
sprintf(message, "%d", operand);
send(hSocket, message, strlen(message), 0);
}
printf("Operator : ");
while (getchar() != '\n') { }
optr = getchar();
send(hSocket, &optr, sizeof(optr), 0);
if (recv(hSocket, message, BUF_SIZE, 0) != 0)
{
ErrorHandling("recv() error");
}
printf("Operation result: %d", atoi(message));
return 0;
}
void ErrorHandling(const char* message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
최대한 간단하게 짜보려했는데 막 짜다보니 지저분한 코드가 나왔다.... 하지만 잘 동작한다!!
계산기 서버, 클라이언트의 예
구현에 앞서 어플리케이션 프로토콜을 먼저 정의하자.(이유는 TCP 소켓은 데이터의 경계가 없기 때문에)
- 클라이언트는 서버에 접속하자마자 피연산자의 개수정보를 1바이트 정수형태로 전달한다.
- 클라이언트가 서버에 전달하는 정수 하나는 4바이트로 표현한다.
- 정수를 전달한 다음에는 연산의 종류를 전달한다. 연산정보는 1바이트로 전달한다.
- 문자 +, -, * 중 하나를 선택해서 전달한다.
- 서버는 연산결과를 4바이트 정수의 형태로 클라이언트에게 전달한다.
- 연산결과를 얻은 클라이언트는 서버와의 연결을 종료한다.
(참고로 윈도우 기반으로 구현했음)
[op_client.c]
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <WinSock2.h>
#pragma comment(lib, "ws2_32.lib")
#define BUF_SIZE 1024
#define RLT_SIZE 4
#define OPSZ 4
void ErrorHandling(const char* message);
int main(int argc, char* argv[])
{
WSADATA wsaData;
SOCKET hSocket;
char opmsg[BUF_SIZE];
SOCKADDR_IN servAdr;
int result, opndCnt, i;
if (argc != 3)
{
printf("Usage : %s <ip> <port> \n", argv[0]);
exit(1);
}
hSocket = socket(PF_INET, SOCK_STREAM, 0);
if (hSocket == INVALID_SOCKET)
{
ErrorHandling("socket() error");
}
memset(&servAdr, 0, sizeof(servAdr));
servAdr.sin_family = AF_INET;
servAdr.sin_addr.S_un.S_addr = inet_addr(argv[1]);
servAdr.sin_port = htons(atoi(argv[2]));
if (connect(hSocket, (PSOCKADDR)&servAdr, sizeof(servAdr)) == SOCKET_ERROR)
{
ErrorHandling("connect() error");
}
else
{
printf("Connected.......... \n");
}
fputs("Operand count: ", stdout);
scanf_s("%d", &opndCnt);
opmsg[0] = (char)opndCnt;
for (i = 0; i < opndCnt; ++i)
{
printf("Operand %d: ", i + 1);
scanf_s("%d", (int*)&opmsg[i * OPSZ + 1]);
}
fgetc(stdin);
fputs("Operator: ", stdout);
scanf_s("%c", &opmsg[opndCnt * OPSZ + 1]);
send(hSocket, opmsg, opndCnt * OPSZ + 2, 0);
recv(hSocket, &result, RLT_SIZE, 0);
printf("Operation result: %d \n", result);
closesocket(hSocket);
WSACleanup();
return 0;
}
void ErrorHandling(const char* message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
1. 피연산자의 바이트 수, 연산결과의 바이트 수를 상수화
2. 해당 예제에서 피연산자의 개수정보를 1바이트 정수형태로 전달했는데, 1바이트로 표현 불가능한 범위의 정수가 입력되면 안되고, 부호없는 양의 정수형태로 하는것이 합리적이다.(예제에서는 그냥 정수로 입력)
3. TCP는 소켓의 통신방식은 데이터 경계가 존재하지 않으므로, send()시 여러번에 나눠서 또는 한번에 보내게 된다.
4. 수신 데이터의 크기가 4바이트이기 때문에 이렇게 한번의 recv() 함수로 충분히 수신 가능하다.
[op_server.c]
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <WinSock2.h>
#pragma comment(lib, "ws2_32.lib")
#define BUF_SIZE 1024
#define OPSZ 4
void ErrorHandling(const char* message);
int Calculate(int opnum, int opnds[], char oprator);
int main(int argc, char* argv[])
{
WSADATA wsaData;
SOCKET hServSocket, hClntSocket;
char opinfo[BUF_SIZE];
int result, opndCnt, i;
int recvCnt, recvLen;
SOCKADDR_IN servAdr, clntAdr;
int clntAdrSz;
if (argc != 2)
{
printf("Usage : %s <port> \n", argv[0]);
exit(1);
}
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
{
ErrorHandling("WSAStartup() error");
}
hServSocket = socket(PF_INET, SOCK_STREAM, 0);
if (hServSocket == INVALID_SOCKET)
{
ErrorHandling("socket() error");
}
memset(&servAdr, 0, sizeof(servAdr));
servAdr.sin_family = AF_INET;
servAdr.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
servAdr.sin_port = htons(atoi(argv[1]));
if (bind(hServSocket, (PSOCKADDR)&servAdr, sizeof(servAdr)) == SOCKET_ERROR)
{
ErrorHandling("bind() error");
}
if (listen(hServSocket, 5) == SOCKET_ERROR)
{
ErrorHandling("listen() error");
}
clntAdrSz = sizeof(clntAdr);
for (i = 0; i < 5; ++i)
{
opndCnt = 0;
hClntSocket = accept(hServSocket, (PSOCKADDR)&clntAdr, &clntAdrSz);
if (hClntSocket == SOCKET_ERROR)
{
ErrorHandling("accept() error");
}
recv(hClntSocket, (char*)&opndCnt, 1, 0);
recvLen = 0;
while ((opndCnt * OPSZ + 1) > recvLen)
{
recvCnt = recv(hClntSocket, &opinfo[recvLen], BUF_SIZE - 1, 0);
recvLen += recvCnt;
}
result = Calculate(opndCnt, (int*)opinfo, opinfo[recvLen - 1]);
send(hClntSocket, (char*)&result, sizeof(result), 0);
closesocket(hClntSocket);
}
closesocket(hServSocket);
WSACleanup();
return 0;
}
void ErrorHandling(const char* message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
int Calculate(int opnum, int opnds[], char oprator)
{
int result = opnds[0], i;
switch (oprator)
{
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;
default:
/* Nothing */
break;
}
return result;
}
1. 피연산자 개수 정보 수신 후 피연산자 정보를 수신.
2. Calculate 함수 호출하면서 피연산자의 정보와 연산자 정보를 인자로 전달.
3. 결과를 클라이언트에게 전송.
#2. TCP의 이론적인 이야기
TCP 소켓에 존재하는 입출력 버퍼
TCP 소켓의 데이터 송수신에는 경계가 없음을 수 차례 말하였다. 따라서 서버가 한번의 write 함수호출을 통해서 40바이트를 전송해도 클라이언트는 네 번의 read 함수호출을 통해서 10바이트씩 데이터를 수신하는 것이 가능하다는 것을 위의 예제를 통해서도 확인하였다. 그런데 이러한 현상이 이상하다. 서버는 데이터를 한번에 40바이트를 전송했는데, 클라이언트가 이를 여유 있게 조금씩 수신하니.. 클라이언트가 10바이트만 먼저 수신했다면, 서버가 보낸 나머지 30바이트는 어디서 대기하고 있을까???? 30바이트가 네트워크 상에서 떠돌며 대기하고 있는 것은 아니지 않겠는가?
write 함수가 호출되는 순간이 데이터가 전송되는 순간이 아니고, read 함수가 호출되는 순간이 데이터가 수신되는 순간이 아니다. 정확히 말하면 write 함수가 호출되는 순간 데이터는 출력버퍼로 이동하고, read 함수가 호출되는 순간이 입력버퍼에 저장된 데이터를 읽어 들이게 된다.
※ 데이터 송/수신
(서버 -> 클라이언트로 write() 호출)
data를 서버의 출력버퍼로 전달하고, 이를 클라이언트의 입력버퍼로 (나눠서 보내든, 한번에 보내든) 전송한다.
(클라이언트 read() 호출)
클라이언트의 입력버퍼에 수신된 data를 읽어들인다.
[특징]
- 입출력 버퍼는 TCP 소켓 각각에 대해 별도로 존재한다.
- 입출력 버퍼는 소켓생성시 자동으로 생성된다.
- 소켓을 닫아도 출력버퍼에 남아있는 데이터는 계속해서 전송이 이뤄진다.
- 소켓을 닫으면 입력버퍼에 남아있는 데이터는 소멸되어버린다.
만약 이런 상황이라면 어떻게 동작할까?
문제) "클라이언트의 입력버퍼 크기가 50바이트인데, 서버에서 100바이트를 전송하였다."
수신측의 입력버퍼 크기 보다 전송데이터의 크기가 크다는 점은 꽤 큰 문제이다.
해결책) "입력버퍼가 채워지기 전에 잽싸게 read함수를 호출해서 데이터를 읽어 들이면 되지 않을까?"
이건 해결책이 될 수 없다.
정답) "입력 버퍼의 크기를 초과하는 데이터는 전송 자체가 불가하다."
즉, 우리가 고민하는 문제는 애초에 고려 할 필요가 없다.
TCP가 데이터의 흐름도 컨트롤 하기 때문이다.
TCP는 슬라이딩 윈도우(Sliding Window)라는 프로토콜이 있다.
이 프로토콜을 간단한 대화로 살펴보자.
- 소켓 A : 야 50바이트까지 보내도 괜찮아!
- 소켓 B : OK!
- 소켓 A : 내가 20바이트 비웠으니까, 70바이트까지 괜찮아!
- 소켓 B: OK!
이렇게 매번 송신측에서는 수신측의 데이터 수신가능한 양을 알 수 있기 때문에 버퍼가 차고 넘쳐서 데이터가 소멸되는 일은 TCP에서 발생하지 않는다.
[write 함수가 반환되는 시점]
write함수가, 그리고 윈도우의 send 함수가 반환되는 시점은 상대 호스트로 데이터의 전송이 완료되는 시점이 아닌, 전송할 데이터가 출력버퍼로 이동이 완료되는 시점이다. 그러나 TCP의 경우는 출력버퍼로 이동된 데이터의 전송을 보장하기 때문에 write 함수는 데이터의 전송이 완료되어야 반환이 된다. 라고 표현한다. 따라서 이 표현에 대한 정확한 이해가 필요하다.!
(즉, 일반적으로 write(send)함수의 반환 시점은 전송할 데이터가 전송측 출력버퍼로 이동이 완료된 시점이지만, TCP의 경우 데이터 신뢰성을 보장하는 통신 방식이기에 출력버퍼로 이동된 데이터가 반드시 상대측 소켓의 입력버퍼로 이동되는 것을 보장하게 된다. 그래서 표현을 할 때 전송이 완료되어야 반환된다 라고 표현을 한 것 뿐이다.)
TCP의 내부 동작원리1 : 상대 소켓과의 연결
TCP 소켓의 생성에서 소멸의 과정까지 거체기 되는 일을 크게 나누면 다음과 같다.
- 상대 소켓과의 연결
- 상대 소켓과의 데이터 송수신
- 상대 소켓과의 연결종료
소켓은 통신을 할 떄 3연결 설정과정에서 3번의 대화를 주고 받는다. 이를 가리켜 Three-way handshaking이라 한다.
[SYN] SEQ : 1000, ACK: -
[SYN+ACK] SEQ : 2000, ACK : 1001
[ACK] SEQ : 1001, ACK : 2001
소켓은 전 이중(Full-duplex) 방식으로 동작하므로 양방향으로 데이터를 주고받을 수 있다. 따라서 데이터 송수신에 앞서 준비과정이 필요하다. 먼저 연결요청을 하는 호스트 A가 호스트 B에게 다음 메시지를 전달하고 있다.
호스트 A -> 호스트 B [SYN] SEQ : 1000, ACK : -
이는 SEQ가 1000, ACK는 비어있음을 뜻하며, 풀어보면
"내가 지금 보내는 이 패킷에 1000이라는 번호를 부여하니, 잘 받았다면 다음에는 1001번 패킷을 전달하라고 내게 말해달라!"
이는 처음 연결요청에 사용되는 메시지이기 때문에 이 메시지를 가리켜 SYN이라 한다. 그리고 SYN은 Synchronization의 줄임 말로써, 데이터 송수신에 앞서 전송되는 '동기화 메시지'라는 의미를 담고 있다. 이어서 호스트 B가 호스트 A에게 다음 메시지를 전달하고 있다.
호스트 B -> 호스트 A [SYN+ACK] SEQ : 2000, ACK : 1001
SEQ가 2000, ACK가 1001을 의미이며 풀어보면,
"내가 지금 보내는 이 패킷에 2000이라는 번호를 부여하니, 잘 받았다면 다음에는 2001번 패킷을 전달하라고 내게 말해달라!"
ACK 1001은 다음을 의미한다.
"좀 전에 전송한 SEQ가 1000인 패킷은 잘 받았으니, 다음번에는 SEQ가 1001인 패킷을 전송하기 바란다."
즉, 처음 호스트 A가 전송한 패킷에 대한 '응답 메시지(ACK 1001)'와 함께 호스트 B의 데이터 전송을 위한 '동기화 메시지(SEQ 2000)'를 함께 묶어서 보내고 있다.
1. 데이터의 송수신에 앞서, 송수신에 사용되는 패킷에 번호를 부여
2. 이 번호 정보를 상대방에게 알림(데이터 손실을 막기 위함)
이렇듯 패킷에 번호를 부여하여 확인 절차를 거치기 때문에 손실된 데이터의 확인 및 재전송이 가능하고, TCP는 손실 없는 데이터의 전송을 보장한다.
호스트 A -> 호스트 B [ACK] SEQ: 1001, ACK : 2001
" 좀 전에 전송한 SEQ 2000인 패킷은 잘 받았으니, 다음 번에는 SEQ가 2001인 패킷을 전송하길 바래"
때문에 ACK 2001이 추가된 형태의 ACK 메시지가 전송되었다. 이로써 호스트 A, 호스트 B 상호간에 데이터 송수신을 위한 준비가 모두 되었음을 서로 인삭하게 되었다.
TCP의 내부 동작원리2 : 상대 소켓과의 데이터 송수신
처음 진행한 Three-way handshaking을 통해서 데이터의 송수신 준비가 끝났으니, 이제 본격적으로 데이터를 송수신 할 차례이다. 방식은 다음과 같다.
[호스트 A -> 호스트 B] SEQ : 1200, 100 byte data
[호스트 B -> 호스트 A] ACK 1301
[호스트 A -> 호스트 B] SEQ : 1301, 100byte data
[호스트 B -> 호스트 A] ACK 1402
...
여기서 호스트 A가 호스트 B에게 총 200바이트를 두 번에 나눠서 (두 개의 패킷에 나눠서) 전송하는 과정을 보였다. 먼저 호스트 A가 100바이트의 데이터를 하나의 패킷에 실어 전송하였는데, 패킷의 SEQ를 1200으로 부여하고 있다. 때문에 호스트 B는 이를 근거로 패킷이 제대로 수신되었음을 알려야 하기에, ACK 1301 메시지를 담은 패킷을 호스트 A에 전송하고 있다.
이 때 ACK 번호가 1201이 아닌 1301인 이유는 ACK 번호를 전송된 바이트 크기만큼 추가로 증가시켰기 때문이다. 이렇듯 ACK 번호를 전송된 바이트 크기만큼 추가로 증가시키지 않으면, 패킷의 전송은 확인할 수 있을지 몰라도, 패킷에 담긴 100바이트가 전부 전송되었는지, 아니면 그 중 일부가 손실되고 80바이트만 전송되었는지 알 방법이 없지 않은가? 그래서 다음의 공식을 기준으로 ACK 메시지르 전송한다.
ACK 번호 -> SYN 번호 + 전송된 바이트 크기 + 1
마지막에 1을 더한 이유는 Three-way handshaking에서도 보였듯이, 다음 번에 전달될 SEQ의 번호를 알리기 위함이다.
이제 데이터가 제대로 도착하지 않았을때를 예로 들어보자.
[호스트 A -> 호스트 B] SEQ 1200, 100 byte data
[호스트 B -> 호스트 A] ACK 1301
[호스트 A -> 호스트 B] SEQ 1301, 100 byte data -----> time out!!!!!!
[호스트 B -> 호스트 A] SEQ 1031, 100 byte data (resend)
[호스트 B -> 호스트 A] ACK 1402
SEQ 1301인 패킷에 100바이트 데이터를 실어서 호스트 B로 전송 -> Time out -> 이런 경우 호스트 A는 일정시간이 지나도 SEQ 1301에 대한 ACK 메시지를 받지 못하기 때문에 재전송을 진행한다.
이렇듯 데이터의 손실에 대한 재전송을 위해서, TCP 소켓은 ACK 응답을 요구하는 패킷 전송 시에 타이머를 동작시킨다. 그리고 해당 타이머가 time-out! 되었을 떄 패킷을 재전송한다.
TCP의 내부 동작원리3 : 상대 소켓과의 연결 종료
TCP 소켓은 연결종료도 매우 우아하게(?) 진행된다. 그냥 연결을 바로 끊어버리면, 상대방이 전송할 데이터가 아직 남았을 수 있기 때문에 상호간에 연결종료의 합의 과정을 거치게 된다.
- 소켓 A : 전 연결을 끊고자 합니다.
- 소켓 B : 아! 그러세요? 잠깐 기다리세요.
- 소켓 B : 네! 저도 준비가 끝났습니다. 그럼 연결을 끊으세요.
- 소켓 A : 네! 그 동안 즐거웠습니다.
먼저 소켓 A가 종료 메시지를 소켓 B에게 전달 -> 소켓 B는 해당 메시지의 수신을 소켓 A에게 알림, 이어서 소켓 B가 종료 메시지를 소켓 A에게 전달 -> 소켓 A는 해당 메시지의 수신을 소켓B에게 알리며 종료의 과정을 마침.
[호스트 A -> 호스트 B] FIN SEQ : 5000, ACK : -
[호스트 B -> 호스트 A] ACK SEQ : 7500, ACK : 5001
[호스트 B -> 호스트 A] FIN SEQ : 7501, ACK : 5001
[호스트 A -> 호스트 B] ACK SEQ : 5001, ACK : 7502
여기서 패킷 안에 삽입된 FIN은 종료를 알리는 메시지를 뜻한다. 즉, 상호간에 FIN 메시지를 한번씩 주고 받고서 연결이 종료되는데, 이 과정이 네 단계에 걸쳐서 진해오디기 때문에 이를 가리켜 Four-way handshaking이라고 부른다. 호스트 B에서 호스트 A로 ACK 5001값을 두 번 보냈는데, 이유는 호스트 A측에서 응답이 없어서 한번 더 보낸것 뿐이다.
[내용 확인문제]
01. TCP 소켓의 연결설정 과정인 Three-way handshaking에 대해서 설명해 보자. 특히 총 3회의 데이터 송수신이 이뤄지는데, 각각의 데이터 송수신 과정에서 주고 받는 데이터에 포함된 내용이 무엇인지 설명해보자.
답)
소켓 연결 설정 시 3번의 상호작용을 함으로써 연결을 하는 방식을 Three-way handshaking이라고 한다.
각각 다음과 같다.
[SYN] SEQ : 1000, ACK : - => 송신 패킷에 1000이라는 번호를 붙여서 동기화 메시지를 보낸다. 추후에 송신할때는 1001을 패킷번호로 붙여서 보내달라고 수신측이 요구하기를 원한다.
[SYN + ACK] SEQ : 2000, ACK : 1001 => 수신측에서 패킷을 잘 받았다면, 패킷에 2000이라는 번호를 붙이고, 다음에 수신측에서 송신측으로 데이터를 또 보낼때 2001을 패킷번호로 붙여서 보내달라고 송신측이 요구하기를 원한다.
그리고 ACK 1001은 1000패킷을 잘 받았으니, 다음에는 SEQ 1001인 패킷을 전송해달라는 것을 의미한다.
[ACK] SEQ : 1001, ACK : 2001 => 송신측에서 2000인 패킷을 잘받았으니 수신측이 송신측으로 보낼때는 1001을 패킷번호로 붙여서 전송해달라는 것을 의미한다.
02. TCP는 데이터의 전송을 보장하는 프로토콜이다. 그러나 인터넷을 통해서 전송되는 데이터는 소멸될 수 있다. 그렇다면 TCP는 어떠한 원리로 중간에 소멸되는 데이터의 전송까지 보장을 하는 것인지 ACK와 SEQ를 대상으로 설명해보자.
답)
호스트A가 호스트 B로 데이터를 전송한다고 해보자.
호스트 A->호스트 B / [SEQ] SYN : 1000, data 100byte,
패킷에 1000 번호를 붙여서 100bytes크기의 데이터를 보냈다.
호스트 B -> 호스트 A / [ACK] ACK : 1101,
호스트 A에서 보낸 패킷 번호 1000에 수신한 데이터의 크기 100을 더하고 + 1을 해서 응답 메시지를 보내어 데이터를 잘 수신했는지에 대한 응답을 보낸다.
그리고 다시 호스트 A -> 호스트 B로 데이터를 전송할때는 해당 ACK값을 SYN으로 사용하여 데이터와 함께 호스트 B로 보낸다. 만약 timeout되어 데이터 수신 응답을 호스트 A가 받지 못하거나, 잘못된 ACK값을 받는다면 재전송하게 된다.
이런 원리로 데이터의 전송을 보장하게 된다.
03. TCP 소켓을 기반으로 write 함수와 read 함수가 호출되었을 때의 데이터 이동을 입력버퍼와 출력버퍼의 상태와 더불어서 설명해보자.
답)
TCP소켓통신에서 송신측 TCP 소켓에서 write()함수를 호출하면 송신측 소켓의 출력버퍼로 데이터가 이동하고, write()함수를 반환한다. 그리고 OS 내부적으로 이를 수신측 입력버퍼로 전송한다. 만약, 출력버퍼 또는 입력버퍼가 꽉 찬 상태라면 더이상 송신이 불가능하다. 그리고 수신측 TCP 소켓에서 read()함수를 호출하면 입력버퍼에 있는 데이터를 읽고, 읽어들인 데이터의 크기를 반환한다. 이 때 만약 입력버퍼가 비어있다면, block이 걸린상태로 버퍼가 채워지기를 기다린다.
04. 데이터를 수신할 상대 호스트의 입력버퍼에 남아있는 여유공간이 50byte인 상황에서 write 함수호출을 통해서 70byte의 데이터 전송을 요청했을 때, TCP는 어떻게 이를 처리하는지 설명해보자.
답)
TCP는 '슬라이딩 윈도우(Sliding Window)'라는 프로토콜이 존재한다. 이 프로토콜은 현재 수신용 소켓의 입력 버퍼가 데이터를 얼마만큼 수용 가능한지를 송신측에 보내고, 만약 출력버퍼에 있는 데이터가 입력버퍼의 크기보다 더 크다면 전송하지 않는다.
05. Chapter 02에서 보인 예제 tcp_server.c(Chapter 01의 hello_server.c)와 tcp_client.c에서는 서버가 전송하는 문자열을 클라이언트가 수신하고 끝낸다. 그런데 이번에는 서버와 클라이언트가 한번씩 문자열을 주고 받는 형태로 예제를 변경해보자. 단! 데이터의 송수신이 TCP 기반으로 진행된다는 사실을 고려하여 문자열 전송에 앞서 문자열의 길이 정보를 4바이트 정수의 형태로 먼저 전송하기로 하자. 즉, 연결이 된 상태에서 서버와 클라이언트는 다음의 유형으로 데이터를 송수신해야 한다.
0006Hello?
그리고 문자열의 전송순서는 상관이 없으며 문자열의 종류도 임의로 결정해도 된다. 단, 총 3회 문자열을 주고받아야 한다.
답)
[tcp_server_5_win.c]
#include <stdio.h>
#include <stdlib.h>
#include <WinSock2.h>
#pragma comment(lib, "ws2_32.lib")
void ErrorHandling(const char* message);
int main(int argc, char* argv[]) {
WSADATA wsaData;
SOCKET hServSock, hClntSock;
SOCKADDR_IN servAddr, clntAddr;
int szClntAddr;
char message[] = "Go to the Hell!!!!";
int sendLen, recvLen;
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, IPPROTO_TCP);
if (hServSock == INVALID_SOCKET)
{
ErrorHandling("socket() error!");
}
memset(&servAddr, 0, sizeof(servAddr));
servAddr.sin_family = AF_INET;
servAddr.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
servAddr.sin_port = htons(atoi(argv[1]));
if (bind(hServSock, (PSOCKADDR)&servAddr, sizeof(servAddr)) == SOCKET_ERROR)
{
ErrorHandling("bind() error!");
}
if (listen(hServSock, 5) == SOCKET_ERROR)
{
ErrorHandling("listen() error!");
}
szClntAddr = sizeof(clntAddr);
hClntSock = accept(hServSock, (SOCKADDR*)&clntAddr, &szClntAddr);
if (hClntSock == INVALID_SOCKET)
{
ErrorHandling("accept() error!");
}
sendLen = sizeof(message);
do
{
printf("send size %d... \n", sendLen);
send(hClntSock, (const char*)&sendLen, sizeof(sendLen), 0);
recv(hClntSock, (char*)&recvLen, sizeof(recvLen), 0);
printf("recv size %d... \n", recvLen);
}
while (recvLen != sendLen);
printf("recv message %s... \n", message);
send(hClntSock, message, sizeof(message), 0);
closesocket(hServSock);
closesocket(hClntSock);
WSACleanup();
return 0;
}
void ErrorHandling(const char* message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
[tcp_client_5_win.c]
#include <stdio.h>
#include <stdlib.h>
#include <WinSock2.h>
#pragma comment(lib, "ws2_32.lib")
#define BUF_SIZE 1024
void ErrorHandling(const char* message);
int main(int argc, char* argv[])
{
WSADATA wsaData;
SOCKET hSocket;
SOCKADDR_IN servAddr;
char message[BUF_SIZE] = { 0, };
int recvLen, msgLen;
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, IPPROTO_TCP);
if (hSocket == INVALID_SOCKET)
{
ErrorHandling("socket() error!");
}
memset(&servAddr, 0, sizeof(servAddr));
servAddr.sin_family = AF_INET;
servAddr.sin_addr.S_un.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!");
}
recv(hSocket, (char*)&msgLen, sizeof(msgLen), 0);
send(hSocket, (char*)&msgLen, sizeof(msgLen), 0);
recvLen = recv(hSocket, message, msgLen, 0);
message[recvLen] = 0;
printf("Message from server %s \n", message);
closesocket(hSocket);
WSACleanup();
return 0;
}
void ErrorHandling(const char* message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
06. 파일을 송수신하기 위한 클라이언트와 서버를 구현하되, 다음 순서의 시나리오를 기준으로 구현해보자.
- 클라이언트는 프로그램 사용자로부터 전송 받을 파일의 이름을 입력 받는다.
- 클라이언트는 해당 이름의 파일전송을 서버에게 요청한다.
- 파일이 존재할 경우 서버는 파일을 전송하고, 파일이 존재하지 않을 경우 그냥 연결을 종료한다.
[tcp_server_file_win.c]
#include <stdio.h>
#include <stdlib.h>
#include <WinSock2.h>
#pragma comment(lib, "ws2_32.lib")
#define NAME_SIZE 256
#define BUF_SIZE 1024
void ErrorHandling(const char* message);
int main(int argc, char* argv[])
{
WSADATA wsaData;
SOCKET hServSock, hClntSock;
SOCKADDR_IN servAdr, clntAdr;
int clntAdrSize;
FILE* stream;
char fileName[NAME_SIZE], buf[BUF_SIZE];
int readLen;
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(&servAdr, 0, sizeof(servAdr));
servAdr.sin_family = AF_INET;
servAdr.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
servAdr.sin_port = htons(atoi(argv[1]));
if (bind(hServSock, (PSOCKADDR)&servAdr, sizeof(servAdr)) == SOCKET_ERROR)
{
ErrorHandling("bind() error");
}
if (listen(hServSock, 5) == SOCKET_ERROR)
{
ErrorHandling("listen() error");
}
clntAdrSize = sizeof(clntAdr);
hClntSock = accept(hServSock, (PSOCKADDR)&clntAdr, &clntAdrSize);
if (hClntSock == INVALID_SOCKET)
{
ErrorHandling("accept() error");
}
else
{
printf("Connected from client : %d \n", hClntSock);
}
recv(hClntSock, fileName, NAME_SIZE-1, 0);
errno_t err = fopen_s(&stream, fileName, "r");
if (err != 0)
{
ErrorHandling("file not found");
}
else
{
while ((readLen = fread_s((void*)buf, BUF_SIZE, 1, BUF_SIZE, stream)) != 0)
{
send(hClntSock, buf, readLen, 0);
}
fclose(stream);
}
closesocket(hClntSock);
closesocket(hServSock);
WSACleanup();
return 0;
}
void ErrorHandling(const char* message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
[tcp_client_file_win.c]
#include <stdio.h>
#include <stdlib.h>
#include <WinSock2.h>
#pragma comment(lib, "ws2_32.lib")
#define NAME_SIZE 256
#define BUF_SIZE 1024
void ErrorHandling(const char* message);
int main(int argc, char* argv[])
{
WSADATA wsaData;
SOCKET hSocket;
SOCKADDR_IN servAdr;
char buf[BUF_SIZE], fileName[NAME_SIZE], newFileName[NAME_SIZE] = "new_";
int recvLen;
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(&servAdr, 0, sizeof(servAdr));
servAdr.sin_family = AF_INET;
servAdr.sin_addr.S_un.S_addr = inet_addr(argv[1]);
servAdr.sin_port = htons(atoi(argv[2]));
if (connect(hSocket, (PSOCKADDR)&servAdr, sizeof(servAdr)) == SOCKET_ERROR)
{
ErrorHandling("connect() error");
}
else
{
printf("Connect.............. \n");
}
fputs("want file name : ", stdout);
scanf_s("%s", fileName, sizeof(fileName));
fileName[strlen(fileName)] = 0;
send(hSocket, fileName, strlen(fileName)+1, 0);
strcat_s(newFileName, NAME_SIZE, fileName);
FILE* stream;
errno_t err = fopen_s(&stream, newFileName, "wt");
if (err != 0)
{
ErrorHandling("wrong create file error");
}
else
{
while ((recvLen = recv(hSocket, buf, BUF_SIZE-1, 0)) != 0)
{
fwrite(buf, 1, recvLen, stream);
}
printf("SUCCESS!! : create file %s. \n", newFileName);
fclose(stream);
}
closesocket(hSocket);
WSACleanup();
return 0;
}
void ErrorHandling(const char* message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
'네트워크 > 열혈 TCP_IP 소켓프로그래밍' 카테고리의 다른 글
#7. 소켓의 우아한 연결종료 (0) | 2022.08.03 |
---|---|
#6. UDP 기반 서버/클라이언트 (0) | 2022.08.02 |
#4. TCP기반 서버/클라이언트 1 (0) | 2022.07.24 |
#3. 주소체계와 데이터 정렬 (0) | 2022.07.20 |
#2. 소켓의 타입과 프로토콜의 설정 (0) | 2022.07.13 |