티스토리 뷰

웹상에서 채팅 프로그램을 구현할 때 가장 힘든 부분이 바로 
HTTP 프로토콜이 접속이 바로 끊기고 상태를 저장하지 않는 접속을 이용한다는 점입니다. 
즉, 소켓 프로그래밍에서 접속을 열고 닫고 하는것이 굉장히 운영체제 차원에서는 비싼 작업인 반면 
이미 열려진 소켓에서 몇바이트 쯤 더 쓰는 것은 속도나 성능 면에서 전혀 문제가 되지 않는다는 것이죠 
(요즘은 인터넷이 빨라서 초당 몇MB씩도 쓰고 하잖아요. 하지만 접속을 여는 데에는 여전히 시간이 걸리죠.) 
그러나 Flash Action Script에서 지원하는 Socket 클래스를 이용하면 자신이 다운로드된 서버와 
접속이 유지된 통신을할 수 있습니다. 
본 프로그램에서는 이점을 이용하여 클라이언트로는 Flash와 자바스크립트를 이용하고 
서버로는 C++로 자체 제작한 서버 프로그램을 이용하여 채팅방을 구현하였습니다. 
본 예제는 Flash Player 9.0을 필요로 하며 Internet Explorer와 Firefox에서 동작합니다. 
먼저 demo 페이지를 보시겠습니다. 
<a href="http://astronote.org/" target=_blank>http://astronote.org/</a> 
마우스 드래그를 통한 창이동, 창 최소화, 창 숨기기 기능을 지원합니다. 
서버 프로그램 
서버 프로그램은 리눅스상에서 c++을 이용하여 개발하였으며 g++을 이용하여 컴파일 하였습니다. 
저는 astronote.org 서버를 집에서 dyndns와 인터넷 공유기의 port mapping을 이용하여 돌리고 있습니다. 
그렇기 때문에 집에서 원하는 포트에 서버를 실행시킬 수 있지만 
웹호스팅 받으시는 분들은 서버상에서 프로그램을 실행시킬 수 없다면 본 예제를 적용할 수 없습니다. 참고하세요. 
일단 서버 소스코드를 올려드리겠습니다. 
#include <sys/types.h> 
#include <sys/stat.h> 
#include <sys/socket.h> 
#include <signal.h> 
#include <unistd.h> 
#include <netinet/in.h> 
#include <arpa/inet.h> 
#include <stdio.h> 
#include <stdlib.h> 
#include <string.h> 
#include <pthread.h> 
#include <stdlib.h> 
#include <vector> 
#include <string> 
using namespace std; 
// 접속된 클라이언트 IP를 저장하기 위한 전역 변수 
struct client 

string ip; 
int fd; 
}; 
class Error {}; // 에러 처리를 위한 Dummy 객체 
vector<client> clients; 
void *thread_comm(void *); 
int load_address(); 
int main(int argc, char **argv) 

    if (argc != 2) 
    { 
        printf("Usage : ./server [port]n"); 
        exit(0); 
    } 
    // socket -> bind -> listen 순서로 
    // 듣기 소켓 생성 
    int server_sockfd; 
    if ((server_sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) 
    { 
        perror("socket error"); 
        exit(0); 
    } 
    int val = 1;    
    if (setsockopt(server_sockfd, SOL_SOCKET, SO_REUSEADDR, (char *) &val, sizeof val) < 0) 

  perror("setsockopt"); 
  close(server_sockfd); 
  exit(0); 
    } 
struct sockaddr_in serveraddr; 
bzero(&serveraddr, sizeof(serveraddr)); 
    serveraddr.sin_family = AF_INET; 
    serveraddr.sin_addr.s_addr = htonl(INADDR_ANY); 
    serveraddr.sin_port = htons(atoi(argv[1])); 
    if (bind(server_sockfd, (struct sockaddr*)&serveraddr, sizeof(serveraddr)) == -1) 
    { 
        perror("bind error"); 
        exit(0);        
    } 
printf("접속을 기다리고 있습니다.n"); 
if (listen(server_sockfd, 5) == -1) 
    { 
        perror("bind error"); 
        exit(0);    
    } 
    while(1) 
    { 
        // 만약 듣기 소켓에 연결이 들어온다면 
        // 새로운 쓰레드를 생성시킨다. 
        // 새로운 쓰레드 생성시 클라이언트 소켓을 쓰레드 
        // 인자로 넘겨서 쓰레드와 클라이언트가 통신하도록 한다.  
    struct sockaddr_in clientaddr; 
  int client_len; 
  int client_sockfd; 
  try 
  { 
  client_sockfd = accept(server_sockfd, (struct sockaddr*)&clientaddr, (socklen_t*)&client_len); 
  client cl; 
  cl.ip = inet_ntoa(clientaddr.sin_addr); 
  cl.fd = client_sockfd; 
  clients.push_back(cl); 
  pthread_t p_thread; 
  if (pthread_create(&p_thread, NULL, thread_comm, (void *)client_sockfd) == -1) 
  { 
    perror("쓰레드 생성 실패n"); 
    exit(0); 
  } 
  } 
  catch (Error&) {} 
    } 
shutdown(server_sockfd, 2); 
    close(server_sockfd); 

// 쓰레드 생성시 넘어온 클라이언트 소켓을 이용해서 클라이언트와 통신한다. 
void *thread_comm(void* data) 

    int sockfd = (int)data; 
string ip; 
for(vector<client>::iterator ci = clients.begin(); ci != clients.end(); ci++) 
  if(ci->fd == sockfd) 
  ip = ci->ip; 
printf("%s 에서 접속..n", ip.c_str()); 
printf("현재 접속자수 : %d 명n", clients.size()); 
char buf[1024]; 
// 모든 사용자에게 접속을 알림 
sprintf(buf, "%s|connected|%dn", ip.c_str(), clients.size()); 
for(vector<client>::iterator ci = clients.begin(); ci != clients.end(); ci++) 

  if(write(ci->fd, buf, strlen(buf)+1) == -1) 
  printf("%s 에서 쓰기 에러n", ip.c_str()); 

char c; 
string line; 
try 

  while(read(sockfd, &c, 1) > 0) 
  { 
  line += c; 
  if (c == 'n') 
  { 
    line = ip + "|" + line; 
    for(vector<client>::iterator ci = clients.begin(); ci != clients.end(); ci++) 
    { 
    if(write(ci->fd, line.c_str(), line.length()+1) == -1) 
    { 
      printf("%s 에서 쓰기 에러n", ip.c_str()); 
      break; 
    } 
    } 
    line.clear(); 
  } 
  } 

catch (Error&) {} 
shutdown(sockfd, 2); 
close(sockfd); 
printf("%s 에서 접속 종료n", ip.c_str()); 
// 벡터에서 삭제한다. 
for(vector<client>::iterator ci = clients.begin(); ci != clients.end(); ci++) 
  if(ci->fd == sockfd) 
  { 
  clients.erase(ci,ci+1); 
  break; 
  } 
try 

  // 자기 자신을 제외한 모든 사용자에게 끊김을 알림 
  sprintf(buf, "%s|disconnected|%dn", ip.c_str(), clients.size()); 
  for(vector<client>::iterator ci = clients.begin(); ci != clients.end(); ci++) 
  { 
  if(write(ci->fd, buf, strlen(buf)+1) == -1) 
    printf("%s 에서 쓰기 에러n", ip.c_str()); 
  } 

catch (Error&) {} 
printf("현재 접속자수 : %d 명n", clients.size()); 
return (void *)NULL; 

서버프로그램은 보시다시피 간단합니다. 
클라이언트측에서 보내주는대로 자신을 포함한 모든 클라이언트에서 broadcasting 해 주는 것 밖에는 특별한 기능이 없습니다. 
소스코드를 업로드하시고 컴파일합니다. 
$ g++ -o chat-server -lpthread server.cpp 
그런 다음 chat-server에 실행옵션 주고 실행합니다. 파라미터로 포트번호를 넘깁니다. 
$ chmod 700 chat-server 
$ ./chat-server 25000 & 
그럼 OK 입니다. 
클라이언트 프로그램 
클라이언트 프로그램은 flash 부분과 javascript 부분으로 나누어집니다. 
먼저 flash를 띄우고 첫번째 프레임에서 F9를 눌러 ActionScript 창을 띄운 후 다음과 같이 입력합니다. 
import flash.external.ExternalInterface; 
import flash.net.Socket; 
var str:String = '; 
var socket:Socket = null; 
socket = new Socket(); 
socket.connect('astronote.org',25000); // 이부분은 서버 설정에 맞게 수정하세요. 
socket.addEventListener(ProgressEvent.SOCKET_DATA, socketDataHandler); 
function sendIt(str:String) : void 

socket.writeMultiByte(str+'n', "euc-kr"); 
socket.flush(); 

function socketDataHandler(event:ProgressEvent):void 

var str = socket.readMultiByte(socket.bytesAvailable, "euc-kr"); 
ExternalInterface.call("received", str); 

ExternalInterface.addCallback("sendIt", sendIt); 
내용은 실행될 때 소켓을 열고 자바스크립트 함수에서 호출할 수 있는 sendIt 이라는 메소드를 노출했습니다. 
서버로부터 자료를 받으면 received 라는 자바스크립트 함수를 호출해 줍니다. 
자신의 서버 설정에 맞게 도메인명과 포트번호를 수정합니다. 
저는 웹서버 설정을 euc-kr로 했기 때문에 euc-kr로 했지만 utf-8로 할 수 도 있습니다. 
이제 Shift+F12를 눌러 컴파일합니다. 
이제 자바스크립트 부분입니다. 
먼저 항상 떠 있는 안보이는 frame을 만들어서 chat.htm을 띄워놓습니다. 여기에 flash로 만든 swf 파일을 올립니다. 
<frameset rows='*,0' border=0 frameborder=0> 
<frame name=main src=/home.htm border=0> 
<frame name=sock src=/chat.htm border=0> 
</frameset> 
다음은 chat.htm의 코드입니다. 
<script src="AC_RunActiveContent.js"></script> 
<script> 
if (AC_FL_RunContent == 0) 

alert("This page requires AC_RunActiveContent.js."); 

else 

AC_FL_RunContent('codebase', 'http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=9,0,0,0', 
'width', '100', 
'height', '100', 
'src', 'astrowar', 
'quality', 'high', 
'pluginspage', 'http://www.macromedia.com/go/getflashplayer', 
'align', 'middle', 
'play', 'true', 
'loop', 'true', 
'scale', 'showall', 
'wmode', 'window', 
'devicefont', 'false', 
'id', 'astrowar', 
'bgcolor', '#000000', 
'name', 'astrowar', 
'menu', 'true', 'allowFullScreen', 'false', 'allowScriptAccess','always', 'movie', 'astrowar', 'salign', '); 

var saved = '; 
var idpool = []; 
var chatnum = 0; 
function findid(ip) 

for(var i = 0; i < idpool.length; i++) 

  if(idpool[i].ip == ip) return idpool[i].name+"<small>("+idpool[i].id+")</small>"; 

return ip; 

function update_chatnum() 

if(parent.main) 

  var cn = parent.main.document.getElementById('chatnum'); 
  if(cn) cn.innerHTML = '현재 접속자수 '+chatnum+'명'; 


function received(s) 

var arr = s.split('|'); 
var str = '; 
switch(arr[1]) 

case 'connected': 
  str = arr[0] + " 접속<br>"; 
  chatnum = arr[2]; 
  update_chatnum(); 
  break; 
case 'login': 
  str = arr[3]+"("+arr[2] + ") 로그인<br>"; 
  if(findid(arr[0]) == arr[0] && arr[2] && arr[3]) idpool[idpool.length] = {ip:arr[0],id:arr[2],name:arr[3]}; 
  break; 
case 'logout': 
  str = arr[3]+"("+arr[2] + ") 로그아웃<br>"; 
  //if(findid(arr[0]) != arr[0] && arr[2] && arr[3]) idpool[idpool.length] = {ip:arr[0],id:arr[2],name:arr[3]}; 
  break; 
case 'disconnected': 
  str = findid(arr[0]) + " 접속 종료<br>"; 
  chatnum = arr[2]; 
  update_chatnum(); 
  break; 
case 'chat': 
  var name = arr[3] + "<small>("+arr[2]+")</small>"; 
  if(arr[3]==') name = arr[0]; 
  str = "<font color="+arr[5]+">"+name + " : " + arr[4] + "</font><br>"; 
  if(findid(arr[0]) == arr[0] && arr[2] && arr[3]) idpool[idpool.length] = {ip:arr[0],id:arr[2],name:arr[3]}; 
  break; 

if(str) 

  saved += str; 
  if(parent.main) 
  if(parent.main.cv) 
  { 
  var p = parent.main.document.createElement('div'); 
  p.innerHTML = str; 
  parent.main.cv.appendChild(p); 
  window.setTimeout('parent.main.cv.scrollTop = parent.main.cv.scrollHeight',100); 
  } 


var hide = false; 
var chat_closed = false; 
var alpha = 100; 
function onalpha(f) 

alpha = f.options[f.selectedIndex].value; 
if(parent.main.cr) 

  parent.main.cr.style.opacity = alpha/100; 
  parent.main.cr.style.MozOpacity = alpha/100; 
  parent.main.cr.style.KhtmlOpacity = alpha/100; 
  parent.main.cr.style.filter = "alpha(opacity=" + alpha + ")"; 


var col = '#000000'; 
function oncolor(f) 

col = f.options[f.selectedIndex].value; 

function getMovieName(movieName) 

if (navigator.appName.indexOf("Microsoft") != -1) return window[movieName]; 
else return document[movieName]; 

var chatx = 0; 
var chaty = 270; 
var sock; 
window.onload = function(){ 
sock = getMovieName("astrowar"); 

</script> 
AC_RunActiveContent.js 파일은 Flash에서 publish 할 때 생성된 것을 변경 없이 그대로 사용하시면 됩니다. 
저는 swf 파일의 이름을 astrowar.swf 로 했습니다. 하지만 다른 이름으로 해도 무방합니다. 
onalpha 함수와 oncolor 함수는 채팅방에서 투명도나 대화색상을 조절하기 위해 사용합니다. 
이제 본 페이지 들어갑니다. 
홈페이지의 모든 페이지에서 load 되는 head.php 등에 아래 코드를 추가하세요. 
<!-- ---------------------------------------------------------------------- --> 
<!-- ------------ 천문노트 채팅방 2007-11-24 오후 11:48 이형철 ------------ --> 
<script> 
var mem_id = '<?=$mem_id?>'; 
var mem_name = '<?=$mem_name?>'; // 이 부분만 자신의 설정에 맞게 수정하면 됩니다. 
</script> 
<div id=chatroom style='z-index:100;position:absolute;top:270px;right:30px;width:200px;display:none;'> 
<div id=titlebar style='padding:3px;background:#F6F6F6;width:100%;'> 
<span id=chatnum style="width:130;"> </span> 
<a href=javascript:chat_hide() style=text-decoration:none;>[ _ ]</a> <a href=javascript:chat_close() style=text-decoration:none;>[ X ]</a> 
</div> 
<div id=chatroominner> 
<div id=chatview style="display:block;padding:5px;width:100%;height:170;"> </div> 
<input name=chatcontent id=chatcontent maxlength=95 style=width:100%  style='border:0;background:whitesmoke;' onkeydown='if(event.keyCode==13) chat(this)' value='메세지를 입력하세요.' onblur="if(!this.value) value='메세지를 입력하세 요.';" onfocus="if(this.value=='메세지를 입력하세요.') this.value=';"> 
<div style=padding:3px;font-size:10px;> 
투명도 : <select id=selalpha name=selalpha  style=font-size:8px; onchange="parent.sock.onalpha(this);cci.focus();"> 
<option value=100>100%</option> 
<option value=90>90%</option> 
<option value=80>80%</option> 
<option value=70>70%</option> 
<option value=60>60%</option> 
<option value=50>50%</option> 
<option value=40>40%</option> 
<option value=30>30%</option> 
<option value=20>20%</option> 
</select> 
색상 : 
<select id=selcolor name=selcolor  style=font-size:10px; style="background-color=000000;color=white;" onchange='parent.sock.oncolor(this);cci.focus();'> 
<option value="#000000" style=background-color=000000>   </option> 
<option value="#00007F" style=background-color=00007F>   </option> 
<option value="#009300" style=background-color=009300>   </option> 
<option value="#FF0000" style=background-color=FF0000>   </option> 
<option value="#7F0000" style=background-color=7F0000>   </option> 
<option value="#9C009C" style=background-color=9C009C>   </option> 
<option value="#D5AAEB" style=background-color=D5AAEB>   </option> 
<option value="#E7E138" style=background-color=E7E138>   </option> 
<option value="#FC7F00" style=background-color=FC7F00>   </option> 
<option value="#AA6600" style=background-color=AA6600>   </option> 
<option value="#00FC00" style=background-color=00FC00>   </option> 
<option value="#009393" style=background-color=009393>   </option> 
<option value="#00FFFF" style=background-color=00FFFF>   </option> 
<option value="#0000FC" style=background-color=0000FC>   </option> 
<option value="#FF00FF" style=background-color=FF00FF>   </option> 
<option value="#7F7F7F" style=background-color=7F7F7F>   </option> 
<option value="#D2D2D2" style=background-color=D2D2D2>   </option> 
</select> 
</div> 
</div> 
</div> 
<script> 
var cr = document.getElementById('chatroom'); 
var cri = document.getElementById('chatroominner'); 
var crt = document.getElementById('titlebar'); 
var cv = document.getElementById('chatview'); 
var ca = document.getElementById('selalpha'); 
var cc = document.getElementById('selcolor'); 
var cci = document.getElementById('chatcontent'); 
// autoscroll 
window.setInterval(function() 

if(target) return; 
if(cr) cr.style.top = parseInt(parseInt(cr.style.top) + 0.1 * (parseInt(document.body.scrollTop) + parent.sock.chaty - parseInt(cr.style.top))); 
// if(cr) cr.style.top = parseInt(parseInt(cr.style.top) + 0.1 * (parseInt(document.body.scrollTop) + 270 - parseInt(cr.style.top))); 
},10); 
function chat_close() 

parent.sock.chat_closed = !parent.sock.chat_closed; 
if(parent.sock.chat_closed) 

  cr.style.display = 'none'; 

else 

  cr.style.display = 'block'; 

window.setTimeout('cv.scrollTop = cv.scrollHeight;',100); 

function chat_hide() 

parent.sock.hide = !parent.sock.hide; 
if(parent.sock.hide) 

  cri.style.display = 'none'; 
  cv.style.height=0; 

else 

  cri.style.display = 'block'; 
  cv.style.height = 170; 


function addOnLoad(func) 

var oldonload = window.onload; 
if (typeof window.onload != 'function') { 
  window.onload = func; 

else { 
  window.onload = function() { 
  oldonload(); 
  func(); 
  } 


function chat(f) 

if(parent.sock) 
if(parent.sock.sock) 
if(parent.sock.sock.sendIt) 

  var str = f.value.replace('|','); 
  parent.sock.sock.sendIt('chat|'+mem_id+'|'+mem_name+'|'+str+'|'+parent.sock.col); 
  f.value = '; 


addOnLoad(function(){ 
if(parent.sock) 

  // 이미 saved에는 많은 것들이 추가되어있을 것이다. 
  if(parent.sock.saved) 
  { 
  while (cv.hasChildNodes()) cv.removeChild(cv.firstChild); 
  var p = document.createElement('div'); 
  p.innerHTML = parent.sock.saved; 
  cv.appendChild(p); 
  window.setTimeout('cv.scrollTop = cv.scrollHeight;',100); 
  } 
  parent.sock.update_chatnum(); 
  for(var i = 0; i < ca.options.length; i++) 
  if(ca.options[i].value == parent.sock.alpha) ca.options[i].selected = true; 
  parent.sock.onalpha(ca); 
  for(var i = 0; i < cc.options.length; i++) 
  if(cc.options[i].value == parent.sock.col) cc.options[i].selected = true; 
  parent.sock.oncolor(cc); 
  cv.style.overflow='auto'; 
  if(parent.sock.chatx) cr.style.left = parent.sock.chatx; 
  parent.sock.hide = !parent.sock.hide; 
  chat_hide(); 
  parent.sock.chat_closed  = !parent.sock.chat_closed; 
  chat_close(); 

}); 
var sMousePos = {x:0,y:0}; 
var sTargetPos = {x:0,y:0}; 
var target = null; 
function getMousePos(e) 

if(e.pageX || e.pageY) return {x : e.pageX, y : e.pageY}; 
return {x : e.clientX + document.body.scrollLeft - document.body.clientLeft, 
  y : e.clientY + document.body.scrollTop  - document.body.clientTop}; 

function getElementPos(e) 

var left = 0; 
var top  = 0; 
while (e.offsetParent) {left += e.offsetLeft; top += e.offsetTop; e = e.offsetParent;} 
left += e.offsetLeft; 
top += e.offsetTop; 
return {x:left, y:top}; 

function inDiv(x, y, div) 

var rt = getElementRect(div); 
if(rt.l > x) return false; 
if(x > rt.r) return false; 
if(rt.t > y) return false; 
if(y > rt.b) return false; 
return true; 

function getElementRect(e) 

var left = 0; 
var top  = 0; 
var e1 = e; 
try 

  while (e.offsetParent) 
  { 
  left += e.offsetLeft; 
  top += e.offsetTop; 
  e = e.offsetParent; 
  } 
  left += e.offsetLeft; 
  top += e.offsetTop; 

catch(err) {} 
return {l: left, t: top, r: left + e1.offsetWidth, b: top + e1.offsetHeight}; 

document.onmouseup = function(){ 
if(target) 

  var pt = getElementPos(target); 
  parent.sock.chaty = pt.y - document.body.scrollTop; 
  parent.sock.chatx = pt.x - document.body.scrollLeft; 

target = null; 
}; 
document.onmousedown = function(e) { 
e = e || window.event; 
var pt = getMousePos(e); 
var p = e.target || e.srcElement; 
if (inDiv(pt.x, pt.y, crt)) 

  sMousePos = pt; 
  target = document.getElementById("chatroom"); 
  sTargetPos = getElementPos(target); 

}; 
document.onmousemove = function(e) { 
e = e || window.event; 
var pt = getMousePos(e); 
if (inDiv(pt.x, pt.y, crt)) 

document.body.style.cursor = 'move'; 

else 

document.body.style.cursor = '; 

if(!target) return; 
target.style.left = sTargetPos.x + (pt.x - sMousePos.x); 
target.style.top = sTargetPos.y + (pt.y - sMousePos.y); 
}; 
</script> 
<!-- ---------------------------------------------------------------------- --> 
본 예제에서는 채팅방을 구현하였지만 Flash Socket과 서버간의 통신을 구현한 소스 코드는 다른 프로젝트에서도 사용될 수 있을거에요. 
PS. 본 프로그램은 천문노트에서 유환용군과 진행하던 astrowar라는 게임 프로젝트의 부산물입니다.

 

출처 : http://mindcrony.com

원문 : http://www.phpschool.com/gnuboard4/bbs/board.php?bo_table=tipntech&wr_id=57535&page=1