티스토리 뷰
PHP로 소켓 서버 작성하기
PHP로 소켓 서버 작성하기
작성자 김영진(cogolda@hanmail.net)
대상 독자
요구사항
1 개요 - 소켓 서버란 무엇인가?
1.1 소켓의 형태
2 PHP 소켓 함수
2.1 PHP에서 소켓 만들기
2.2 실용 서버 만들기
2.3 실용적인 예
2.4 보안
가능한 기능 추가와 확장
저자에 관해
알아두기
이 자료는 http://www.zend.com/zend/tut/tutorial-staub3.php/에 있는
Writing Socket Servers in PHP를 제가 허접번역 및 내용을 추가 및 생략한 것입니다.
이미 다 아시는 내용 이시겠지만, 이해해 주시면 감사하겠습니다.
질문이나 번역을 바라는 주제가 있으시면,
이메일 또는 코멘트를 이용해 주세요. 참고로 전 영어, PHP 둘다 왕초보^^;.
대상 독자
인터넷 소켓 서버를 만들기 위해 PHP 소켓 함수 사용에 관심있는 분들..
미리 준비할 것
* PHP 소켓 라이브러리. 이것은 컴파일 할 때 -enable-sockets 설정 옵션을 주시면 됩니다.
* PHP의 CLI (Command Line Interface) 버전. 이것은 소켓서버가 커멘드 라인에서 실행하기 때문입니다.
* 리눅스 운영체제
비록 리눅스를 사용한 따라하기이지만, 윈도우나 유닉스환경에서도 됩니다.
윈도우에서, PHP 소켓은 php.ini에서 externsion=php_sockets.dll부분의 주석을 제거해야
사용할 수 있습니다.
1 개요 - 소켓 서버란 무엇인가?
소켓 서버는 소켓 서버에 들어오는 요청과 응답을 대기하고있는 특정 포트에 할당하는 서비스입니다.
이메일 서비스(POP3, SMTP)와 웹서버는 소켓 서버의 좋은 예입니다.
HTTP(Web)서버는 들어오는 요청에 대해 포트 80에서 대기하고,
서버 안에 있는 HTML과 다른 파일(이미지, 동영상,문서)을 클라이언트 사용자에게 서비스 합니다.
소켓 서버는 주로 서비스나 데몬으로 끊임없이 실행됩니다.
쉽게 설명하자면, 우리가 네트워크 프로그래밍을 한다는 의미는 소켓이 제공하는 함수를 이용하여 프로그래밍을 한다는 의미입니다.
일상적으로, 소켓 프로그래밍과 네트워크 프로그래밍은 거의 같은 의미로 사용되고 있습니다.
1.1 소켓의 종류
정보가 인터넷으로 보내질 때, 그것은 주로 패킷으로 나누어집니다.
왜냐하면 큰 용량을 패킷이라는 작은 단위로 쪼갠후에 보내야 하기 때문입니다.
패킷으로 정보를 쪼갤 때 두가지 다른 프로토콜이 있는데, 정보 형태에 대해 전송 필요조건에 의존하기 때문입니다.
* TCP(Transmmission Control Protocol) - 전송 패킷은 다른 끝에서 번호가 붙여지고 끝에가서 조립된다. 그들은전체 메시지를 형성하기 위해 조립된다 .
TCP는 데이터의 손실이 없습니다.(만약 패킷을 잃어버리면, 재전송합니다.), 이메일처럼 완전한 받아야 하는 파일을 보낼때 적합합니다.
* UDP(User Datagram Protocol) - 이것은 비연결 프로토콜입니다.
TCP처럼 IP 프로토콜 위에서 실행됩니다.
그 차이는 UDP는 약간의 에러 복구 서비스를 제공하고 신뢰성이 없습니다.
UDP는 특별히 음악, 동영상 스트리밍처럼 스트리밍 데이타에 적합합니다.
2. PHP 소켓 함수
PHP 는 저 수준에서 소켓을 처리할 수 있습니다.
PHP3에서, PHP는 fsockopen()과 관련 함수로 처리하는 소켓을 도입하였습니다.
(네트워크 부분은 PHP공식 매뉴얼 http://php.net/network를 보세요).
PHP4에서는, PHP의 소켓은 BSD 스타일 소켓에 저수준 연결의 도입으로 멋지게 확장되었습니다.
참고: PHP에서 소켓 함수는 여전히 실험적이지만, 다음 버전에서 더욱 강화될 것입니다.
PHP 소켓함수는 잘만 작성하면 쓸만하다는 것을 테스트는 보여줍니다.
2.1. PHP로 소켓 만들기
PHP에서 저수준 소켓을 만드는 것은 C와 유닉스 소켓 프로그래밍에서 소켓 함수를 사용한것과 매우 비슷합니다.
간단한 예제로 시작해봅시다. 9000 포트에서 연결을 대기하는 소켓 서버는 입력으로 문자열을 받아들이고, 모든 공백 문자는 제거하고, 반환한다.
#!/usr/local/bin/php -q
<?
// 무한정 실행하기 위해 시간한계를 0으로 설정한다.
set_time_limit (0);
// 대기할 IP 주소와 포트번호를 설정한다
$address = '192.168.0.100';
$port = 9000;
// TCP 소켓을 만든다.
$sock = socket_create(AF_INET, SOCK_STREAM, 0);
// IP 주소와 포트번호를 소켓에 결합
socket_bind($sock, $address, $port) or die('Could not bind to address');
// 접속을 위해 대기를 시작한다
socket_listen($sock);
/* 들어오는 요청을 받아들이고 자식 프로세스로 그들을 처리한다 */
$client = socket_accept($sock);
// 클라이언트가 입력한 1024 바이트를 읽는다.
$input = socket_read($client, 1024);
// 입력받은 문자열에서 공백을 제거한다.
$output = ereg_replace("[ \t\n\r]","",$input).chr(0);
// 클라이언드에 출력을 보낸다.
socket_write($client, $output);
// 자식 프로세스를 닫는다
socket_close($client);
// 주 소켓을 닫는다
socket_close($sock);
?>
이 프로그램을 실행하려면, 첫 줄 #!/usr/local/bin/php -q 가 PHP CLI(or CGI) binary의 위치에 있어야 합니다.
여러분은 소스파일의 실행모드를 바꾸는 것이 필요합니다.
(chmod 755 socket_server.php)
실행하려면, 커멘드 라인에서 ./socket_server.php치고 엔터.
외관상 이 프로그램은 아무일도 않합니다.
그럼 각각의 라인을 자세히 살펴봅시다.
* #!/usr/local/bin/php -q
PHP CLI 실행파일을 실행합니다. -q 옵션을 쓰면 HTTP 헤더를 출력하지 않습니다.
* $sock =sock_create(AF_INET, SOCK_STREAM, 0)
'주인' 소켓을 만듭니다. 이 소켓은 들어오는 요청을 위해 대기할 것이고, 클라이언트를 위해 새로운 소켓을 생성할것입니다.
PHP manual에 보면 (http://www.php.net/socket_create): AF_INET는 IPV4 프로토콜의 도메인 타입입니다.
참고: 만약 UDP 소켓을 열기위해서는, SOCK_STREAM을 지우고 SOCK_DGRAM을 쓰십시오.
* socket_bind($sock, $address, $port) or die('Could not bind to address')
소켓을 입력된 주소와 포트에 결합합니다.
* socket_listen($sock)
저장된 포트번호에서, 들어오는 연결을 대기합니다. 만약 연결되면, 자식 소켓을 생성할 것이다.
* $cliend = socket_accept($sock)
마스터 소켓에서 접속을 받아들인다.
* $input = socket_read($client, 1024)
받아들인 소켓에서 1024 바이트를 읽는다,
* $output = ereg_replace("[\t\n\r]","",$input).chr(0)
정규식을 사용하여 모든 공백 문자를 제거한다.
*socket_write($client, $output)
스트림 소켓으로 테이터를 전송한다.
2.2 실제 서버 제작하기
지금 여러분은 소켓을 설정하고 대기하기 위해 필요한 기본 절차를 배웠습니다.
여러분은 쓸모있는 서버를 만들 준비가 되어있습니다.
위에 있는 소스코드를 보면, 이 프로그램은 단 한번 실행되고 종료됩니다.
그것은 소켓서버를 생성하기위해 요구되는 단계를 설명하기에는 좋습니다.
그러나 그것은 현실 상황에서는 적합하지 않다.
여러분의 프로그램이 실행 되고, 들어오는 요청에 응답하자마다. 프로그램이 종료되기를 원치않을 것입니다.
다시말하면, 계속 실행되야만 한다.
우리는 그러므로 프로그램은 계속 실행하기 위한 방법이 필요합니다.
우리가 명확히 exit 문을 명령할때까지, 우리는 while(true) { /* 놀지말고 일 좀 해라 */ } 이렇게 반복문을 계속적으로 사용할수 있습니다.
우리는 위의 예를 다음과 같은 기능을 추가하여 확장할 것입니다.
* 끝없이 프로그램을 실행하게한다.
* 종료 기능을 만든다.
* 여러명이 접속해도 처리할수 있도록 한다.
#!/usr/local/bin/php -q
<?
// 끝없이 실행하기 위해 시간 한계를 0으로 설정한다
set_time_limit (0);
// 서버가 대기할 ip 주소와 port 번호를 설정한다.
$address = '192.168.0.100';
$port = 9000;
// 동시에 접속할 수 있는 사용자를 10명으로 한정한다.
$max_clients = 10;
// 클라이언드 정보를 얻을 배열
// 다시 말하자면, 사용자가 10명을 동시에 받아들이겠다면,
// 배열 크기를 10개로 잡아야 합니다.
$clients = Array();
// TCP 스트림 소켓 생성
$sock = socket_create(AF_INET, SOCK_STREAM, 0);
// 소켓을 아이피주소/포트에 결합
socket_bind($sock, $address, $port) or die('주소 지정에 실패했습니다.');
// 연결을 대기를 시작한다.
socket_listen($sock);
// 무한 루프 실행
while (true) {
// 읽기위해 클라이언트 대기 소켓을 설정한다
$read[0] = $sock;
for ($i = 0; $i < $max_clients; $i++)
{
if ($client[$i]['sock'] != null)
$read[$i + 1] = $client[$i]['sock'] ;
}
// socket_select()에 블럭킹 호출을 설정한다.
$ready = socket_select($read,null,null,null);
/* 만약 새로운 접속이 되면, 그것을 클라이언트 배열에 추가한다 */
if (in_array($sock, $read)) {
for ($i = 0; $i < $max_clients; $i++)
{
if ($client[$i]['sock'] == null) {
$client[$i]['sock'] = socket_accept($sock);
break;
}
elseif ($i == $max_clients - 1)
print ("너무 많은 사용자")
}
if (--$ready <= 0)
continue;
} // 조건문 if in_array의 끝
// 만약 클라이언트가 쓰기를 시도하면, 바로 그것을 처리한다
for ($i = 0; $i < $max_clients; $i++) // for each client
{
if (in_array($client[$i]['sock'] , $read))
{ // 사용자로부터 입력을 받아서..
$input = socket_read($client[$i]['sock'] , 1024);
// 만약 입력이 없으면...
if ($input == null) {
// Zero length string meaning disconnected
unset($client[$i]);
}
$n = trim($input);
// 만약 클라이언트가 'exit'를 입력하면,
if ($input == 'exit') {
// 요청에 따라 연결을 종료한다
socket_close($client[$i]['sock']);
// 만약 아니면...
} elseif ($input) {
// 공백문자를 제거하고,
$output = ereg_replace("[ \t\n\r]","",$input).chr(0);
// 사용에게 소켓 스트림을 통하여 문자열을 보낸다.
socket_write($client[$i]['sock'],$output);
}
} else {
// 소켓 종료
socket_close($client[$i]['sock']);
unset($client[$i]);
}
}
} // while문 끝
// 주인 소켓 종료
socket_close($sock);
?>
기본 기능은 처음 예제와 같다. 다만 추가된 특징은 사용자가 문자열 'exit'를 프로그램에 보내면,
프로그램은 연결을 끝낼것이다.
이 프로그램은 반복문을 제외하고, 처음 프로그램과 매우 비슷하다.
소스는 4개의 기본 블럭이 있다.
1 읽기위해 소켓 설정
2 새로운 클라이언트를 대기하고 $client 배열로 그것을 설정한다.
3 클라이언트 대기하고 입력을 기록한다.
4 클라이언트 입력을 처리한다.
이 예에서, 새 함수는 socket_select($read, null, null, null)입니다.
이 함수는 소켓 배열에서 select() 시스템 호출을 실행하고 상태가 바뀔때까지 기다립니다.
이것은 그 상태가 바뀔때까지 막고 있다가, 누군가 접속하여 그 상태가 바뀌면 그것을 처리합니다.
마지막 요점으로, 여러분은 연결된 다른 클라이언드에 정보를 뿌리고 싶을때(예를 들면 다-대-다 채팅 환경을 말한다)를 알아낼 수있습니다.
이것은 다음과 같은 코드로 저장될수 있다.
<PRE>
$output = '이것은 제가 여러명에게 보내고 싶은 메시지입니다'.chr(0);
for ($j = 0; $j < MAX_CLIENTS; $j++) // 각각의 클라이언트
{
if ($client[$j]['sock']) {
socket_write($client[$j]['sock'], $output);
}
}
원문에서 broadcast라는 말이 나오는데 우리 말로는 '방송'으로 번역하고,
여기서는 한번에 여러명에게 정보를 보내는 것을 애기합니다. 다르게 애기하면 '대량 살포' 이런 의미입니다.
socket_select()함수 역시 중요한데. 부가 설명을 하자면,
select라는 말처럼 기다리고 있다가, 튀는 놈, 예를 들면 사용자가 접속하면, 그 놈을 선택(select)하여
넘겨주면 그걸 처리해주는 것입니다.
2.3 실용적인 사용
지금 우리는 소켓 서버 생성의 기초를 배웠다. 만약 한계가 있다면, 우리의 상상력입니다.
* 채팅 서버(텍스트 또는 그래픽 기반). 이것은 재미있으면서 진짜 어플리케이션이 될 수 있습니다.
* 실시간 정보 스트리밍 (뉴스, 주식..기타등등)
* 스트리밍 멀티미디어 (이미지, 동영상과 사운드)
* 인증 서버
* 간단한 웹, POP3, SMTP 그리고 FTP 서버.
좀더 자세하게 말하자면, 소켓 라이브러리는 서버와 마찬가지로 클라이언트 프로그램을 만들 수 있다.
2.4 보안
보안은 접근 가능한 온라인 프로그램 생성할때 고려되어야 한다.
이것은 계속 실행되는 서버 소켓과같이 일반적인 PHP 스크립트에도 해당됩니다.
여러분이 보안 정책을 계획할때, 고려하는 많은 사실이 있다. 여기 몇가지 나열하겠다.
* 파일 접근 - 여러분은 파일 접근을 제한해야한다. 만약 웹서버같은 것이 파일 접근을 허락하면
, 여러분은 특정 폴더에서 파일에 접근하게 해야한다. 다른 좋은 개념은
* 인증 - 보안에 약한 서버를 위해, 여러분은 인증을 사용하기를 추천한다. 여러분이
플래시나, 비주얼 베이직으로 사용자 프론트-엔드를 만들더라도, 그것은 신뢰성이 없습니다.
누군가 네트워크 접속과 "sniff"할수 없을 것이다.
인증을 하기 위한 한가지 좋은 방법으로, 사용자가 성공적으로 스스로 인증하기 전까지 어떤 시도도 허락하지 않는 것이다.
(위에서 예를 들면 인증 하자마자, $client[$i]['authenticated'] = true )
* 암호화 - 암호화는 중요한 정보를 막기위한 대단히 좋은 방법입니다. 암호화는 특히 위에 애기한 인증과 함께 사용하면
대단히 유용합니다. 운이 좋게도 PHP는 뛰어난 암호화 라이브러리를 제공합니다.
자세한 내용은 (http://www.php.net/mcrypt)
3. 가능한 기능 추가와 확장
* PCNTL을 사용하여 프로세스 제어와 쓰레드를 추가한다.
* 프론트-앤드 인터페이스(다시 말해 GUI 화면)는 C++, VB, Flash (XMLSockets를 사용), 자바 또는 TCP/IP 또는 UDP 소켓을 지원하는 모든 소프트 웨어를 사용하려 작성할 수 있다.
* 만약 서버가 시험삼아 돌아가게되면, 당신은 모니터에 에러 메시지에 출력하는 대신 텍스트 화일이나 데이터 베이스 기록되는 사용자 에러 핸들링 함수를 생성을 원할것이다.
콘솔 채팅 프로그램입니다. 테스트는 php 4.2.1 + Alzza 6.2에서 했습니다. 별 대
단한 기능이 있는 채팅 프로그램은 아니고 그냥 소켓 프로그램 예제 정도로 쓰면
좋은 정도입니다. socket_select, pcntl_fork 등의 함수를 사용하기 때문에 php 컴
파일 시에 enable-socket, enable-pcntl 옵션을 줘야 합니다.
-----------------------------------
1. Server.
#!/usr/bin/php
<?
//-----------------------
class Select{
var $fhs;
//-----------------------
function Select(&$fh){
$this->fhs = array($fh);
}
//-----------------------
function add(&$fh){
array_push($this->fhs, $fh);
}
//-----------------------
function remove(&$fh){
for($i = 0; $i < count($this->fhs); $i++){
if($this->fhs[$i] == $fh){
array_splice($this->fhs, $i, 1);
break;
}
}
}
//-----------------------
function can_read($limit = 5){
$read_fhs = $this->fhs;
socket_select($read_fhs, $write_fhs = null, $exception_fhs = null, $limit);
return $read_fhs;
}
//-----------------------
function can_write($limit = 0){
$write_fhs = $this->fhs;
socket_select($read_fhs = null, $write_fhs, $exception_fhs = null, $limit);
return $write_fhs;
}
}
//-----------------------
class Guest{
var $fh;
var $name;
function Guest($fh, $name){
$this->fh = $fh;
$this->name = $name;
}
}
//-----------------------
class Lobby{
var $fh;
var $sel;
var $guests;
//-----------------------
function Lobby(){
$this->guests = array();
}
//-----------------------
function open($addr, $port){
$this->fh = socket_create(AF_INET, SOCK_STREAM, 0)
or die("create error!!n");
socket_setopt($this->fh, SOL_SOCKET, SO_REUSEADDR, 1)
or die("setopt error!!n");
socket_bind($this->fh, $addr, $port) or die("bind error!!n");
socket_listen($this->fh, 5) or die("listen error!!n");
$this->sel = new Select($this->fh);
print "Wating...n";
}
//-----------------------
function close($g){
socket_close($this->fh);
}
//-----------------------
function work(){
while(true){
foreach($this->sel->can_read() as $fh){
if($fh == $this->fh){
print "Got connection.n";
$new = socket_accept($this->fh)
or die("accept error!!n");
$this->sel->add($new);
socket_write($new, "## Input your name: ##n");
}
else{
$buf = socket_read($fh, 1024);
if($buf){
printf("Received: %sn", $buf);
$g = $this->get_guest($fh);
if(! $g){
$this->add_guest(new Guest($fh
, $buf));
continue;
}
$this->talk2all(sprintf("%s: %sn"
, $g->name, $buf));
}
else{
if($this->is_guest($fh))
{$this->del_guest($fh);}
$this->sel->remove($fh);
socket_close($fh);
print "A client has been removed.n";
}
}
}
}
}
//-----------------------
function talk2all($buf){
foreach($this->guests as $g){
socket_write($g->fh, $buf);
}
}
//-----------------------
function get_guest(&$fh){
for($i = 0; $i < count($this->guests); $i++){
if($fh == $this->guests[$i]->fh){return $this->guests[$i];}
}
return null;
}
//-----------------------
function is_guest(&$fh){
for($i = 0; $i < count($this->guests); $i++){
if($fh == $this->guests[$i]->fh){return true;}
}
return false;
}
//-----------------------
function add_guest(&$g){
array_push($this->guests, $g);
$this->talk2all("## " . $g->name . " has entered. ##n");
printf("count($this->guests) = %dn", count($this->guests));
}
//-----------------------
function del_guest(&$fh){
for($i = 0; $i < count($this->guests); $i++){
if($fh == $this->guests[$i]->fh){array_splice($this->guests, $i);}
}
printf("count($this->guests) = %dn", count($this->guests));
}
}
//-----------------------
//main
set_time_limit(0);
$lob = new Lobby();
$lob->open("127.0.0.1", 8040);
$lob->work();
?>
-----------------------------------
2. Client.
#!/usr/bin/php
<?
set_time_limit(0);
$fh = socket_create(AF_INET, SOCK_STREAM, SOL_TCP)
or die("create error!!n");
socket_connect($fh, "127.0.0.1", 8040) or die("connect error!!n");
$pid = pcntl_fork();
if($pid == -1){die("could not fork");}
if($pid == 0){
while(true){
$in = fopen("php://stdin", "r");
$line = fgets($in, 255);
$line = trim($line);
if($line){socket_write($fh, $line);}
}
}
else{
while(true){
print socket_read($fh, 1024);
}
}
?>
'프로그래밍 > PHP' 카테고리의 다른 글
[PHP] 클래스 제작 예(기초) (0) | 2008.11.18 |
---|---|
PHP+Perl Shell Binding (0) | 2008.08.25 |
[PHP - shm_attach] 공유메모리 관련 참고. (0) | 2008.08.13 |
php로 메일받아 파싱 하기 (0) | 2008.06.22 |
php - SMTP Mail Class (0) | 2008.06.22 |
- Total
- Today
- Yesterday