✨ 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를 한다.
➕ 파일 디스크립터 테이블
이런 파일 디스크립터 테이블을 통해서 위의 과정들이 모두 일어난다...
🔧아직 다 정리하지는 못했지만, 해당 레파지토리에서 관련 함수들을 찾을 수 있을것이다. (./common/tiny/tiny.c)
Tiny 서버 전반적인 흐름 정리
'CS' 카테고리의 다른 글
레지스터 종류 (1) | 2024.11.04 |
---|---|
Pintos Project1 -키워드 정리 (코드로 이해하기) / 프로세스, 스레드, 멀티 스레딩 문제 (0) | 2024.11.02 |
네트워크 핵심 키워드 정리 (네트워크 계층,소켓,CGI,HTTP,Proxy 등등) (2) | 2024.10.28 |
정렬 알고리즘 (삽입,선택,버블,셸,퀵,힙,병합 정렬) (0) | 2024.09.11 |
1장. 컴퓨터 시스템 (Computer Systems A Programmer's Perspective) (1) | 2024.09.08 |