요즘 원래 본업에서 많이 벗어나 C#과 Windows Mobile로 계속 삽질을 하고있다. 가끔씩 내가 왜 이러고 있는건가? 하는 생각이 들기도 하지만 나름 재미있기도 하다. 이번엔 MSDN을 참고하며 C#으로 socket programming 삽질을 해 보았다. 빨리 이 삽질을 끝내고 본업으로 돌아갔으면 좋겠다.
Socket 프로그래밍을 위한 namespace
Socket을 초기화하기 위해 사용할 namespace는 다음과 같다.
using System.Net;
using System.Net.Sockets;
using System.Threading;
System.Net.Sockets 는 Socket 클래스를 사용하기 위함이며 System.Net 은 IPAddress 와 IPEndPoint 클래스를 사용하기 위함이다.
Server
서버는 특정 포트를 열고 클라이언트의 접속을 기다리다가(Listen) 클라이언트가 접속을 시도하면 Accept해준다. IPAddress 클래스로 host IP를 나타내며 IPEndPoint 클래스로 hostIP와 서버의 포트번호를 지정한 후 server socket에 bind 한다. 그리고 Listen() 을 수행하면 서버는 클라이언트로부터 접속을 기다릴 준비가 된 것이다. 이 때 Listen() 함수의 인자는 접속가능한 클라이언트의 최대 갯수이며 이 코드에서는 하나의 클라이언트만 접속을 허용한다. 그리고 server.Blocking = ture 는 server socket이 blocking mode에서 동작한다는 것으로 Accept() 와 Receive() 함수가 blocking 함수로 동작한다.
한가지 중요한 것은 클라이언트가 서버에 접속할 때 서버를 초기화 할 때 입력해 준 host IP로 접속해야 한다는 것이다. 내부적으로 테스트를 위한 것이라면 strIP 대신 "127.0.0.1" 을 입력해 주고 클라이언트에서 "127.0.0.1" 로 접속을 해도 되지만 그렇지 않은 경우는 꼭 Dns.GetHostName() 과 Dns.Resolve() 를 이용해 host IP를 알아내는 것이 좋다.
Socket server;
int port = 8000;
01.try02.{ 03. server = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); 04. 05. IPHostEntry host = Dns.Resolve(Dns.GetHostName()); 06. string strIP = host.AddressList[0].ToString(); // Get the local IP address 07. IPAddress hostIP = IPAddress.Parse(strIP); 08. 09. IPEndPoint ep = new IPEndPoint(hostIP, port); 10. server.Bind(ep); 11. server.Blocking = true; // The server socket is working in blocking mode 12. server.Listen(1); 13.} 14.catch (SocketException exc) 15.{ 16. MessageBox.Show(exc.ToString()); 17.}C#에서 thread를 사용하는 방법은 다음과 같다. Thread 클래스 인스턴스를 선언하고 생성자에서 ServerProc() 을 thread procedure를 정의해 준 후 Start() method를 호출해 thread를 시작한다.
Server에서 client와 데이터를 주고 받을 때는 server에서 client를 accept하고 연결을 할당해 준 소켓을 통해서 이루어진다. 아래 예에서는 나오지 않았지만 client로 데이터를 전송하기 위해서는 client.Send() method를 이용한다.
Thread threadServer;
threadServer = new Thread(new ThreadStart(ServerProc));
threadServer.Start();
그리고 ServerProc() 에서는 다음과 같이 client의 접속과 데이터 전송을 기다린다.
01.public void ServerProc() 02.{ 03. while (true) 04. { 05. // If a client is not connected 06. if (bConnected == false) 07. { 08. client = server.Accept(); 09. bConnected = true; 10. } 11. // If a client is connected, wait for data from client 12. else13. { 14. int len; 15. try16. { 17. len = client.Receive(buffer); 18. if (len == 0) // If the client is disconnected 19. { 20. bConnected = false; 21. 22. client.Disconnect(true); 23. this.Invoke(new EventHandler(UpdateServerUI)); 24. } 25. else // If data from client is arrived 26. { 27. received = System.Text.Encoding.ASCII.GetString(buffer); 28. this.Invoke(new EventHandler(UpdateServerUI)); 29. } 30. } 31. catch (SocketException exc) 32. { 33. MessageBox.Show(exc.ToString()); 34. bConnected = false; 35. } 36. } 37. } 38.}Accept()는 blocking method이기 때문에 client에서 접속하기를 기다리다 client가 접속을 하면 client socket에 연결을 할당을 해 주고 다음 명령으로 넘어간다. 마찬가지로 Receive() method 도 blocking method로 데이터가 도착할 때 까지 기다리다가 데이터가 도착하면 데이터의 길이와 데이터를 출력하고 다음 명령으로 넘어간다.
Socket 클래스가 명시적으로 제공하지 않는것 중 하나는 client가 접속을 했다가 접속을 끊는것을 알아내는 method나 property이다. MSDN에서는 Receive() method 에서 다음과 같이 설명하는 부분이 있다.
If the remote host shuts down the Socket connection with the Shutdown method, and all available data has been received, the Receive method will complete immediately and return zero bytes.
즉 client가 접속을 끊으면 Receive() method는 0을 return 한다. 따라서 위 코드에서 len 이 0인 경우 연결을 해제하고 bConnected flag를 false로 설정하여 다시 다른 client가 접속하기를 기다린다.
또한 client 프로그램이 올바르게 접속을 종료하지 않고 종료되는 경우 SocketException이 발생하기 때문에 이에 대한 예외처리를 해 주었다. 그리고 socket으로 받은 데이터를 main Form 의 컨트롤에 표시해주기 위해 UI를 업데이트하는 함수를 invoke해 주었다. main Form과 socket을 위한 thread는 서로 다른것이기 때문에 socket의 thread 에서 main Form의 데이터를 임의로 변경하는 것이 허용되지 않기 때문에 이러한 방법을 사용한다.
통신이 종료되면 다음과 같이 소켓을 닫고 thread도 종료한다.
server.Close();
threadServer.Abort();
Client
Client가 서버로 접속하기 위해서는 Connect() method로 접속할 host와 포트번호를 지정해 주면 된다. 다음 코드는 txtAddress 라는 TextBox 에 입력된 IP주소로 위 서버의 예와 같이 8000번 포트로 접속한다.
Socket client;
client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
client.Connect(IPAddress.Parse(txtAddress.Text), 8000);
서버에 접속이 되었다면 다음과 같이 Send() method를 이용해 서버로 데이터를 전송할 수 있다.
byte[] buffer = System.Text.Encoding.ASCII.GetBytes(txtTransmit.Text);
client.Send(buffer);
txtTransmit.Text = "";
그리고 접속 종료시는 Disconnect() method를 이용해 연결을 끊는다. 이 때 함수 인자는 현재 연결을 종료 후 이 소켓을 다시 사용할지에 대한 flag이다.
client.Disconnect(false);
실행 결과
다음 화면과 같이 접속을 하고 Hello, Server 라고 보내주면
이 예제는 매우 간단한 테스트를 위한 코드이며 client에도 server와 같이 thread 가 돌아가며 서버에서 오는 데이터를 polling 하는 부분이 추가되어야 원활한 양방향 통신을 할 수 있다.
Windows Mobile 을 위한 porting
Windows Mobile에서의 동작을 위해서는 다음 사항을 변경해야 한다.
1.Client 에서 server로 접속시에 Connect() method 의 인자는 IPEndPoint만을 받기 때문에 다음과 같이 수정되어야 한다.
IPEndPoint ep = new IPEndPoint(IPAddress.Parse(txtAddress.Text), 8000);
client.Connect(ep);
2. socket.Disconnect() method가 없다. 대신 Shutdown() method를 이용한다.
3. byte[] 에서 string으로 변환하기 위한 유용한 툴인 System.Text.Encoding.ASCII.GetString() 함수의 인자가 다르다. byte[], 시작 인덱스, 문자열 길이가 필요하다.
System.Text.Encoding.ASCII.GetString(buffer, 0, len);
그 외의 부분은 모두 .NET compact framework에서도 호환되는 것이기 때문에 모바일 플랫폼에서도 잘 동작한다. 다음은 Windows Mobile 6.0 Professional 이 설치된 스마트폰 GB-P100 에서 서버와 통신하는 화면을 캡쳐한 것이다. 이 프로그램은 위에서 설명한 Client에 thread를 추가해 Receive() method로 계속해서 데이터를 받아 아래 화면에 표시를 해 준다.
참고자료
[1] http://msdn.microsoft.com/en-us/library/system.net.sockets.socket.aspx
[2] http://msdn.microsoft.com/en-us/library/system.net.ipaddress(VS.71).aspx
[3] http://msdn.microsoft.com/en-us/library/system.threading.thread.aspx
'프로그래밍 > C#' 카테고리의 다른 글
| [c#]Thread를 이용한 파일복사하기-프로그래스바 (0) | 2011/03/15 |
|---|---|
| Rijndael(AES) 암/복호화 코드(C#) (1) | 2011/02/23 |
| C#으로 Socket programming 하기 (0) | 2010/11/26 |
| Convert.ToInt32()와 int.Parse()의 차이 (0) | 2010/11/26 |
| 스레드 동기화를 위한 AutoResetEvent와 ManualResetEvent (0) | 2010/11/26 |
| C#에서 Confirm Dialog 띄우기. (0) | 2010/11/19 |
