본문 바로가기

.NET

[C#] 동기 소켓 프로그래밍 예제

소켓 프로그래밍의 기본 개념을 알고 있다면 구현은 2가지 방법으로 가능하다.

 

[C#] 소켓 프로그래밍의 기본 개념

 

소켓을 사용하려면 동기 / 비동기 방식 중 하나를 선택한다.

동기 방식을 쓰면 비교적 간단하지만 요청/응답 중에는 어플리케이션의 다른 기능들이 동작하지 않는다. 말 그대로 어플리케이션의 메인쓰레드와 동기로 동작하기 때문에 메인쓰레드가 할 일을 모두 잡아먹는다.

반면에 비동기 방식은 어플리케이션의 메인쓰레드와 비동기로 동작하기 때문에 메인쓰레드는 간섭하지 않는다. 따라서 메인쓰레드로는 나머지 원하는 작업을 진행시킬 수 있고 백그라운드에서 소켓의 송수신이 이루어진다.

 

이번에는 우선 동기방식부터 다루려고 한다.

서버와 클라이언트 순서는 상관 없지만 일단 클라이언트 소켓에 관한 예제이다.

 

Client Socket

byte[] bytes = new byte[1024];

응답을 받았을 때 담을 바이트 배열을 선언해준다.

 

 

IPHostEntry ipHostInfo = Dns.GetHostEntry(Dns.GetHostName());  
IPAddress ipAddress = ipHostInfo.AddressList[0];  
IPEndPoint remoteEP = new IPEndPoint(ipAddress, 12345);

TCP/IP 소켓을 위한 기본적인 것들을 설정해준다.

ip 주소를 정해주고 소켓 연결을 위해 IPEndPoint 객체를 생성한다.

IPEndPoint() 의 두번째 인자는 포트 번호로서 본인이 사용하고자 하는 포트를 열고 적어주면 된다.

 

 

Socket sender = new Socket(ipAddress.AddressFamily, SocketType.Stream, ProtocolType.Tcp );

위에서 설정해준 값들을 가지고 소켓 객체를 생성한다.

 

 

sender.Connect(remoteEP);

서버 소켓에게 연결 요청을 보낸다.

 

 

byte[] msg = Encoding.ASCII.GetBytes("This is a test<EOF>");
int bytesSent = sender.Send(msg);    

int bytesRec = sender.Receive(bytes);

소켓에 제대로 연결이 됐다면 이제 데이터 통신을 진행할 수 있다.

원하는 데이터를 위의 msg와 같이 바이트 배열로 담아 Send() 함수를 통해 보낸다.

그리고 Receive() 함수로 응답을 받고 매개변수로 처음에 선언해준 바이트 배열을 넣어줌으로써 응답 데이터를 받아낸다. 만약 이 단계에서 문제가 생기거나 응답이 오지 않는다면 동기 방식이기 때문에 프로그램은 다른 일을 하지 않고 계속 Receive() 함수에 걸려 서버로부터 응답을 기다린다.

 

 

sender.Shutdown(SocketShutdown.Both);  
sender.Close();

데이터 송수신 작업이 끝났다면 소켓을 닫아 종료한다.

Shutdown() 함수로 연결을 끊을 수 있고 Close() 로 소켓을 닫는다.

 

 

클라이언트 소켓의 전체 코드는 다음과 같다.

using System;  
using System.Net;  
using System.Net.Sockets;  
using System.Text;  
  
public class SyncSocketClient {  
    public static void StartClient() {    
        byte[] bytes = new byte[1024];  
   
        try {    
            IPHostEntry ipHostInfo = Dns.GetHostEntry(Dns.GetHostName());  
            IPAddress ipAddress = ipHostInfo.AddressList[0];  
            IPEndPoint remoteEP = new IPEndPoint(ipAddress,11000);  
    
            Socket sender = new Socket(ipAddress.AddressFamily,   
                SocketType.Stream, ProtocolType.Tcp );  
  
            try {  
                sender.Connect(remoteEP);  
     
                byte[] msg = Encoding.ASCII.GetBytes("This is a test<EOF>");  
    
                int bytesSent = sender.Send(msg);    
                int bytesRec = sender.Receive(bytes);  
    
                sender.Shutdown(SocketShutdown.Both);  
                sender.Close(); 
            } catch (Exception e) {  
                // handle exception
            }  
  
        } catch (Exception e) {  
            // handle exception  
        }  
    }  
  
    public static int Main(String[] args) {  
        StartClient();  
        return 0;  
    }  
}

예외를 발생시킬 수 있는 경우가 너무 많으므로 당연히 try-catch 문으로 예외처리를 해준다.

 

 

이제 서버 소켓을 구현할 차례다.

연결되는 부분만 다르고 나머지는 똑같기 때문에 사실상 별 다를건 없다.

 

Server Socket

public static string data = null;
byte[] bytes = new Byte[1024];  

IPHostEntry ipHostInfo = Dns.GetHostEntry(Dns.GetHostName());  
IPAddress ipAddress = ipHostInfo.AddressList[0];  
IPEndPoint localEndPoint = new IPEndPoint(ipAddress, 12345);  
  
Socket listener = new Socket(ipAddress.AddressFamily, SocketType.Stream, ProtocolType.Tcp);

여기까지는 완전히 같다.

포트번호를 맞춰주고 소켓 객체를 생성해 통신할 준비를 한다.

 

 

listener.Bind(localEndPoint);
listener.Listen(10);

생성한 소켓에 설정해 둔 IPEndPoint 객체를 바인드하고 Listen() 함수를 써서 클라이언트로부터 연결 요청을 기다린다. Listen() 함수의 파라미터 10은 연결 큐의 최대 길이로 요청을 최대 몇개까지 받아놓을 수 있을지를 정하는 파라미터이다.

 

 

Socket handler = listener.Accept();

조금전에 사용하던 listener는 Listen() 함수를 써서 연결을 담당하려고 생성했던 소켓이다. 그리고 그 소켓이 연결이 완료되면 Accept() 함수가 호출되며 연결된 소켓을 반환하고 반환된 소켓을 사용하면 클라이언트로부터 오는 데이터를 받을 수 있다. 이 예제에서는 그 반환된 소켓의 이름을 handler라고 한다.

 

 

while (true) {  
	int bytesRec = handler.Receive(bytes);  
	data += Encoding.ASCII.GetString(bytes,0,bytesRec);  
	if (data.IndexOf("<EOF>") > -1) {  
		break;  
	}  
}  

byte[] msg = Encoding.ASCII.GetBytes(data);
handler.Send(msg);

handler.Shutdown(SocketShutdown.Both);  
handler.Close();

클라이언트로부터 받아온 데이터를 앞서 만들어두었던 바이트 배열에 담는다. 문장의 끝까지 담기 위해 반복문을 돌리고 앞서 선언해놓은 string 값에 받아오는 대로 파싱해 넣는다. 문장의 끝을 if 문에서 판별하고 끝이라면 반복문을 빠져나간다.

다 받아온 데이터를 msg라는 바이트 배열에 담아 다시 클라이언트로 보내준다. 당연히 꼭 이렇게 똑같이 보내줄 필요는 없고 본인이 원하는 작업을 수행한 후 그에 맞는 응답을 보내주면 된다. 데이터 송/수신이 끝나면 마찬가지로 Shutdown(), Close() 함수로 소켓을 닫고 통신을 종료한다.

 

 

서버 소켓의 전체 코드는 다음과 같다.

using System;  
using System.Net;  
using System.Net.Sockets;  
using System.Text;  
  
public class SyncSocketListener {   
    public static string data = null;  
  
    public static void StartListening() {    
        byte[] bytes = new Byte[1024];  
  
        IPHostEntry ipHostInfo = Dns.GetHostEntry(Dns.GetHostName());  
        IPAddress ipAddress = ipHostInfo.AddressList[0];  
        IPEndPoint localEndPoint = new IPEndPoint(ipAddress, 12345);  
  
        Socket listener = new Socket(ipAddress.AddressFamily, SocketType.Stream, ProtocolType.Tcp);  
 
        try {  
            listener.Bind(localEndPoint);  
            listener.Listen(10);  

            while (true) {  .  
                Socket handler = listener.Accept();  
                data = null;  
                  
                while (true) {  
                    int bytesRec = handler.Receive(bytes);  
                    data += Encoding.ASCII.GetString(bytes,0,bytesRec);  
                    if (data.IndexOf("<EOF>") > -1) {  
                        break;  
                    }  
                }  
                
                byte[] msg = Encoding.ASCII.GetBytes(data);  
                handler.Send(msg);  
                
                handler.Shutdown(SocketShutdown.Both);  
                handler.Close();  
            }  
        } catch (Exception e) {  
            // handle exception
        }  
    }  
  
    public static int Main(String[] args) {  
        StartListening();  
        return 0;  
    }  
}