2008년 2월 28일 목요일

소켓을 이용한 파일전송 프로그램...

Socket For Delphi...

const MAXCLIENT = 99; //최대접속자수제한.

const MAXBUF = 4106; //메시지 4K + 전송상태메세지
const BUFSIZE = 4105; //버퍼크기 0부터시작.
const RBUFSIZE = 4095; // 실질적인 메시지를 저장하기 위한 버퍼크기 0부터시작.

//제가 사용한 여러명 접속유지관리의 방법은 각 클라이언트 소켓의 핸들을 저장하는 방법을 사용했습니다. 편의에 따라서 접속자의 ip를 사용하셔도 상관없을듯합니다.
type TClientsInfo = record //여러명의 접속자를 관리하기 위해 제가 임의로 만든 레코드.
handle : integer; //각 연결클라이언트 소켓의 핸들값을 저장하기 위해서
Ip : string; //노파심에 ip도 보관합니다
FStream : TFileStream; //각접속자에 대한 전송파일에 대한 스트림핸들을 저장
FSpare :integer; //4k로 쪼개진 파일의 여분크기를 저장
Used : Boolean; //다음사용자들이 레코드를 잘찾아갈수있게 하기위해서
end;
// 패킷이 손실되는 경우가 발생하길래 연결유지를 위해서 넣어놨습니다. 현재는 서버에서만 처리가 되지만 클라이언트에서도 패킷손실 경우를 체크하여서 파일전송이 이루어져야 온전한 파일을 주고받을수있습니다. 만약 실행파일일 경우에는 하나의 패킷이라도손실되어 버린다면 실행이 안되겠죠? 동영상이나 음악파일등은 상관없슴다.
사용 방법은 아래에도 나오겠지만 타이머를 이용하여 5초간격씩 사용되고있는 파일스트림의 크기를 저장하여놓고 비교하여(두번비교) 계속적인 변화가 없다면 서버에서 다시 클라이언트에게 파일을 전송하라는 메시지를 보냅니다. 그러면 클라이언트는 계속적인 파일을 연속하여 보내게 됩니다. 왜 이런냐 하면 핸드셰이킹(주고받고) 방식을 이용하기 때문에 중간에 메시지가 분실되어 버린다면 계속적인 작업이 이루어지지 않기 때문입니다. 현재 서버에서 클라이언트에 메시지를 보낼 때 보내라는 메시지만 나오지만 지금 얼만큼 받았다는 메시지를 함께보낸다면 클라이언트가 분석하여 언제적 파일부분부터 보내야 하는지 알고 그다음부터 보내는 부분을 처리한다면 정말 완전한 파일을 보내실 수 있습니다.
type TCurrentState = record
cnt : integer; // 몇번비교가 이루어졌는지 값저장.
cursize : integer; //이전의 크기값을 저장
Used : Boolean;
end;

//파일정보는 처음에 오는 패킷에 들어있기 때문에 받아서 전송이 종료될때까지 계속적으로 보관해야 합니다.
type TFilesInfo = record //전송받는 파일의 정보를 저장합니다
FileName : string; //파일이름
FileSize : integer; //파일크기
FileSpare : integer; //4K씩 잘려나간 파일의 나머지부분.
end;
='''''''''''''''''''''''''''''''''''중략'''''''''''''''''''''''''''''''''''''
implementation
var
ClientsInfo : array[0..MAXCLIENT] of TClientsInfo; //클라이언트 정보저장배열 전역선언
CurrentState : array[0..MAXCLIENT] of TCurrentState; //현상태 정보저장배열 전역선언

function GetFileInfo(rcvStr: String):TFilesInfo;
var
Finfo : TFilesInfo; //파일저장구조체선언
info : string;
begin
//처음에 전송시작을 알리는 패킷의 형식은 아래와 같습니다.chr(11) = [#11]
//특정 캐릭터로 구분한 것은 요즘은 파일이름이 지맘대로이기 때문에 다 소화해낼려니 어쩔수가 없음. 관리도 훨씬 편하고..^^
//COPYSTART[#11]FILENAME[#12]FILESIZE[#13]FILESPARE[#14]
info := Copy(rcvStr, 1, pos(#14, rcvStr)); // 받은 버퍼값을 저장
delete(info, 1, pos(#11, info)); // 전송시작부분 잘라냄 COPYSTART부분
with FInfo do
begin
FileName := copy(info, 1, pos(#12, info) - 1); //12번문자까지 잘라서 파일이름알아냄
delete(info, 1, pos(#12, info));
FileSize := strtoint(copy(info, 1, pos(#13, info) - 1));//13번까지 잘라서 파일크기알아냄
delete(info, 1, pos(#13, info));
FileSpare := strtoint(copy(info, 1, pos(#14, info) - 1));//파일의 4K나머지 알아냄.
end;
Result := FInfo; //전송되는 파일정보리턴
end;
// 아래부분은 접속되어지는 클라이언트의 핸들을 저장하기 위한 부분입니다.
// TclientInfo레코드의 used를 사용하여 핸들저장할 공간을 찾아갑니다.
procedure SetHandle(hdl: integer);
var
m : integer;
begin
// 최대접속자까지 검사하면서 이미 접속된 사용자인지 새접속자인지 구분합니다.
for m := 0 to MAXCLIENT do
if (ClientsInfo[m].handle = hdl) and ( ClientsInfo[m].Used = True) then
Break;

if m = 100 then //최대검색하고나면 m값이 100이어서 1감소
dec(m);
//최대접속자까지 검사한후 m은 계속사용되어지는데 아래구문 처리안하면 핸들이 없을경우 항상 99값을 가지겠죠?이걸막고 처음부터 다시 포문을 돌려서 비어있는 공간에 핸들을 저장합니다.
if (m = MAXCLIENT) and (ClientsInfo[m].Used = False) and (ClientsInfo[m].Handle <> hdl) then
for m := 0 to MAXCLIENT do
if ClientsInfo[m].Used = False then //사용되지 않는곳에 저장
begin
ClientsInfo[m].handle := hdl;
ClientsInfo[m].Used := True;
Break;
end;
end;

//*** 또 한가지 여담.. 모두들 아시겠지만 서버가접속 클라이언트에게 메시지를 보낼 때
//socket.connections[M].sendbuf()이런식으로보낸담니다. M값을 잘처리해야지 올바르게 클라이언트에게 패킷을 보낼수가 있겠죠? 소켓에서 만약 5명이 접속해서 파일을 전송하고 있는데 3번째 놈이 작은파일을 보내어서 빨리끝났다고 합시다. 그리고 잠시후에 새로운 접속자가 들어왔다면 그넘의 패킷전송관리하기 위한 인덱스는 몇번이 될까요? 6번일까요 3번일까요? 3번이랍니다.. 따라서 위와 같이 핸들값을 저정하시면 다른 리스트를 사용하지 않고도 쉽게 구현이 가능하다고 생각합니다.

//아래것은 핸들을 취하기 위함입니다.
function GetHandle(hdl: integer): integer;
var
m : integer;
begin
for m := 0 to MAXCLIENT do
if (ClientsInfo[m].handle = hdl) and (ClientsInfo[m].Used = True) then
Break;
Result := m;
if m = 100 then
dec(m);

if (m = MAXCLIENT) and (ClientsInfo[m].Handle <> hdl) and (ClientsInfo[m].Used = False) then
Result := -1; // 핸들이 없다면 –1값을 리턴합니다. 이미닫혔을경우입니다.
end;
//핸들이 없다면 새접속자라고 보면 되겠죠?^^

procedure TForm1.FileSocketClientRead(Sender: TObject; Socket: TCustomWinSocket);
var
RcvData : array[0..BUFSIZE] of char;
cmd, StrData : String;
hdl, m : integer;
begin
Socket.ReceiveBuf(RcvData, MAXBUF); //버퍼값을 읽어들입니다.
hdl := socket.Handle;

cmd := copy(RcvData, 1, 10); //초기에 10바이트 읽어서 어떤값인지값인지 검사
//COPY_START이면 전송시작부분..COPY_DATA_이면 전송데이타..
//COPY_END__이면 전송잔여데이타(마지막데이타)
if cmd = 'COPY_START' then
SetHandle(hdl); //전송처음이기 때문에 당연히 핸들을 저장해야겠죠.. 검사도 함해보구요
m := GetHandle(hdl); //핸들이 저장된 공간의 인덱스를 가져옵니다.

if m <> -1 then //만약 핸들이 인덱스에 있는 넘이라면 IP주소를 나중에 연결지향을 위해서 저장.
ClientsInfo[m].Ip := Socket.RemoteAddress; //IP주소저장

if m <> -1 then //핸들이 있는 놈만 파일전송을 위한 루틴 실행합니다.
begin
if cmd = 'COPY_START' then
FileReceiveState(1, RcvData, m) // 초기메세지 받았을 때 클라이언트에게 응답처리조건
else if cmd = 'COPY_DATA_' then
FileReceiveState(2, RcvData, m) // 데이타메세지 받았을 때 클라이언트에게 응답처리조건
else if cmd = 'COPY_END__' then
FileReceiveState(3, RcvData, m);//전송종료 메시지 받았을 때 "
end
else
Socket.Close;
end;

//NUM값이 1이면 전송초기 2이면 전송데이타 3이면 전송종료메세지
procedure TForm1.FileReceiveState(Num: integer; RcvData: array of Char; m: integer);
var
Buffer: array[0..RBUFSIZE] of char; //받은파일내용 저장버퍼
SendBuf: array[0..BUFSIZE] of char; //응답내용저장버퍼
FInfo: TFilesInfo; //파일정보
str : string;
begin
Fillchar(SendBuf, MAXBUF, #0); //먼저 응답할버퍼를 초기화
Fillchar(Buffer, RBUFSIZE+1, #0); //실질적인 데이타 저장버퍼초기화

Case Num of
1 : begin //파라메터가 처음초기화부분일 때 처리
str := copy(rcvData, 1, length(rcvData));
FInfo := GetFileInfo(Str); //파일정보추출
Str := ExtractFileName(FInfo.FileName); //파일명만 추출함
ClientsInfo[m].FSpare := FInfo.FileSpare; //파일의 패킷여분사이즈
Try
//추출한 파일정보를 가지고 파일생성
ClientsInfo[m].FStream := TFileStream.Create(str, fmCreate or fmOpenWrite);
ClientsInfo[m].Used := True;
StrCopy(SendBuf, PChar('COPY_START'));
//잘받았다는 메시지를 보냄
FileSocket.Socket.Connections[m].Sendbuf(SendBuf, MAXBUF);
Except //예외처리
application.MessageBox('파일을 열수 없습니다.','오류', MB_OK);
with ClientsInfo[m] do
begin
Handle := -1; //핸들값과 다음 사용자를 위해서 초기화
Used := False;
end;
End;
end;
2 : if ClientsInfo[m].Used = True then // 핸들을 얻었을 때 사용중인 인덱스일경우에만 처리함
begin
Move(rcvData[10], Buffer, MAXBUF-10); //처음 10바이트를 제외한 나머진 파일데이타임니다. MOVE는 알겠죠?^^
ClientsInfo[m].FStream.Write(Buffer, MAXBUF-10); //저장된 파일스트림의 핸들을 가지고 파일을 계속 덧붙여나감. CLOSE하기 전까진 파일은 오픈상태입니다..
StrCopy(SendBuf, PChar('COPY_DATA_')); //데이터받았다는 메시지전송
FileSocket.Socket.Connections[m].Sendbuf(SendBuf, MAXBUF);
end;
3 : if ClientsInfo[m].Used = True then //파일의 전송종료메세지처리
begin
Move(rcvData[10], Buffer, MAXBUF - 10); //읽을때는 버퍼의 크기만큼 읽어야함
//나머지 부분이기 때문에 버퍼의 내용중 파일에 쓸때는 접속정보중 여분부분의 크기만큼만 파일에 씀.
ClientsInfo[m].FStream.Write(Buffer, ClientsInfo[m].FSpare);
StrCopy(SendBuf, PChar('COPY_END__')); //끝까지 잘받았다는 메시지전송
FileSocket.Socket.Connections[m].Sendbuf(SendBuf, MAXBUF);
end;
end;
end;

procedure TForm1.Button2Click(Sender: TObject);
begin
if FileSocket.Active then
FileSocket.Close;
end;

//클라이언트의 접속이 끊어졌을 때 접속정보란을 초기화 해야합니다. 다음넘을 위해서…
procedure TForm1.FileSocketClientDisconnect(Sender: TObject; Socket: TCustomWinSocket);
var
hdl : integer;
m : integer;
begin
m := -1;
Try
if Socket.Connected then
begin
hdl := Socket.Handle;
m := GetHandle(hdl); // 끊어지기 바로전에 핸들에 대한 인덱스 얻음
end;

if m <> -1 then //배열에 존재하는 핸들이라면
if ClientsInfo[m].Used = True then //사용되고 있다는 정보를 안 사용한다는
//정보로 변경합니다.
with ClientsInfo[m] do
begin
Handle := -1;
Used := False;
FStream.Free;
FileSocket.Socket.Connections[m].Disconnect(m); //연결끊음
end;

except
end;
end;

//에러발생시도 접속종료시와 마찬가지로 처리
procedure TForm1.FileSocketClientError(Sender: TObject;
Socket: TCustomWinSocket; ErrorEvent: TErrorEvent;
var ErrorCode: Integer);
var
hdl : integer;
m : integer;
begin
m := -1;
try
if socket.Connected then
begin
hdl := Socket.Handle;
m := GetHandle(hdl);
end;

if m <> -1 then
if ClientsInfo[m].Used = True then
with ClientsInfo[m] do
begin
Handle := -1;
Used := False;
FStream.Free;
FileSocket.Socket.Connections[m].Disconnect(m);
end;
except
end;
end;

//아래구문은 연결유지를 위해서 만들어진것입니다.
procedure TForm1.Timer1Timer(Sender: TObject);
var
m : integer;
SendBuf: array[0..BUFSIZE] of char;
begin
for m := 0 to MAXCLIENT do
if (ClientsInfo[m].Used = TRUE) and (ClientsInfo[m].FStream.Size > 0) then
with CurrentState[m], ClientsInfo[m] do
if cursize <> FStream.size then
begin
cursize := FStream.Size;
cnt := 0;
end
else
begin
//타이머가 두번돌동안 받은 파일사이즈의 크기가 변함이 없다면 오거나 가거나 둘중에 패킷이 손실되었다는것으로 간주하고 다시 서버에서 COPY_DATA_라는 패킷메세지를 보낸다. 그러면 계속적인 전송이 가능해진다. ^^.
inc(cnt);
if cnt = 2 then
begin
FillChar(SendBuf, MAXBUF, #0);
StrCopy(SendBuf, PChar('COPY_DATA_'));
FileSocket.Socket.Connections[m].Sendbuf(SendBuf, MAXBUF);
cnt := 0;
end;
end;
end;
end.

//이상입니다 긴글 보아주셔서 감사합니다. 좋은 내용이 있으시면 강좌좀..^^
항상행복하세요.
질문있으시면 메이로 질문을 부탁드립니다. Lifeg003@dreamwiz.com

클라이언트도 추가합니다..
procedure Moves(var Src, Dest: array of char; pos, size: integer);
var
m, sz : integer;
begin
sz := pos + Length(Src);
if sz <= Size then
for m := 0 to Length(Src)-1 do
Dest[m+pos] := Src[m];
end;

procedure GetFileInfo(FN: String);
var
SearchRec: TSearchRec;
Re: integer;
begin
FileInfo.Size := -1;
Re := Sysutils.FindFirst(FN, faAnyFile, SearchRec);

if Re = 0 then
with FileInfo do
begin
Name := FN;
Size:= SearchRec.Size;
SendNum := size div (MAXBUF-10);
FSpare := size mod (MAXBUF-10);
end;
FindClose(SearchRec);
end;

procedure TForm1.FileServState(num: integer);
var
SendBuf : array[0..BUFSIZE] of char;
FileContent : array[0..RBUFSIZE] of char;
flsize : integer;
begin
case num of
1 : begin
try
FStream:= TFileStream.Create(FileInfo.Name, fmOpenRead);
except
Application.MessageBox('화일을 오픈할수 없습니다', '파일열기 실패', MB_OK);
Exit;
end;
TotalCnt := 0;
FillChar(SendBuf, MAXBUF, #0);
StrCopy(SendBuf, PChar('COPY_START'+#11+FileInfo.Name+#12+InttoStr(FileInfo.Size)+#13+InttoStr(FileInfo.FSpare)+#14));
ClientSocket1.Socket.SendBuf(Sendbuf, MAXBUF);
label2.Caption := '전송시작!';
Application.ProcessMessages;
end;
2 : begin
FillChar(SendBuf, MAXBUF, #0);
FillChar(FileContent, MAXBUF-10, #0);
FStream.ReadBuffer(FileContent, MAXBUF-10);
StrCopy(SendBuf, 'COPY_DATA_');
//Move(FileContent, SendBuf[10], Length(SendBuf));//MAXBUF);
Moves(Filecontent, SendBuf, 10, MAXBUF);
ClientSocket1.Socket.SendBuf(Sendbuf, MAXBUF);
Inc(TotalCnt); flsize := TotalCnt * 4096;
label2.Caption := '전송: '+inttostr(flsize)+'/'+inttostr(FileInfo.Size);
Application.ProcessMessages;
end;
3 : begin
FillChar(SendBuf, MAXBUF, #0);
FillChar(FileContent, MAXBUF-10, #0);
StrCopy(SendBuf, 'COPY_END__');
FStream.ReadBuffer(FileConTent, FileInfo.FSpare);
MoveS(FileContent, SendBuf,10, MAXBUF);
ClientSocket1.Socket.SendBuf(SendBuf, MAXBUF);
label2.Caption := '전송완료!';
Application.ProcessMessages;
end;
end;
end;

procedure TForm1.Button1Click(Sender: TObject);
begin
ClientSocket1.Open;
end;

procedure TForm1.Button2Click(Sender: TObject);
begin
OpenDialog1.Filter := 'Execute files (*.exe)*.EXEAll files (*.*)*.*';
if OpenDialog1.Execute then
begin
Edit1.text := OpenDialog1.FileName;
// if trim(Edit1.text) <> '' then
// begin
// GetFileInfo(edit1.text);
// FileServState(1);
// end;
end;
end;

procedure TForm1.ClientSocket1Read(Sender: TObject;Socket: TCustomWinSocket);
var
RcvStr : array[0..MAXBUF] of char;
State : string;
begin
Socket.ReceiveBuf(RcvStr, MAXBUF);
State := copy(RcvStr, 1, 10);

if State = 'COPY_START' then
begin
if FileInfo.SendNum = 0 then
FileServState(3)
else
FileServState(2);
end
else if State = 'COPY_DATA_' then
begin
if TotalCnt = FileInfo.SendNum then
FileServState(3)
else
FileServState(2)
end
else if State = 'COPY_END__' then
begin
//Showmessage('파일전송완료');
FStream.Free;
clientSocket1.Close;
end;
end;

procedure TForm1.ClientSocket1Error(Sender: TObject;
Socket: TCustomWinSocket; ErrorEvent: TErrorEvent;
var ErrorCode: Integer);
begin
clientSocket1.Close;
// showmessage('소켓종료');
FStream.Free;
end;

procedure TForm1.Button3Click(Sender: TObject);
begin
if trim(Edit1.text) <> '' then
begin
GetFileInfo(edit1.text);
FileServState(1);
end;
Edit1.Clear;
end;

댓글 없음: