이번 주차는 프록시 서버 구현이었다. 처음에는 엥? 내가 서버를 어케 구현? 그게 구현으로 되는거였어? 했는데 이론을 좀 읽고 나니까 정리가 되었다.
어차피 소켓이든 뭐든 CPU 입장에서는 파일 입출력으로 생각하기 때문에 소켓을 열어서 연결해주기만 하면 파일에 읽고 쓰는거랑 똑같이 할 수 있는데, 소켓을 열어주는 정도는 이미 구현된 함수가 있어서 너무 로우레벨까지는 고민하지 않아도 되었다.
그동안 계속 프론트를 해와서 http 요청을 보내는 입장으로 생각하고 있었는데, 이건 서버다! 정해진 요청 방식에 따라 뭘 보내면 처리해서 응답해주는게 아니라 내가 그 요청 방식에 따라 처리해서 보내줘야 되는거였음. HEAD 메소드 처리할려고 하다가 음? 그럼 뭐라고 입력해서 보내야하지? 생각했는데 그냥 내가 받을 때 요청 타입이 HEAD면 분기하는 로직 짜주면 되는거였다.
==로 바로 안된다. strcmp 쓰자. 근데 자꾸 까먹어서 디버깅하면서 다시 깨달음ㅋㅋㅋprintf에서 마지막에 개행문자를 생략하면 출력이 안된다. 자꾸 프린트문만 건너뛰길래 뭔가 했는데 개행문자 문제였음. clientfd가 name or servce not known 에러를 내뱉었다. clientfd에는 클라이언트 이름과 포트번호밖에 넘기지 않기 때문에 그 두 개가 맞게 넘어가면 잘못될 게 없는데 뭐가 문제지?라고 고민하며 머리를 싸멨는데 에러메세지를 여러번 찍어보다 보니 뭔가 줄바꿈이 이상하게 뜨는걸 발견했다. 포트나 호스트네임 줄바꿈 문제인가?라는 생각이 들어 이것 저것 실험해 본 결과 그 문제가 맞았다. telnet으로 http request 보낼 때 엔터를 누르면 맨 뒤에 개행이 \r\n으로 들어가는데, 내가 마지막 자리만 제거하는걸로 처리해서 \r이 남아있던 거였다. C에서 문자열을 처리한 적이 많지 않다 보니 개행문자가 정말 골머리를 썩이는군...strstr 메소드를 사용할 때는 매개변수가 반드시 쌍따옴표로 들어가야 한다. 그게 한글자일지라도....! 따옴표로 하면 세그폴트가 생긴다. c언어에서 chr과 str는 분명히 다른 자료형이기 때문에. 꼭 따옴표를 사용하고 싶다면 strchr을 사용하자.분명히 curl이랑 telnet으로 테스트할때는 잘 나오는데 드라이버만 돌리면 점수가 안 나온다. html은 잘 되는데 csapp.c부터 안 되길래 아예 파일에 저장해서 비교해봤는데 눈으로 봐도 똑같고 text-compare 돌려도 완전히 같다. 웹의 text-compare가 개행문자나 공백은 못 잡아내나 싶어서 diff 써서 비교해봐도 같다고 나옴. 이건 개행문자 차이밖에 없다 싶음. 그러다 문득 html과 csapp.c의 순서를 바꿔서 실행해보니 csapp가 안되는 게 아니라 연속적인 테스트 여러개 중에서 맨 처음것만 되는 거였다.
curl -o로 다운받아서 봐도 다른 요청을 보냈는데 같은 내용이 두개 담긴다. send_response의 버퍼가 초기화가 안됐나 했는데 그것도 아니다. 어쨌든 응답이 제대로 안 오는거니까 send_response쪽이 문제인 줄 알고 열심히 봤는데 캡짱 전공자인 동기가 서버쪽에서 받은 request를 찍어보니까 아예 GET 요청부터 같은 path로 들어가는게 원인이었다.
그럼 프록시쪽 send_request부터 잘못됐다는 뜻이다. uri가 잘못 들어간 것 같은데 프린트로 uri 찍어봤을 때는 또 잘 들어가 있다. 환장하겠네.... 또다른 디버깅왕 동기의 솔루션은 read_request의 buffer 초기화였다. 아마 C언어를 사용할 때 꽤나 흔하게 일어나는 버퍼때문에 입력이 씹히는 문제 같았다.
C언어의 입력은 버퍼에 저장이 됐다가 변수에 들어가는 식으로 중간 과정을 한번 더 거치는데, 엔터를 눌렀을 때의 개행문자가 변수에 같이 입력이 안되고 버퍼에 남아있다가 다음 입력을 받을 때 변수 초기화가 개행문자로 되는 것. 그래서 while의 조건인 strcpm(buf, "\r\n")에서 걸려서 아예 while문 자체를 못 들어가고 request 받는 것 자체를 못하는 거였다.
휴.... C언어에서 문자열을 별로 다뤄본 적이 없다보니 개행문자가 여러모로 정말 말썽이다.
concurrency를 할 때는 좀 새로운 문제가 있었다. 나는 책에 나온doit함수를 사용하지 않고 내가 생각한 대로 함수들을 나눠 구현했기 때문에 스레드를 생성할 때 변수들을 구조체로 넘겨야 했다. 그런데 다른 변수는 다 잘 넘어가는데, connfd만!! 넘어가기 전에는 소켓 넘버가 잘 저장돼있는데 넘어가면 갑자기 주소값을 가지고 있어서 문제였다. 검색해도 안 나와서 지피티한테 물어본 결과.
내 기존 코드는 while문 안에서 구조체를 선언했기 때문에 구조체가 스택에 저장되고 스레드에는 그 구조체에 대한 주소값을 넘긴다. 그런데 그 구조체의 멤버인 connfdp는 while문 안에서 선언된 변수이기때문에 해당 블록이 끝나고 while문 맨 위로 돌아가 새로운 context를 생성하면 다른 값으로 덮어씌워진다. 문제는 여기서 생기는데, while문 블록이 끝나도 스레드는 독립적으로 살아있기 때문에 race condition에 따라 달라진 connfdp에 접근할 지도 모른다는 것.
따라서 해결책으로는 variables를 malloc으로 동적할당 하는 것. 그러면 매 while 루프의 context마다 자신만의 독집적인 variables 인스턴스를 가지게 되면서 다른 스레드나 코드에 의해 값이 덮어씌워질 일이 없어지게 된다.
사실 이건 책을 읽을 때 connfd가 아니라 connfdp로 공간을 할당받아 저장하는걸 왜인지 고민해보았으면 미리 알았을 문제였다. 거기서도 while문 반복마다 새로운 connfd에 의해 기존의 connfd가 이미 덮어씌워졌는데 스레드가 접근할까봐 malloc으로 스택이 아닌 메모리에 저장한 것이었다.