안녕하세요. 언제나휴일입니다.
1. 해야 할 일
지난 강의에서 Wafer 코팅 공장과 중앙 관제에서 자신의 IP 주소를 얻어오는 부분을 구현하였습니다.
이번 강의에서는 공장에서 FactoryServer를 가동하는 부분을 구현할 거예요.
그리고 중앙 관제에서 FactoryClient로 모니터링 정보를 수신할 제어 서버의 끝점(IP 주소 및 포트) 정보를 전송하는 부분을 구현할 거예요.
2. MsgType 구현
앞으로 Wafer 코팅 공장과 중앙 관제 사이에 메시지를 주고 받을 때 언제나 메시지 종류와 내용을 보내기로 할게요.
메시지 종류는 MsgType 열거형으로 정의할게요.
여기에서는 제어 서버의 끝점 정보를 전송하는 메시지 타입을 정의할게요.
WaferLineCommLib에 새항목으로 MsgType.cs 파일을 추가하여 다음처럼 구현합니다.
namespace WaferLineCommLib { public enum MsgType { MSG_CF_ADDSI } }
3. FactoryClient 구현
중앙 관제에서 공장에 제어 신호를 전송하는 FactoryClient를 정의합시다.
먼저 WaferLineCommLib에 FactoryClient.cs를 추가하세요.
namespace WaferLineCommLib { public class FactoryClient { } }
생성자에서 IP 주소와 포트 정보를 입력 인자로 받아 멤버 필드에 설정합니다.
string fip; int fport; public FactoryClient(string fip,int fport) { this.fip = fip; this.fport = fport; }
중앙 관제에서 모니터링에 사용할 자신의 끝점(IP 주소와 포트) 정보를 전송할 메서드를 정의합시다.
public void SendMyInfo(string ip, int port) { }
소켓에 메시지를 보낼 때 byte 배열로 전송합니다.
여기에서는 128바이트 공정 길이로 보내기로 할게요.
byte[] packet = new byte[128];
packet에 정보를 순차적으로 기록하기 위해 MemoryStream을 사용할게요.
그리고 기록하기 편하게 하기 위해 BinaryWriter를 이용할게요.
MemoryStream ms = new MemoryStream(packet); BinaryWriter bw = new BinaryWriter(ms);
여기에서는 메시지 타입과 끝점(IP 주소와 포트) 정보를 기록한 후에 패킷을 전송합니다.
패킷을 전송하는 부분은 별도의 메서드 SendPacket을 만들어 사용합시다.
bw.Write((int)MsgType.MSG_CF_ADDSI); bw.Write(ip); bw.Write(port); bw.Close(); ms.Close(); SendPacket(packet);
SendMyInfo 메서드 코드는 다음과 같습니다.
public void SendMyInfo(string ip, int port) { byte[] packet = new byte[128]; MemoryStream ms = new MemoryStream(packet); BinaryWriter bw = new BinaryWriter(ms); bw.Write((int)MsgType.MSG_CF_ADDSI); bw.Write(ip); bw.Write(port); bw.Close(); ms.Close(); SendPacket(packet); }
SendPacket 메서드를 정의합시다.
private void SendPacket(byte[] packet)
먼저 IPv4, TCP 프로토콜을 사용할 소켓을 생성합니다.
멤버 필드의 끝점 정보(fip, fport) 개체를 생성하여 FactoryServer에 연결합니다.
Socket sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); IPAddress addr = IPAddress.Parse(fip); IPEndPoint ep = new IPEndPoint(addr, fport); sock.Connect(ep);
입력 인자로 전달받은 패킷을 전송하고 소켓을 닫습니다.
sock.Send(packet); sock.Close();
다음은 현재까지 작성한 FactoryClient.cs 소스 코드입니다.
using System.IO; using System.Net; using System.Net.Sockets; namespace WaferLineCommLib { public class FactoryClient { string fip; int fport; public FactoryClient(string fip, int fport) { this.fip = fip; this.fport = fport; } public void SendMyInfo(string ip, int port) { byte[] packet = new byte[128]; MemoryStream ms = new MemoryStream(packet); BinaryWriter bw = new BinaryWriter(ms); bw.Write((int)MsgType.MSG_CF_ADDSI); bw.Write(ip); bw.Write(port); bw.Close(); ms.Close(); SendPacket(packet); } private void SendPacket(byte[] packet) { Socket sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); IPAddress addr = IPAddress.Parse(fip); IPEndPoint ep = new IPEndPoint(addr, fport); sock.Connect(ep); sock.Send(packet); sock.Close(); } } }
4. 제어 서버 끝점 정보 수신 이벤트 인자 정의
WaferLineCommLib에 RecvStstEndPtEventArgs.cs를 추가합니다.
제어 서버의 끝점 정보를 수신하였을 때 콜백 처리를 위한 이벤트 인자와 대리자를 정의합시다.
이벤트 인자에 끝점 정보인 IP 주소와 포트를 가져오기 할 수 있는 속성을 제공합니다.
using System; using System.Net; namespace WaferLineCommLib { public delegate void RecvStsEndPtEventHandler(object sender, RecvStsEndPtEventArgs e); public class RecvStsEndPtEventArgs:EventArgs { public IPAddress IPAddress { get; } public int Port { get; } public RecvStsEndPtEventArgs(IPAddress ipaddr, int port) { IPAddress = ipaddr; Port = port; } } }
5. FactoryServer 구현
WaferLineCommLib에 FactoryServer.cs를 추가합니다.
FatrocyServer 클래스에는 RecvStstEndPtEvent를 이벤트 멤버로 캡슐화합니다.
namespace WaferLineCommLib { public class FactoryServer { public event RecvStsEndPtEventHandler RecvStsEndPoint;
생성자에서 ip 주소와 포트 정보를 입력 인자로 받아 멤버 필드에 설정할게요.
public string IP { get; } public int Port { get; } public FactoryServer(string ip,int port) { IP = ip; Port = port; }
서버를 가동하는 Start 메서드를 정의합시다.
소켓 생성 => 네트워크 인터페이스와 결합 => 백 로그 큐 크기 설정 => 클라이언트 연결 요청 대기 및 수락 Loop을 수행하는 것이 기본적인 TCP 서버의 절차입니다.
Socket sock; public void Start() { sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); IPAddress addr = IPAddress.Parse(IP); IPEndPoint endp = new IPEndPoint(addr, Port); sock.Bind(endp); sock.Listen(5); AcceptLoop(); } private void AcceptLoop() { while(true) { Socket dosock = sock.Accept(); DoIt(dosock); } }
그런데 FactroyServer의 Start 메서드를 호출한 곳에서는 클라이언트 연결 요청 대기 및 수락하는 Loop부분에서 블락킹되는 것을 원하지 않을 수 있습니다.
이를 위해 비동기로 Start를 수행할 수 있게 비동기 대리자를 이용할게요.
delegate void StartDele(); public void StartAsync() { StartDele dele = Start; dele.BeginInvoke(null, null); }
DoIt 메서드에서는 packet을 수신한 후 메시지 타입에 따라 처리 메서드를 호출하는 형태로 구현합니다.
private void DoIt(Socket dosock) { byte[] packet = new byte[128]; dosock.Receive(packet); MemoryStream ms = new MemoryStream(packet); BinaryReader br = new BinaryReader(ms); MsgType msgtype = (MsgType)br.ReadInt32(); switch(msgtype) { case MsgType.MSG_CF_ADDSI: SetAddressProc(br); break; } br.Close(); ms.Close(); dosock.Close(); }
SetAddressProc에서는 끝점 정보를 읽어온 후 등록한 이벤트 핸들러에게 통보합니다.
private void SetAddressProc(BinaryReader br) { IPAddress ipaddr = IPAddress.Parse(br.ReadString()); int port = br.ReadInt32(); if(RecvStsEndPoint != null) { RecvStsEndPoint(this, new RecvStsEndPtEventArgs(ipaddr, port)); } }
다음은 FactoryServer.cs 소스 코드입니다.
using System.IO; using System.Net; using System.Net.Sockets; namespace WaferLineCommLib { public class FactoryServer { public event RecvStsEndPtEventHandler RecvStsEndPoint; public string IP { get; } public int Port { get; } public FactoryServer(string ip,int port) { IP = ip; Port = port; } delegate void StartDele(); public void StartAsync() { StartDele dele = Start; dele.BeginInvoke(null, null); } Socket sock; public void Start() { sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); IPAddress addr = IPAddress.Parse(IP); IPEndPoint endp = new IPEndPoint(addr, Port); sock.Bind(endp); sock.Listen(5); AcceptLoop(); } private void AcceptLoop() { while(true) { Socket dosock = sock.Accept(); DoIt(dosock); } } private void DoIt(Socket dosock) { byte[] packet = new byte[128]; dosock.Receive(packet); MemoryStream ms = new MemoryStream(packet); BinaryReader br = new BinaryReader(ms); MsgType msgtype = (MsgType)br.ReadInt32(); switch(msgtype) { case MsgType.MSG_CF_ADDSI: SetAddressProc(br); break; } br.Close(); ms.Close(); dosock.Close(); } private void SetAddressProc(BinaryReader br) { IPAddress ipaddr = IPAddress.Parse(br.ReadString()); int port = br.ReadInt32(); if(RecvStsEndPoint != null) { RecvStsEndPoint(this, new RecvStsEndPtEventArgs(ipaddr, port)); } } } }
6. Manager 정의
WaferLine 공장 시뮬레이션 응용에서는 소켓 통신 부분은 Manager 클래스에서 담당하게 할게요.
이를 위해 WaferLine 공장 시뮬레이션 프로젝트에 Manager 클래스를 추가하세요.
namespace WaferLine_공장_시뮬레이션 { public class Manager { } }
Manager 클래스에는 RecvStsEndPtEventHandler 형식의 이벤트 멤버를 캡슐화합니다.
public event RecvStsEndPtEventHandler RecvStsEndPoint;
Manager 형식 개체는 여러 개를 생성하지 못하게 단일체 패턴을 적용하게요.
생성자의 접근 지정을 private으로 설정하고 정적 멤버에 단일체 개체를 참조합니다.
외부에서 접근할 수 있게 정적 멤버 Singleton 속성을 제공할게요.
private Manager() { } static Manager manager = new Manager(); static public Manager Singleton { get { return manager; } }
FactoryServer를 가동하는 메서드를 제공합시다.
FactoryServer 개체를 생성합니다.
RecvStsEndPoint 이벤트 핸들러를 등록한 후 비동기 모드로 가동합니다.
public void StartServer(string ip,int port) { FactoryServer fs = new FactoryServer(ip, port); fs.RecvStsEndPoint += Fs_RecvStsEndPoint; fs.StartAsync(); }
이벤트 핸들러에서는 이벤트 멤버에 등록한 이벤트 핸들러에게 정보를 bypass 합니다.
private void Fs_RecvStsEndPoint(object sender, RecvStsEndPtEventArgs e) { if(RecvStsEndPoint != null) { RecvStsEndPoint(this, e); } }
현재까지 작성한 Manager.cs 소스 코드입니다.
using System.Net; using WaferLineCommLib; namespace WaferLine_공장_시뮬레이션 { public class Manager { public event RecvStsEndPtEventHandler RecvStsEndPoint; private Manager() { } static Manager manager = new Manager(); static public Manager Singleton { get { return manager; } } public void StartServer(string ip,int port) { FactoryServer fs = new FactoryServer(ip, port); fs.RecvStsEndPoint += Fs_RecvStsEndPoint; fs.StartAsync(); } private void Fs_RecvStsEndPoint(object sender, RecvStsEndPtEventArgs e) { if(RecvStsEndPoint != null) { RecvStsEndPoint(this, e); } } } }
7. MainForm 추가 구현
MainForm의 Load 이벤트 핸들러에서 Manager 개체에 이벤트 핸들러를 등록합니다.
private void MainForm_Load(object sender, EventArgs e) { cbox_ip.DataSource = MyNetwork.Addresses; Manager manager = Manager.Singleton; manager.RecvStsEndPoint += Manager_RecvStsEndPoint; }
이벤트 핸들러에서는 수신한 정보로 ts_lb의 Text 속성을 설정합니다.
private void Manager_RecvStsEndPoint(object sender, RecvStsEndPtEventArgs e) { ts_lb.Text = string.Format("{0}:{1} 연결", e.IPAddress, e.Port); }
btn_set의 Click 이벤트 핸들러를 등록한 후 구현합시다.
입력한 끝점 정보로 서버를 가동합니다.
private void btn_set_Click(object sender, EventArgs e) { int port = int.Parse(tbox_port.Text); string ip = cbox_ip.Text; Manager manger = Manager.Singleton; manger.StartServer(ip, port); }
8. 중앙 관제의 CentralForm 추가 구현
중앙 관제의 CentralForm에 btn_set_fa의 Click 이벤트 핸들러를 등록합니다.
이벤트 핸들러에서는 입력한 끝점 정보로 FactoryClient 개체를 생성합니다.
FactoryClient fc; private void btn_set_fa_Click(object sender, EventArgs e) { string fip = tbox_fa_ip.Text; int fport = int.Parse(tbox_fa_port.Text); fc = new FactoryClient(fip, fport); }
btn_set_me의 Click 이벤트 핸들러도 등록합니다.
자신의 끝점 정보를 얻어와서 FactoryClient 개체를 통해 정보를 전송합니다.
private void btn_set_me_Click(object sender, EventArgs e) { string ip = tbox_me_ip.Text; int port = int.Parse(tbox_me_port.Text); fc.SendMyInfo(ip, port); }