지우너

[뇌를 자극하는 TCP/IP 소켓 프로그래밍] 4장 본문

책/정리

[뇌를 자극하는 TCP/IP 소켓 프로그래밍] 4장

지옹 2024. 11. 14. 12:43

네트워크는 노드와 링크로 이루어져 있다.

노드는 정보를 저장하고 처리하거나 혹은 다른 노드로 전송하는 일을 한다.

링크는 노드와 노드를 연결하는 통로. 정보는 이 링크를 따라서 이동.

어떤 노드에서 다른 노드로 이동하기 위해서 한 개 이상의 노드와 링크를 거치게 되는 데, 이를 경로라고 한다.

 

노드와 링크는 하드웨어로 하드웨어를 활용하기 위한 소프트웨어가 필요하다.

도로망을 운용하기 위해서는 도로와 자동차 등의 하드웨어 외에도 주행 규칙, 신호 등의 규칙과 규칙을 집행하기 위한 소프트웨어가 필요.

 

인터넷도 마찬가지다. 컴퓨터와 컴퓨터를 연결하는 회선만 있다고 해서 인터넷이 저절로 작동하는 것은 아니다.

정보를 어떻게 주고 받을지, 어떻게 경로를 찾을지, 수많은 노드(컴퓨터)는 어떻게 관리 할 것인지에 대한 규약(프로토콜)

그리고 규약을 집행(실행)할 소프트웨어가 필요하다.

 

전화기를 이용한 통신 과정

  1. 전화기를 설치한다
  2. 전화를 건다. 혹은 기다린다.
  3. 통화를 한다.
  4. 끊는다.

인터넷은 전화망을 그대로 모방했다. 소켓 통신 과정은 다음과 같다.

이를 실행하기 위한 개발도구(방법) 중 추천하는 것은 소켓 라이브러리에서 제공하는 소켓 함수들이다.

  1. 소켓을 만든다 → socket 함수 사용
  2. 클라이언트: 연결을 시도한다 → connect 함수 사용. 서버: 연결을 기다린다 → accept 함수 사용
  3. 소켓으로 통신한다. → read 함수, write 함수 사용
  4. 소켓을 닫는다. → close 함수 사용

고급 소켓 프로그램은 위의 기본 골격 위에 멀티 통신과 성능 향상 등을 위한 살을 붙인다. 이게 전부다.

 

 

위에서는 6개의 함수가 사용되고 있는데, 여기에 2개의 함수를 더하면 완전한 서버/클라이언트 네트워크 프로그램 쌍을 만들 수 있다.

  1. socket(): 소켓 생성(전화망에 전화기 설치)
  2. bind(): 소켓 특성을 정의(전화기에 전화번호 부여)
  3. listen(): 수신 대기열 생성(연결을 기다림)
  4. connect(): 발신자 연결 시도(통화 걸기), accept(): 수신자 연결 수락(통화 받기)
  5. read(), write(): 데이터 통신(통화)
  6. close(): 연결 종료(통화 종료)

bind함수: 소켓의 특성을 정의. 즉, 소켓이 사용할 프로토콜, IP 주소, 서비스 포트 번호 등을 지정. 클라이언트 프로그램은 bind 함수를 통해 설정한 포트 번호로 접속을 시도함.

listen함수: 전화를 걸었는데, 상대방이 통화 중일 경우 상대방이 전화를 받을 수 있는 상태가 될 때까지 대기. 수신 대기열이기 때문에 서버 프로그램에만 필요한 함수

 

클라이언트 프로그램

socket()→connect()→read()/write()→close()

서버 프로그램

socket()→bind()→listen()→accept()→read()/write()→close()

 

socket()은 자바의 ServerSocket 같고, accept()로 만들어지는 연결 소켓은 Socket socket = serverSocket.accept(); 으로 만드는 Socket 같다.

 

socket(): 소켓 생성

#include <sys/types.h>
#include <sys/socket.h>

int socket(int domain, int type, int protocol);

 

[반환값] int socket_descriptor (=소켓 지정 번호)

socket함수는 endpoint 소켓을 생성. -1이면 실패, 0이상이면 성공을 의미. 반환되는 int 형 값은 소켓 객체를 '가리키는 숫자'

  • endpoint 소켓은 클라이언트에서는 '연결 요청'을 위한 용도, 서버에서는 (클라이언트의 요청을)'듣기' 위한 용도이다.
  • 서버라면 클라이언트의 연결을 받아들이기 위해 최전선에 놓여있는 소켓을 의미.
  • 클라이언트라면 인터넷으로 나아가기 위한 접점 역할.

[매개변수] int domain

  • AF_UNIX: 시스템 내부 영역에서 프로세스와 프로세스 간의 통신을 위해 사용.
  • AF_INET: 인터넷 영역에서 물리적으로 서로 멀리 떨어진 컴퓨터 사이의 통신을 위해 사용. IPv4를 사용
  • AF_INET6: AF_INET과 동일. IPv6를 사용

[매개변수] int type

  • SOCK_STREAM: 연결 지향의 TCP/IP 기반 통신에서 사용
  • SOCK_DGRAM: 데이터그램 방식의 UDP/IP 기반 통신에서 사용
  • SOCK_RAW: TCP/IP를 직접 다룰 필요가 있을 때 사용

[매개변수] int protocol

  • IPPROTO_TCP: TCP 프로토콜로 AF_INET 도메인과 SOCK_STREAM 유형과 함께 사용됨.
  • IPPROTO_UDP: UDP 프로토콜로 AF_INET 도메인과 SOCK_DGRAM 유형과 함께 사용됨.

 

bind(): 소켓 특성을 정의

소켓을 생성한 다음 IP 주소와 서비스를 위한 포트를 할당해야 한다. bind() 함수를 이용해서 소켓에 IP와 포트를 할당할 수 있다.

IP 주소를 할당해야 원하는 컴퓨터를 찾아갈 수 있고, 포트를 지정해야 원하는 서비스 프로그램에 연결을 시도할 수 있다. 

 

bind 함수는 서버 프로그램에 포트를 고정시키기 위해 사용.

(클라이언트는 자신의 아무 남는 포트나 이용해서 서버의 포트에 접속하기 때문에 bind()를 사용할 필요가 없음)

#include <sys/types.h>
#include <sys/socket.h>

int bind(int sockfd, struct sockaddr *my_addr, socklen_t addrlen);

 

[반환값] int 성공여부

성공 0,  실패 -1

[매개변수] int sockfd

socket() 함수로 생성된 endpoint 소켓

[매개변수] struct sockaddr *my_addr

IP주소와 port 번호 저장을 위한 변수가 있는 구조체

구조체의 멤버 변수

sin_family: 주소 체계를 지정. 인터넷 영역에서 사용할 때는 AF_INET을 사용.

sin_addr: 연결할 인터넷 주소를 지정하기 위해 사용. 서버 프로그램은 기다리기만 할 뿐 연결 대상이 없어서 'INADDR_ANY'사용. 인터넷 주소로 '0.0.0.0', 모든 주소로 대기하겠다는 의미. 모든 주소로 대기해야 하는 이유는 하나의 컴퓨터가 여러 인터넷 주소를 가질 수 있기 때문.

sin_port: 서버에서는 기다릴 포트 번호, 클라이언트에서는 연결할 서버의 포트 번호.

[매개변수] socklen_t addrlen

두 번째 인자의 데이터 크기.

컴퓨터는 2번째 입력된 데이터를 어디까지 읽어야 하는지 모름.

AF_INET 소켓 유형은 struct sockaddr_in, AF_UNIX 유형은 struct sockaddr_un을 사용함.

bind함수는 struct sockaddr만 받기 때문에 형변환을 해야함.

listen(): 수신 대기열 생성(연결을 기다림)

수신 대기열은 FIFO 형식으로 처리 → queue 이용

#include <sys/socket.h>
int listen(int sockfd, int backlog);

[반환값] int 성공 여부

성공 0, 실패 -1

[매개변수] int sockfd

socket()함수로 얻은 소켓의 번호

[매개변수] int backlog

수신 대기열의 크기. 정해진 크기는 없으나, 일반적으로 '5'로 지정함.

accept(): 수신자 연결 수락

수신 대기열에 있는 클라이언트의 연결 요청 확인.

연결 요청을 성공적으로 가져오면 클라이언트와 통신을 위한 연결 소켓을 생성

#include <sys/types.h>
#include <sys/socket.h>

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

 

[반환값] int 소켓 지정 번호

성공 시 0보다 큰 값 반환. 클라이언트와 직접 연결된 'connected socket(연결소켓)'의 번호.

socket()에서 만든 듣기 소켓과는 전혀 다른 별도의 소켓.

듣기 소켓은 클라이언트의 연결 요청을 확인하는(듣는) 역할

연결 소켓은 실제 클라이언트와의 통신을 위함.

듣기 소켓과 연결 소켓을 구분하는 이유는 클라이언트와 통신하는 중에 다른 클라이언트의 연결 요청을 받기 위함!

 

 

[매개변수] int sockfd

 

[매개변수] struct sockaddr *addr

 

[매개변수] socklen_t *addrlen

 

connect(): 발신자 연결 시도(통화 걸기)

클라이언트는 connect()로 연결 요청을 한다. 서버는 accept()하면서 기다리다가 connect() 요청이 들어오면 연결해주는 것.

 

#include <sys/types.h>
#include <sys/socket.h>

int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen);

 

 

read(): 데이터 통신(통화)

파일(소켓)에서 count 크기 만큼 데이터를 읽어서 buf에 저장하는 함수.

#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);

[반환값] ssize_t

수행 결과가 성공이면 읽은 데이터의 크기(byte 단위)를 반환, 실패하면 -1 반환

ssize_t는 int의 typedef형. 실제로는 WORDSIZE에 따라 int형 혹은 long int 형의 typedef가 되기도 함.

 

[매개변수] int fd

열린 파일의 파일 지정 번호. 소켓 프로그램에서 fd는 소켓 지정 번호.

클라이언트: socket()으로 생성된 소켓

서버: accept()로 생성된 소켓

[매개변수] void *buf

읽어들인 데이터가 저장될 버퍼 변수

[매개변수] size_t count

읽어들일 데이터의 count 크기

write(): 데이터 통신(통화)

#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);

 

[반환값] sszie_t

성공하면 쓴 데이터의 크기, 실패하면 -1

[매개변수] int fd

연결된 소켓의 지정 번호

[매개변수] const void *buf

보낼 데이터가 저장되어 있는 버퍼

[매개변수] size_t count

보낼 데이터의 크기

close(): 연결 종료(통화 종료)

사용하지 않는 소켓 종료. 종료시키지 않는다면, 만들어진 소켓은 시스템 자원을 계속 소모 = 시스템 자원 낭비.

반드시 사용 후 소켓 자원을 시스템에 반납해줘야 한다.

#include <unistd.h>
int close(int sockfd)