PHP로 HTTP 서버 구현하기 - 04 - 가장 간단한 서버
Updated (2017.10.18)
이번 글과 관련된 내용이라 조금 더 추가해봤습니다.
하단 Updated 참고.
가장 간단한 서버
이제부터 하나씩 기능을 붙여가며 commit & push를 할 예정이다.
PHP7.1/mac 환경에서 공부한 결과를 적는 거라서, 윈도 머신에선 안 통하는 설명도 있으리라.
소스 위치는 https://github.com/youngiggy/phttp
PHP로 HTTP 서버 구현하기 - 01에서 언급한 가장 간단한 서버부터 시작해볼까?
|
딱 봐도 어려울 것 없는 코드다.
127.0.0.1이란 IP에 1337 포트를 열었다.
왜 하필 1337이냐고? 그냥! 저 블로그 쓴 사람이 그렇게 써서 따라했다.
사용하는 프로그램에 따라 이미 포트가 선점되었을 수도 있다.
- 기본으로 이 포트를 쓰는 프로그램이 있는지 이런 곳에서 찾아봐도 좋고
- 쉘에서
lsof -PiTCP -sTCP:LISTEN
포트가 열려있는지 확인해봐도 좋다
그 다음 while 루프로 뱅글뱅글 돌면서 연결이 들어오는 지 확인한다.
stream_socket_accept
함수가 말을 못하게 @로 입을 막자.
- ♪ No alarms and no surprises
- 하지만 안 막는다고 뭐 대단한 걸 볼 일은 없을 것이다
연결이 된 클라이언트가 있다면, stream_copy_to_stream
를 통해 클라이언트가 보낸 스트림을 고스란히 돌려주자.
고스란히…
가장 간단한 클라이언트
가장 간단한 서버가 완성됐으니 터미널을 열어 php ./app/server.php
로 일을 시키고 돌아오자.
새로운 터미널을 열어 echo "hello hello" | nc 127.0.0.1 1337
를 입력하면 hello hello
라고 반갑게 인사하는 것이 보일 것이다.
너무 시시하니까 가장 간단한 클라이언트를 만들어 잘 돌아가는 지 구경해보자.
아직 브라우저에서 HTTP 프로토콜로 붙을 상황은 아니므로 간단한 TCP 연결 클라이언트로 테스트 한다.
역시 PHP Socket Programming, done the Right Way에서 예시로 보여준 것을 기본으로 시작한다.
|
역시 별 거 없는 코드다.
stream_socket_client
으로 연결을 시도하고,
HTTP 메시지를 만들어 준다.
- HTTP 메시지는 시작줄, 헤더, 공백, 바디로 이뤄졌다. 참고
이 메시지를 fwrite를 통해 스트림에 써 준다.
02편에서 얘기한 것처럼, stream_socket_client
이 리턴하는 stream resource에는 fopen, fread같은 filesystem 함수를 사용할 수 있다.
실행
터미널을 하나 열고 php ./app/client.php
로 클라이언트를 실행해보자.
아무 반응이 없을 것이다.
하지만 조금만 더 기다려보면, 1분 후 timeout으로 client가 종료되고 자신이 서버에 전송한 값이 화면에 찍한다.
왜 그럴까?
다르게 재현해보자.
서버를 다시 실행하고 클라이언트를 다시 실행한 후, 이번엔 서버를 죽여보자.
이때도 클라이언트와 연결이 바로 끊어지고 서버에 전송한 값이 되돌아와 화면에 찍힌다.
우선 timeout 1분은 환경마다 다를 수 있는데, php.ini에 default_socket_timeout에 정의되어 있다.
client 소스의 response 부분을 아래와 같이 바꿔보자.
/* |
그럼 아래와 같이 나오던 것이
GET / HTTP/1.0 |
아래와 같이 바로 찍힌다.
GET |/ HT|TP/1|.0 |
stream_get_contents
의 두번째 인자는 maxlength라서 지정한 만큼만 스트림에서 읽어온다.
그래서 읽어온 만큼 바로바로 화면에 echo 하는 것이다.
- 보기 좋으라고(?)뒤에 |를 붙여봤다.
하지만 이렇게 해도 클라이언트는 종료되지 않는다.
stream_get_contents
가 계속 스트림에 뭔가 들어오는지 지켜보고 있어서 while loop가 끝나지 않는다.
이 악순환의 고리를 끊으려면 서버에서 연결을 끊어버리면 간단한데, 우리 코드에선 클라이언트로 들어오는 스트림을 바로 돌려주기 때문에 쉽지 않다.
클라이언트에서 loop를 종료하려면 어디가 끝인지를 확인해야만 한다.
서버가 Content-Length를 돌려주게끔 해주면 좋겠지만 우리 서버는 아직 바보니까 클라이언트가 좀 더 고생을 하자.
$messageLenth = strlen($message); |
Updated
이 글 이후로 좀 더 진행해보다가 meta 정보를 이용하는 방법까지는 여기에 묻어두는 것이 좋을 것 같아서 내용을 추가한다.
stream_get_contents
으로 데이터를 가져온 다음 현재 스트림의 상태를 보려면 stream_get_meta_data를 통해 확인할 수 있다.
여기에는 다음과 같은 정보가 리턴된다.
Array |
어쩐지 eof나 unread_bytes를 쓰면 될 것 같다.
/* |
이렇게 하니 소중한 고객님의 연결이 끊겼습니다.
그러나,
문서를 보면 unread_bytes가 의미하는 것은,
unread_bytes (int) - the number of bytes currently contained in the PHP's own internal buffer. |
즉, PHP 자체의 내부 버퍼의 내용을 보여주는 것이라서 스크립트에서 활용하지 않을 것을 추천하고 있다.
Note: You shouldn’t use this value in a script.
그럼 포기하고 다른 방법을 찾아보자.
우선 client 쪽 소스에서 unread_bytes를 검사하는 부분만 제거한 다음,
/* |
연결이 안 끊어지는 것을 반드시 확인하자.
그 다음,
서버쪽 소스에서 stream_set_blocking으로 non-blocking 모드로 바꿔보자.
stream_set_blocking($client, false); |
즉, 서버의 전체 소스는 아래와 같이 변할 것이다.
|
클라이언트를 실행하면 바로 연결이 끊긴다.
stream_set_blocking
함수의 두번째 인자 mode가 false로 들어가면 non-blocking 모드로 진입하며,
클라이언트 쪽에서는 스트림으로 더이상의 데이터가 들어오는 것을 기다리지 않는다.
이렇게 하면 굳이 스트림을 쓸 이유가 없기 때문에 이 역시도 좋은 방법은 아닐 것이다.
결론
아주 간단한 서버와 아주 간단한 클라이언트를 아주 간단히 살펴봤다.
도저히 못 써먹겠으니 다음 글에서는 이를 좀 더 개선해보자.
(뭘 어떻게 바꿀 지는 아직 계획없음)