일단 진행시켜

[소켓 프로그래밍] TCP 클라이언트 프로그램에 대해 알아보자! 본문

🌐 Network 기본부터 차근차근

[소켓 프로그래밍] TCP 클라이언트 프로그램에 대해 알아보자!

2024. 10. 1. 15:57

TCP 클라이언트 프로그램

1.1. TCP 클라이언트 프로그램 구현(작성) 절차

 

클라이언트는 socket()으로 소켓을 개설하는 것을 시작으로 한다. 

 

연결형 서비스를 이용하기 위해 connect()를 호출하여

서버와 연결을 요청하고 연결이 이루어지면

send()와 recv()를 사용하여 데이터를 송수신하고

작업이 종료되면 close()로 소켓을 닫는다.

 

이거를 단계별로 상세히 정리해보려고 한다.

 

 

 

 

1. socket(), 소켓 개설

사용할 프로토콜(TCP, UDP 소켓)을 선택한다.

프로토콜 소켓
TCP SOCK_STREAM
UDP SOCK_DGRAM

 

클라이언트는 먼저 socket()으로 소켓을 개설해야 하는데 이때 프로토콜 체계를 PF_INET로 선택하고, 서비스 타입은 SOCK_STREAM으로 선택한다.

 

socket() 호출 시 트랜스포트 프로토콜만을 지정하게 되는데 socket() 수행 시 내부적으로 발생하는 동작은 아래 그림과 같다.

 

1.2. socket() 호출시 소켓 번호와 소켓 인터페이스의 관계

 

소켓 프로그래밍을 위해서는 5가지 요소를 지정해주어야 한다.

1. 사용할 트랜스포트 프로토콜(스트림 or 데이터그램)

2. 자신과 상대방의 IP 주소

3. 자신과 상대방의 port 번호

 

쉽게말해, 사용할 프로토콜을 지정하여 socket()을 호출하면 새로 생성된 소켓과 소켓번호가 반환된다.

 

 

 

 

2. connect(), 서버에 연결 요청

클라이언트는 connect()를 호출하기 전에 연결하고자 하는 서버의 주소를 지정해주어야 한다.

4바이트의 IP 주소2바이트의 포트 번호를 포함하는 소켓 주소 구조체 sockaddr_in을 구현해야 한다.

 

sockaddr_in: 연결하고자 하는 서버의 주소 지정(IP, port)

int connect (                        
  int s,                             // 서버와 연결시킬 소켓번호
  const struct sockaddr *addr,       // 상대방 서버의 소켓주소 구조체  
  int addrlen);                      // 구조체 *addr의 크기

 

이때 클라이언트는 bind()를 사용하여 자신이 사용할 포트 번호를 명시적으로 지정할 필요가 없다.

TCP와 같은 연결형 클라이언트에서는 conenct()를 호출할 때, TCP가 임의의 포트 번호를 지정해 주기 때문이다.

 

1.3. connect() 호출시 소켓 번호와 소켓 주소의 관계

 

 

 

 

 

3. send(), recv(), 데이터 송수신

TCP 소켓의 데이터 송수신 메서드는 아래와 같다.

문법 인자
int send(int s, char* buf, int length, int flags); s 소켓번호
buf 전송할 데이터가 저장된 버퍼
length buf 크기
flags default: 0
int write(int s, const void* buf, int length) s 소켓번호
buf 전송할 데이터가 저장된 버퍼
length buf 길이
int recv(int s, char* buf, int length, int flags); s 소켓번호
buf 수신 데이터를 저장할 버퍼
length buf 길이
flags default: 0
int read(int s, void* buf, int length); s 소켓번호
buf 수신 데이터를 저장할 버퍼
length buf의 길이

 

메시지 송신 함수들은 실제로 미시지 크기를 바이트 단위로 리턴하며,

메시지 수신 함수는 실제로 읽은 메시지 크기를 바이트 단위로 리턴한다.

 

 

TCP 소켓 송신 함수 사용시,

TCP 소켓에서 write(), send()를 실행하면 메시지는 TCP 계층의 송신 버퍼(send buffer)에 저장된다.

즉 write() 문이 리턴되면, 자신의 TCP에 있는 송신 버퍼에 메시지가 들어간 것.

그리고 송신 버퍼에 담긴 것!= 메시지 전송 완료가 아님을 주의해야 한다!

 

TCP 소켓 수신 함수 사용 시,

recv(), read()는 스트림형(TCP) 소켓을 통하여 패킷을 송신한다.

데이터를 수신할 소켓 번호, 수신 버퍼, 읽을 데이터 크기를 지정하여, 실제로 읽은 데이터 크기를 바이트 단위로 리턴한다.

 

 


스트림 소켓에서는

하나의 IP 패킷이 한 번에 전송할 수 있는 최대 메시지 크기(MSS) 보다 큰 메시지를 송신 버퍼에 저장하고 wirte()나 send()를 호출할 수 있는데,

이때 전체 메시지가 MSS 크기로 자동 분할 되어 전송된다.

전체 데이터를 IP 패킷 단위로 (자동) 분할 전송하면 분실 여부를 파악할 수 있고 필요에 따라 재전송 요구 가능!(TCP 신뢰성) 이것이 스트림 소켓의 장점

 

 

 

그러나 데이터그램(UDP) 소켓에서는

사용자가 전송 요구한 메시지 크기가 UDP가 한 번에 전송할 수 있는 최대 메시지 크기보다 크면

에러가 발생하거나 앞부분 일부만 전송된다.(신뢰성 x)

 

 

 

4. close(), 소켓 닫기

close()는 클라이언트나 서버 누구나 먼저 호출할 수 있다.

close(s);

 

close()를 호출한 시점에 서버나 클라이언트에 송신 버퍼에 담겨 있으나 아직 전송 못했거나 전송 중인 패킷이 있을 수도 있다!

그럴 경우를 대비하여 close()는 디폴트로 이러한 패킷들을 모두 처리한 후에 소켓을 닫을 수 있도록 되어 있다.

 

close() 호출 전에 shutdown() 시스템 콜을 하여 directon 값에 따라 종료를 진행한다.

종료
1 패킷 전송 종료
0 수신 종료
2 송수신 모두 종료

 

shutdown(s, direction);

 

 

partial close

클라이언트가 더 이상 전송할 데이터가 없으면 direction=0으로 하여 shutdown()을 호출하면 된다.

이를 partial close라고 한다.

 

이때 네트워크 시스템에서 상대측 서버에게 end-of-file 신호를 보내어 이를 수신한 서버도 전송할 데이터가 없다면 해당 연결을 종료하면 된다.

 

 

setsockopt()

close()호 출시 미처리된 패킷을 처리하는 또 다른 방법은 타임아웃이다.

일정 시간을 지정하여 해당 시간 동안만 미처리된 패킷을 기다리게 할 수도 있다.

이를 위해서는 close() 호출 전에 setsockopt()를 호출하여 소켓 동작의 옵션을 바꿔주어야 한다.(이건 추후 다시 공부해서 정리하겠다^!^)

 

 

 

 

 

 

 


 

오늘 공부한 내용을 토대로 TCP 에코 프로그램 코드를 작성해보려고 한다!

이거는 다음 장에 기록해야지🥰🥰