CS

네트워크 소켓 및 함수 정리 (CS:APP)

Jerry_K 2024. 10. 29. 19:19

✨ getaddrinfo( )

int getaddrinfo(const char *hostname, const char *port,
			const struct addrinfo *hints, struct addrinfo **result);
  •  hostname과 port로 소켓 주소를 얻음
  • IPv4 / IPv6 모두를 지원
  • hostname은 얻고자 하는 호스트 이름  (NULL은 로컬 호스트 주소)
  • hints는 결과 리스트 필터링을 위한 옵션 
  • result 는 결과 리스트의 헤드
  • 성공 시 0을 반환 / 실패 시 오류 코드 반

 

✨ socket( )

int socket(int domain, int type, int protocol);
  • 네트워크 통신에서 사용되는 소켓 생성
  • domain은 통신에 사용될 주소 체계 ( AF_INET(IPv4), AF_INET6(IPv6) )
  • type은 통신 성격을 나타냄 ( SOCK_STREAM(TCP), SOCK_DGRAM(UDP) )
  • protocol은 사용할 특정 프로토콜을 지정 
  • 반환값으로 소켓 디스크립터를 반환

 

✨ connect( ) 

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • sockfd (socket 함수로 생성된 fd)
  • addr은 서버의 주소 정보를 담고 있는 구조체 (서버 IP 주소와 port번호 포함)
  • addrlen은 sizeof(struct sockaddr_in) 같이 사용
    • 함수가 올바른 메모리 크기 참조
    • 주소 정보를 안전하게 참조하려면 구조체 끝까지 접근 필요
  • 클라이언트가 connect 함수를 호출하면, 해당 소켓의 로컬 IP 주소와 임의의 포트 번호 자동으로 설정
    • 운영체제의 소켓 시스템이 클라이언트 소켓에 로컬 주소와 포트 자동 할당 함
  • 반환값으로 0을 반환하고 실패하면 -1 반환

 

 

💡  처음 네트워크에서 Ip와 Port가 가장 핵심 중에 핵심이다  (이후에는 Socket fd가 핵심)

Q, 맨 처음에 Socket fd도 없이 어떻게 Client의 Clientfd가 Server로 전달 될까 ? 
  • Clinet fd는 IP 주소와 Port를 통해 Server에게 전달
  • 좀 더 구체적으로 Client는 서버의 IP 주소로 서버 컴퓨터로 접근 
    • 이 부분도 좀 신기한데, IP 주소는 고유주소이니 라우터 같은거를 통해서 충분히 접근 가능 할듯 
  • 그리고 Port를 통해서 해당 서버의 어플리케이션(서비스)에 접근
  • 그러면 서버 컴퓨터 OS에 연결대기큐로 현재 접속하려는 Client fd 추가  (자세한거는 accept 함수 부분에 있음)
  •  ex ) CS:APP의 tiny 서버  : Ip를 통해서 서버에 접속하고, Port로 tiny 파일에 접근 (너무 신기하다 !)

가장 근복적으로 맨 처음 socket fd 없이 어떻게 Server로 Client fd가 전달되는지 궁금했다. 

 

 

 

IP와 Port에 대해서 정리해보자면 아래와 같다. 

* IP 주소는 특정 컴퓨터 (호스트)를 식별하는 추상화
* Port 주소는 특정 어플리케이션을 식별하는 추상화

 

IP주소와 Port에대한 개념들은 대부분 알고 있지만,

이것들이 실제로 작동하는 것을 상상하니 너무나 신기하다. 

그리고 결국 저 IP와 Port도 또상화이다 ... 

 

→ 컴퓨터 시스템 구조의 핵심은 추상화 아닐까 ? 

 

 

Q. 그러면 원격 접속도 Ip와 Port로 가능하지 않을까 ?
  • 원격 제어 하고자 하는 컴퓨터의 IP 주소로 상대방 컴퓨터에 접근
  • Port로 (프로그램 제어 권한이 있는) 어플리케이션(프로세스)에 접근
  • 보통 윈도우 OS는 3389 포트로 RDP (Remote Destop Protocol) 접속
  • SSH는 포트 22로 원격으로 서버에 접속하여 터미널 명령 실행
    • AWS 원격 접속때 사용
    • ex) ssh username@server_ip_address 
  • 실제로는 훨씬 더 복잡하겠지만, 큰 틀은 이런 느낌이다. 

 

 

✨ bind( )  

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

 

  • 소켓에 특정 IP 주소와 포트 번호 할당
  • 소켓이 특정 주소와 포트에 "묶이도록"
  • 클라이언트는 이 소켓을 통해 서버에 연결 가능 (정체성 부여)
  • 정확하게는 클라이언트가 서버의 IP와 포트로 접속할 수 있는 위치 마
  • 반환값으로 성공시 0을 실패시 -1을 반환

 

✨ listen( )  

int listen(int sockfd, int backlog);
  • 서버 소켓이 클라이언트의 연결 요청을 받을 수 있도록 설정
  • 수동 소켓으로 전환 (포트에 들어오는 연결 요청을 대기할 수 있게 만듬)
    • 기본적으로 소켓 상태는 active 상태로 클라이언트 소켓처럼 동작할 준비
    • 파일 디스크립터 테이블에서 sockfd에 매칭된 "active" 소켓의 상태를 "passive" 소켓으로 변경
    • 커널이 파일 디스크립터 테이블에서 sockfd 관련된 소켓 상태 업데이트
    • 파일 디스크립터 테이블은  PCB안에 있고, PCB는 커널안에 있다.
    • 커널은 프로세스들끼리 공유 가능 
  • backlog는 연결 대기 큐의 최대 크기 설정 (서버가 동시에 수용할 수 있는 최대 연결 요청 수)
  • 반환값으로 성공 시 0을 반환 

 

서버 코드에서 Open_listenfd (getaddrinfo → socket → bind  → listen ) 부분이 서버를 열어주는 부분이다.

이 함수를 호출함으로써 포트에 맞는 로컬 호스트로의 접근이 가능하다.

 

✨ accept( )  

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
  • sockfd는 passive소켓으로 설정된 서버 소켓의 FD
  • addr은 클라이언트 주소 정보를 저장할 sockaddr 구조체 포인터  (NULL 설정 가능)
  • 서버 소켓이 연결 대기 큐에 대기하고 있는 클라이언트 연결 요청 수락 및 새로운 연결  설정
    • 연결 대기 큐는 운영체제 내부에서 관리
    • 큐의 내부 상태는 표준 라이브러리 함수로 접근할 수 없음
  • accept 함수가 호출되면 클라이언트와 통신할 준비 마침
  • 반환 값은 연결된 소캣의 FD  반환 (클라이언트 통신을 위한 전용 소켓)
  • accept 함수도 결국 (원하는 것을 들어주는) 요정  ! 

 

✨ rio_writen( )

ssize_t Rio_writen(int fd, void *usrbuf, size_t n);
  • wite 함수 같은 경우 요청한 바이트 수만큼 항상 쓰지 못할 수 있음
  • rio_writen은 소켓이나 FD에 데이터를 안전하게 쓰기 위해 만들어진 함수
  • 네트워크 프로그래밍에서 데이터를 끊김 없이 전체를 전송하기 위한 목적
  • 모든 데이터를 전송할 때까지 반복해서 쓰기 수행
// 파일 디스크립터 4가 가리키는 파일에 buf를 기록
Rio_writen(4, buf, strlen(buf)); 

// 파일 디스크립터 5가 가리키는 파일에 동일한 buf를 기록
Rio_writen(5, buf, strlen(buf));
  • 다른 FD 경우 서로 다른 대상에 기록
  • Client FD와 Connect FD는 서로 다른 FD 값을 가짐
    • 서버와 클라이언트 FD는 서로 다른 프로세스 내에 위치하므로 같은 소켓을 나타내더라고 다른  FD를 가짐

 

 

✨ rio_readlineb( )

ssize_t rio_readlineb(rio_t *rp, void *usrbuf, size_t maxlen);
  • FD에서 데이터를 한 줄씩 읽음
  • 한 줄 단위로 데이터를 읽는 작업 간편하게 수행 (보통 URI 읽기 위해)
  • 특정 크기만큼 데이터를 읽어서, 읽는 양을 조절
  • rp에 파일 디스크럽터와 버퍼 정보 포함
  • usrbuf는 읽어들인 데이터를 저장할 버퍼
    • fd가 다르면 각각 파일 디스크립터마다 별도의 usrbuf 사용
  • 반환값으로 읽어들인 바이트 수 반환

 

✨ rio_readn( )

ssize_t rio_readn(int fd, void *usrbuf, size_t n);
  • 매개변수로 fd (file descriptor)로 받음 
  • 버퍼링 없이 지정된 바이트 수 읽기
  • 연속적이고 정확히 n 바이트 읽어 옴
  • 네트워크 소켓이나 지정한 바이트 수를 정확히 읽는데 유용

 

✨ rio_readnb( )

ssize_t rio_readnb(rio_t *rp, void *usrbuf, size_t n);

 

  • 구조체에 저장된 버퍼 사용하여 데이터 읽음
  • 버퍼링하여 지정된 바이트 수 읽기
  • 한번에 여러 바이트를 rio_t 버퍼에 저장
  • 필요한 만큼만 usrbuf로 전달
  • 버퍼링 덕분에 네트워크 소켓에서 효율적으로 데이터를 읽음

 

✨ rio_read( )

ssize_t rio_read(rio_t *rp, char *usrbuf, size_t n);
  • 파일 디스크립터에서 특정 바이트 수만큼 데이터를 읽어어는 함수
  • 데이터를 효율적으로 읽기 위해 버퍼를 사용 (필요 데이터만큼 채움)
  • rp는 파일 디스크립터, 버퍼 정보 등을 포함 
  • usrbuf는 읽어들인 데이터를 저장
  • 반환값으로 성공적으로 읽어온 바이트 수 반환

 

 

소켓에서의 write,read  vs  일반 write,read 함수

  • 소켓 상황 
    • 소켓을 통해 데이터 송수신할 때 사용
    • write 함수를 사용하면 데이터를 소켓 버퍼에 write 할 수 있음
    • read 함수를 사용하면 소켓 버퍼에서 데이터를 읽어올 수 있음
    • 소켓의 write/read는 네트워크 상에서 데이터를 전송 및 수신에 사용
  • 일반 상황
    • 파일 시스템에 있는 파일을 읽고 쓰는데 사용
    • 파일의 write 함수를 사용하면 파일에 데이터를 기록할 수 있음
    • 파일의 read 함수를 사용하면 파일에서 데이터를 읽을 수 있음
    • 파일 I/O의 write/read는 로컬 파일 시스템에서 데이터를 저장하고 불러오는데 사용
write(fd, bufp, nleft)
read(rp->rio_fd, rp->rio_buf,sizeof(rp->rio_buf));

 

소켓 fd를 쓰면 write( ), read( ) 부분이 더 특별해지는 점을 기억해두자 ! 

 

 

 

✨ 동적 파일 구현 

CS:APP 11장 네트워크의 tiny 서버를 구현한다.

그 중에서 동적 파일을 작성하는 부분이 있는데, 이 부분도 너무 신기해서 기록해본다. 

void serve_dynamic(int fd, char *filename, char *cgiargs) {
    char buf[MAXLINE], *emptylist[] = { NULL }; 
   
    if (Fork() == 0) {            
        setenv("QUERY_STRING", cgiargs, 1);       
        Dup2(fd, STDOUT_FILENO);                  
        Execve(filename, emptylist, environ);     
    }
   
    Wait(NULL);   
}

 

  • 이전에 코드 내용들은 생략하고 중요한 부분만 살펴보자. 
  • 먼저 부모 프로세스 fork를 해서 자식 프로세스를 만듬 
  • 위의 if 문은 자식 프로세스인 경우만 실행됨 (자식 프로세스의  return 0 ) 
  • serenv로 QUERY_STRING은 cgiags (실제 문자열) 로 환경변수 설정
  • Dup2로 STDOUT_FILENO (표준 입출력)을 fd(client fd)로 바꿔 줌
    • 이 부분이 진짜 신기하다 ! 
    • printf, put 같은 표준 입출력은 보통 터미널에서 뜨게 된다.
    • 근데 Dup2 함수를 통해서 표준 입출력을 fd(client fd)로 덮어씌우면, printf, put 의 입출력은 연결된 clinet 소켓으로 가게 된다.
  • Execve 함수로  filename 경로에 해당하는 파일을 자식 프로세스에 덮어씌움
    • 부모의 코드, 데이터는 다 사라지지만, 좀 전에 설정한 환경변수 남아있음
    • 해당 tiny 서버에서 filename은 adder 파일의 상대 주소로 adder 파일 실행
    • adder 파일에는  printf가 있는데, 이게 터미널에 출력되는게 아니라 소켓을 통해 실제 클라이언트에게 전달된다. 
    • (터미널에 뜨지 않고 클라이언트에게 전달되는거는 표준 입출력을 cline fd로 바꿔서 ! )
    • 보통 fork 뜨고 execve를 한다. 

 

➕  파일 디스크립터 테이블

 

 

이런 파일 디스크립터 테이블을 통해서 위의 과정들이 모두 일어난다... 

 

 


 

 

 

GitHub - jerry-1211/network: network CS:APP

network CS:APP . Contribute to jerry-1211/network development by creating an account on GitHub.

github.com

 

🔧아직 다 정리하지는 못했지만, 해당 레파지토리에서 관련 함수들을 찾을 수 있을것이다.  (./common/tiny/tiny.c)

 


Tiny 서버 전반적인 흐름 정리