작성자 : 불늑대
작성일 : 2007/5/29
수정일 :
소스환경 : .netframework 2.0 , VS2005
filewolf.zip
1) C#으로 파일전송 소켓을 만들었습니다.
2) 비동기 소켓 3)쓰레드와 풀을 이용한 소켓
일단 소켓은 다들 알다시피 서로다른 위치에 있는 프로그램들을 네트워크안에서 서로 통신이 가능하도록 하는
링크단자 혹은 계층이라고 합니다.
이번에 구현된 소켓은 .NET 에서 제공되는 네트워크 클래스를 이용하여 고수준의 소켓을 제작을 하였습니다.
이용된 클래스는 TcpListener 와 TcpClient 두 클래스 입니다.
여기서 고수준이라 하면 저수준 소켓구현방법도 있겠죠.
저수준 소켓이란 소켓 연결에 대해 세세한 컨트롤이 필요할때 저수준 소켓으로 구현합니다.
하지만 일반적인 소켓통신에 있어서는 닷넷에서 제공되는 고수준의 TcpListener 와 TcpClient 가
그런데로 잘 작동되며 안정적이고 쉽게 구현할수있습니다.
일단 소켓과 전송프로토콜등에 대해 알고싶다면 인터넷에 많은 정보가 있으니깐 보시고... 구현을 해보겠습니다.
소켓을 구현할려면 당연 서버와 클라이언트가 있어야겠죠
서버화면입니다.
특별한건 없고 콘솔 실행파일로 만들었습니다. 다음은
클라이언화면 입니다.
클라이언트 화면은 업로드 시킬 파일을 선택하고 업로드 버튼을 클릭하여 서버에 파일을 전송하는 윈폼입니다.
밑에 텍스트 박스는 실행결과를 보여줍니다.
그럼 서버측 소스를 볼까요
일단 app.config 파일에 포트와 파일이 저장되게될 서버측 경로를 설정했습니다.
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<appSettings>
<add key="PORT" value="1234"/>
<add key="FILEPATH" value="C:\FILE_DOWN\"/>
</appSettings>
</configuration>
요렇게요.
포트는 그냥 1234로 했고 FILEPATH 는 파일이 전송되어 저장될 경로입니다. 서버를 실행하게 되면 C드라이브 밑에 FILE_DOWN
폴더가 만들어지게 했습니다.
전역변수로
static StreamWriter _log = null;
static string _Path = ConfigurationSettings.AppSettings["FILEPATH"].ToString();
요렇게 두개를 선언했습니다. _log는 로그기록를 파일에 남기는 StreamWriter 변수입니다.
보통 배치성 프로그램에서는 상태나 오류발생을 보기위해 파일로 로그를 남기죠.
_Path 변수는 파일이 저장될 경로와 로그파일이 들어갈 경로입니다.
자 이제 첫프로그램의 진입 메소드인 Main 메소를 보겠습니다.
TcpListener listener = new TcpListener(portNum); try
{
listener.Start();
Console.WriteLine("=== 불늑대 서버를 시작합니다.{0}==================",DateTime.Now.ToString("yyyy년 MM월 dd일 HH:mm"));
_log.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm") + " : 불늑대 서버를 시작 하였습니다.");
_log.Flush();
while (true)
{
TcpClient handler = listener.AcceptTcpClient();
if (handler != null)
{
try
{
p.ClientHandle(handler);
}
catch (Exception ex)
{
_log.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm") + " : ***ERROR***************");
_log.WriteLine("- MESSAGE -");
_log.WriteLine(ex.Message);
_log.WriteLine("- Client -");
_log.WriteLine(handler.Client.AddressFamily.ToString());
_log.WriteLine("*****************************");
_log.Flush();
}
}
}
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
finally
{
_log.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm") + " : 불늑대 이미징 서버를 종료 하였습니다.");
_log.Flush();
listener.Stop();
_log.Close();
}
}
굉장히 간단합니다. 이것이 닷넷 클래스인 TcpListener 의 편리함입니다.
TcpListener listener = new TcpListener(portNum);
일단 TcpListener에 포트번호를 인자로 주고 생성합니다.
그리고
listener.Start();
Start 메소드를 호출하여 클라이언트의 접속을 기달립니다.
그리고 다음
TcpClient handler = listener.AcceptTcpClient();
루프안에서 클라이언트가 접속을 시도하면 클라이언트를 받습니다. 연결된 거죠
그리고 클라이언트의 통신을 처리하고(이건 밑에 설명해드리겠습니다)
다음 클라이언트의 접속을 받기위해 루프문이 다시 돌게 됍니다.
이게 끝입니다. 간단하죠.
그럼 접속된 클라언트와 실제 통신맺고 파일 전송을 하는 부분을 보겠습니다.
TcpClient ClientSocket;
NetworkStream networkStream;
handler.ReceiveTimeout = 100;
ClientSocket = handler;
networkStream = ClientSocket.GetStream();
try
{
if (networkStream.CanRead && networkStream.CanWrite)
{
byte[] ReadByte;
ReadByte = new byte[ClientSocket.ReceiveBufferSize];
int BytesRead = networkStream.Read(ReadByte, 0, (int)ReadByte.Length);
string filename = Encoding.GetEncoding("ks_c_5601-1987").GetString(ReadByte, 0, BytesRead);
Byte[] sendBytes = Encoding.GetEncoding("ks_c_5601-1987").GetBytes(_Path + @"\" + filename);
networkStream.Write(sendBytes, 0, sendBytes.Length);
/*전송받을 이미지 사이즈를 받음*/
int ByteSize = 0;
Byte[] FileSizeBytes = new byte[ClientSocket.ReceiveBufferSize];
ByteSize = networkStream.Read(FileSizeBytes, 0, FileSizeBytes.Length);
int MaxFileLength = Convert.ToInt32(Encoding.ASCII.GetString(FileSizeBytes, 0, ByteSize));
/*전송준비작업을 완료했다고 클라이언트에 전해줌*/
byte[] ReadyTransBytes = new byte[ClientSocket.ReceiveBufferSize];
ReadyTransBytes = Encoding.ASCII.GetBytes("READY");
networkStream.Write(ReadyTransBytes, 0, ReadyTransBytes.Length);
FileStream fs = new FileStream(_Path + @"\" + filename, FileMode.Create, FileAccess.Write, FileShare.None);
if (filename != string.Empty)
{
byte[] myReadBuffer = new byte[1024];
int numberOfBytesRead = 0;
do
{
numberOfBytesRead = networkStream.Read(myReadBuffer, 0, myReadBuffer.Length);
fs.Write(myReadBuffer, 0, numberOfBytesRead);
}
while (fs.Length < MaxFileLength);
//while(networkStream.DataAvailable)
}
fs.Flush();
fs.Close();
}
return true;
}
catch
{
throw;
}
finally
{
networkStream.Flush();
networkStream.Close();
ClientSocket.Close();
}
붉은색으로 칠한부분을 주의깊게 보세요
클라이언트로 부터 파일크기를 받는다 ->
파일받을준비가되었다고 READY를 보넨다 - >
받은 파일크기 만큼 루프를 돌면서 클라이언트로 부터 바이너리형태의 파일을 받아 로컬에 저장한다
중요한 부분이다. 어떤 분들은
while(networkStream.DataAvailable) #DataAvailable 프로퍼티는 데이터 수신이끝나면 false를 반환
"이렇게 루프를 돌면 안되요?"
혹은 그냥
"networkStream.Read로 다 받으면안되나요?"
사실 안되는건 아니다 로컬에서 클라이언트 서버 테스트 해보면 잘될것이다. 하지만 소켓통신이란게
네트워크 상황에 따라 데이터를 다보내어도 받는쪽에서 다받지 못하고 그냥 넘어가게 되는 경우가 있습니다.
이런식으로 파일사이즈를 받아서 그 사이즈 만큼 루프를 돌며 1024정도 되는 버퍼에 담아 합치는 방법있습니다.
불늑대도 이 예제를 만들면서 닷넷소켓클래스를 너무 믿은 탓일까? 짧게나마 삽질을 했다는....
자 그럼 클라이언트 소스를 보자
/*파일 사이즈를 클라이언트로 전달*/
networkStream.Write(Encoding.ASCII.GetBytes(file.Length.ToString()), 0, Encoding.ASCII.GetBytes(file.Length.ToString()).Length);
/*클라이언트 측에서 준비되었는지 확인하고 준비되었다면 파일전송*/
int BytesRead2 = 0;
byte[] ConfirmByte = new byte[tcpClient.ReceiveBufferSize];
BytesRead2 = networkStream.Read(ConfirmByte, 0, (int)ConfirmByte.Length);
if (serverFileName.Length > 0 && BytesRead2 > 0)
{
byte[] FileBytes;
FileBytes = new byte[fs.Length];
fs.Read(FileBytes, 0, FileBytes.Length);
networkStream.Write(FileBytes, 0, FileBytes.Length);
textBox1.Text += "*** 불늑대 서버에 파일 업로드를 완료했습니다.\r\n";
}
클라이언트도 간단하다. 서버에서 데이터를 수신할때 처럼
발신하는 파일의 사이즈를 먼저 보넨다 ->
송신준비가 되었다는 서버의 메세지를 받는다->
파일을 전송한다.
이로써 심플한 서버/클라이언트 파일 전송 프로그램을 보았습니다...
앞으로
2) 비동기 소켓 3)쓰레드와 풀을 이용한 소켓
순으로 계속 올릴 예정입니다.