[C#] C#.net에서의 시리얼통신 기초

by Blogger 하얀쿠아
2017. 4. 18. 23:15 소프트웨어 Note/C#

C#.net에서의 시리얼통신 기초


C#은 시리얼 통신에 대한 모든것을 개발자가 구현할 필요 없이 매우 쉽고 간단하게 사용할 수 있는 객체를 지원한다. 
그것은 System.IO.Port namespace에 포함되어있는 System.IO.Ports.SerialPort 인데, Visual Basic 6.0 에서 지원하던 Comm 컨트롤과 매우 유사해 사용은 간단했다.

참고로 이 글은 .net framework 3.5 기준으로 작성됐다.






객체 생성

SerialPort 객체를 Form에 끌어넣어주면 된다.
SerialPort 객체는 Device Components 에 있다. 
아래 그림을 참고하자.


또한, 아래와 같이 namespace 추가가 되었는지 코드를 확인해보고, 안되어있다면 추가하도록 한다.

using System.IO.Ports;


초기화

사용하고자 하는 포트의 번호와, BaudRate(나는 보통 '보 레이트' 라고 읽는다)를 설정하는 것이 Serial통신의 시작이다.

SerialPort.PortName 속성은 COM1, COM2 등 몇번 COM port를 사용할지 설정하면 된다.
SerialPort.BaudRate 속성은 통신 속도를 설정한다.
일반적인 경우 이 두가지만 설정하면 된다.

아래 코드를 참고하자.

SerialPort SP = new SerialPort();

SP.PortName = "COM1";
SP.BaudRate = (int)38400;
SP.DataBits = (int)8;
SP.Parity = Parity.None;
SP.StopBits = StopBits.One;
SP.ReadTimeout = (int)500;
SP.WriteTimeout = (int)500;

Baud rate, Stop bits, Data bits등 시리얼 통신을 위한 여러 설정들은 위와 같은 방법으로 정의할 수 있다.
위 예에서 적용된 설정들은 이 설정들을 적용하지 않았을 경우 default값으로 적용되는 값들이다.

한가지 팁을 드리자면, 실제 사용 가능한 시리얼 포트가 몇번인지 확인하는 방법은 아래와 같다.
SerialPort.GetPortNames() 라는 method가 있는데, 이를 사용하면 사용가능한 모든 port의 이름목록을 얻어 올 수 있다.
이후 foreach문을 사용하면 모든 사용 가능한(물리적으로) 시리얼 포트를 찾을 수 있다.

foreach (string comport in SerialPort.GetPortNames())
{
    //각각의 'comport'는 사용가능한 포트이름이다.
    //이것을 리스트뷰나 콤보박스에 추가해서 디스플레이 해주는 등의 처리 코드를 넣는다.
}

Parity로 사용할 수 있는 값
  • EVEN
  • MARK
  • NONE
  • ODD
  • SPACE

Stop Bits로 사용할 수 있는 값
  • None
  • One
  • OnePointFive
  • Two
 

Port 열고 닫기


여기까지 설정이 끝났다면 단순히 Open() 메소드를 사용하여 적용된 설정과 함께 포트를 열 수 있다.
2개의 method만 기억하면 된다. Open( )과 Close( )가 그것이다.
SerialPort.Open(), SerialPort.Close() method를 이용해 포트를 열고 닫아주면 된다.
또한 필요한 Exception에 대한 핸들링도 주로 이곳에서 하게 된다.

코드를 작성하다 보면 종종 Open( )이 잘 됐는지, 혹은 아직 Open안된 상태인지 확인을 해야 할 때가 있다.
이를 판단하기위한 속성값이 있는데, SerialPort.IsOpen 이다.
이 IsOpen 속성을 이용하여 포트가 정상적으로 열렸는지 확인 할 수 있다.

SP.Open();

if (SP.IsOpen)
{   
   // 포트 오픈 성공
}
else
{
   // 포트 오픈 실패.
}


데이터 전송 및 수신

데이터 전송은 간단하다. 
'txtSend' 라는 텍스트 박스가 있다고 가정하자.
이 txtSend 에 입력되어있는 값을 전송하려면 다음과 같은 코드 한줄이면 된다.

SP.Write(txtSend.Text);

혹은 아래와 같이 Line단위로 처리하는 method도 사용가능하다.

SP.WriteLine("It’s a test.");
SP.ReadLine();

Read작업의 경우 ReadByte, ReadChar, ReadExisting, ReadLine 중 필요한 메소드를 골라서 사용하면 된다.
Win32에서 Read작업의 경우 쓰레드를 만들고 이벤트를 감시하여 해당 이벤트(시리얼로 데이터가 들어오는)가 발생하면 특정 루틴으로 넘겨주는 방법을 사용했는데, 닷넷에서도 마찬가지의 방법을 사용한다.
SerialPort가 데이터를 받으면 다음과 같은 event handler 가 호출된다.

private void SP_DataReceived(object sender, System.IO.Ports.SerialDataReceivedEventArgs e)

하지만 SerialPort로 받아온 데이터를 읽어올 때 주의사항이 있다.
DataReceived 이벤트 핸들러에서 main thread의 resource에 곧바로 접근했다가는 thread exception을 접하게 된다.
이것을 해결하는 방법을 간단히 다루는것이 이 C#을 이용한 시리얼 통신에 대한 글의 목표이다.


그리고 SerialPort.ReadExisting() 를 호출하면 SerialPort 객체의 버퍼에 남아있는 데이터들을 모두 읽어오게 된다.
하지만 이 데이터들을 main form 의 ListBox 에 넣으려고 하는 순간 exception이 발생했다.
MSDN 을 검색해 보면 다음과 같은 설명이 나와있다.
굵은 부분이 핵심이다.

The DataReceived event is raised on a secondary thread when data is received from the SerialPort object. 
Because this event is raised on a secondary thread, and not the main thread, attempting to modify some elements in the main thread, such as UI elements, could raise a threading exception. 
If it is necessary to modify elements in the main Form or Control, post change requests back using Invoke, which will do the work on the proper thread.


요약하면 SerialPort객체를 통해 호출되는 DataReceived 이벤트 핸들러는 secondary thread이고, main form 은 main thread이다. 
즉, 서로 다른 thread 이다. 
그런데 Serial Port 의 thread 에서 main form 의 thread 의 객체를 modify 하려고 하기 때문에 발생하는 문제이다.

이 문제를 해결하기 위해서 secondary thread인 SerialPort_DataReceived() event는 main thread의 event handler를 invoke 하도록 했다.
main thread에서 invoke되는 event handler에서는 SerialPort 를 통해 받은 데이터를 이용해 main form을 수정한다.

private void SP_DataReceived(object sender, System.IO.Ports.SerialDataReceivedEventArgs e)
{
    this.Invoke(new EventHandler(SerialReceived));
}


즉, SerialPort 가 데이터를 받아올 때마다 main form 에서 SerialReceived() event handler가 호출되며, 이 event handler에서 main form 의 객체를 조작하게 된다.
이로써 thread exception 이 일어나는 문제가 해결된다.

private void SerialReceived(object s, EventArgs e)
{
    String strReceive = SP.ReadExisting();
    listReceive.Items.Add(strReceive);
}


데이터 패킷화


public static void SetPacket(uint uCommand, uint uData , ref byte[] btBuf, ref uint uLen)
{
BitConverter.GetBytes(STARTCODE).CopyTo(btBuf, uLen);
uLen += sizeof(uint);

BitConverter.GetBytes(SESSIONNO_UNKNOWN).CopyTo(btBuf, uLen);
uLen += sizeof(uint);

const uint DATALENGTH = sizeof(uint) + sizeof(uint);     //CMD + DATA length
//btBuf.SetValue(DATALENGTH, sizeof(uint) + uLen);
BitConverter.GetBytes(DATALENGTH).CopyTo(btBuf, uLen);
uLen += sizeof(uint);

BitConverter.GetBytes(uCommand).CopyTo(btBuf, uLen);
uLen += sizeof(uint);

BitConverter.GetBytes(uData).CopyTo(btBuf, uLen);
uLen += sizeof(uint);

BitConverter.GetBytes(ENDCODE).CopyTo(btBuf, uLen);
uLen += sizeof(uint);
}


부록

아래 그림은 시리얼통신용으로 사용하는 D-SUB 커넥터를 캐드로 그린것이다.
자주 사용하는 TXD, RXD, GND 를 표시해놓은 그림이다. 
종종 시리얼통신을 사용할 때마다 핀 배열이 헷갈릴 때가 있는데, 그때마다 찾아보는 그림이다.






이 댓글을 비밀 댓글로
    • 2017.02.15 17:09
    비밀댓글입니다

[C#] WPF Thread

by Blogger 하얀쿠아
2012. 2. 28. 18:46 소프트웨어 Note/C#

WPF Threading

 

WPF는 새로운 스레드 프로그래밍에 대한 보다 단순화된 모델을 제시한다.

대부분의 응용프로그램은 하나의 스레드만으로 실행 되지는 않는다. WPF 역시 렌더링 스레드(Rendering Thread), 사용자 입출력을 담당하는 UI 스레드(UI Thread) 등 여러개의 스레드가 동시에 실행된다. 

개발자가 여러 개의 스레드를 다룰수록 복잡하고 디버깅 등의 어려움이 따르게 마련이다.

 

Dispatcher 와 DispatcherObject

Dispatcher는 스레드에 포함된 작업큐를 말한다.

작업큐에는 수행해야 할 아이템들이 대기하게 되고 정해진 순서에 따라 수행이 된다. 

당연히 Dispatcher 스레드 간에는 공유 될 수 없고 하나의 스레드에만 속해야 한다.

Dispatcher에는 사용자가 직접 대기를 시키던 혹은 운영체제에 의해 대기가 되던 수행해야 할 작업들이 대기하게 된다.

 

DispatcherObject는 자신이 생성되고 실행되는 스레드의 Dispatcher에 접근이 가능하도록 설계된 객체이다. 

WPF의 많은 객체들이 여기에서 파생 된다.

DispatcherObject는 Dispatcher라는 하나의 속성을 지원하는데 스레드의 Dispatcher에 접근하는 방법이다.

Distatcher에 수행해야 할 작업을 삽입하려면 Invoke 나 BeginInvoke 메소드를 통해 한다. Invoke나 BeginInvoke는 비동기 처리를 의미한다.

 

delegate void Work();

private void button1_Click(object sender, RoutedEventArgs e)

{

Dispatcher.Invoke(DispatcherPriority.Normal,new Work(WorkOne) );

}

private void WorkOne()

{

if (button1.CheckAccess())

{

button1.Content = "Hello";

}

}


 

위 코드는 button1을 클릭 했을 때 this 객체의 Dispatcher 에 WorkOne()을 작업으로 할당하고 있다. 

button1.CheckAccess()는 button1에 Dispatcher가 접근 할 수 있는지를 알아보는 메소드로 bool 값을 반환한다. 

VerifyAcces() 메소드도 지원하는데 접근이 안 될 경우 예외를 발생하는 차이가 있다.

 

DispatcherPriority는 많이 세분화 되었다. 이제 10단계로 나누어진다.

이전 닷넷 버전의 스레드 관리 모델보다 정교하다.

 

  1. Invalid 열거형 : 값이 -1이다. 잘못된 우선 순위이다.

  2. Inactive 열거형 : 값이 0이다. 작업이 처리되지 않는다.

  3. SystemIdle 열거형 : 값이 1이다. 시스템이 유휴 상태일 때 작업이 처리된다.

  4. ApplicationIdle 열거형 : 값이 2이다. 응용 프로그램이 유휴 상태일 때 작업이 처리된다.

  5. ContextIdle 열거형 : 값이 3이다. 백그라운드 작업이 완료된 후 작업이 처리된다.

  6. Background 열거형 : 값이 4이다. 유휴 상태가 아닌 다른 모든 작업이 완료된 후 작업이 처리된다.

  7. Input 열거형 : 값이 5이다. 입력과 동일한 우선 순위로 작업이 처리된다.

  8. Loaded 열거형 : 값이 6이다. 레이아웃과 렌더링이 종료되었지만 입력 우선순위의 항목이 처리되기 전에 작업이 처리된다. 특히 Loaded이벤트를 발생시킬 때 사용된다.

  9. Render 열거형 : 값이 7이다. 렌더링과 동일한 우선 순위로 작업이 처리된다.

  10. DataBind 열거형 : 값이 8이다. 데이터 바인딩과 동일한 우선 순위로 작업이처리된다.

  11. Normal 열거형 : 값이 9이다. 일반 우선 순위로 작업이 처리된다. 일반적인응용 프로그램 우선 순위이다.

  12. Send 열거형 : 값이 10이다. 다른 비동기 작업 전에 작업이 처리된다.가장 높은 우선 순위이다.

 

단일 스레드를 이용한 UI 제어

 

TextBox와 Button 하나를 이용한 시나리오로 버턴은 시작 상태에서 Start라는 Text를 가지고 있다. 

클릭과 동시에 TextBox에는 값이 1 만큼 씩 증가 되어 표시된다. 

그리고 버튼의 Text는 Stop 으로 바뀐다. Stop 버턴을 클릭하면 멈추고 Resume 으로 바뀐다. 

이러한 가운데 TextBox text 는 상황에 맞게 증가와 정지를 반복한다.

 

bool runok = false;

 

delegate void Work();

private void button1_Click(object sender, RoutedEventArgs e)

{

runok = !runok;

if (runok)

{

button1.Dispatcher.BeginInvoke(DispatcherPriority.SystemIdle, new

Work(Display));

button1.Content = "Stop";

} else {

button1.Content = "Resume";

}

}

public void Display()

{

int k = int.Parse(textBox1.Text);

k++;

textBox1.Text = k.ToString();

if(runok)

button1.Dispatcher.BeginInvoke(DispatcherPriority.SystemIdle, new Work(Display));

}



[##_http://catnest.tistory.com/script/powerEditor/pages/1C%7Ccfile2.uf@147A89484F4CA23E2BE200.jpg%7Cwidth=%22253%22%20height=%22150%22%20alt=%22%22%20filename=%22threadstart_lse805.jpg%22%20filemime=%22image/jpeg%22%7C_##]


 

하나의 스레드를 사용해 위의 시나리오를 구현하는 것은 이전의 스레드 제어 모델을 이용하면 다소 복잡 할 수 있겠지만 예제에서는 Button 의 Dispatcher에 WorkItem을 비동기로 대기시키고 이때 DispatcherPriority를 조정 함으로써 간단히 구현 했다.

 

 

백그라운드 스레드를 이용한 UI 처리


백그라운드 스레드에서 UI 스레드에 접근해서 UI 변경을 시도 할 때 크로스 스레드 문제가 발생하고 응용 프로그램의 실행이 차단되는 경우가 종종 있다.

아래의 코드는 기존의 닷넷에서 Windows UI를 처리하던 패턴과 닮아 있다.

아래의 코드에서 delegate를 비동기로 호출함으로써 백그라운드 스레드를 생성한다. 

UI를 처리하는 스레드와는 다른 스레드가 생성 된다. 버튼은 비활성화 시켜서 사용자의 접근을 차단하고 대기하게 한다.

그러나 버튼 이외의 컨트롤은 활성화가 유지 된 상태이다.


delegate void Work();

delegate void UpdateUI();

private void button1_Click(object sender, RoutedEventArgs e)

{

textBox1.Text = "처리 중입니다";

button1.Content = "처리중";

new Work(Processing).BeginInvoke(null, null);

button1.IsEnabled = false;

}

 

private void Processing()

{

Thread.Sleep(6000);

button1.Dispatcher.BeginInvoke(

System.Windows.Threading.DispatcherPriority.Normal, new

UpdateUI(Display));

}

private void Display()

{

textBox1.Text = "처리 완료";

button1.Content = "처리 완료";

button1.IsEnabled = true;

}


 


아래의 그림에서와 같이 처리중이라는 메시지와 함께 버턴은 비활성화 되어있다.

델리게이트 호출 내에서 button1의 Dispatcher 에 UI 변경 작업을 할당함으로서 크로스 스레드 문제는 해결 되었다.


[출처] WPF Thread|작성자 




니시오카

델리게이트를 비동기 호출을 통한 백그라운드 작업 호출 델리게이트 내에서 Dispatcher 에 접근 해 UI를 갱신하는 패턴은 WPF의 백그라운드 작업의 전형적인 사례가 될 만하다.

 


멀티스레드


앞에서 본 것처럼 하나의 스레드로도 많은 것을 처리 할 수 있지만 여러 개의 스레드로 작업하는 것이 더 편리 할 수도 있다. 

대표적인 시나리오로 웹서핑의 경우 하나의 브라우져 보다 여러 개를 사용하는 것이 편리하다. 또한 하나의 작업이 긴 시간을 점유하더라도 문제가 되지를 않는다.

 

private void NewWindowHandler(object sender, RoutedEventArgs e)

{

Thread newWindowThread = new Thread(new

ThreadStart(ThreadStartingPoint));

newWindowThread.SetApartmentState(ApartmentState.STA);

newWindowThread.IsBackground = true;

newWindowThread.Start();

}

 

private void ThreadStartingPoint()

{

Window1 tempWindow = new Window1();

tempWindow.Show();

System.Windows.Threading.Dispatcher.Run();

}


 

새로운 스레드를 생성하고 콜백을 호출하는 코드는 이전 버전과 같다. 

새로운 스레드 상에서 새로운 Window를 생성하고 호출하는 데 별도의 스레드이므로 독립적인 작업을 수행 할 수 있다.

 

System.Windows.Threading.Dispatcher.Run();

부분은 스레드의 기본 프레임을 실행하기 위함이다.

[출처] WPF Thread|작성자 니시오카컴포넌트 상의 스레드



컴포넌트상의 스레드


컴포넌트는 비주얼을 갖지 않는 구성요소 인데 WPF에서 컴포넌트를 호출 할 때 호출 스레드의 컨텍스트를 어떻게 다룰 것 하는 문제를 해결 해야 한다.

DispatcherSynchronizationContext는 호출 컨텍스트의 Dispatcher의 경량 버전으로 호출 스레드의 Dispatcher를 접근 한다.

 

class MyComponent:Component

{

public void RunASync()

{

DispatcherSynchronizationContext cx = (DispatcherSynchronizationContext)

DispatcherSynchronizationContext.Current;

SendOrPostCallback callback = new SendOrPostCallback(DoEvent);

cx.Post(callback, null);

}

 

private void DoEvent(object o)

{

//Do something...

}

}



Dispatcher에서 Invoke 나 BeginInvoke처럼 Send 와 Post를 제공한다.

Send는 동기를 Post는 비동기를 지원한다. 위의 코드는 비동기 코드의 패턴을 나타내고 있다.

 


Dispatcher 비활성화를 통한 잠금


WPF 이전의 스레드 동기화 방법은 객체에 Lock을 사용하던 방법 ,스레드 직렬화, 이벤트모델,mutex 등을 사용 했다.WPF에서는 Dispatcher의 DisableProcessing를 이용해 다음과 같이 사용 한다.

 

DispatcherProcessingDisabled dpd= Dispatcher.DisableProcessing()

 

// 비 활 성 화 영 역

dpd.Dispose();


 

비활성화의 효과로는 CLR 잠금에서 내부적으로 메시지를 펌프하지 않고 DispatcherFrame 개체 푸시가 허용되지 않고 메시지 처리가 허용되지 않는다.

[출처] WPF Thread|작성자 니시오카

[출처]



 
 WPF Thread|작성자 니시오카


이 댓글을 비밀 댓글로
    • 신입개발자
    • 2017.02.06 00:43
    안녕하세요 친절한 설명에 감탄하면서 너무 잘보고 도움이 많이 되었습니다.
    한가지 궁금한게 있는데요
    • 신입개발자
    • 2017.02.06 00:51
    첫번째 예시코드에서 Button1_Click 함수에서 Dispatcher.Invoke(DispatcherPriority.Normal,new Work(WorkOne));
    이게 this.Dispatcher.Invoke(DispatcherPriority.Normal,new Work(WorkOne)); 랑 똑같은데

    제가 알기로는 wpf가 쓰레드가 두개가 있는데
    1. ui컨트롤을 소유하는 ui쓰레드
    2. ui컨트롤을 갖지않는 작업쓰레드
    이렇게 알고 있는데

    질문이 있습니다.
    1.작성자 분께서는 Dispatcher가 쓰레드의 작업 큐를 말하는거라고 하셨는데 이 Dispatcher가 ui쓰레드중에서 하나의 쓰레드 작업큐 를 말하는건가요?
    2.쓰레드는 프로세스에서 최소 한개이상 (메인쓰레드(ui쓰레드)) 가있고 메인쓰레드로 부터 여러쓰레드를 만드는거라고 알고있는데 Dispatcher.Invoke()를 하게 되면 메인쓰레드에서 별도의 쓰레드하나를 생성해서 이작업을 해라! 라고 하는건가요?
    아니면 자신이 가지고 있는 메인쓰레드에게 this.Dispatcher.Invoke()를 하여 이걸 실행해라 라고 한건가요?
    this.Dispatcher.Invoke() 가 자기자신에서의 어떤 Dispatcher를 말하는건지 여러모로 전반적인 개념이 없는 것같습니다 .ㅠ.ㅠ.

    긴글 읽어주셔서 감사합니다.

[C#] 닷넷 프레임워크 기반의 소켓 프로그래밍

by Blogger 하얀쿠아
2012. 2. 28. 18:02 소프트웨어 Note/C#

출처 :  http://www.devtimes.com/45 

요약 (Summary)

이 문서는 닷넷 프레임워크 환경과 이기종 환경 간의 소켓 통신을 위한 간략한 개요를 제시합니다. 특히 HTTP 또는 SOAP 등의 텍스트 기반의 통신이 아닌, C/C++ 개발자들이 선호하는 구조체 패킷을 이용한 통신에 초점을 두었습니다.

뿐만 아니라 이를 지원하기 위한 소켓 라이브러리(C# 2.0 지원)도 함께 제공합니다. MIT 라이센스를 채택하였으며, 라이센스에 명시된 바와 같이 상업적인 목적으로도 사용할 수 있습니다.


소개 (Introduction)

이 문서는 닷넷 프레임워크 기반의 소켓 프로그램을 작성하려는 개발자를 위해 작성되었습니다. 닷넷 개발자는 C/C++ 등으로 작성된 서버/클라이언트와 통신하기 위한 모듈 또는 소프트웨어를 제작해야 하는 상황에 부닥치게 됩니다. 닷넷 환경에서의 소켓 프로그래밍에 익숙하지 않은 개발자는 많은 어려움에 직면하게 됩니다.

이 문서는 다음과 같은 문제점을 해결하기 위한 가이드라인을 제시합니다.

  • 닷넷 환경에서 구조체를 이용한 소켓 통신 구현이 어렵습니다.
  • 닷넷 환경과 이기종 환경 간의 통신에서는 데이터의 타입 변환을 신중히 생각해야 합니다.
  • 그밖에 문자열 변환 등에서 부닥치게 되는 예기치 못한 문제들이 있습니다.

또한 닷넷 환경에서 소켓 통신을 할 때 자주 쓰이는 주요 함수 또는 패턴을 기술합니다.

이 문서의 주요 섹션은 다음과 같이 구성되어 있습니다.


시스템 요구사항 (System Requirements)

이 문서 상에 기술된 작업을 위한 시스템 요구사항은 다음과 같습니다.

  • Microsoft Windows® 2000, Windows XP Professional, or Windows 2003 operating system

  • Microsoft .NET Framework Software Development Kit (SDK), version 1.1.

이 문서 상에 기술된 작업이 테스트된 환경은 다음과 같습니다.

  • Microsoft Windows® 2000 Professional operating system

  • Microsoft .NET Framework Software Development Kit (SDK), version 1.1

  • Microsoft Visual Studio® 2003 development system

  • Microsoft Windows® XP Professional operating system

  • Microsoft .NET Framework Software Development Kit (SDK), version 2.0

  • Microsoft Visual Studio® 2005 development system


참고 문헌 (Related Links)


누가 이 문서를 읽어야 하는가? (Who Should Read This Document?)

이 문서는 C/C++ 소켓 프로그래밍에 익숙하지만 닷넷 프레임워크 환경에서 소켓 프로그래밍을 해야 하는 개발자를 위해 가이드라인을 제시합니다.


반드시 알아야 할 것 (What You Must Know)

C/C++ 환경에서의 소켓 프로그래밍에 대한 기본적인 지식이 필요합니다. 관리되는 메모리의 구조 및 관리되지 않는 메모리의 구조에 대한 차이를 이해하고 있어야 합니다. 닷넷 기반에서 Binary 데이터를 다루기 위해 필요한 기본적인 지식이 필요합니다.


새로운 사항 (What’s New)

  1. 2006사용자 정의 함수 ExtendedTrim의 버그 수정.

  2. 2007.07.25소켓 라이브러리 추가


닷넷 환경에서 구조체는 관리되는 메모리 구조를 갖는다

일반적인 C/C++에서는 구조체를 이용하여 소켓 통신을 합니다. 다음은 인증을 위한 구조체를 정의하고, 이를 통해 통신을 하는 예제 코드입니다.

[표: 인증을 위한 패킷 구조체]

[C]
typedef struct tag_BIND
{
char szID[16];
char szPWD[16];
} BIND;

#define BIND_SIZE sizeof(BIND)

[표: 인증 패킷 송신 예제]

[C]
if( SendData(sockfd,(char*)&packetbind,BIND_SIZE) != BIND_SIZE )
{
// Error Handling
cout << "ERROR : BIND 송신 실패" << endl;
return 0;
}

닷넷 환경에서는 위와 같이 구조체를 사용한 소켓 통신이 지원되지 않습니다.

[표: C#으로 작성된 인증 패킷 구조체

[C#]
public struct BIND
{
public char szID[16];
public char szPWD[16];
};

만약 위와 같은 구조체를 선언해서 전송하면, 수신하는 측에서는 16 bytes + 16 bytes을 받아야 합니다. 그러나 실제로는 약 100 bytes의 데이터를 수신하게 됩니다. 이는 닷넷 환경에서 구조체가 관리되는 메모리 구조를 갖기 때문입니다.


닷넷 환경에서의 구조체를 이용한 소켓 통신 구현 방법

닷넷 환경에서 구조체를 이용한 소켓 통신을 지원하기 위한 방법은 크게 두 가지로 나뉩니다.

  • 마샬링(Marshaling)을 이용한 구조체 사용.
  • 바이너리 포매터(Binary Formatter)의 사용.

마샬링을 이용하면, C/C++에서와 비슷한 방식으로 소켓 프로그래밍을 할 수 있습니다. 하지만 이 문서에서는 두 번째 방법을 사용합니다.


패킷 송신 방법

다음과 같은 인증 요청 패킷을 송신해야 한다고 가정합니다.

[표: 인증 요청 패킷]

[C]
typedef struct tag_BIND
{
int nCode;
char szPWD[16];
} BIND;

이러한 구조의 패킷을 보내기 위해 다음과 같은 코드가 필요합니다.

[C#]
// 패킷 사이즈 
public int BODY_BIND_SIZE = 4 + 16;

// 인증 패킷 구조체 
public struct Bind
{
public int nCode;
public string strPWD;
}

// 인증 패킷 구조체를 바이트 배열로 변환하는 함수. 
public byte[] GetBytes_Bind(Bind bind)
{
byte[] btBuffer = new byte[BODY_BIND_SIZE];

MemoryStream ms = new MemoryStream(btBuffer,true); 
BinaryWriter bw = new BinaryWriter(ms);

// nCode 
bw.Write( IPAddress.HostToNetworkOrder( bind.nCode ) );

// szPWD 
try
{
byte[] btPWD = new byte[16];
Encoding.Default.GetBytes(bind.strPWD,0,bind.strPWD.Length,btPWD,0);
bw.Write( btPWD );
}
catch(Exception ex)
{
// Error Handling 
}

bw.Close();
ms.Close();

return btBuffer;
}

// 사용 예제
Bind bind = new Bind();
bind.nCode = 12345;
bind.strPWD = “98765”;

byte[] buffer = GetBytes_Bind(bind);

socket.Send(buffer);

BinaryWriter를 사용하여 4 bytes의 Integer값과 16 bytes의 문자열을 바이트 배열에 써넣습니다. 위의 GetBytes_Bind를 실행하면 다음과 같은 구조의 바이트 배열이 생성됩니다. 이때 Integer 타입의 nCode를 Network Byte Order로 변환하는 것에 주의하십시오.

9

8

7

6

5

\0

\0

\0

\0

\0

\0

\0

\0

\0

\0

\0

파란색: Integer 타입의 nCode
녹색: String 타입의 strPWD
흰색: NULL로 구성된 바이트 배열

패킷 수신

위에서 송신한 인증 요청에 대한 응답을 다음과 같은 형태로 받는다고 가정합니다.

[표: 인증 요청 패킷]

[C] 
typedef struct tag_BIND_ACK
{
int nCode;
char szPWD[16];
int nResult;
} BIND_ACK;

위와 같은 패킷을 수신하기 위해서는 다음과 같은 코드가 필요합니다.

[C#] 
public struct BindAck
{
public int nCode;
public string strPWD;
public int nResult;
}

public static BodyAck0 GetBindAck(byte[] btBuffer)
{
BindAck bindAck = new BindAck();

MemoryStream ms = new MemoryStream(btBuffer,false);
BinaryReader br = new BinaryReader(ms);

// Integer 타입의 nCode
bindAck.nCode = IPAddress.NetworkToHostOrder( br.ReadInt32() );

// 16 bytes의 문자열
bindAck.strPWD = ExtendedTrim( Encoding.Default.GetString(br.ReadBytes(16)) );

// Integer 타입의 nResult
bindAck.nResult = IPAddress.NetworkToHostOrder( br.ReadInt32() );

br.Close();
ms.Close();

return bindAck;
}

// 문자열 뒤쪽에 위치한 NULL을 제거한 후에 공백문자를 제거한다.
public string ExtendedTrim(string source)
{
string dest = source;

int index = dest.IndexOf('\0');
if( index > -1 )
{
dest = source.Substring(0,index+1);
}
return dest.TrimEnd('\0').Trim();
}

// 사용 예제
int BIND_ACK_SIZE = 4 + 16 + 4;

byte[] buffer = new byte[BIND_ACK_SIZE];

if( socket.Receive(buffer) == BIND_ACK_SIZE )
{
BindAck bindAck = GetBindAck(buffer);
}
else
{
// ERROR HANDLINGM
}

위에서 ExtendedTrim() 라는 함수를 정의해서 사용했습니다. 이 함수가 필요한 이유에 대해서는 다음 장에서 설명하겠습니다.


주요 함수

  1. System.Net.IPAddresss.NetworkToHostByteOrder(…)

    1. 요약

      네트워크 바이트 순서에서 호스트 바이트 순서로 숫자를 변환합니다. NetworkToHostByteOrder 함수는 오버로딩 되어 있어서 short, int, long 타입을 모두 변환할 수 있습니다.

    2. 설명

      서로 다른 컴퓨터에서는 멀티바이트 정수 값 내에서 바이트의 순서를 지정하는 데 서로 다른 규칙을 사용합니다. 일부 컴퓨터에서는 MSB(최상위 바이트)를 먼저 배치(big-endian 순서)하지만 다른 컴퓨터에서는 LSB(최하위 바이트)를 먼저 배치(little-endian 순서)합니다. 바이트 순서가 다르게 지정된 컴퓨터를 사용하려면 네트워크를 통해 보내는 모든 정수 값을 네트워크 바이트 순서로 보냅니다.

    3. 예제

      [C#] 
      public void NetworkToHostOrder_Long(long networkByte)
      {
      long hostByte;
      // Converts a long value from network byte order to host byte order. 
      hostByte = IPAddress.NetworkToHostOrder(networkByte);
      
      Console.WriteLine("Network byte order to Host byte order of {0} is {1}", networkByte, hostByte);
      }
  2. System.Net.IPAddresss.HostToNetworkByteOrder(…)

    1. 요약

      호스트 바이트 순서에서 네트워크 바이트 순서로 숫자를 변환합니다. HostToNetworkByteOrder 함수는 오버로딩 되어 있어서 short, int, long 타입을 모두 변환할 수 있습니다.

    2. 설명

      여기를 참고하십시오.

  3. System.Text.Encoding.Default.GetBytes(…)

    1. 요약

      지정된 String 이나 문자의 전부 또는 일부를 바이트 배열로 인코딩한 후, 패킷을 전송합니다. Encoding 타입은 Default 외에도 여러 가지가 있습니다.

    2. 인코딩 타입

      1. Default: 시스템의 현재 ANSI 코드 페이지에 대한 인코딩을 가져옵니다.

      2. ASCII: ASCII(7비트) 문자 집합에 대한 인코딩을 가져옵니다.

      3. BigEndianUnicode

        1. 요약

          big-endian 바이트 순서로 유니코드 형식에 대한 인코딩을 가져옵니다.

        2. 설명

          유니 코드 문자는 big-endian과 little-endian의 두 가지 바이트 순서로 저장할 수 있습니다. Intel 컴퓨터 같이 little-endian 플랫폼을 사용하는 컴퓨터에서는 일반적으로 유니코드 문자를 little-endian 순서로 저장하는 것이 보다 효율적이지만 대부분의 다른 플랫폼에서는 유니코드 문자를 big-endian 순서로 저장합니다.

          유니코드 파일은 big-endian 플랫폼에서는 16진수 0xFE 0xFF로 표시되고 little-endian 플랫폼에서는 16진수 0xFF 0xFE로 표시되는 바이트 순서 표시(U+FEFF)로 구별할 수 있습니다.

      4. Unicode: little-endian 바이트 순서로 유니코드 형식에 대한 인코딩을 가져옵니다.

      5. UTF7: UTF-7 형식에 대한 인코딩을 가져옵니다.

      6. UTF8: UTF-8 형식에 대한 인코딩을 가져옵니다.

    3. 예제

      [C#]
      string strPWD = "password";
      
      try
      {
      byte[] btPWD = new byte[16];
      Encoding.Default.GetBytes(strPWD,0,strPWD.Length,btPWD,0);
      }
      catch(Exception ex)
      {
      // Error Handling 
      }
  4. System.Text.Encoding.Default.GetString(…)

    1. 요약

      지정된 바이트 배열을 지정된 인코딩 타입의 문자열로 디코딩합니다.

    2. 인코딩 타입

      인코딩 타입을 참조하십시오.

    3. 예제

      다음 예제에서는 GetString의 오버로드된 버전에 대한 사용법을 보여 줍니다. 사용할 수 있는 다른 예제를 보려면 개별 오버로드 항목을 참조하십시오.

      [C#] 
      private string ReadAuthor(Stream binary_file) {
      System.Text.Encoding encoding = System.Text.Encoding.UTF8;
      // Read string from binary file with UTF8 encoding 
      byte[] buffer = new byte[30];
      binary_file.Read(buffer, 0, 30);
      return encoding.GetString(buffer);
      }
  5. 사용자 정의 함수 ExtendedTrim(…)

    1. 요약

      NULL 종료 문자열을 유니코드 기반의 System.String 으로 변환하기 위한 함수입니다. 종료문자 NULL 이후의 문자열을 제거합니다.

    2. 함수 정의

      // 문자열 뒤쪽에 위치한 NULL을 제거한 후에 공백문자를 제거한다.
      public string ExtendedTrim(string source)
      {
      string dest = source;
      
      int index = dest.IndexOf('\0');
      if( index > -1 )
      {
      dest = source.Substring(0,index+1);
      }
      return dest.TrimEnd('\0').Trim();
      }
    3. 설명

      아래와 같은 코드를 고려해보겠습니다.

      [C#] 
      string strPWD = "password";
      byte[] btPWD = new byte[16]; 
      
      try 
      { 
      Encoding.Default.GetBytes(strPWD,0,strPWD.Length,btPWD,0); 
      } 
      catch(Exception ex) 
      { 
      // Error Handling 
      }

      위의 코드가 실행되면 바이트 배열 btPWD의 구조는 다음과 같습니다.

      p

      a

      s

      s

      w

      o

      r

      d

      \0

      \0

      \0

      \0

      \0

      \0

      \0

      \0

      녹색: String 타입의 strPWD
      흰색: NULL로 구성된 바이트 배열

      바이트 배열 btPWD를 다시 문자열로 변환해보겠습니다.

      [C#] 
      string strPassword = Encoding.Default.GetString(btPWD);
      strPassword.Trim();
      Console.WriteLine(strPassword + "!");

      위의 코드에서 기대했던 출력값은 password!였습니다. 그러나 실제로 코드를 실행시켜보면 password !라고 출력됩니다. 클래스 System.String 에 정의된 Trim()함수는 문자열 양측에 놓인 공백문자는 제거하지만, NULL은 제거하지 않습니다.

      이 문제 때문에 여러 가지 기능이 예치기 않게 오작동할 수 있습니다. 특히 이벤트 로그에 NULL이 들어간 문자열을 기록하려고 시도하면, 문제가 발생합니다. 다음 예제에서 위에서 생성한 문자열을 이벤트 로그에 기록합니다.

      [C#] 
      // Create the source, if it does not already exist.
      if(!EventLog.SourceExists("MySource"))
      {
      EventLog.CreateEventSource("ySource", "MyNewLog");
      Console.WriteLine("CreatingEventSource");
      }
      
      // Create an EventLog instance and assign its source.
      EventLog myLog = new EventLog();
      myLog.Source = "MySource";
      
      // Write an informational entry to the event log. 
      string strLog = String.Format("{0}{1}{2}",strPassword,Environment.NewLine,strPassword);
      
      myLog.WriteEntry(strLog);

      위의 예제에서 기대했던 출력 값은 다음과 같습니다.

      password
      password

      그러나 실제로는 다음과 같은 출력을 얻게 됩니다.

      password

      문자열 strLog 중간에 NULL이 포함되어 있기 때문에, 뒤에 이어지는 문자열은 기록되지 않습니다. 일반적인 C/C++ 프로그래밍에서는 처음 문자열 뒤쪽에 위치한 NULL을 제외하고, 두 번째 문자열을 연결합니다. 다음의 예제 코드를 보십시오.

      [C++] 
      char szPassword[16];
      memset(szPassword,NULL,sizeof(szPassword));
      sprintf(szPassword,"%s","password");
      
      char szBuffer[128];
      memset(szBuffer,NULL,sizeof(szBuffer));
      sprintf(szBuffer,"%s%s",szPassword,szPassword);
      
      printf("%s",szBuffer);

      위의 코드에서 szPassword 두 개를 연결하여 szBuffer에 저장합니다. 이 경우에 다음과 같은 메모리 구조가 됩니다.

      p

      a

      s

      s

      w

      o

      r

      d

      p

      a

      s

      s

      w

      o

      r

      d

      \0

      \0

      \0

      \0

      \0

      \0

      녹색 : 첫번째 문자열
      파란색 : 두번째 문자열

      위에서 알 수 있듯이 일반적으로 C/C++ 프로그래밍에서는 NULL을 제외하고 두 문자열을 연결합니다. 그러나 닷넷에서 기본으로 제공되는 Trim() 함수는 NULL을 제외하지 않고 두 문자열을 연결합니다. 그 때문에 앞서 제시한 이벤트로그 예제에서는 다음과 같은 메모리 구조를 갖게 됩니다.

      p

      a

      s

      s

      w

      o

      r

      d

      \0

      \0

      p

      a

      s

      s

      w

      o

      r

      d

      \0

      녹색 : 첫번째 문자열
      파란색 : 두번째 문자열

데이터 타입

일반적으로 C/C++의 경우에 CPU의 종류에 따라 숫자형 데이터 타입의 크기가 달라집니다. 32 bits CPU 환경에서 Long Integer 타입은 4 bytes(32 bits)이지만, 64 bits CPU 환경에서는 8 bytes (64 bits)가 됩니다. 보통의 경우라면 닷넷 개발자는 32 bits환경의 이기종 서버/클라이언트와 통신하는 소프트웨어를 제작하게 됩니다. 이때 다음의 표를 참고하여 데이터 변환을 하도록 해야 합니다.

형식

범위

크기

.Net Framework 형식

Sbyte

-128 ~ 127

부호 있는 8비트 정수

System.SByte

Byte

0 ~ 255

부호 없는 8비트 정수

System.Byte

Char

U+0000 ~ U+ffff

유니코드 16비트 문자

System.Char

Short

-32,768 ~ 32,767

부호 있는 16비트 정수

System.Int16

Ushort

0 ~ 65,535

부호 없는 16비트 정수

System.UInt16

Int

-2,147,483,648 ~ 2,147,483,647

부호 있는 32비트 정수

System.Int32

Uint

0 ~ 4,294,967,295

부호 없는 32비트 정수

System.UInt32

Long

?9,223,372,036,854,775,808 ~ 9,223,372,036,854,775,807

부호 있는 64비트 정수

System.Int64

Ulong

0 ~ 18,446,744,073,709,551,615

부호 없는 64비트 정수

System.UInt64

예를 들어 32 bits CPU 환경일지라도 .Net Framework 하에서 Long Integer 타입의 크기는 8 bytes입니다.

이 댓글을 비밀 댓글로

[C#] .NET 이 아닌 native 코드로 작성된 외부 DLL 사용 방법(파라미터의 타입을 알 수 없는 경우)

by Blogger 하얀쿠아
2012. 2. 8. 18:56 소프트웨어 Note/C#
외부 메소드의 파라미터가 char* 인 경우(데브피아 C# 마을, 질문&답변)
 

가장 일반적으로 포인터로 넘겨서 값을 받아올경우 할당되어 있는 방으로 넘겨 받아야 합니다.
하지만 DllImportAttribute 클래스를 이용하여 맵핑 하시기에 어려움을 겪으시는것 같네요.
해당 매개변수는 호출지점에 구도에 따라 string, StringBuilder, byte, unsafe.... 맵핑이 가능합니다.
 
원초적인 방법으로 IntPtr 을 이용한 메모리 활당하여 이용하는 방법도 가능합니다.
 
사용하시는 " CoreTTS_DLL_API.dll " 상용컴포넌트에 대해서 어떠한 정보를 모르기에
매개 변수나 필드를 비관리 코드로 마샬링식별을 싱글 바이트 null로 끝난다는 기준으로 예제코드를 작성하였습니다.
또한 System.Text.StringBuilder 타입 대신 System.String 타입을 사용하셔도 무방합니다.

[DllImport("CoreTTS_DLL_API.dll"), EntryPoint="VOICE_PLAY",

  CallingConvention = CallingConvention.StdCall, CharSet=CharSet.Ansi )] 

public static extern int VOICE_PLAY(

                                    [MarshalAs(UnmanagedType.LPStr)] StringBuilder Language , 

                                    [MarshalAs(UnmanagedType.LPStr)] StringBuilder text);

 
이 댓글을 비밀 댓글로

[C#] 메소드 동기화

by Blogger 하얀쿠아
2011. 7. 21. 01:38 소프트웨어 Note/C#
동기화를 원하는 몇개의 메소드가 클래스 안에 있다고 가정하자.
(물론 그것들은 여러개의 스레드에 의해 동시에 사용될 수 없게 하려는 상황이라고 가정한다.)

자바 프로그래머라면 스레드 사용시 메소드 동기화를 위해 단지 다음과 같은 방법을사용 하면 된다는걸 알고 있을것이다.

public synchronized void methodName() {...}


C#에서는 이와같은 효과를 주기 위해 어떻게 해야 할까.

자바를 먼저 공부했던 나는 C#을 공부하면서 이와같은 의문을 가졌고, 찾은 해결방법은 다음과같다.


방법1. Just wrap the entire content of your method in a lock statement.
(메소드의 전체내용을 lock 문장으로 둘러 싸면 된다)
public class MyClass
{
public void MyMethod()
{
lock(typeof(MyClass))
{
// 메소드의 내용
//
}
}
}



방법2.
[MethodImpl(MethodImplOptions.Synchronized)]
public void MyMethod()
{
// Contents of method
}

Both are not exactly the same and the second one is more like the
synchronized keyword in Java but in general people use locks in C# as it
gives you more fine grained control.


출처 : http://bytes.com/topic/c-sharp/answers/274531-c-synchronized-methods
이 댓글을 비밀 댓글로

[C#] 구조체

by Blogger 하얀쿠아
2011. 7. 8. 12:22 소프트웨어 Note/C#
구조체는 클래스와 동일한 구문으로 대부분 형식을 공유하지만 클래스보다 제한적이며 다음과 같은 특징을 갖는다.

▷ 구조체는 값 형식이고 클래스는 참조 형식이다.
▷ 클래스와 달리 구조체는 new 연산자를 사용하지 않고 인스턴스화 할 수 있다.
▷ 구조체는 생성자를 선언할 수 있으나 반드시 매개 변수를 사용해야 한다.
▷ 구조체는 다른 구조체 또는 클래스에서 상속될 수 없으며, 클래스의 기본 클래스가 될 수 없으며 모든 구조체는 System.Object 를 상속하는 System.ValueType에서 직접 상속한다. 
▷ 구조체는 인터페스이를 구현할 수 있다.
▷ 구조체를 nullable 형식으로 사용할 수 있고 여기에 null값을 할당할 수 있다.
▷ 구조체 선언 내에서 필드는 const 또는 static으로 선언한 경우에만 초기화 할 수 있다.
▷ 구조체에서는 매개변수가 없는 생성자인 기본 생성자나 소멸자를 선언할 수 없다. 

 
구조체는 다음과 같이 struct 키워드를 통해 정의한다.

public struct StructLogic
{

//필드, 속성, 메서드, 이벤트

 
구조체 블록 내부에 추가되는 필드, 속성, 메서드, 이벤트를 통칭하여 구조체 멤버라고 함.
클래스의 기능을 부분적으로 제한한 것이 구조체라 생각하면 이해가 쉬울 것. 
Tags
이 댓글을 비밀 댓글로