TCP 제로 윈도우(Zero Window) 방지를 위한 고성능 멀티스레드 큐 아키텍처
리시브(Receive) 스레드와 처리(Process) 스레드를 엄격히 분리하고,
경량 프로세스(LWP) 스타일의 멀티스레드가 공유 큐를 소비하는 구조에서
TCP 제로 윈도우(Zero Window) 현상을 원천 차단하고 시스템 안정성을 극대화하기 위한 상세 설계 내용입니다!
예제 코드는 Rust로 작성되었습니다.
목차
1.
TCP 제로 윈도우 발생 원인 분석
2.
아키텍처 핵심 설계 원칙
3.
세부 구현 전략 및 기술 요소
4.
OS 커널 및 소켓 버퍼 최적화 튜닝
5.
최종 아키텍처 흐름도
6.
요약 및 체크리스트
1. TCP 제로 윈도우 발생 원인 분석
1.1 윈도우 메커니즘의 이해
TCP는 흐름 제어(Flow Control) 를 위해 수신 측이 남은 커널 버퍼 공간의 크기를
송신 측에 알려주는 수신 윈도우(Receive Window, RWIN) 기술을 사용합니다.
[송신 스레드] ---- 데이터 전송 (TCP Packet) ----> [커널 수신 버퍼 (Rx Buffer)]
│ (소켓 읽기 지연 시 적체)
▼
[수신 스레드] <--- 소켓 읽기 (recv / read) --------- [애플리케이션 영역]
Plain Text
복사
1.2 제로 윈도우 발생 조건
단계 | 설명 |
커널 버퍼 적체 | 리시브 스레드가 소켓에서 데이터를 읽어가는 속도보다 네트워크로 들어오는 속도가 빠를 때 발생 |
동기화 병목 | 리시브 스레드가 데이터를 읽었더라도, 공유 큐(Queue)에 넣는 과정에서 락 경쟁(Lock Contention) 으로 지연되면 커널 버퍼를 제때 비우지 못함 |
최종 결과 | 커널 수신 버퍼가 가득 차면 OS는 송신 측에 Window Size: 0 패킷을 전송 → 모든 네트워크 전송이 일시 중단되며 서비스 처리량이 급감 |
핵심!!!!!!!!: 읽기 속도 < 유입 속도 또는 큐 삽입 지연이 누적되면 커널 버퍼가 가득 차고, OS가 강제로 송신을 멈춰버립니다.
2. 아키텍처 핵심 설계 원칙
핵심 전략: "리시브 스레드에게 그 어떤 무거운 작업도 부여하지 않는다."
2.1 리시브 스레드 (생산자) 역할 최소화
•
오직 I/O만 담당: 소켓 버퍼에서 바이트를 긁어오는 작업만 수행
•
노 파싱 (No Parsing): 프로토콜 분석, 패킷 경계 확인, 역직렬화(Deserialization) 등 연산이 필요한 모든 작업은 리시브 스레드에서 제외
•
원시 데이터 투하: 읽어 들인 원시 바이트 블록(Raw Byte Block) 을 그대로 큐에 밀어 넣음
2.2 처리 스레드 풀 (소비자, Multi-LWP)의 독립성
•
무거운 로직 전담: 큐에서 원시 데이터를 꺼낸 후 패킷을 파싱하고 비즈니스 로직을 수행
•
멀티 CPU 코어 활용: 여러 LWP 스레드가 병렬로 큐를 소비하여 처리 속도를 극대화
역할 분리 (Separation of Concerns)
┌──────────────────────┐ ┌──────────────────────────┐
│ 리시브 스레드 (1개) │ push │ 처리 스레드 풀 (N개) │
│ - recv() 만 수행 │ ─────▶ │ - 파싱 / 역직렬화 │
│ - 가볍고 빠름 │ 큐 │ - 비즈니스 로직 / DB / IO │
│ - 절대 블로킹 금지 │ │ - CPU 코어별 병렬 처리 │
└──────────────────────┘ └──────────────────────────┘
Plain Text
복사
3. 세부 구현 전략 및 기술 요소
3.1 넌블로킹 I/O 및 이벤트 주도 리시브
리시브 스레드가 특정 소켓에 묶여 블로킹되면 다른 소켓에서 제로 윈도우가 터집니다.
•
Epoll / Kqueue 활용: 리눅스 환경에서는 Edge-Triggered epoll 을 사용하여
소켓에 데이터가 도착한 순간 버스트(Burst)하게 소켓 버퍼를 전부 비워야 함
•
Non-blocking Socket: 소켓을 O_NONBLOCK 으로 설정하여
EAGAIN 또는 EWOULDBLOCK 신호가 올 때까지 루프를 돌며 초고속으로 데이터를 수신
Rust 예제 — Non-blocking 소켓에서 버퍼를 전부 비우기 (Drain Loop)
Edge-Triggered epoll에서는 WouldBlock이 나올 때까지 한 번에 끝까지 읽어야 이벤트 누락이 없습니다.
use std::io::{ErrorKind, Read};
use std::net::TcpStream;
/// 소켓에 들어온 데이터를 EAGAIN/WouldBlock 이 발생할 때까지 모두 읽어
/// 원시 바이트 블록 단위로 큐에 밀어 넣는다. (No 파싱)
fn drain_socket<F>(stream: &mut TcpStream, mut on_block: F) -> std::io::Result<()>
where
F: FnMut(Vec<u8>),
{
// 반드시 논블로킹 모드여야 한다.
stream.set_nonblocking(true)?;
let mut buf = [0u8; 64 * 1024]; // 64KB 버스트 읽기
loop {
match stream.read(&mut buf) {
Ok(0) => break, // 상대가 연결 종료 (FIN)
Ok(n) => {
// 파싱하지 않고 원시 바이트만 복사해 큐로 전달
on_block(buf[..n].to_vec());
}
// 커널 버퍼를 모두 비웠다 → 루프 종료, 다음 epoll 이벤트 대기
Err(e) if e.kind() == ErrorKind::WouldBlock => break,
// 시그널에 의한 인터럽트 → 재시도
Err(e) if e.kind() == ErrorKind::Interrupted => continue,
Err(e) => return Err(e),
}
}
Ok(())
}
Rust
복사
실전에서는 mio, tokio, 또는 직접 epoll(예: libc/nix 크레이트)을 사용합니다.
아래 3.5에 tokio 기반 비동기 버전을 별도로 첨부했습니다.
3.2 고성능 락프리 큐(Lock-Free Queue) 설계
여러 스레드가 하나의 큐에 접근하므로 일반적인 뮤텍스(Mutex) 락을 쓰면
리시브 스레드가 대기 상태에 빠져 제로 윈도우의 원인이 됩니다.
•
링 버퍼 기반 락프리 큐(Ring Buffer Base): 생산자와 소비자의 포인터(Index)를 분리하고
Atomic(Compare-And-Swap) 연산을 사용하여 락 없이 데이터를 안전하게 교환
•
단일 생산자 - 다중 소비자(SPMC) 구조:
◦
리시브 스레드는 단 하나(Single Producer) 이므로 큐의 Write Pointer 경쟁이 없어 삽입 속도가 극대화
◦
소비자는 여러 LWP(Multi-Consumer)이므로 읽기 포인터만 원자적 연산으로 제어
Rust 예제 (A) — 크레이트 활용 (실무 권장)
직접 락프리 자료구조를 구현하는 것은 위험합니다. 검증된 크레이트를 쓰는 것이 정석입니다.
// Cargo.toml
// [dependencies]
// crossbeam-queue = "0.3" # 락프리 MPMC/SPSC 큐
use crossbeam_queue::ArrayQueue;
use std::sync::Arc;
use std::thread;
fn main() {
// 유한 크기(Bounded) 락프리 링 버퍼 — 3.3의 "방안 B"에 해당
let queue: Arc<ArrayQueue<Vec<u8>>> = Arc::new(ArrayQueue::new(1024));
// --- 단일 생산자 (리시브 스레드) ---
let producer_q = Arc::clone(&queue);
let producer = thread::spawn(move || {
for i in 0..10_000u32 {
let raw = i.to_be_bytes().to_vec(); // 원시 바이트 블록
// push 가 실패하면(큐가 가득 참) → 백프레셔 발동 지점 (3.3 참고)
while producer_q.push(raw.clone()).is_err() {
// 큐가 가득 참: 리시브를 잠시 멈춰 TCP 흐름 제어 유도
std::hint::spin_loop();
}
}
});
// --- 다중 소비자 (LWP 워커 풀) ---
let mut workers = Vec::new();
for id in 0..4 {
let consumer_q = Arc::clone(&queue);
workers.push(thread::spawn(move || {
loop {
match consumer_q.pop() {
Some(raw) => process_packet(id, &raw), // 무거운 로직 전담
None => std::hint::spin_loop(), // 큐가 비어 있음
}
}
}));
}
producer.join().unwrap();
// (실제 코드에서는 종료 신호 채널로 워커를 정리)
let _ = workers;
}
/// 처리 스레드: 파싱 + 비즈니스 로직 (CPU 바운드 작업)
fn process_packet(worker_id: usize, raw: &[u8]) {
// 여기서 비로소 파싱/역직렬화/비즈니스 로직 수행
let _value = u32::from_be_bytes(raw.try_into().unwrap_or([0; 4]));
// ... DB / 비즈니스 처리 ...
let _ = worker_id;
}
Rust
복사
Rust 예제 (B) — SPSC 링 버퍼 핵심 원리 (CAS 개념 학습용)
단일 생산자-단일 소비자 링 버퍼의 포인터 분리 + Atomic 동작 원리를 직접 구현한 예시입니다.
(다중 소비자는 pop 인덱스를 compare_exchange 로 경쟁시키면 확장됩니다.)
use std::cell::UnsafeCell;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
/// 고정 크기 락프리 링 버퍼 (SPSC). 용량은 2의 거듭제곱이어야 마스킹이 가능.
pub struct RingBuffer<T> {
buffer: Box<[UnsafeCell<Option<T>>]>,
capacity: usize,
mask: usize,
head: AtomicUsize, // 생산자만 증가 (write pointer)
tail: AtomicUsize, // 소비자만 증가 (read pointer)
}
unsafe impl<T: Send> Sync for RingBuffer<T> {}
impl<T> RingBuffer<T> {
pub fn new(capacity: usize) -> Arc<Self> {
assert!(capacity.is_power_of_two(), "capacity must be a power of two");
let buffer = (0..capacity)
.map(|_| UnsafeCell::new(None))
.collect::<Vec<_>>()
.into_boxed_slice();
Arc::new(Self {
buffer,
capacity,
mask: capacity - 1,
head: AtomicUsize::new(0),
tail: AtomicUsize::new(0),
})
}
/// 생산자: 큐가 가득 차면 Err(value) 반환 → 백프레셔 신호
pub fn push(&self, value: T) -> Result<(), T> {
let head = self.head.load(Ordering::Relaxed);
let tail = self.tail.load(Ordering::Acquire);
if head.wrapping_sub(tail) >= self.capacity {
return Err(value); // FULL → 리시브 멈춤(의도적 Zero Window)
}
let slot = &self.buffer[head & self.mask];
unsafe { *slot.get() = Some(value); }
// Release: 위 데이터 쓰기가 head 공개보다 먼저 보이도록 보장
self.head.store(head.wrapping_add(1), Ordering::Release);
Ok(())
}
/// 소비자: 큐가 비면 None
pub fn pop(&self) -> Option<T> {
let tail = self.tail.load(Ordering::Relaxed);
let head = self.head.load(Ordering::Acquire);
if tail == head {
return None; // EMPTY
}
let slot = &self.buffer[tail & self.mask];
let value = unsafe { (*slot.get()).take() };
self.tail.store(tail.wrapping_add(1), Ordering::Release);
value
}
}
Rust
복사
Ordering 주의: Release(쓰기 공개) / Acquire(읽기 동기화) 쌍이 메모리 가시성을 보장합니다.
실무에서는 직접 구현 대신 crossbeam-queue(MPMC) 또는 rtrb(SPSC)를 권장합니다.
3.3 백프레셔(Backpressure)와 메모리 관리 전략
처리 스레드가 감당할 수 없을 정도로 트래픽이 밀려올 때 시스템이 취해야 할 선택지입니다.
전략 | 특징 | 장점 | 단점 / 위험성 |
A. 무제한 큐 (Unbounded) | 큐 크기 제한 없음. 힙 메모리를 계속 할당 | 제로 윈도우가 절대 발생하지 않음 | 순간 폭주 시 메모리 고갈 → OOM Crash (프로세스 급사) 위험 |
B. 유한 큐 (Bounded) | 큐 최대 크기 제한. 임계치 도달 시 리시브 제어 | 메모리가 안전하게 보호됨 (시스템 안정성 확보) | 큐가 차면 리시브가 멈추므로 의도적인 TCP 제로 윈도우 발생 |
Rust 예제 — 유한 큐 기반 백프레셔
use crossbeam_queue::ArrayQueue;
use std::sync::Arc;
enum PushResult {
Ok,
Backpressure, // 큐가 가득 참 → 리시브를 멈춰 TCP Zero Window 유도
}
fn try_enqueue(queue: &Arc<ArrayQueue<Vec<u8>>>, raw: Vec<u8>) -> PushResult {
match queue.push(raw) {
Ok(_) => PushResult::Ok,
Err(_rejected) => {
// 핵심: 여기서 절대 무한 재할당(unbounded)하지 않는다.
// 리시브 루프를 잠시 멈추면 커널 Rx 버퍼가 차고,
// OS가 송신 측에 Window=0 을 보내 흐름을 자연스럽게 제어한다.
PushResult::Backpressure
}
}
}
Rust
복사
3.4 워커 스레드(LWP) 동적 확장 (Dynamic Scaling)
큐에 데이터가 쌓이는 속도를 실시간으로 모니터링하여 병목을 강제로 돌파합니다.
•
임계치 모니터링: 큐의 잔여 용량이 70% 를 넘어서면 비상 상황으로 인지
•
스레드 동적 포크: CPU 물리 코어 자원이 남아 있다면 LWP 스레드를 실시간으로 추가 생성(Spawn)하여 큐 소비 속도를 끌어올림
•
자원 회수: 트래픽이 안정되어 큐가 완전히 비면 최소 스레드 개수만 남기고 유휴 스레드를 정리
Rust 예제 — 큐 점유율 기반 워커 동적 확장
use crossbeam_queue::ArrayQueue;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::Arc;
use std::thread;
use std::time::Duration;
struct Pool {
queue: Arc<ArrayQueue<Vec<u8>>>,
capacity: usize,
active_workers: AtomicUsize,
min_workers: usize,
max_workers: usize,
}
impl Pool {
/// 큐 점유율이 70% 이상이고 코어 여유가 있으면 워커를 추가 spawn
fn scale(self: &Arc<Self>) {
let occupancy = self.queue.len() as f64 / self.capacity as f64;
let current = self.active_workers.load(Ordering::Relaxed);
if occupancy > 0.70 && current < self.max_workers {
self.active_workers.fetch_add(1, Ordering::Relaxed);
let pool = Arc::clone(self);
let worker_id = current + 1;
thread::spawn(move || pool.worker_loop(worker_id, true /* shrinkable */));
println!("[SCALE-UP] worker {} spawned (occupancy={:.0}%)", worker_id, occupancy * 100.0);
}
}
fn worker_loop(self: Arc<Self>, id: usize, shrinkable: bool) {
let idle = AtomicBool::new(false);
loop {
match self.queue.pop() {
Some(raw) => {
idle.store(false, Ordering::Relaxed);
// 파싱 + 비즈니스 로직
let _ = raw;
}
None => {
// 자원 회수: 동적 생성된 워커는 큐가 비면 최소 인원만 남기고 종료
if shrinkable
&& self.active_workers.load(Ordering::Relaxed) > self.min_workers
{
self.active_workers.fetch_sub(1, Ordering::Relaxed);
println!("[SCALE-DOWN] worker {} retired", id);
return;
}
thread::sleep(Duration::from_millis(1));
}
}
}
}
}
Rust
복사
실무에서는 rayon, tokio의 런타임 또는 워크-스틸링 스케줄러로 동적 확장을 대체하는 경우가 많습니다.
3.5 Tokio 기반 비동기 리시브 (실전 예제)
OS별 epoll/kqueue/IOCP를 추상화한 tokio로 동일 아키텍처를 구현하면
플랫폼 종속성을 없애면서도 동일한 "리시브-처리 분리 + 유한 큐 백프레셔"를 달성할 수 있습니다.
// Cargo.toml
// [dependencies]
// tokio = { version = "1", features = ["full"] }
use tokio::io::AsyncReadExt;
use tokio::net::{TcpListener, TcpStream};
use tokio::sync::mpsc::{self, error::TrySendError};
#[tokio::main]
async fn main() -> std::io::Result<()> {
let listener = TcpListener::bind("0.0.0.0:9000").await?;
// 유한(bounded) 채널 = 백프레셔 내장. 큐가 차면 송신이 대기/거절됨.
let (tx, mut rx) = mpsc::channel::<Vec<u8>>(1024);
// --- 처리(소비자) 태스크 풀: CPU 바운드는 spawn_blocking 으로 분리 ---
tokio::spawn(async move {
while let Some(raw) = rx.recv().await {
tokio::task::spawn_blocking(move || {
// 무거운 파싱 / 비즈니스 로직
let _ = raw;
});
}
});
// --- 리시브(생산자) 루프 ---
loop {
let (socket, _addr) = listener.accept().await?;
let tx = tx.clone();
tokio::spawn(async move { receive_loop(socket, tx).await });
}
}
async fn receive_loop(mut socket: TcpStream, tx: mpsc::Sender<Vec<u8>>) {
let mut buf = vec![0u8; 64 * 1024];
loop {
match socket.read(&mut buf).await {
Ok(0) => break, // 연결 종료
Ok(n) => {
let raw = buf[..n].to_vec(); // No 파싱 → 원시 바이트만
// try_send: 큐가 가득 차면 더 이상 읽지 않음 → 커널 Rx 버퍼가 차고
// OS가 Zero Window 로 송신 측을 자연 제어 (의도적 백프레셔)
match tx.try_send(raw) {
Ok(_) => {}
Err(TrySendError::Full(_)) => {
// 큐 포화: 잠시 대기하여 흐름 제어 유도
tx.send(buf[..n].to_vec()).await.ok(); // 또는 await로 블록
}
Err(TrySendError::Closed(_)) => break,
}
}
Err(_) => break,
}
}
}
Rust
복사
4. OS 커널 및 소켓 버퍼 최적화 튜닝
애플리케이션이 아무리 빨라도 OS 커널 단의 완충 지대(Buffer Space) 가 너무 작으면
찰나의 순간에 제로 윈도우가 발생할 수 있습니다.
4.1 시스템 최대 수신 버퍼 확장 (Linux 환경)
리눅스 서버 환경의 기본 TCP 버퍼 크기를 크게 확장하여 트래픽 버스트를 견디도록 설정합니다.
# 현재 설정 확인
sysctl net.ipv4.tcp_rmem
# 커널 설정 파일 수정 (/etc/sysctl.conf)
# [최소크기 초기크기 최대크기] 순서 설정 (최대 16MB 또는 32MB 권장)
net.ipv4.tcp_rmem = 4096 87380 16777216
net.core.rmem_max = 16777216
# 설정 반영
sysctl -p
Bash
복사
4.2 애플리케이션 소켓 버퍼 수동 할당
소켓이 생성되는 시점에 setsockopt 시스템 콜을 이용하여 수신 버퍼 크기를 강제로 상향합니다.
원본 C 예제
int sock = socket(AF_INET, SOCK_STREAM, 0);
int rcvbuf_size = 16 * 1024 * 1024; // 16MB
// 소켓 수신 버퍼 크기 강제 고정
setsockopt(sock, SOL_SOCKET, SO_RCVBUF, &rcvbuf_size, sizeof(rcvbuf_size));
C
복사
Rust 예제 — socket2 크레이트로 SO_RCVBUF 설정
// Cargo.toml
// [dependencies]
// socket2 = "0.5"
use socket2::{Domain, Protocol, Socket, Type};
use std::net::SocketAddr;
fn build_tuned_listener(addr: SocketAddr) -> std::io::Result<std::net::TcpListener> {
let socket = Socket::new(Domain::IPV4, Type::STREAM, Some(Protocol::TCP))?;
// 수신 버퍼 16MB 로 상향 (커널 net.core.rmem_max 한도 내에서 적용됨)
socket.set_recv_buffer_size(16 * 1024 * 1024)?;
// 논블로킹 + 주소 재사용
socket.set_reuse_address(true)?;
socket.set_nonblocking(true)?;
socket.bind(&addr.into())?;
socket.listen(1024)?;
Ok(socket.into())
}
Rust
복사
5. 최종 아키텍처 흐름도 (Data Flow)
[ 외부 송신 단말 ]
│
│ (TCP/IP 패킷 전송)
▼
[ OS Kernel Rx Buffer ] <--- sysctl로 최대 16MB 확보
│
│ (Epoll Edge-Triggered / Non-blocking Read) - 지연 요소 0%
▼
[ 리시브 스레드 (Producer) ]
│
│ (No 파싱 / 원시 바이트 블록 그대로 복사)
▼
[ 공유 큐 (Lock-Free / Ring Buffer) ] <--- 유한 크기로 메모리 보호
│
├───────────────────────┼───────────────────────┐ (Atomic CAS Pop)
▼ ▼ ▼
[ LWP 스레드 1 ] [ LWP 스레드 2 ] [ LWP 스레드 N ]
(패킷 파싱 & 비즈니스) (패킷 파싱 & 비즈니스) (동적 확장 워커)
Plain Text
복사
6. 요약 및 체크리스트
리시브 스레드에서 패킷 분석 코드를 모두 제거했는가?
→ 바이트 그대로 큐에 밀어 넣어야 함
공유 큐가 뮤텍스 락 병목을 유발하고 있진 않은가?
→ 가능하면 CAS 기반 락프리 링 버퍼 도입 (crossbeam-queue 등)
처리 스레드 내부에 동기식 DB Read/Write나 파일 I/O가 포함되어 있는가?
→ 워커 스레드 내부에서도 무거운 I/O는 다시 비동기 처리하거나 스레드를 대폭 늘려야 함
(Rust: spawn_blocking / 전용 IO 풀 분리)
리눅스 커널의 tcp_rmem 수신 버퍼가 충분히 확장되어 있는가?
유한 큐(Bounded)로 메모리를 보호하고, 포화 시 의도적 백프레셔를 적용하는가?
부록 — 권장 Rust 크레이트 정리
용도 | 크레이트 | 비고 |
락프리 MPMC/SPSC 큐 | crossbeam-queue | 유한(ArrayQueue)·무한(SegQueue) 모두 지원 |
초고속 SPSC 링 버퍼 | rtrb | 단일 생산자-단일 소비자 전용 |
비동기 런타임 (epoll/kqueue/IOCP 추상화) | tokio | mpsc 채널로 백프레셔 내장 |
저수준 이벤트 루프 | mio | epoll/kqueue 직접 제어 |
소켓 옵션(SO_RCVBUF 등) | socket2 | setsockopt 래퍼 |
데이터 병렬 처리 | rayon | 워크-스틸링 스레드 풀 |
비동기 진단/프로파일링 | tokio-console, flamegraph | 병목 추적 |
•
packet loss : 패킷이 유실된 경우, 보안으로 인해서 이런 경우도 많다.
•
TCP Out of Order : 패킷의 순서가 뒤바뀐 경우
•
Retransmission과 Duplicate ACK : 재전송과 중복 ACK, 네트워크 혼잡상황에 돌입
•
Zero-Window : 명백하게 응용 프로그램의 논리적 오류일 가능성이 매우 높은 유형으로, 수신측 버퍼에 여유공간이 하나도 없는 경우 -> 코드 다시 짜야함
2MB 파일 보낼때 일어나는일
•
세그먼트가 1000개 이상 나올텐데
•
증가하는 속도가 처음에는, 지수함수적으로 증가하다가, 로그함수적으로 증가하다가, 수렴하는 형태로 빠진다.
•
세그먼트 하나 최대 사이즈는 MSS(maximum segment size) : 1460Bytes
•
RTT(Round Trip Time) : 패킷이 보내진 후, 다시 돌아오는 시간
•
RTT가 너무 길어지면 안되어서 처음에는 지수함수적으로 증가한다.
•
그러다가 1,2,3,4 패킷을 보내다가 3 패킷이 유실되면 서버측에서는 ACK 3를 보내고, 클라이언트는 3을 받아서 3,4를 재전송한다. 이때 서버입장에서는 Retransmission을 받고, 클라이언트입장에서는 Duplicate ACK를 받는다.
•
만약에 1, 2 오고나서 4, 3 오면 TCP Out of Order가 발생하눈건대, 이걸 서버측에서 알아서 보정해주는건 SACK(Selective Acknowledgment)이다.
•
SACK은 ACK를 보낼 때, 특정 패킷에 대한 ACK를 보내는 것이 아니라, 패킷의 순서를 보정하기 위해 보내는 ACK이다. 3번만 다시 보내주는 것이다.
•
TCP Out of Order 일 경우 wait를 해서 뒤늦게 오는걸 할 경우 무제한으로 기다릴수도 없고, 다시 요청하는것도 네트워크 효율이 떨어질 수 있어서 TCP가 그래서 어렵다
•
네트워크 윈도우(Network Window) : 송신자가 수신자의 확인 응답(ACK)을 기다리지 않고 한 번에 전송할 수 있는 데이터의 최대 크기(바이트 단위)
•
TCP 슬라이딩 윈도우(Sliding Window) : 송신자가 수신자의 처리 속도나 네트워크 상황을 초과하여 데이터를 보내지 않도록, 한 번에 보낼 수 있는 데이터의 양(윈도우 크기)을 동적으로 조절하며 패킷의 흐름을 제어하는 통신 기법
•
서버랑 클라언트는 슬라이딩 윈도우를 공유하나요? > 아닙니다
•
TCP는 표준은 없고 규정이 있다. 윈도우 TCP랑 리눅스 TCP가 달랐고, wait 시간을 얼마나 길게 뺄껀지를 SW로 맘대로 구현 가능하다.
•
윈도우 사이즈 (Window Size) = '여유 공간의 크기'수신 버퍼(Buffer) 중 애플리케이션이 아직 읽어가지 않고 비어 있는 공간의 크기를 뜻합니다.이 공간은 고정되어 있지 않고, I/O 상황에 따라 실시간으로 늘어났다 줄어들었다 하
•
슬라이딩 윈도우 (Sliding Window) = '여유 공간을 유지하며 이동하는 방식'그 여유 공간(윈도우 사이즈) 만큼만 데이터를 계속 채워 넣는 전송 메커니즘입니다.데이터를 안전하게 처리했다는 확인이 오면, 딱 그만큼 창문(Window)을 옆으로 밀어서(Slide) 새로운 여유 공간을 확보하고 다음 데이터를 받아들인다.
•
윈도우 크기가 공유가 안 되기 때문에, 수신측의 윈도우 크기를 고려하지 않고, 그보다 크게 보낸다던가 하는 장애가 생길수 있는건가요? -> 이게 Zero-window
•
Zero-Window : 수신측 버퍼에 여유공간이 하나도 없는 경우
리시브랑 처리 별도 스레드로 무조건 구분해야한다. LWP도 비슷한 개념으로 자료구조를 따로 만들어서 Queue를 만들어, 여러 스레드가 Queue에서 꺼내도록
안녕하세요
•
관련 기술 문의와 R&D 공동 연구 사업 관련 문의는 “glory@keti.re.kr”로 연락 부탁드립니다.
Hello 
•
For technical and business inquiries, please contact me at “glory@keti.re.kr”
