Skip to content

Commit

Permalink
docs: TCP 핸드셰이킹 정리:
Browse files Browse the repository at this point in the history
  • Loading branch information
Parkjju committed Jul 27, 2024
1 parent 86c136f commit a84a41c
Show file tree
Hide file tree
Showing 11 changed files with 313 additions and 1 deletion.
Binary file added docs/.vuepress/assets/CS/4-1.jpeg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/.vuepress/assets/CS/4-2.jpeg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/.vuepress/assets/CS/5-1.jpeg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/.vuepress/assets/CS/5-2.jpeg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/.vuepress/assets/CS/5-3.jpeg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/.vuepress/assets/CS/5-4.jpeg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions docs/.vuepress/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,8 @@ module.exports = {
'/CS/network-1',
'/CS/network-2',
'/CS/network-3',
'/CS/network-4',
'/CS/network-5',
],
},
{
Expand Down
2 changes: 2 additions & 0 deletions docs/CS/network-1.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ int main(void)
1. `int open(const char *path, int flag);`: path에 파일 이름을 전달하고 flag에 파일 오픈 모드를 전달한다.
2. 파일 오픈 모드는 `O_CREATE`, `O_TRUNC`등 여러가지가 존재한다. 위 코드에서는 파일 생성 모드, 쓰기 전용 모드, 기존 데이터 삭제 모드를 적용하게 된다.
3. `ssize_t write(int fd, const void * buf, size_t nbytes)`: fd에는 쓰기 연산을 진행할 파일 디스크립터를 전달한다. buf에는 전송할 데이터 주소값을 전달한다. nbytes에는 전송할 데이터 바이트 수를 전달한다.
- 함수 호출 성공시 전달한 바이트 수를 반환한다.
```c
// 파일 읽기
Expand All @@ -199,3 +200,4 @@ int main(void)

1. `open`함수에 `O_RDONLY`모드를 전달하며 파일을 생성한다.
2. `ssize_t read(int fd, void *buf, size_t nbytes)`: fd에 파일 디스크립터를 전달한다. buf에 읽어들인 데이터를 저장할 버퍼 사이즈를 전달한다. nbytes에 얼마의 사이즈만큼 읽어들일지 전달한다.
- 함수 호출 성공 시 수신한 바이트 수를 반환한다.
110 changes: 109 additions & 1 deletion docs/CS/network-3.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,4 +64,112 @@ if (bind(serv_sock, (struct sockaddr*) &serv_addr, sizeof(serv_addr)) == -1)
위 코드에서 `servaddr_in` 구조체가 `bind` 함수 인자에 전달된다. 근데 함수 매개변수 타입이 `sockaddr`이므로 타입 캐스팅을 진행하고, 메모리 레이아웃이 동일하기 때문에 형 변환이 정상적으로 이루어진다.
`sin_zero`변수는 아예 사용조차 되지 멤버이다.
`sin_zero`변수는 아예 사용조차 되지 않는 멤버이다.
## 네트워크 바이트 순서와 인터넷 주소 변환
CPU가 빅 엔디안, 리틀 엔디안 등인지에 따라 상위 바이트를 메모리 주소에 저장하는 방법이 달라진다. 이는 네트워크 통신 과정에서 각 호스트별로 데이터 표현 방식이 다르다는 것을 의미한다.
이를 위해 **네트워크 바이트 순서**라는 체계를 도입하게 된다. 빅 엔디안과 리틀 엔디안이 **호스트 바이트 순서이다.** 이는 CPU별로 다른 데이터 저장방식을 의미한다.
네트워크 통신 표준을 위해 **네트워크 바이트 순서는 빅 엔디안을 기준으로 한다.** 이러한 작업은 개발자가 직접 하는 것이 아닌 **소켓이 내부적으로 엔디안 변환 처리를 해준다.**
**소켓 생성 이후에는** 엔디안 변환 처리를 내부적으로 자동으로 해주지만 **소켓 생성 과정에서는** 데이터 엔디안 처리를 직접 해줘야 한다.
1. `unsigned short htons(unsigned short);`: 호스트 바이트 순서를 네트워크 순서로 바꿔라
2. `unsigned short ntohs(unsigned short);`: 네트워크 바이트 순서를 호스트 바이트 순서로 바꿔라
3. `unsigned long htonl(unsigned long);`, `unsigned long ntohl(unsigned long);`
`h`는 호스트, `n`은 네트워크, `s`는 `short`자료형, `l`은 `long` 자료형을 의미한다.
기본적으로 CPU별로 엔디안 시스템이 어떻게 갖춰져 있는지 호스트마다 다르기 때문에 코드 작성시 변환 함수는 호출해주는 것이 안전하다.
## 인터넷 주소의 초기화와 할당
```c
#include <arpa/inet.h>
in_addr_t inet_addr(const char * string);
```

`inet_addr`함수는 `211.214.107.99`와 같이 점이 찍힌 10진수 문자열을 전달했을 때 문자열 정보를 참조하여 32비트 IP정수형 주소를 반환해준다. 반환 시 네트워크 바이트 주소인 빅 엔드안으로 변환해준다.

```c
#include <stdio.h>
#include <arpa/inet.h>

int main(int argc, char *argv[])
{
char *addr1="127.212.124.78";

unsigned long conv_addr=inet_addr(addr1);
if(conv_addr==INADDR_NONE)
printf("Error occured! \n");
else
printf("Network ordered integer addr: %#lx \n", conv_addr);

return 0;
}
```
IP주소를 반환받아서는 `sockaddr_in` 구조체의 IP주소 저장 멤버에 데이터를 할당해야 한다. `inet_addr`을 통해 반환받은 IP주소를 구조체 멤버에 직접 할당해도 되지만, 멤버의 참조를 `inet_aton`함수에 전달하면 반환 성공 시 해당 멤버 주소를 참조하여 직접 IP주소 할당까지 해준다.
```c
#include <arpa/inet.h>
// 성공 시 1, 실패 시 0 반환
int inet_aton(const char * string, struct in_addr * addr);
```

`inet_ntoa`는 네트워크 바이트 주소 데이터를 눈으로 인식하기 쉬운 문자열 형태로 반환해준다.

아래는 인터넷 주소 초기화 전체 코드이다.

```c
struct sockaddr_in addr; // 소켓 데이터 구조체 선언
char *serv_ip = "211.217.168.13"; // IP주소 문자열 선언
char *serv_port = "9190"; // 포트번호 문자열 선언
memset(&addr, 0, sizeof(addr)); // 소켓 데이터 모든 멤버 초기화
addr.sin_family = AF_INET; // 주소체계 지정 - IPv4

// inet_addr로 IP 주소 문자열 네트워크 바이트 주소로 변환
addr.sin_addr.s_addr = inet_addr(serv_ip);

// 문자열 기반 "9190" 데이터를 정수형으로 변환하고 네트워크 바이트 주소로 변환 및 할당
addr.sin_port = htons(atoi(serv_port));
```
`INADDR_ANY`는 현재 실행중인 컴퓨터의 IP주소를 불러와준다.
```c
addr.sin_addr.s_addr = htonl(INADDR_ANY);
```

:::tip 루프백 주소 127.0.0.1

흔히 로컬호스트라고 부르는 127.0.0.1 주소는 루프백 주소를 의미한다. 네트워크로 나아가지 않고 프로그램이 실행되는 환경으로 다시 되돌아오게끔 약속되어 있는 주소이다.

:::

소켓에 인터넷 주소를 할당하기 위해서는 `bind`함수를 호출하게 된다.

```c
#include <sys/socket.h>

// 성공 시 0, 실패 시 1을 반환한다.
int bind(int sockfd, struct sockaddr *myaddr, socklen_t addrlen);
```
1. `sockfd`: 주소정보를(IP & PORT) 할당할 소켓의 파일 디스크립터
2. `myaddr`: 주소정보를 지니는 구조체 변수 주소 값
3. `addrlen`: `myaddr` 구조체 변수의 길이정보
```c
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");
```
84 changes: 84 additions & 0 deletions docs/CS/network-4.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
---
title: TCP/IP 소켓 프로그래밍 4
tags: ['Network', 'Computer Science', 'iOS']
---

## TCP / IP 프로토콜 스택

`TCP/IP``프로토콜 스택이다.` 프로토콜이 스택 형태로 쌓여 있다는 것을 의미한다. 인터넷 기반이 데이터 송수신을 목적으로 설계된 스택을 의미한다.

소켓 프로그래밍 기준으로는 데이터 송수신 과정을 네 영역으로 계층화하여 이야기 할 수도 있다.

1. `LINK 계층`: 물리적인 계층
2. `IP 계층`: 라우팅 알고리즘 표준화, 경로 설정
3. `TCP 계층 or UDP 계층`: 구체적인 데이터 전송 방식의 구분
4. `APPLICATION 계층`:

> OSI 7 계층 모델은 물리 계층, 데이터 링크 계층, 네트워크 계층, 전송 계층, 세션 계층, 표현 계층, 응용 계층으로 구성되어 있다.
각 영역은 영역별로 전문화 되어 있으며, 동시에 표준화 되어 있다. 라우터는 IP계층에서 동작하는데 IP계층에서 따르는 라우팅 알고리즘을 라우터 내에 적용해야 하는 것이 바로 표준화가 갖는 의미이다.

소켓을 생성하면 보통 `LINK`, `IP`, `TCP & UDP` 계층까지 자동화해준다. 개발자는 `APPLICATION` 계층에서 컨트롤만 잘 해주면 된다. 전송 형태를 개발자가 정의하고 소켓 생성 및 데이터 전송 타이밍, 형태 등 다양하게 직접 커스텀해주면 된다.

## TCP 서버의 함수 호출 순서

1. `socket()`: 소켓 생성
2. `bind()`: 소켓 주소 할당, bind 호출 단계 까지는 소켓의 용도가 결정되지 않은 상태이다.
3. `listen()`: 연결요청 대기, 해당 단계를 통해 리스닝 소켓 (서버 소켓)이라는 정체성이 확립된다.
4. `accept()`: 연결 허용
5. `read() / write()`: 데이터 송수신
6. `close()`: 연결 종료

연결 요청 대기 상태로 진입하는 `listen`함수가 중요하다.

```c
#include <sys/type.h>

// 성공 시 0, 실패 시 -1 반환
int listen(int sock, int backlog);
```
1. `sock`: 연결 요청 대기상태에 두고자 하는 소켓 파일 디스크립터를 전달한다.
2. `backlog`: 연결요청 대기 큐 크기정보를 전달한다. 클라이언트의 연결 요청 갯수를 제한할 수 있다.
리스닝 소켓은 생성 뒤에 클라이언트로부터 날라오는 요청들을 연결 요청 대기 큐에 push하는 작업을 할 뿐이다. 연결 요청 대기큐에서 데이터를 pop하여 클라이언트 소켓과 통신 연결을 형성하는 작업은 `accept`함수 호출을 통해 이루어진다.
```c
#include <sys/socket.h>
// 소켓 시 생성된 소켓 파일 디스크립터, 실패 시 -1 반환
int accept(int sock, struct sockaddr * addr, socklen_t * addrlen);
```

1. `sock`: 서버 소켓 (리스닝 소켓)의 파일 디스크립터 전달. 파일 디스크립터가 전달되는 이유는 연결 요청 대기 큐와 서버 소켓이 1대1 대응 관계이기 때문이다. 서버 소켓이 2개 생성된다는 의미는 연결 요청 대기큐가 2개 생성된다는 의미이다.
2. `addr`: 연결 요청을 한 클라이언트 소켓 주소정보를 담을 주소 값. 함수호출 완료 이후 addr 변수에 클라이언트 주소 정보가 채워진다.
3. `addrlen`: `addr` 변수의 크기를 바이트 단위로 전달한다. 변수의 참조를 전달해야 하며, 함수 호출 완료 이후에는 연결 요청된 클라이언트의 주소 정보 길이가 바이트 단위로 계산되어 할당된다.

## TCP 클라이언트 함수 호출 순서

1. `socket()`: 소켓 생성
2. `connect()`: 연결 요청. 함수 호출 후 **클라이언트 소켓은 BLOCK 상태가 된다.** 해당 상태에서 빠져나오기 위해서는 서버로부터 `accept`가 호출되었음을 확인 한 뒤에 이루어진다.
3. `read() / write()`: 데이터 송수신
4. `close()`: 연결 종료

```c
#include <sys/socket.h>

int connect(int sock, const struct sockaddr * servaddr, socklen_t addrlen);
```
`connect`함수 각 인자가 하는 역할은 리스닝 소켓의 `accept`함수 인자의 역할과 동일하다.
:::warning accept 함수 호출로 생성된 소켓의 포트 번호는?
accept로 생성된 소켓의 포트번호는 **리스닝 소켓 시 할당한 포트 번호와 동일하다.** 편의상 accept 소켓이라고 명명했을 때, 서로 다른 클라이언트 소켓으로부터 서버의 accept 소켓으로 연결이 이루어질 때 어떤 데이터를 전송해야 할지 선택해야 하는 상황을 가정해보자.
![4-1](../.vuepress/assets/CS/4-1.jpeg)
accept 소켓의 경우 클라이언트와 연결이 맺어질 때 클라이언트의 소켓 주소를 보유하게 된다. 그렇기 때문에 `accept`함수에서 `addr`변수가 존재하게 되는 것이다.
:::
![4-2](../.vuepress/assets/CS/4-2.jpeg)
위 그림에서 `listen`함수 호출 이후에 클라이언트로부터 `connect`요청이 언제 들어올 줄 알고 `accept`를 호출하는 걸까? 서버는 `accept`함수 호출 이후에 BLOCK 상태가 되어 대기하게 되고, 클라이언트로부터 `connect` 호출이 이루어지는 경우 `accept`를 빠져나와 데이터 송수신이 본격적으로 이루어지게 된다.
116 changes: 116 additions & 0 deletions docs/CS/network-5.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
---
title: TCP/IP 소켓 프로그래밍 5
tags: ['Network', 'Computer Science', 'iOS']
---

## 클라이언트 & 서버코드 문제점

아래는 서버 소켓 코드이다. `accept`호출 이후 `connect`로 요청이 들어온 클라이언트 소켓 파일 디스크립터를 `clnt_sock`에 저장하게 된다. 이후 read & write로 데이터 읽고 쓰기를 진행한다.

```c
// 서버 소켓 코드
clnt_sock=accept(serv_sock, (struct sockaddr*)&clnt_adr, &clnt_adr_sz);
if(clnt_sock==-1)
error_handling("accept() error");
else
printf("Connected client %d \n", i+1);

while((str_len=read(clnt_sock, message, BUF_SIZE))!=0)
write(clnt_sock, message, str_len);

close(clnt_sock);
```
아래는 클라이언트 소켓 코드 중 일부이다. 아래 클라이언트 코드에서는 메시지를 입력받고 서버에 전달한 뒤, `connect`이후 서버로부터 쓰여진 해당 문장을 그대로 읽어들여 출력하게 된다.
```c
fputs("Input message(Q to quit): ", stdout);
fgets(message, BUF_SIZE, stdin);
if(!strcmp(message,"q\n") || !strcmp(message,"Q\n"))
break;
write(sock, message, strlen(message));
str_len=read(sock, message, BUF_SIZE-1);
message[str_len]=0;
printf("Message from server: %s", message);
```

TCP 소켓의 경우 데이터 경계가 존재하지 않기 때문에 클라이언트로부터 입력받은 문자열 데이터가 어떤 기준으로 구분되는지 알지 못한다. 클라이언트로부터 10바이트 문자열을 입력받더라도 서버가 5바이트 데이터만 읽고 쓰기가 가능하도록 코드를 작성했다면 해당 데이터만큼만 클라이언트에게 전송하게 된다.

## 문제점 개선

클라이언트의 코드를 다음과 같이 수정하면 된다.

```c
// 문자열 길이
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);
```
1. `write`가 리턴하는 입력 문자열에 대한 바이트 수를 변수에 저장해둔다.
2. `read`를 통해 읽어들인 바이트 수를 0으로 초기화 해둔 카운트 변수에 더해준다.
3. 카운트 변수에 저장되 바이트 수가 `write`에서 반환받은 바이트 수보다 적으면, 서버로부터 데이터를 아직 다 읽어들이지 못한 것이므로 반복문을 통해 데이터 읽어들이기를 다시 요청한다.
위 코드를 통해 명심할 점은 TCP 기반의 소켓이 데이터 경계가 없기 때문에 데이터 경계 구분이 필요한 로직을 구성할 경우에 클라이언트 단에서 구분지을 로직을 구현해야 한다는 것이다!
## TCP 입출력 버퍼
대부분의 입출력 과정에 버퍼가 필요한 이유는 서로 관계가 맺어진 채로 서로간의 입출력에만 집중할 수 없기 때문이다. 다른 작업을 하는 과정에서 버퍼를 살펴보았을 때 처리할 데이터가 있으면 가져가서 작업을 처리하게 되는 방식이다.
TCP 프로토콜도 이러한 이유로 입출력 버퍼가 필요하다. 아래 그림을 살펴보자
![5-1](../.vuepress/assets/CS/5-1.jpeg)
`write()` & `read()` 함수 호출은 그 즉시 데이터의 송수신이 이루어진다는 것이 아니라, 데이터를 입출력 버퍼에 실어 나르는 행위를 의미한다. 입출력 버퍼에 데이터가 적재되고 난 이후에 OS가 알아서 데이터를 가져다가 사용하는 방식이다.
`write` 호출 이후 출력버퍼에 데이터를 보유한 채로 TCP 프로토콜에 따라 OS가 데이터 수신자에게 데이터를 보내도 되는지 물어본다. 수신자는 송신자에게 50바이트까지 데이터를 가져와도 된다고 알람을 보내고, 이에 따라 송신자는 데이터를 수신자 입력 버퍼에 전달한다.
데이터 수신자의 입력버퍼 상황에 따라 데이터가 미닫이 창문이 차근히 옆으로 밀려나는 듯한 모습을 보이기 때문에 이를 슬라이딩 윈도우 프로토콜이라고 부른다.
슬라이딩 프로토콜로 인해 버퍼가 흘러 넘쳐서 데이터가 소실되는 일은 없게 된다.
## TCP 동작 1. 상대 소켓과의 연결
다음은 소켓 통신 과정에서의 메시지 종류를 나타낸다.
![5-2](../.vuepress/assets/CS/5-2.jpeg)
1. `SYN`: `SEQ`는 보내는 데이터 패킷의 번호를 의미한다.
2. `SYN+ACK`: `ACK: 1001`은 1000번 데이터 수신이 정상적으로 이루어졌고 1001번 부터 다시 송신하라는 의미이다. 호스트 B에서 보내는 응답 메시지를 `SEQ 2000`번에 할당해서 호스트 A에게 송신한다.
3. `ACK`: 호스트 A가 다시 보내는 메시지는 `SEQ 1001`번이고, 호스트 B에게 다음 메시지부터는 2001부터 패킷 번호를 할당해서 보내면 된다고 응답한다.
전달하고자 하는 실제 데이터가 정상적으로 송수신이 이루어졌음 외에도, 잘 받았음을 나타내는 알람 목적의 메시지도 `SEQ`넘버가 할당되어 송수신 된다.
## TCP 동작 2. 상대 소켓과의 데이터 송수신
데이터 송수신 과정에서 `ACK`번호는 전송된 바이트 크기만큼 증가시킨다. 즉 `ACK = SEQ + 전송된 바이트 크기 + 1`에 해당한다.
![5-3](../.vuepress/assets/CS/5-3.jpeg)
데이터 송신자의 SEQ를 확인하고 수신자는 `ACK번호를 수신한 데이터 크기 + increment`를 해줌으로써 데이터를 손실없이 잘 받았음을 묵시적으로 알리게 된다.
또한 SEQ 전송 이후 타이머가 작동하게 되고 SEQ에 대한 ACK가 전송되지 않은 경우 동일한 SEQ를 기준으로 데이터를 재전송하게 된다.
## TCP 동작 3. 상데 소켓과의 연결 종료
종료에 대한 알람까지 송수신자가 서로 주고받음으로써 여전히 데이터를 전달할 의사가 있는지 서로 알 수 있다.
![5-4](../.vuepress/assets/CS/5-4.jpeg)
1. 데이터 송신자가 `FIN`메시지를 SEQ 번호와 함께 보낸다.
2. 수신자가 `FIN` 메시지를 받고 `SEQ + 1` ACK를 보낸다.
3. 수신자도 송신자에게 `FIN`을 보낸다.
4. 송신자도 수신자에게 `ACK`를 보냄으로써 최종적인 소켓 통신이 마무리된다.
FIN 메시지까지 서로 주고받음으로써 이제 더 이상 전달할 데이터가 없음을 확신하고 통신을 종료한다.

0 comments on commit a84a41c

Please sign in to comment.