study/NETWORK PROGRAMMING
04-1. [Linux/Ubuntu] 네트워크 프로그래밍 - TCP 기반 서버/클라이언트
김팥빵_
2025. 3. 28. 16:20
TCP 프로토콜 스택
- TCP/IP 프로토콜 스택
- 인터넷 기반의 데이터 송수신을 목적으로 설계된 스택
- 7계층으로 세분화 되며, 4계층으로 표현함
- [ Application 계층 ]
- │ │
- [ TCP 계층 ] [ UDP 계층 ]
- └――――┬――――┘
- [ IP 계층 ]
- │
- [ LINK 계층 ]
- TCP/UDP 소켓의 프로토콜 스택 비교
- TCP 소켓의 스택 Flow -> sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
- UDP 소켓의 스택 Flow -> sock = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP);
- ip계층도 헤더를 달리 전달하기 위해 달리한다.
Link & IP 계층
- Link 계층의 기능과 역할 (TCP/IP 프로토콜)
- LAN, WNA, MAN 등과 같은 물리적인 네트워크 표준 관련 프로토콜이 정의된 영역
- MAC 주소(비정상적인 경로로 LAN을 구성하지 않는 이상 회사마다 고유한 번호를 갖기 때문에 절대 중복x)를 사용해 Frame 단위로 데이터 전송
- 오류제어, 흐름제어
- IP 계층의 기능과 역할
- Internet Protocol
- "경로 전송"과 관련이 있는 프로토콜 -> 목적지로 데이터를 전송하기 위해 어떤 경로를 거쳐갈 것인가?
- 오류 발생에 대한 해결 방법이 없음
TCP/UDP 계층
- TCP/UDP 계층의 기능과 역할
- 실제 데이터의 송수신과 관련 있는 계층 : 전송계층(Transport layer) 이라고도 함
- TCP
- 데이터의 전송을 보장하는 프로토콜 = 신뢰성있는 프로토콜
- UDP에 비해 복잡하다
- 데이터 전송 중 "확인 과정"(그림 참고)을 거침 -> 신뢰성 보장
- 데이터가 전송되지 못하면, 일정 시간 이후 재전송 함.
- UDP
- 신뢰성을 보장하지 않는 프로토콜
참고) Wireshark 설치
- windows에서는 ubuntu에서 명령어로 바로 설치가능
- 전송 계층, 링크 계층 등 관련해서 디버깅할 때 유용
- 필터링해서 포트마다 데이터 송수신 패킷들 확인 가능!
Application 계층
- Application 계층
- 프로그래머에 의해서 완성되는 계층
- 응용 프로그램의 프로토콜을 구성하는 계층
- 소켓을 기반으로 프로토콜의 설계 및 구현
- 소켓을 생성하면 Link, IP, TCP/UDP 계층에 대한 내용은 감춰진다.
- 응용 프로그래머는 Application 계층의 완성도에 집중한다.
TCP 헤더
- Source port (송신측 포트번호)
- (설정하지 않을 경우), 랜덤 번호로 자동 할당됨
- ex. client의 포트번호 (명시하지 않아도 자동할당 되기 때문에 코드에 넣지 않아도 무방)
- Destination port (수신측 포트번호)
- 명확하게 입력함.
- ex. 9190 (server 포트 번호)
- Sequence number*
- 전송하는 데이터의 순서를 표시
- 수신측은 데이터의 순서를 파악하고 재조립함
- Acknowledgement number* (if ACK set)
- sequence number의 응답으로 보내는 아이
- 데이터를 받은 수신자가 다음 시퀀스 번호를 할당해서 전송( = 응답 메세지)
- Flags (NS~FIN)
- SYN: 연결 시작을 위해 사용
- FIN: 상대방과의 연결 종료를 요청
- PSH: 송수신 데이터가 버퍼에 다 찰 때까지 기다리지 않고 즉시 전송(Push)
TCP 서버의 함수호출 순서
- TCP 서버의 기본적인 함수호출 순서
- Server
- socket() : 소켓 생성
- ↓
- bind() : 주소 할당 -> bind() 함수까지 호출이 되면 주소가 할당된 소켓을 얻음
- ↓
- listen() : 연결 대기 -> listen() 함수의 호출을 통해서 연결요청이 가능한 상태가 됨
- ↓
- accept() : 연결 허용
- ↓
- read()/write() : 데이터 송수신
- ↓
- close() : 연결 종료
- Server
- 연결 요청 대기 상태로의 진입
- listen() 함수
#include <sys/socket.h>
int listen(int sock, int backlog);
-> 성공 시 0, 실패 시 -1 반환
- sock
- 클라이언트의 연결 요청을 처리하기 위해 "대기 상태에 두는 소켓 디스크립터"
- 서버 소켓(리스닝 소켓)
- backlog
- 연결 요청 대기 큐(queue)의 "크기"
- 큐의 크기가 5로 설정하면, 클라이언트의 연결 요청을 5개까지 대기시킬 수 있음
※ 참고!)
- 연결 요청도 일종의 데이터 전송이다!
- 연결 요청을 받아들이기 위해 하나의 소켓이 필요함
- 이 소켓을 서버 소켓 또는 리스닝 소켓이라고 함( 그래서 서버 에 socket()으로 받아온 fd를 리스닝 소켓으로 쓰고, 그 뒤에 accept으로 받아온 fd(파일 디스크립터)를 따로 받아와서 client의 연결용 소켓으로 쓰는 거.
- listen() 함수 호출로 소켓ㅇ르 리스닝 소켓이 되게 함.
- socket.h 파일 확인
- socket.h 파일 찾기(Ubuntu 기준)
$ find /usr/include -name socket.h
/usr/include/linux/socket.h
/usr/include/asm-generic/socket.h
/usr/include/x86_64-linux-gnu/bits/socket.h
/usr/include/x86_64-linux-gnu/asm/socket.h
/usr/include/x86_64-linux-gnu/sys/socket.h
- listen()의 backlog값 확인
- /usr/include/x86_64-linux-gnu/bits/socket.h에 정의되어 있음
- " #define SOMAXCONN = 4096 " (리눅스 시스템에 따라 다름)
- /sbin/sysctl –a | grep somaxconn 명령어로 확인 가능
$ sudo /sbin/sysctl -a | grep somaxconn
[sudo] password for xxx:
net.core.somaxconn = 4096
PF_INET vs. AF_INET 비교
- PF(Protocol Family) : 프로토콜 체계 중 하나
- AF(Address Family) : 주소 체계 중 하나
- 서로 다른 의미지만 같은 상수값을 가지기 때문에 같이 써도 무방하다.
서버 : 클라이언트의 연결 요청 수락(Accept)
- accept() 함수
- 연결 요청 정보를 참조하여 / 클라이언트와 통신을 위한 별도의 소켓을 생성함
- 생성된 소켓을 이용하여 데이터 송수신이 진행됨
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
-> 성공 시 소켓 디스크립터, 실패 시 -1 반환
- IP Header(ip 주소 존재) / TCP Header(포트 번호) / Data
- addr
- 연결 요청을 한 클라이언트의 주소 정보를 담을 변수의 주소
- addrlen
- 두 번째 매개변수 addr의 크기(바이트 단위)
- accept() 함수는 연결 요청 정보를 참조하여 client와 통신을 위한 별도의 소켓을 생성함
- 이렇게 생성된 소켓을 이용해 데이터 송수신이 진행됨(read / write)
TCP 기반 서버, 클라이언트의 함수 호출 관계
- connect()
- 클라이언트의 경우 소켓을 생성한 다음, 서버와 연결을 위해 connect() 함수 호출
- connect()함수를 호출할 때, serv_addr 주소값 변수를 통해 서버의 주소 정보 전달
- 서버의 listen()함수 호출 이후에 / client의 connect()함수 호출이 유효함
Iterative 서버의 구현
- Iterative 서버 (반복 서버)
- 여러 클라이언트의 연결 요청 수락을 위해 어떤 방식으로 서버를 구현해야 될 것인가?
- 서버가 반복적으로 accept() 함수 호출
- 계속된 클라이언트의 연결 요청을 수락할 수 있음
- 단, 최대 단점은 / 동시에 둘 이상의 클라이언트에게 서비스를 제공할 수 없다는 거.
- 기존 클라이언트 소켓을 close()하고 다른 클라이언트를 accept() 하는 형태가 된다.
- 한 순간에 하나의 클라이언트에게만 서비스 제공
- 다음은 참고하여 공부하고 있는 책의 제공된 소스코드를 보며 적은 것임.
- *서버 (echo_server.c)
- server에서 while문을 빠져나오는 조건: strlen에 0이 저장되면 빠져나온다. -> 0이되는 조건은 상대방이 close()함수를 호출하면 "0" -> 소켓을 닫았을 때
- read()함수일 때는 상대방이 연결을 끊었을 때 0을 리턴(상대방이 close를 호출한 경우)
- server의 serv_sock은 accept용도 / write(clnt_sock~) : clnt_sock는 데이터 통신용도
- 결국엔 한명하고만 통신하고 백로그의 크기를 고려해 이쪽의 통신이 끊기면 다른 클라이언트에 연결해서 통신을 한다.
- *클라이언트 (echo_client.c)
- 화면에서 입력받은것에서 write(sock, message, strlen(message));를 이용해 서버로 전송
- 그리곤 다시 read함수를 호출한다. ->server에선 while문으로 그대로 되돌려줌. -> client가 바로 read()함수로 받음
- 그러니까 << client (write()) -"hello! world!"-> (read()) server (write()) - "hello! world!" -> (read()) client >> 순으로 그대로 되돌려주는 식
- 되돌려주는 걸 echo라고 한다.
- read()함수는 문자 그대로를 가져온다. -> 따라서 read()로 받아온 후에 message[str_len] = 0;으로 문자열 끝에 쓰레기값 출력되는 걸 막는다.
Echo client의 문제점
- 문제점
- TCP
- 한 번의 read() 함수 호출로 앞서 전송된 문자열 전체를 읽을 수 있다고 가정
- 서버가 전송한 문자열의 일부분만 읽혀질 수도 있음
- 전송할 데이터의 크기가 큰 경우
- OS(kernel)은 내부적으로 여러 조각으로 나누어서 클라이언트에 전송(MTU)
- TCP
- 문제점 확인하기
- Echo server의 코드
while((str_len=read(clnt_sock, message, BUF_SIZE))!=0)
write(clnt_sock, message, str_len);
- 서버는 데이터의 경계를 구분하지 않고, 수신한 데이터를 그대로 전송
- write() 함수 호출 횟수와 무관하게 수신한 데이터를 전송하면 됨
- 두 번의 write() 함수 호출을 통해 데이터를 전송하거나, 세 번의 write()함수 호출을 통해 데이터를 전송하는 것은 문제가 되지 않음.
- Echo client의 코드
write(sock, message, strlen(message));
str_len=read(sock, message, BUF_SIZE-1);
- read() 함수 호출을 통해 자신이 전송한 문자열을 한 번에 수신하기를 원함
- 데이터의 경계를 구분해야 됨 -> 이런 데이터 송수신 방식은 문제가 됨.
- TCP의 read(), write() 함수 호출은 데이터의 경계를 구분하지 않기 때문
- 해결책(다시보기)
- recv_len : 실제 수신된 바이트 수를 누적, read함수의 리턴값을 받아온다. -> client 왈, "내가 보낸 것만큼 체크해라!"라는 뜻
- write 함수로 보낸 데이터의 길이를 리턴한 str_len 변수만큼 읽어들여야 한다.
- 전송한 바이트 수만큼 데이터를 수신할 때까지 반복해야 함.
- 데이터가 엄청 클 때, 이 방식을 통해 체크 필요
TCP의 동작 원리
- TCP 소켓에 존재하는 입출력 버퍼
- 입출력 버퍼는 TCP 소켓 각각에 대해 별도로 존재한다.
- 입출력 버퍼는 소켓 생성시 자동으로 생성된다.
- 소켓을 닫아도 출력 버퍼에 남아있는 데이터는 계속해서 전송이 이뤄진다.
- 소켓을 닫으면 입력 버퍼에 남아있는 데이터는 소멸되어버린다.
- TCP의 동작 원리 #1: 연결 설정 단계
- 연결 설정 단계 : Three-way handshaking
- (SYN -> SYN+ACK -> ACK)
- TCP의 동작 원리 #2: 데이터 송수신
- TCP의 동작 원리 #3: 상대 소켓과의 연결 종료
- Four-way handshaking
- FIN(host A) - ACK(host B) - FIN(host B) - ACK(host A)
- Q) Four-way handshaking 과정을 거쳐서 연결을 종료하는 이유
- A) 일방적 종료로 인한 데이터의 손실을 막기 위함