본문 바로가기
Study/네트워크 프로그래밍

TCP

by misty2913 2022. 2. 23.
반응형

TCP (Transmission Control Protocol)

  • 데이터 전송과정의 컨트롤

TCP/IP 프로토콜 스택

  • 표준화 작업을 통한 ‘개방형 시스템 (Open System)’
LINK 계층- 물리적인 영역의 표준화 - LAN, WAN, MAN 등 같은 네트워크 표준과 관련된 프로토콜을 정의하는 영역
IP 계층- 목적지로 데이터를 전송하기 위한 중간경로 (경로탐색) - 하나의 데이터 패킷이 전송되는 과정에 중점 - 데이터를 전송할 때마다 거쳐야 할 경로를 선택, but, 경로가 읽정치 않음 → 데이터 손실, 오류 등의 문제 발생할 수 있음 → 오류발생에 대한 대비가 되어있지 않은 프로토콜
TCP/UDP 계층 (Transport 계층)- IP계층에서 알려준 경로정볼르 바탕으로 데이터의 실제 송수신 수행 - TCP : 확인절차를 걸쳐서 신회성 없는 IP에 신뢰성을 부여한 프로토콜
Application 계층- 프로그램의 성격에 따라 클라이언트와 서버간의 데이터 송수신에 대한 약속 (규칙) 존재

TCP 서버의 기본적인 함수호출 순서

함수기능TCP 동작
socket()소켓 생성
bind()소켓의 주소정보(IP, PORT) 할당
listen()연결요청이 가능한 상태로 변경- 연결요청 대기상태
accept()연결요청에 대한 수락- ‘연결요청 대시 큐’에서 대기중인 클라이언트의 연결요청을 수락
  • 연결요청 대기상태
    #include <sys/socket.h>   // Linux
    
    int listen(int sock, int backlog);
    // 성공 시 0, 실패 시 -1 반환
    • sock : 서버 소켓 (리스닝 소켓) 파일 디스크립터
    • backlog : 연결요청 대기 큐 (Queue)의 크기정보 전달. 클라이언트의 연결요청을 몇개까지 대기시킬 수 있는지
      • 서버의 경우에는 최소 15이상을 전달
  • 클라이언트의 연결요청 수락
    #include <sys/socket.h>
    
    int accept(int sock, struct sockaddr * addr, socklen_t * addrlen);
    // 성공 시 파일 디스크립터, 실패 시 -1 반환
    • 함수 호출 성공 시 내부적으로 데이터 입룰력에 사용할 소켓 생성, 그 소켓의 파일 디스크립터 반환
      • 소켓이 자동 생성
      • 연결요청을 한 클라이언트 소켓에 연결까지 이뤄짐

TCP 클라이언트의 기본적인 함수호출 순서

함수기능TCP 동작
socket()소켓 생성
connect()연결요청을 진행- 연결요청 - 자동으로 소켓에 IP와 PORT 정보가 할당됨 → bind 함수를 명시적으로 호출할 필요 없음
  • 연결요청
    #include <sys/socket.h>
    
    int connect (int sock, struct sockaddr * servaddr, socklen_t addrlen);
    // 성공 시 0, 실패 시 -1 반환
    • sock : 클라이언트 소켓의 파일 디스크립터
    • servaddr : 연결요청 할 서버의 주서정보를 담은 변수의 주소값
    • 상황
      • 서버에 의해 연결 요청이 접수됨 = 클라이언트의 연결요청 정보가 서버의 연결요청 대기 큐에 등록된 상황

      → 당장 서비스가 이뤄지지 않을 수 도 있음

      • 네트워크 단절 등 오류상황이 발생해서 연결요청이 중단됨

TCP 소켓에 존재하는 입출력 버퍼

  • write 함수가 호출되는 순간 데이터는 출력 버퍼로 이동
  • read 함수가 호출되는 순간 입력버퍼에 저장된 데이터를 읽음
  • 특징
    • 입출력 버퍼는 TCP 소켓 각각에 대해 별도로 존재
    • 입출력 버퍼는 소켓생성시 자동으로 생성
    • 소켓을 닫아도 출력버퍼에 남아있는 데이터는 계속해서 전송이 이뤄진다
    • 소켓을 닫으면 입력버퍼에 남아있는 데이터는 소멸된다
  • 슬라이딩 윈도우 (Sliding Window)
    • 데이터의 흐름까지 컨트롤

TCP 동작원리 (흐름제어)

  • 상대 소켓과의 연결
    • Three-way handshaking
      • A → SYN(1000, 0) → B
      • A ← SYN+ACK(2000, 1001) ← B
      • A → ACK(1001, 2001) → B
  • 상대 소켓과의 데이터 송수신
    • ACK 번호 = SEQ 번호 + 전송된 바이트 크기 + 1 (다음번에 전달된 SEQ 번호를 알리기 위함)
  • 상대 소켓과의 연결종료
    • Four-way handshaking
      • A → FIN (5000, 0) → B
      • A ← ACK (7500, 5001) ← B
      • A ← FIN (7501, 5001) ← B
      • A → ACK (5001, 7502) → B

우아한 연결종료

  • 완전종료 : close / closesocket
    • 데이터를 전송하는 것과 수신하는 것이 불가능한 상황
    • 데이터의 송수신에 사용되는 스트림의 일부만 종료 (Half-close)하는 방법
      • 전송은 가능하지만 수신은 불가
      • 수신을 가능하지만 전송은 불가
  • 하나의 스트림만 끊는 것
    #include <sys/socket.h>
    
    int shutdown(int sock, int howto);
    • sock : 종료할 소켓의 파일 디스크립터
    • howto : 종료방법에 대한 정보 전달
      SHUT_RD입력 스트림 종료- 입력 스트림 종료, 입력관련 함수 호출 안됨
      SHUT_WR출력 스티림 종료- 데이터 전송 불가능 - 출력 버퍼에 아직 전송되지 못하고 남아있는 데이터가 존재하면 해당 데이터는 목적지로 전송
      SHUT_RDWR입출력 스트림 종료- 입력 스트림과 출력 스트림이 모두 종료 - shutdown 함수를 2번 호출 하는것과 같음
  • 출력 스트림을 종료하면 상대 호스트로 EOF가 전송됨


소켓의 다양한 옵션

소켓의 다양한 옵션

  • SOL_SOCKET : 소켓에 대한 가장 일반적인 옵션들
  • IPPROTO_IP : IP 프로토콜에 관련된 사항들
  • IPPROTO_TCP : TCP 프로토콜에 관련된 사항들
Protocol LevelOption NameGetSet기능
SOL_SOCKETSO_SNDBUFOO
소켓 기본SO_RCVBUFOO
SO_REUSEADDROO
SO_KEEPALIVEOO
SO_BROADCASTOO
SO_DONTROUTEOO
SO_OOBINLINEOO
SO_ERROROX
SO_TYPEOX소켓 타입 확인 - 소켓의 타입은 소켓 생성 시 한번 결정. 변경 불가
IPPROTO_IPIP_TOSOO
IP 프로토콜IP_TTLOO
IP_MULTICAST_TTLOO
IP_MULTICAST_LOOPOO
IP_MULTICAST_IFOO
IPPROTO_TCPTCP_KEEPALIVEOO
TCP 프로토콜TCP_NODELAYOONagle 알고리즘 사용 중단
TCP_MAXSEGOO

getsockopt & setsockopt

  • 소켓의 옵션을 확인할 때 호출하는 함수
    #include <sys/socket.h>
    
    int getsockopt(int sock, int level, int optname, void *optval, socklen_t *optlen);
    // 성공 시 0, 실패 시 -1 반환
    • sock : 옵션확인을 위한 소켓의 파일 디스크립터 전달
    • level : 확인할 옵션의 프로토콜 레벨 전달
    • optname : 확인할 옵션의 이름 전달
    • optval : 확인결과의 저장을 위한 버퍼의 주소 값 전달
    • optlen : optval로 전달된 주소 값의 버퍼크기를 담고있는 변수의 주소 값
      • 함수호출이 완료되면 이 변수에 optval에서 반환된 옵션정보의 크기가 바이트 단위로 저장됨

  • 소켓의 옵션을 변경할 때 호출하는 함수
    #include <sys/socket.h>
    
    int setsockopt(int sock, int level, int optname, const void *optval, socklen_t optlen);
    // 성공 시 0, 실패 시 -1 반환
    • sock : 옵션변경을 위해 소켓의 파일 디스크립터 전달
    • level : 변경할 옵션의 프로토콜 레벨 전달
    • optname : 변경할 옵션의 이름 전달
    • optval : 변경할 옵션정보를 저장한 버퍼의 주소값 전달
    • optlen : optval로 전달된 옵션정보의 바이트 단위 크기 전달

SO_REUSEADDR

  • 주소할당 에러(Binding Error)
    • 동일한 PORT 번호를 기준으로 서버를 재실행하면 “bind() error” 문제 발생. 일정시간 후 (약 3분) 재실행 시 정상적으로 실행
  • Time-wait 상태
    • 연결 해제 후 소켓이 바로 소멸되지 않고 일정시간 거침 (먼저 연결종료 요청한 server/client 인 경우)
      • client 도 존재하나 client의 소켓은 port 번호가 임의로 할당되기 때문에 유동적 → 신경쓸 필요가 없음
    • 해당 소켓의 PORT번호가 사용중인 상태
    • 종료명령에 대한 ACK 전송을 위해 존재
  • 주소 재할당
    • 재빨리 서버를 재시작 해야하는 경우 존재
      • 네트워크 상황이 원활하지 못하는 경우 Time-wait 상태가 언제까지 지속될 지 모름
    • SO_REUSEADDR 상태 변경 (Default 0 - FALSE)

TCP_NODELAY

  • 네이글(Nagle) 알고리즘
    • 네트워크상에서 돌아다니는 패킷들의 흘러 넘침을 막기 위해서 1984년에 제안된 알고리즘
    앞서 전송한 데이터에 대한 ACK 메시지를 받아야만, 다음 데이터를 전송하는 알고리즘
    • ACK를 기다리는 동안 나머지 데이터가 출력버퍼에 쌓이게 되며, ACK 수신시 하나의 패킷으로 전송됨
    • 기본적으로 TCP 소켓에 적용됨
      • Nagle 알고리즘을 적용하지 않으면 네트워크 트래필(traffic)에 좋지 않은 영향을 미침
      • 네트워크의 효율적인 사용을 위해 반드시 적용
    • 전송하는 데이터의 특성에 따라 효율성이 달라짐
      • 용량이 큰 파일 데이터 전송 : 파일 데이터를 출력버퍼에 밀어넣는 작업이 오래 걸리지 않음 → Nagle 알고리즘을 적용하지 않아도 출력 버퍼를 다 채운 상태에서 패킷 전송 → 패킷의 수가 크게 증가하지 않음으로 ACK를 기다리지 않고 연속해서 데이터 전송하는게 더 빠름
      • 데이터의 특성을 정확히 판단하지 않은 상태에서는 Nagle 알고리즘 사용해야 함
    • Nagle 알고리즘 중단
      // Nagle 알고리즘 사용 종료 설정
      int opt_val = 1;
      setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (void*)&opt_val, sizeof(opt_val));
      // Nagle 알고리즘 사용 확인 - 설정 (0), 미설정 (1)
      int opt_val;
      socklen_t opt_len;
      opt_len = sizeof(opt_val);
      getsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (void*)&opt_val, &opt_len);

소켓 옵션 예재

SO_TYPE (Windows)

  • SOL_SOCKET 레벨, SO_TYPE 옵션이용 소켓 타입 정보(TCP or UDP) 확인
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    
    // Socket
    #include <WinSock2.h>
    #pragma comment(lib,"ws2_32")
    
    
    #define BUF_SIZE	1024
    void ErrorHandling(char* message);
    
    // SOL_SOCKET 옵션 활용 소켓 타입 (TCP or UDP) 확인하는 예제
    int main(int argc, char* argv[])
    {
    	WSADATA wsaData;
    
    	SOCKET hTcpSock, hUdpSock;
    	int state, len, sock_type;
    
    	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
    		ErrorHandling("WSAStartup() error!");
    
    	len = sizeof(sock_type);
    	hTcpSock = socket(PF_INET, SOCK_STREAM, 0);
    	hUdpSock = socket(PF_INET, SOCK_DGRAM, 0);
    
    	printf("SOCK_STREAM: %d \n", SOCK_STREAM);
    	printf("SOCK_DGRAM: %d \n", SOCK_DGRAM);
    
    	state = getsockopt(hTcpSock, SOL_SOCKET, SO_TYPE, (void*)&sock_type, &len);
    	if (state)
    		ErrorHandling("getsockopt() error!");
    	printf("Socket type one: %d \n", sock_type);
    
    
    	state = getsockopt(hUdpSock, SOL_SOCKET, SO_TYPE, (void*)&sock_type, &len);
    	if (state)
    		ErrorHandling("getsockopt() error!");
    	printf("Socket type two: %d \n", sock_type);
    
    	WSACleanup();
    	return 0;
    }
    
    
    void ErrorHandling(char* message)
    {
    	fputs(message, stderr);
    	fputc('\n', stderr);
    	exit(1);
    }

SO_SNDBUF & SO_RCVBUF (Windows)

  • 소켓 생성 시 입력버퍼, 출력버처 기본 생성
  • 설정을 요구한 대로 크기가 그대로 반영되지 않음, 최소한의 버퍼 크기 등을 고려하여 설정됨
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    
    // Socket
    #include <WinSock2.h>
    #pragma comment(lib,"ws2_32")
    
    
    void ErrorHandling(char* message);
    void ShowSocketBufSize(SOCKET sock);
    
    // SOL_SOCKET 옵션 활용 소켓 타입 (TCP or UDP) 확인하는 예제
    int main(int argc, char* argv[])
    {
    	WSADATA wsaData;
    
    	SOCKET hSock;
    	int sndBuf, rcvBuf, state;
    
    	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
    		ErrorHandling("WSAStartup() error!");
    
    
    	hSock = socket(PF_INET, SOCK_STREAM, 0);
    	ShowSocketBufSize(hSock);
    
    	sndBuf = 1024 * 3, rcvBuf = 1024 * 3;
    	state = setsockopt(hSock, SOL_SOCKET, SO_SNDBUF, (char*)&sndBuf, sizeof(sndBuf));
    	if (state == SOCKET_ERROR)
    		ErrorHandling("setsockopt() error!");
    
    	state = setsockopt(hSock, SOL_SOCKET, SO_SNDBUF, (char*)&rcvBuf, sizeof(rcvBuf));
    	if (state == SOCKET_ERROR)
    		ErrorHandling("setsockopt() error!");
    	
    	ShowSocketBufSize(hSock);
    	closesocket(hSock);
    
    	WSACleanup();
    	return 0;
    }
    
    
    void ErrorHandling(char* message)
    {
    	fputs(message, stderr);
    	fputc('\n', stderr);
    	exit(1);
    }
    
    void ShowSocketBufSize(SOCKET sock) 
    {
    	int sndBuf, rcvBuf, state, len;
    
    	len = sizeof(sndBuf);
    	state = getsockopt(sock, SOL_SOCKET, SO_SNDBUF, (char*)&sndBuf, &len);
    	if (state == SOCKET_ERROR)
    		ErrorHandling("getsockopt() error");
    
    	len = sizeof(rcvBuf);
    	state = getsockopt(sock, SOL_SOCKET, SO_SNDBUF, (char*)&rcvBuf, &len);
    	if (state == SOCKET_ERROR)
    		ErrorHandling("getsockopt() error");
    
    	printf("Input buffer size: %d \n", rcvBuf);
    	printf("Output buffer size: %d \n", sndBuf);
    }

반응형

댓글