[sw정글 7주차] 이엑스이씌븨와 뤼오 시리즈물(execve, rio series)

Date:     Updated:

카테고리:

태그:

my_cube

(malloc에 이어 공부량이 가장 많았던 주였던것 같다. 멍하니 낙서를하며 스트레스관리를 해나갔다)

🚀개요

프로그램이 어떻게 실행될지에 관한 이야기. 통신은 어떻게 실행될까?

클라이언트(나)가 서버(인터넷)에게 요청을 할 수 있을 것이다. 그리고 서버(인터넷)은 나에게 답변을 줄 수 있을 것이다. 클라이언트(내)가 맥도날드주방(서버)에게 요청을 할 수도 있을 것이다. 그리고 맥도날드주방(서버)는 나에게 햄버거(답변)을 줄 수 있을 것이다.

그렇다면 프록시 서버는 무엇일까?

proxy_in_dict

그렇다 프록시는 대리해 주는 것이다. 맥도날드 주방에 햄버거를 직접 요청할 수 있겠지만, 나는 사실 매장직원(프록시)을 통해서만 주문할 수 있다.

내가(클라이언트) 소통할 수 있는 곳은 오직 매장직원(프록시 서버)뿐이다.

🧑‍💼프록시란?

고객(클라이언트) 입장에서는, 내 정체를 숨길 수 있어서 좋다. 누가 음식을 시켰는지 익명성을 유지할 수 있다. 또다른 장점은 캐싱해줄 수 있다는 것이다. 내가 365일 매일매일 불고기버거만 시킨다면, 매장직원은 내 얼굴을 보자마자 불고기버거를 주방에 주문을 넣을 것이다. 혹은 만들어놓은 불고기버거를 바로 줄수도 있을 것이다.

주방(서버) 입장에서도 마찬가지이다. 누가 햄버거를 만들어주었는지 정체를 숨길 수 있다. 마찬가지로 익명성이 보장되는 것이다. 이는 보안적 관점에서도 이득이 있다. 만약 불고기버거를 만든 사람에게 머리카락이 들어갔다고 화를 내고싶어도 낼 수 없다. 내가 대화할 수 있는 사람은, 그저 밝게 웃어주고있는 맥도날드 매장 직원이다. 아무리화가나도 해당 주방직원(서버의 특정 위치)을 공격하기가 힘든 것이다.

📚Contents

앞서 말한 클라이언트-프록시-서버 통신과정을 C언어로 짜보았다. 이번 프로젝트를 하면서 결국 알게된것은, 소켓통신을 어떻게 하는지에 대한 상세한 코드구현과, 리눅스는 이마저도 또하나의 파일로 바라본다는 그 관점이다. 이러한 방식의 추상화가 잘 이해가 되지 않았었지만 과제를 하며 알게된 것 같다.

개념 자체는 이해하고 나면 쉬웠지만, 굳이 궁금증을 가지고 깊이 들어가면 끝도없이 들어갈 수 있다는 사실을 알게 되었다. 그리고 이러한 네트워크 및 소켓통신에 관한자료는 널리고 널려있으므로, 그 와중 알게된 몇가지 그나마 마이너한 지식들을 공유해보고자 한다.

soo_yeon_tcp

( 출처: a반의 킹갓 동료인 수연누나의 불로그)

🏭execve

🏭execve는 무엇일까?

CSAPP 책이라던지, OS, 네트워크 등 다양한 곳에서 해당 함수를 만나게 된다. 그냥 사실 무의식적으로 사용한다.

execve() 발음하기도 힘든 함수이다. 엑셋븨이, 이엑스이씨븨이? 입에 달라붙지 않아 검색을 해보게 된 함수이다. 사실 이 함수는 exec로 생각하면 된다. 우리가 아는 그 execute(실행하다) 어원에서 출발한 함수명인 것이다. exec()라는 함수에 v, e 옵션이 붙은 함수이다. l, p, v, e 등 다양한 옵션이 있기 때문에 exec+ve와 함께, exec+lp, exec+v, exec+le 등등 옵션에 따라 파생된 다양한 함수가 있는 것이다. 그렇다면 원점으로 돌아오겠다. execve()는 무얼 하는 함수일까?

이 함수는 결국 exec()함수의 시리즈물로써 포식스에서 제공되는 함수들 중 하나이다. v옵션은 벡터로 인자들을 받겠다는 뜻이고, e옵션은 environment variables를 받겠다는 뜻이다. (사족을 붙이자면, l옵션은 list형을 의미, p옵션은 path에 관한 옵션이다.)

정리하면, 결국,

execve()함수는 프로그램을 execute를 할 때, 벡터값으로 환경변수를 받는 함수이다.

🏭execve사용

만약 GET /cgi-bin/adder?15000&213 HTTP/1.1 이라는 요청을 받으면 어떻게 될까?

📕첫번째로, fork를 호출하여 자식프로세스를 만든다

📕두번째로, 그리고 execve에 넣어주기 위한 CGI를 미리 만들어둔다. 이경우 QUERY_STRING을 입력받은 15000&213로 설정해둔다.

📕세번째로, 마침내 execve를 호출한다.

📕네번째로, 호출된 execve 를 통해 /cgi-bin/adder 프로그램을 자식 컨텍스트에서 실행시킨다.

execve_in_proxycode

다시 맥도날드 이야기로 돌아가자면, 내가 주문한 불고기버거라는 옵션을 CGI라는 공통된 규약(이자 프로토콜)에 담아 주방으로 전달해주는 것이다. 그리고 그 과정을 execve을 통해 실행시키는 것이다.

🆕개행

과제로 주어진 서버를 만들다 보면 \r\n\r\n이라는 코드를 만나게 된다. 이를 \r\n으로 변경하면 원하는대로 동작하지 않는다. 그리고 그에대한 디버깅은 불가능하다.

일단 이건 그냥 규칙이다. 알고있어야 한다.

서버를 만들 때, \r\n을 통해 들어온 요청을 파싱한다. 어디까지가 헤더인지를 알 수 있는 것이다. 디버깅을 할 수 없는 이유중 하나는, 크롬으로 보낸 요청에 대한 응답을 크롬도 \r\n으로 파싱을 하기 때문이다. 따라서 우리가 응답으로 만들어주는 패킷을 \r\n\r\n\r\n으로 변경한다고 해서 갑자기 오작동을 일으키는 원인을 찾을 수 없는것이다. 크롬도 으레 그렇게 파싱을 하기 때문에 우리들의 코드에서는 그에대한 해답을 찾을 수 없는 것이다. 우리가 파싱하는 방식대로 크롬도 그렇게 파싱을 하고있다. 그리고 그건 공통규약이니까 알고있어야 한다.

🆕개행에 관한 이야기(사족)

사실 위의 개행은… 저번게시글에서 30시간동안 코딩을했던 형이 개행에 관한 정보를 놓침으로써 3시간동안 고민했던 문제이다. 이제 막 소켓통신을 알아가는 우리에게 디버깅을 하기 너무 힘들었던 것 같다. 해결해주고 싶은 마음에 검색하다 알게된 사족을 여기에 붙여본다.

git_carriage_return

깃에 파일을 올릴 때, 우리들의 컴퓨터 시스템에 따라 우리들의 엔터를 다르게 인식한다. 따라서, 아래와 같은 명령어로 깃 환경설정을 해주면 호환성좋게 코드를 공유할 수 있다.

#window
git config --global core.autocrlf true

#mac
git config --global core.autocrlf input


🆚rio_readnb vs rio_readn🆚

드디어 올것이 왔다.🚀🚀 rio_read에 관한 이야기이다. 일단 RIO는 무엇일까? “Robust Input Output”이라는 뜻이다. 튼튼한 입출력 함수라는 것이다. 카톡을 보냈는데 만약 절반만 보내진다면 정말 끔찍할것이다. 그러므로 우리는 튼튼한입출력 장치가 필요한 것이다. 둘의 차이는 b가 붙었다는 것이다. 버퍼를 사용한다는 뜻이다.

30000이라는 메시지를 서버로 보내게 된다면 어떻게 될까? 이 내용이 텍스트일 경우 5+1(null)이므로 6바이트가 필요하다. 하지만 바이너리일 경우 short 자료형으로 보내게 되면 8바이트를 사용하게 된다. 텍스트는 더 적은 공간을 필요로 하고, 호환성도 좋다. 안녕 잘자~라는 메시지를 보내게 된다면, 그냥 이처럼 텍스트로 슝~ 보내면 된다. 하지만 저걸 바이너리로 보낼 경우… 많이 귀찮아 진다. 인코딩 디코딩까지 해주어야하기에 범용성이 떨어진다. 한국어만 있을까? 다른 언어까지 섞여들어가면 많이 귀찮아지기 때문에, 일반적인 경우 범용성 좋게 텍스트로 데이터를 전송하고 이경우에는 rio_readnb를 사용하면 된다.

하지만 바이너리로 보낼 경우, 버퍼가 없는게 훨씬 효율성이 좋으므로 rio_readn을 사용하면 된다. 이처럼 바이너리 전송할때 쓰면 좋을 rio_readnread함수로 구현이 되어있고, 우리의 범용성 좋은 rio_readnb함수는 rio_read함수로 구현되어있다. 아마 proxy통신을 하며 텍스트 메시지를 보낼 때, 버퍼가 있는 rio_readnb를 사용하다 버퍼가 없는rio_readn을 사용하면 텍스트가 끊겨서 전송되는 현상을 실험해본 분들도 있을 것이다.

통신을 할 때, 이녀석들은 본인들의 버퍼를 꽉 채워다가 가져다 준다. 따라서 어디까지 읽었는지를 기록할 rio_bufptr라는 포인터로 기억을 해둔다. 따라서 버퍼를 사용하는 친구들인 rio_readlinebrio_readnb를 번갈아가며 사용해도 한 소켓 안에서는 rio_bufptr로 어디까지 읽었는지 기억하기 때문에 문제가 없는 것이다.

#define RIO_BUFSIZE 8192
typedef struct {
    int rio_fd;                /* Descriptor for this internal buf */
    int rio_cnt;               /* Unread bytes in internal buf */
    char *rio_bufptr;          /* Next unread byte in internal buf */
    char rio_buf[RIO_BUFSIZE]; /* Internal buffer */
} rio_t;

따라서 위처럼 생긴 rio_t구조체를 공유하며, 어디까지 읽었는지를 명확하게 표시해주는 버퍼를 이용한 함수들은 어떤 방식으로 불려도 상관이 없지만, 이를 버퍼없이 사용하는 친구들과 혼용해서 사용할 경우 ( rio_t구조체를 공유하지 않는) 버퍼없는 친구들이 이상한 곳(이미 버퍼친구들이 읽은 곳)을 읽어들이기 때문에 패킷구성을 엉뚱하게 읽어들이는 것이다.

버퍼시리즈들로만 구성하자!!

마치며

TCP/IP, OSI 7계층 등의 통신 모델들의 특정 부분만 파도 어마어마한 양이라는 것을 알게 되었다(HTTP 1.0과 1.1의 차이점 등). 몇년에 걸쳐 배워나가도 모자랄 네트워크 지식을 짧은 시간안에 이해하기는 힘드므로, 프록시 서버를 C로 직접 구현을 하며 소켓통신 개념을 익힐 수 있는 기회를 주었다고 생각한다. 일정부분은 추상화되어있기에 그대로 썼지만, 뭔가 지식에 대한 갈망이 느껴졌다. 어느 한 부분에 꽂혀 토끼굴로 들어가는것도 좋지 않지만, 또 제대로 알지도 못한채 프로그래밍을 하는 습관도 좋지 않다는것을 알고있다. 나에게 당장 필요하며, 중장기적으로 더 큰 리턴을 줄 수 있는 공부범위(어느정도 까지 파고들어서 공부해야하는지)를 잘 계산해나가는게 요령이지 않을까 싶다.



😵배우면서 깨달은 내용을 정리해 보았습니다. 틀린 것 같은 개념을 아래 댓글에 달아주시면 감사합니다😵

🌜 Thank you for reading it. Please leave your comments below😄

맨 위로 이동하기

swjungle survive 카테고리 내 다른 글 보러가기

댓글 남기기