출처 : http://drake.kr/71788
PHP를 이용한 다중 연결 소켓 통신 (1)
프리랜서
프로그래머
<jinoos@jinoos.com>
- 차례
- 1. 소개
- 1.1. PHP 컴파일 하기
- 1.2. PHP의 소켓 함수
- 2. 소켓 서버 만들기
- 3. 소켓 클라이언트 만들기 와 실행
- 4. 결론
이문서의 배포는 자유로우나 최소한 제작자의 정보는 제외하지 않고 배포해 주세요.
문서가 존재하는 모든곳에 답변을 드릴수 없으므로 질문은 홈페이지(http://www.jinoos.com)에서만 받습니다.
1. 소개
앞으로 몇번에 걸처서 PHP를 이용한 다중 연결 소켓 통신에 대해서 알아보겠습니다. PHP는 확장 함수로 socket 계열 함수를 지원합니다. 하지만 이것은 컴파일시 --with-socket 옵션을 주어 컴파일 해야 합니다. 클라이언트 소켓에 관한 함수 fsockopen()는 이곳에서 사용하지 않습니다.
1.1. PHP 컴파일 하기
앞으로 설명할 내용들은 일반적인 아파치 모듈로서의 PHP가 아닌 독립적으로 실행 가능한 PHP CGI형태입니다. 이를 위해서 재 컴파일이 필요하며 이때 소켓에 관한 옵션을 추가하게 됩니다.
PHP 4.3.1 버전을 이용했습니다.
#] tar -zxvf php-4.3.1.tar.gz
#] cd php-4.3.1
#] ./configure --with-socket
#] make
make 를 한후 make install 을 하지말고 php-4.3.1 디렉터리를 보면 php 라는 실행파일이 생성된것을 알수 있다. 그럼 이제 프로그램 짤 준비는 다?다. 간단한가? 간단하다. -_-a
1.2. PHP의 소켓 함수
PHP의 소켓 함수 몇가지를 알아보고 넘어 가도록 하자. PHP의 소켓 함수는 C sys/socket.h 에 정의 되어있는 함수들과 매우 유사하다. 참고 하도록 하자.
- socket_create ( int domain, int type, int protocol )
소켓의 연결 자원을 생성 합니다. 소켓 함수를 사용할때 기본으로 쓰입니다.
domain 파라메터에는 AF_UNIX 또는 AF_INET 으로 유닉스 도메인 소켓 또는 인터넷 소켓으로 지정합니다.
type 파라메터에는 STREAM, SOCK_DGRAM, SOCK_SEQPACKET, SOCK_RAW, SOCK_RDM, or SOCK_PACKET 등의 소켓의 종류가 설정 됩니다.
protocol 파라메터에는 TCP또는 UDP 설정(SOL_UDP or SOL_TCP)을 합니다.
- socket_connect ( resource socket, string address [, int port] )
서버소켓으로 접속할때 사용하는 함수. 소켓 클라이언트에서 사용하는 함수 입니다.
- socket_setopt ( resource socket, int level, int optname, int )
소켓에 옵션을 지정합니다. 크게 중요하지 않은 함수 입니다.-_-
- socket_bind ( resource socket, string address [, int port] )
서버 소켓에 주소와 포트번호를 부여 합니다. 서버 소켓에서 필수적인 작업입니다.
만약 AF_UNIX로 지정했다면 port 파라메터는 생략이 가능합니다.
- socket_listen ( resource socket [, int backlog] )
서버 소켓을 클라이언트 대기 상태로 만듭니다.
socket_listen() 함수를 호출한 이후부터 서버 소켓은 클라이언트가 접속하는지 아닌지 귀를 기울이는 일을 하게 되는겁니다. 서버 소켓 사용에서 필수 적입니다.
backlog 파라메터는 받아들일 클라이언트의 최대 수를 지정합니다. 생략하면 최대 갯수로 설정됩니다.
리눅스에서 소켓의 최대 갯수는 ulimit -a 를 해보시면 open files 항목이 소켓 최대 갯수와 상응합니다.
- socket_accept ( resource socket )
클라이언트 접속을 허가 할때 사용하는 함수 입니다.
함수 호출후 새로운 소켓 리소스가 리턴되는데 이것은 기존에 서버 소켓 리소스에서 생성된 클라이 언트와 통신하는 새로운 소켓 리소스 입니다.
- socket_getpeername ( resource socket, string &addr [, int &port] )
클라이언트의 접속 정보를 가져 옵니다.
- socket_read ( resource socket, int length [, int type] )
소켓에서 length만큼의 정보를 읽어 들입니다.
type 읽어들일 타입을 결정 합니다.
- socket_write ( resource socket, string buffer [, int length] )
소켓에 정보를 써넣는 함수로 마지막 인자를 생략하면 buffer의 길이가 자동으로 대입됩니다.
- socket_close ( resource socket )
소켓 연결을 끊는다.
2. 소켓 서버 만들기
이제 실전으로 들어가 보겠다. 오늘 만들것은 간단하게 클라이언트가 접속하면 클라이언트의 접속 정보를 출력하고 클라이언트에게 현재 날자와 시간을 전송한후 종료 합니다.
엄밀히 말하면 오늘 만들 소켓 서버는 다중 사용자를 위한 시스템은 아니다. 하지만 오늘은 간단히 소켓의 개념을 집고 넘어가는 의미로 생각하자.
PHP를 CGI형태로 실행하기 위해서 소스코드 상단에
#!/usr/local/bin/php -q
<?php
..
..
..
?>
처럼 프로그래밍을 합니다. 이것은 Perl스크립트나 기타 쉘 스크립트에서 전처리기를 설정하는것과 같은 방법 입니다. -q 옵션은 http protocol 헤더를 제거 하는 옵션입니다.
#!/usr/local/bin/php -q
<?php
define("_IP", "123.123.123.12");
define("_PORT", "65000");
$sSock = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_bind($sSock, _IP, _PORT);
socket_listen($sSock);
while($cSock = socket_accept($sSock))
{
socket_getpeername($cSock, $addr, $port);
echo "SERVER >> client connected $addr:$port n";
$date = date("Y/m/d H:i:s");
socket_write($cSock, $date);
socket_close($cSock);
echo "SERVER >> client Close.n";
}
?>
서버는 지속적으로 클라이언트를 기다리는 형태의 모습을 하고 있습니다.
3. 소켓 클라이언트 만들기 와 실행
클라이언트는 서버에 접속을 하고 서버쪽에서 보내온 메시지를 읽은뒤 출력하고 종료합니다.
주의
프로그램상의 아이피는 가상으로 만든 것입니다. 테스트시 적절한 아이피로 변경해 주세요.
#!/home/dimeclub/www/bin/php/php -q
<?php
define("_IP", "123.123.123.12");
define("_PORT", "65000");
$sock = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_connect($sock, _IP, _PORT);
echo "CLIENT >> socket connect to "._IP.":"._PORT."n";
$date = socket_read($sock, 4096);
echo "CLIENT >> this time is $date n";
socket_close($sock);
echo "CLIENT >> socket closed.n";
?>
너무 간단하다고 돌 날아오겠네요. -_-a. 위 두 파일을 server.php, client.php라 저장하고 실행권한을 준뒤 실행해 보도록 하겠습니다.
아래 화면의 #]는 쉘 커맨드 입니다.
#]
#] ll
total 8
-rwxr-xr-x 1 jinoos jinoos 387 May 7 04:59 client.php
-rwxr-xr-x 1 jinoos jinoos 495 May 7 04:59 server.php
#]
#] ./server.php &
[1] 23833
#]
#] ./client.php
SERVER >> client connected 123.123.123.12:45188
SERVER >> client Close.
CLIENT >> socket connect to 123.123.123.12:65000
CLIENT >> this time is 2003/05/07 04:59:49
CLIENT >> socket closed.
#]
#] ./client.php
SERVER >> client connected 123.123.123.12:45358
SERVER >> client Close.
CLIENT >> socket connect to 123.123.123.12:65000
CLIENT >> this time is 2003/05/07 05:01:03
CLIENT >> socket closed.
#]
#]
위 내용은 서버프로그램을 백그라운드로 실행하고, 짧은 시간동안 두번 클라이언트로 서버에 접속한 모습입니다. 원하던데로 출력된것을 확인할수 있습니다.
같은 머신 상에서 접속하게 된다면 서버와 클라이언트에서 같은 IP를 출력하게 될것입니다. 그리고 잘보면 클라이언트의 PORT가 변경되었는데 클라이언트의 머신의 경우 일정 대역의 포트중 사용하지 않는 포트를 자동으로 사용하는 것입니다. 이것을 프로그램상으로 조정하는것은 아닙니다. OS에서 자동적으로 할당된것을 사용하는것입니다.
4. 결론
오늘은 간단하게 PHP를 이용해서 소켓함수를 사용하는 것을 해보았습니다. 다음시간에는 PHP의 Process Control Functions 함수들을 이용해서 Fork를 사용한 오늘 보다 좀더 진보된 다중 접속 서버 소켓을 만들어 보겠습니다.
PHP를 이용한 다중 연결 소켓 통신 (2)
프리랜서
프로그래머
<jinoos@jinoos.com>
- 차례
- 1. 소개
- 2. SELECT
- 2.1. socket_select() 함수
- 3. 프로그램 작성
- 3.1. 서버 만들기
- 3.2. 클라이언트 만들기
- 3.3. 실행하기
- 4. 결론
이문서의 배포는 자유로우나 최소한 제작자의 정보는 제외하지 않고 배포해 주세요.
문서가 존재하는 모든곳에 답변을 드릴수 없으므로 질문은 홈페이지(http://www.jinoos.com)에서만 받습니다.
1. 소개
저번강좌(PHP를 이용한 다중 연결 소켓 통신 (1))에 간단한 서버/클라이언트 프로그램을 만들어 보았습니다. 하지만 이것은 많은 부분이 부족하다는 생각들을 하셨을껍니다.
오늘 시간에는 socket_select() 함수를 통해서 다중의 클라이언트 요청을 처리하는 프로그램을 짜 보겠습니다.
2. SELECT
저번강좌(PHP를 이용한 다중 연결 소켓 통신 (1))의 server.php 에서
while($cSock = socket_accept($sSock))
{
$date = date("Y/m/d H:i:s");
socket_write($cSock, $date);
socket_close($cSock);
}
위코드는 클라이언트 소켓이 접속하기를 대기하는 동작입니다. 하지만 socket_accept()함수는 호출된뒤에 소켓연결 요청이 들어오면 요청 처리를 하게 됩니다. 프로그램 상에선 메시지를 전송하고 곧바로 클라이언트와 소켓을 종료하고 다시 accept 상태(blocking모드)로 전환되는 형태입니다.
이와같은 경우는 접속된 클라이언트와 소켓 접속이 반드시 끊어져야(아니면 당연히 끊어야 한다던지) 다른 클라이언트의 접속을 처리 할수 있습니다. 하지만 불규칙 적으로 클라이언트가 서버에 요청(소켓접속이 아님)을 하는 상황이라면 문제가 될것이 자명 합니다.
이와 같은 상황을 처리 하기 위해서 다량의 socket묶음을 동시에 감시하는 SELECT가 있습니다. PHP에서는 socket_select() 함수가 바로 그것입니다.
2.1. socket_select() 함수
- int socket_select ( resource &read, resource &write, resource &except, int tv_sec [, int tv_usec] )
read 인자에는 읽기 이벤트를 감시할 Socket Array가 참조됩니다. socket_select() 함수는 Array 묶음으로 들어오는 소켓들을 감시하다가 이벤트가 발생하면 해당인자를 읽기 이벤트가 발생한 Socket Array로 변환하고 Blocking 상태가 해제 됩니다.
write 인자역시 read 인자와 같은 역할로 쓰기 이벤트가 발생할시에 사용된다.(하지만. -_- 어디에 필요하단 말인가. 본인은 이부분이 이해가 잘 안간다. 혹시 알고있다면 알려주기 바란다. except 역시 본인이 사용법을 알아내지 못했다.)
중요한 인자인 tv_sec는 select가 block 모드로 대기하고 있는 timeout을 설정합니다. 이것을 유용하게 이용하면 일정간격으로 어떠한 작업을 수행하는것도 가능합니다. NULL로 설정하면 timeout없이 대기 합니다.
3. 프로그램 작성
전장에서 select 함수에 대해 몇가지를 알아 보았습니다. 부족하지만 사용하기엔 크게 문제가 없을 것입니다.
오늘 작성한 프로그램은 저번강좌(PHP를 이용한 다중 연결 소켓 통신 (1)) 에서 작성한 프로그램과 기능은 같지만 약간 복잡한 형태입니다.
서버소켓을 생성한뒤에 클라이언트를 대기하고, 클라이언트가 접속하면 클라이언트 소켓을 클라이언트 소켓 묶음으로 저장한뒤에 감시합니다. 새로운 클라이언트가 접속하면 다시 클라이언트 묶음에 저장하고 감시를 반복하며, 클라이언트에서 time라는 메시지를 수신하면 현재 시간을 바로 송신하고, quit라는 메시지를 수신하면 클라이언트와 접속을 끊습니다.
3.1. 서버 만들기
시작코드는 저번 강좌와 많이 비슷합니다. 다만 무한 루프를 돌면서 서버 소켓과 클라이언트 소켓 묶음을 Array로 생성하여 select의 Read 이벤트를 감시합니다.
...
while(1)
{
$sockArr = array_merge(array($sSock), $cSock);
if(socket_select($sockArr, $tWrite = NULL, $tExcept = NULL, _TIMEOUT) > 0)
{
... (1)
}
}
...
Write 이벤트와 Except 이벤트는 무시하기 위해서 NULL 처리 되었습니다. timeout은 주어졌지만 본 프로그램에선 크게 의미 없는 상황입니다.
위 코드중 socket_select()에서 이벤트가 발생되면 $sockArr 변수에 리턴된 Array를 (1)에서 분석합니다.
...
foreach($sockArr as $sock)
{
if($sock == $sSock)
{
... // (2)
}else
{
... // (3)
}
}
...
서버 소켓에 클라이언트가 접속을 요청했을때 $sockArr에 서버 소켓이 리턴됩니다. 그리하여 (2)에서 클라이언트 소켓을 socket_accept()함수로 접속을 허용합니다.
만약 리턴된 소켓이 서버 소켓이 아니라면 클라이언트와 통신중인 소켓입니다. 당연하겠죠. array_merge(array($sSock), $cSock) 로 서버 소켓과 클라이언트 소멧만을 merge했으니까요.
(3)에서는 클라이언트에서 보내는 정보를 읽어들이고 이를 분석해서 처리 하는 부분이 들어 갑니다. 유의해서 봐야 할 부분은
$buf = socket_read($sock, 4096);
// 접속 종료
if(!$buf)
{
}
// 메시지 수신 이벤트
else
{
}
$buf 의 내용이 없을때 클라이언트가 접속을 종료함을 의미 한다는 것입니다.
자 그럼 이제 실제 코드를 보겠습니다.
#!/home/dimeclub/www/bin/php/php -q
<?php
set_time_limit(0);
define("_IP", "111.222.333.12");
define("_PORT", "65000");
define("_TIMEOUT", 10);
$cSock = array();
$cInfo = array();
$sSock = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_bind($sSock, _IP, _PORT);
socket_listen($sSock);
while(1)
{
$sockArr = array_merge(array($sSock), $cSock);
if(socket_select($sockArr, $tWrite = NULL, $tExcept = NULL, _TIMEOUT) > 0)
{
foreach($sockArr as $sock)
{
// Listen 하고 있는 서버 소켓일 경우
// 새로운 클라이언트의 접속을 의미
if($sock == $sSock)
{
$tSock = socket_accept($sSock);
socket_getpeername($tSock, $sockIp, $sockPort);
$cSock[$tSock] = $tSock;
$cInfo[$tSock] = array('ip'=>$sockIp, 'port'=>$sockPort);
msg("client connect : ".$sockIp.":".$sockPort."n");
}
// 클라이언트 접속해 있는 소켓중 하나일경우
// 해당 클라이언트에서 이벤트가 발생함을 의미
else
{
$buf = socket_read($sock, 4096);
// 접속 종료
if(!$buf)
{
exceptSocket(&$cSock, &$cInfo, $sock);
msg("client connection broken : ".$sockIp.":".$sockPort."n");
}
// 메시지 수신 이벤트
else
{
msg("recive data : ".$buf."n");
$thisSockInfo = $cInfo[$sock];
$cmd = substr($buf, 0, 4);
switch($cmd)
{
// 시간전송
case "time":
msg("client(".$thisSockInfo['port'].") time data requestn");
socket_write($sock, date("Y/m/d H:i:s"));
break;
// 종료
case "quit":
msg("client(".$thisSockInfo['port'].") quit requestn");
socket_write($sock, "quit");
socket_close($sock);
exceptSocket(&$cSock, &$cInfo, $sock);
break;
default:
msg("client(".$thisSockInfo['port'].") invalid command $cmdn");
break;
}
}
}
}
}
}
function exceptSocket(&$sockSet, &$infoSet, $sock)
{
unset($sockSet[$sock]);
unset($infoSet[$sock]);
// array_merge 함수에서 error 발생을 막기위한 처리
if(count($sockSet)==0)
{
$sockSet = array();
$infoSet = array();
}
}
function msg($msg)
{
echo "SERVER >> ".$msg;
}
?>
server.php로 저장합니다. 역시 실행권한은 있어야 겠지요.
3.2. 클라이언트 만들기
클라이언트 역시 몇가지를 수정하여 쉘에서 사용자의 입력을 받아 들이도록 처리하였습니다.
클라이언트는 서버와 소켓을 연결한뒤 사용자의 키입력을 기다렸다 time을 입력시 서버에 time메시지를 전송하고 quit입력시 프로그램을 종료 합니다. PHP CGI를 이용해서 사용자 키입력을 받아들이는 역할은 아래 함수처럼 구현되었습니다.
function read_data()
{
$in = fopen("php://stdin", "r");
$in_string = fgets($in, 255);
fclose($in);
return $in_string;
}
fopen("php://stdin", "r") 로 원래 의미는 리눅스 시스템에서 fopen("/dev/stdin", "r"), fopen("/proc/self/fd/0", "r"), fopen("dev/fd/0", "r") 입니다. 표준입력을 받아들이는 역할로 사용자 키입력을 받아들이죠.
만약 표준출력 또는 표출에러 출력을 받아들이려면 fopen("php://stdout","r"), fopen("php://stderr", "r") 로 구현할수 있습니다.
클라이언트는 time 라는 서버에 시간 데이터를 요청하는 메시지와 quit 라는 종료 두 메시지를 제외하고는 나머지는 무시합니다. 코드상에는 invalid command (not send) 라고 출력하지만 그이상의 어떠한 행동도 하지 않고 무시해 버립니다.
#!/home/dimeclub/www/bin/php/php -q
<?php
define("_IP", "111.222.333.12");
define("_PORT", "65000");
$sock = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_connect($sock, _IP, _PORT);
msg("socket connect to "._IP.":"._PORT."n");
while(1)
{
msg("Enter command time or quit : ");
// 사용자의 명령어를 입력받습니다.
$stdin = ereg_replace("n|r", "", read_data());
$stdin = substr($stdin, 0, 4);
// time 또는 quit 메시지 말고는 무시 합니다.
if($stdin == "time" || $stdin == "quit")
{
msg("Input command : ".$stdin."n");
}else
{
msg("invalid command (not send) : ".$stdin."n");
continue;
}
socket_write($sock, $stdin);
$sMsg = socket_read($sock, 4096);
if(substr($sMsg, 0, 4) == 'quit')
{
socket_close($sock);
exit;
}else
{
msg("recived data : ".$sMsg."n");
}
}
// 표준입력을 받아 값을 리턴하는 함수
function read_data()
{
$in = fopen("php://stdin", "r");
$in_string = fgets($in, 255);
fclose($in);
return $in_string;
}
// 로그를 출력합니다. 디버그용
function msg($msg)
{
echo "CLIENT >> ".$msg;
}
?>
특별히 난해한 부분은 없을꺼라 생각합니다. 역시 위 소스를 client.php로 저장하고 실행권한을 줍니다.
3.3. 실행하기
서버를 실행하고 클라이언트를 실행합니다. 클라이언트#1을 실행후 time 메시지를 날리면서 클라이언트#2 실행후 종료, 클라이언트#3 실행후 종료, 그리고 다시 클라이언트#1을 종료 하였습니다.
클라이언트#1
#] ./client.php
CLIENT >> socket connect to 111.222.333.12:65000
CLIENT >> Enter command time or quit : time
CLIENT >> Input command : time
CLIENT >> recived data : 2003/05/09 22:32:29
CLIENT >> Enter command time or quit : time
CLIENT >> Input command : time
CLIENT >> recived data : 2003/05/09 22:32:34
CLIENT >> Enter command time or quit : time
CLIENT >> Input command : time
CLIENT >> recived data : 2003/05/09 22:32:41
CLIENT >> Enter command time or quit : time
CLIENT >> Input command : time
CLIENT >> recived data : 2003/05/09 22:32:50
CLIENT >> Enter command time or quit : time
CLIENT >> Input command : time
CLIENT >> recived data : 2003/05/09 22:32:57
CLIENT >> Enter command time or quit : quit
CLIENT >> Input command : quit
#]
클라이언트#2,#3
#] ./client.php
CLIENT >> socket connect to 111.222.333.12:65000
CLIENT >> Enter command time or quit : time
CLIENT >> Input command : time
CLIENT >> recived data : 2003/05/09 22:32:31
CLIENT >> Enter command time or quit : quit
CLIENT >> Input command : quit
#] ./client.php
CLIENT >> socket connect to 111.222.333.12:65000
CLIENT >> Enter command time or quit : time
CLIENT >> Input command : time
CLIENT >> recived data : 2003/05/09 22:32:47
CLIENT >> Enter command time or quit : quit
CLIENT >> Input command : quit
#]
같은 상황의 서버쪽 실행화면입니다.
#] ./server.php
SERVER >> client connect : 111.222.333.12:32850 -- (4)
SERVER >> client connect : 111.222.333.12:32868 -- (5)
SERVER >> recive data : time
SERVER >> client(32850) time data request
SERVER >> recive data : time
SERVER >> client(32868) time data request
SERVER >> recive data : time
SERVER >> client(32850) time data request
SERVER >> recive data : quit
SERVER >> client(32868) quit request -- (6)
SERVER >> recive data : time
SERVER >> client(32850) time data request
SERVER >> client connect : 111.222.333.12:32938 -- (7)
SERVER >> recive data : time
SERVER >> client(32938) time data request
SERVER >> recive data : time
SERVER >> client(32850) time data request
SERVER >> recive data : quit
SERVER >> client(32938) quit request -- (8)
SERVER >> recive data : time
SERVER >> client(32850) time data request
SERVER >> recive data : quit
SERVER >> client(32850) quit request -- (9)
(4)에서 클라이언트포트 32850, (5)에서 32868 이렇게 두개의 클라이언트가 접속했습니다. 서버는 두개의 클라이언트에서 time 메시지를 수신하면서 동작하다 (6)에서 하나의 클라이언트가 접속을 종료하고 (7)에서 다시 하나의 클라이언트가 32938 클라이언트 포트로 다시 접속했습니다. 그리고 다시 time 메시지를 주고 받다 (8)에서 하나의 클라이언트가 퇴장하고 그리고 (9)에서 마지막 클라이언트 까지 종료 되었습니다.
32850, 32868 그리고 32938이 각각 클라이언트 #1, #2, #3 입니다.
4. 결론
오늘은 SELECT라는 함수를 이용한 다중 소켓 연결 서버를 만들어 보았습니다. SELECT는 간단한 구조에서 다중의 클라이언트를 받아들일수 있는 방법으로 이용됩니다. 또한 SELECT 기능은 단일소켓연결의 timeout 구현에도 사용할수 있는데 이것은 여러분들이 해보시기 바랍니다.
다음 강좌에서는 PHP의 Process Control Functions을 이용해서 Fork를 이용한 다중 연결 소켓 통신을 해보겠습니다.
PHP를 이용한 다중 연결 소켓 통신 (3)
프리랜서
프로그래머
<jinoos@jinoos.com>
- 차례
- 1. 소개
- 2. pcntl_fork() 함수
- 3. PHP 컴파일 하기
- 4. 프로그램 작성
- 4.1. 서버 만들기
- 4.2. 클라이언트 만들기
- 4.3. 실행하기
- 5. 결론
이문서의 배포는 자유로우나 최소한 제작자의 정보는 제외하지 않고 배포해 주세요.
문서가 존재하는 모든곳에 답변을 드릴수 없으므로 질문은 홈페이지(http://www.jinoos.com)에서만 받습니다.
1. 소개
이번강좌에는 fork를 이용해서 새로운 프로세스를 생성하여 생성된 자식 서버프로세스가 클라이언트를 담당하는 형태를 구연해 보겠습니다.
PHP에서 fork함수로는 Process Control 함수의 pcntl_fork() 함수가 있습니다. Process Control 함수는 기본함수가 아니기 때문에 컴파일시 옵셥으로 추가시켜야 합니다.
2. pcntl_fork() 함수
- int pcntl_fork ( )
함수 호출후 리턴값에 0이면 자식 프로세스이며 >0 이면 부모 프로세스로 자식 프로세스의 PID번호를 리턴 받습니다. error발생시에는 -1 값을 가집니다.
포크 함수는 포크 함수를 실행한 프로세스와 동일한 자식 프로세스를 생성합니다. 동일한 자식 프로세스라는 의미는 프로세스 계보상의 깊이만 다를뿐 동작은 똑같은 쌍둥이를 만드는 것 입니다.
자식 프로세스는 부모 프로세스의 메모리를 복사해서 클론을 만들고 리소스(파일 지시자, DB 커넥션, 소켓 커넥션 등)은 공유합니다.
간단하게 pcntl_fork() 코드를 살펴 보겠습니다.
<?php
$i = 0;
$pid = pcntl_fork();
// error
if($pid == -1)
{
echo "fork error";
// 부모 프로세스
}elseif($pid > 0)
{
for(;$i<10;$i++)
{
echo "Parent Process $i : $in";
}
// 자식 프로세스
}elseif($pid == 0)
{
for(;$i<10;$i+=2)
{
echo "Child Process $i : $in";
}
}
?>
부모 프로세스는 $i 값이 1씩, 자식 프로세스는 $i 값이 2씩 증가하는 프로그램 입니다. 결과는 각자 해보시기 바랍니다.
3. PHP 컴파일 하기
첫번째 강좌(PHP를 이용한 다중 연결 소켓 통신 (1)) 에서 소켓 함수를 사용하기 위해 --with-sockets 옵션을 주어 컴파일 하였습니다.
오늘은 소켓 함수와 Process Control 함수를 추가시켜 컴파일 해보겠습니다.
#] tar -zxvf php-4.3.1.tar.gz
#] cd php-4.3.1
#] ./configure --with-sockets --enable-pcntl
#] make
역시 php 실행파일이 생성됩니다.
4. 프로그램 작성
오늘 작성할 서버와 클라이언트의 구조는 아래와 같습니다.
┌───────┐ ┌───┐
┌─(Fork)─┤Child Process ├─(socket)─┤Client│
│ └───────┘ └───┘
┌───────┐ │ ┌───────┐ ┌───┐
│Master Process├─┼─(Fork)─┤Child Process ├─(socket)─┤Client│
└───────┘ │ └───────┘ └───┘
│ ┌───────┐ ┌───┐
└─(Fork)─┤Child Process ├─(socket)─┤Client│
└───────┘ └───┘
좀더 단순화 되고 직관적으로 표현되었군요.
Child Process 한나가 Client 하나를 독립적으로 마크하는 구조입니다.
연결이 끊어진 Child Process는 바로 소멸됩니다. 새로운 클라이언트가 참여하면 바로 Master Process는 pcntl_fork함수를 이용해서 Child Process를 생성하죠.
4.1. 서버 만들기
서버의 구조를 간단히 살펴보면
소켓생성
소켓바인트및 리슨
while(새로운연결수락)
{
포크
if(자식프로세스)
{
while(메시지수신)
{
메시지 처리
if(quit메시지)
{
소켓닫기
종료
}
}
}
}
구조 입니다. 메시지 처리 부분은 지난 강좌(PHP를 이용한 다중 연결 소켓 통신 (2))의 메시지 처리 부분과 동일하며 select처리 대신 fork를 이용한 처리 입니다.
#!/usr/local/bin/php -q
<?php
set_time_limit(0);
define("_IP", "111.222.333.12");
define("_PORT", "65000");
$sSock = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_bind($sSock, _IP, _PORT);
socket_listen($sSock);
pcntl_signal(SIGCHLD, SIG_IGN);
while($sock = socket_accept($sSock))
{
socket_getpeername($sock, $sockIp, $sockPort);
msg("client connect : ".$sockIp.":".$sockPort."n");
$pid = pcntl_fork();
msg("forkn");
if($pid == -1)
{
msg("fork failedn");
exit;
// 자식 프로세스 일때
}if($pid == 0)
{
while(1)
{
$buf = socket_read($sock, 4096);
// 접속 종료
if(!$buf)
{
msg("client connection broken : ".$sockIp.":".$sockPort."n");
exit;
}
// 메시지 수신 이벤트
else
{
msg("recive data : ".$buf."n");
$cmd = substr($buf, 0, 4);
switch($cmd)
{
// 시간전송
case "time":
msg("client(".$sockPort.") time data requestn");
socket_write($sock, date("Y/m/d H:i:s"));
break;
// 종료
case "quit":
msg("client(".$sockPort.") quit requestn");
socket_write($sock, "quit");
socket_close($sock);
exit;
break;
default:
msg("client(".$sockPort.") invalid command $cmdn");
break;
}
}
}
}
}
function msg($msg)
{
echo "SERVER >> ".$msg;
}
?>
역시 server.php로 저장하고 실행권한을 줍니다.
4.2. 클라이언트 만들기
클라이언트는 지난 강좌지난 강좌(PHP를 이용한 다중 연결 소켓 통신 (2))에서 사용한 클라이언트 프로그램을 수정없이 그대로 사용합니다.
4.3. 실행하기
server.php를 실행후 client.php를 3번 실행하고 프로세스와 프로세스 트리를 확인해보겠습니다.
server.php 실행 화면
#] ./server.php
SERVER >> client connect : 111.222.333.12:38276 -- (1)
SERVER >> fork
SERVER >> fork
SERVER >> recive data : time
SERVER >> client(38276) time data request
SERVER >> client connect : 111.222.333.12:38396 -- (2)
SERVER >> fork
SERVER >> fork
SERVER >> recive data : time
SERVER >> client(38396) time data request
SERVER >> client connect : 111.222.333.12:38559 -- (3)
SERVER >> fork
SERVER >> fork
SERVER >> recive data : time
SERVER >> client(38559) time data request -- (4)
SERVER >> recive data : quit
SERVER >> client(38276) quit request -- (5)
SERVER >> recive data : quit
SERVER >> client(38396) quit request -- (6)
SERVER >> recive data : quit
SERVER >> client(38559) quit request -- (7)
client는 (1), (2), (3)에서 3번 실행하여 동일하게 time 메시지를 송신 및 데이타를 수신하고 하고 quit 했습니다.
#] ./client.php
CLIENT >> socket connect to 111.222.333.12:65000
CLIENT >> Enter command time or quit : time
CLIENT >> Input command : time
CLIENT >> recived data : 2003/05/21 16:18:34
CLIENT >> Enter command time or quit : quit
CLIENT >> Input command : quit
#]
아래는 (3),(7)시점에서 두번 프로세스 현황을 확인(ps, pstree)한 결과 입니다.
#] ps -xa | grep server.php
30947 pts/3 S 0:00 /usr/local/bin/php -q ./server.php
31203 pts/3 S 0:00 /usr/local/bin/php -q ./server.php
31287 pts/3 S 0:00 /usr/local/bin/php -q ./server.php
31372 pts/3 S 0:00 /usr/local/bin/php -q ./server.php
31467 pts/7 S 0:00 grep server.php
#] pstree
init-+-crond
...
...
|-sshd-+-sshd---bash---server.php---3*[server.php]
| |-3*[sshd---bash---client.php]
| `-sshd---bash---pstree
...
...
`-xinetd
#]
#] ps -xa | grep server.php
30947 pts/3 S 0:00 /usr/local/bin/php -q ./server.php
31521 pts/7 S 0:00 grep server.php
#] pstree
init-+-crond
...
...
|-sshd-+-sshd---bash---su---bash---server.php
| |-3*[sshd---bash]
| `-sshd---bash---pstree
...
...
`-xinetd
#]
(3) 시점에는 fork 3번 실행한 순간이므로 부모 프로세스와 자식 프로세스 3개, 총 4개의 프로세스가 실행되고 있는것을 확인할수 있습니다.
pstree의 경우는 server.php---3*[server.php]처럼 Master Process 한개와 Child Process 3개로 표현되어 있습니다. 문론 메시지도 잘 전송 되었구요.. ^^
5. 결론
오늘은 PHP의 Process Control Function을 이용하여 다수의 클라이언트 요청처리를 해보았습니다.
fork방식은 select방식보다 간단한 구조로 구현하기 간편하다는 장점도 있지만, 다중 프로세스 구조라 프로세스간 통신을 위해서 부차적인 IPC를 구현해야 할 상황이 생길수도 있다는 점이 단점이라 할수 있습니다.
다음 강좌에는 mysql과 지금까지 배운 소켓 통신을 가지고 간단한 채팅 클라이언트/서버를 만들어 보겠습니다.