6. 중앙 관제 6.4 연결 시 공장 라인 정보 전송 [Wafer 코팅 시뮬레이션]

안녕하세요. 언제나휴일입니다.

1. 해야 할 일

현재 WaferLine 공장과 중앙 관제 사이에 연결하는 것까지 구현하였습니다.

이번에는 연결하였을 때 공장의 Wafer 코팅 라인 정보를 중앙 관제에 전송하는 부분을 구현하기로 할게요.

이번 강의에서 전송할 정보는 현재 공장의 WaferLine과 코팅할 Wafer 개수입니다.

2. MsgType에 멤버 추가

이번에 추가할 메시지는 라인 추가와 Wafer 추가 메시지입니다.

(MSG_CF_ADDSI는 이전 강의에서 추가한 메시지입니다.)

namespace WaferLineCommLib
{
    public enum MsgType
    {
        MSG_CF_ADDSI,
        MSG_FC_ADDLN,
        MSG_FC_ADDWF
    }
}

3. 이벤트 인자 형식 및 대리자 정의

Wafer 추가에 관한 이벤트 형식과 대리자는 이미 WaferLineLib에 정의하였습니다.

이번에는 라인 추가에 관한 이벤트 형식과 대리자를 정의합시다.

WaferLineCommLib에 AddLineEventArgs.cs를 추가하세요.

이벤트 인자 형식에서 제공할 속성은 라인 번호입니다.

이벤트 인자 형식과 대리자를 정의하는 것은 기술적으로 어려운 것이 아니죠.

다만 설계 단계에서 필요한 이벤트가 무엇인지 판단할 수 있어야겠죠.

using System;

namespace WaferLineCommLib
{
    public delegate void AddLineEventHandler(object sender, AddLineEventArgs e);
    public class AddLineEventArgs:EventArgs
    {
        public int No { get; }
        public AddLineEventArgs(int no)
        {
            No = no;
        }
    }
}

4. ControlClient 클래스 추가

WaferLineCommLib에 ControlClient 클래스를 추가합니다.

ControlClient는 공장에서 WaferLine의 상태 변화를 중앙 관제에 전달하는 역할을 수행합니다.

생성자에서 중앙 관제의 IP 주소와 포트 정보를 입력받아 멤버 필드에 설정합니다.

namespace WaferLineCommLib
{
    public class ControlClient
    {
        IPAddress cip;
        int cport;
        public ControlClient(IPAddress cip,int cport)
        {
            this.cip = cip;
            this.cport = cport;
        }

이번 강의에서는 Wafer 추가와 Line 추가를 전송하는 부분을 구현할 거예요.

        public bool SendAddWafer(int no,int bwcnt)
        {
            return false;
        }
        public bool SendAddLine(int no)
        {
            return false;
        }

ControlClient에서 메시지를 전송할 때 메시지 타입과 정보는 차이가 있지만 서버에 연결해서 패킷을 전송하는 부분을 같습니다.

이 부분을 먼저 구현합시다.

소켓 생성 => 연결 => 메시지 전송 => 소켓 닫기 순으로 수행합니다.

        bool SendPacket(byte[] packet)
        {
            try
            {
                Socket sock = new Socket(AddressFamily.InterNetwork,
                    SocketType.Stream, ProtocolType.Tcp);
                IPEndPoint ep = new IPEndPoint(cip, cport);
                sock.Connect(ep);
                sock.Send(packet);
                sock.Close();
                return true;
            }
            catch
            {
                return false;
            }
        }

SendAddWafer 메서드를 구현합시다.

메시지를 소켓으로 전송할 때 byte 배열을 사용합니다.

여기에서는 128 바이트의 고정 길이로 전송할게요.

메시지를 byte 배열에 순차적으로 쓰기 및 읽기하기 쉽게 MemoryStream 개체를 사용합시다.

그리고 BinaryWriter를 이용하여 쉽게 MemoryStream에 기록하게요.

기록한 후에는 SendPacket 메서드를 호출하여 전송합니다.

        public bool SendAddWafer(int no,int bwcnt)
        {
            byte[] packet = new byte[128];
            MemoryStream ms = new MemoryStream(packet);
            BinaryWriter bw = new BinaryWriter(ms);
            bw.Write((int)MsgType.MSG_FC_ADDWF);
            bw.Write(no);
            bw.Write(bwcnt);
            bw.Close();
            ms.Close();
            return SendPacket(packet);
        }

SendAddLine 메서드도 전송할 메시지 타입과 정보만 차이가 있을 뿐입니다.

        public bool SendAddLine(int no)
        {
            byte[] packet = new byte[128];
            MemoryStream ms = new MemoryStream(packet);
            BinaryWriter bw = new BinaryWriter(ms);
            bw.Write((int)MsgType.MSG_FC_ADDLN);
            bw.Write(no);            
            bw.Close();
            ms.Close();
            return SendPacket(packet);
        }

현재까지 작성한 ControlClient.cs 소스 코드입니다.

using System.IO;
using System.Net;
using System.Net.Sockets;

namespace WaferLineCommLib
{
    public class ControlClient
    {
        IPAddress cip;
        int cport;
        public ControlClient(IPAddress cip,int cport)
        {
            this.cip = cip;
            this.cport = cport;
        }
        public bool SendAddWafer(int no,int bwcnt)
        {
            byte[] packet = new byte[128];
            MemoryStream ms = new MemoryStream(packet);
            BinaryWriter bw = new BinaryWriter(ms);
            bw.Write((int)MsgType.MSG_FC_ADDWF);
            bw.Write(no);
            bw.Write(bwcnt);
            bw.Close();
            ms.Close();
            return SendPacket(packet);
        }
        public bool SendAddLine(int no)
        {
            byte[] packet = new byte[128];
            MemoryStream ms = new MemoryStream(packet);
            BinaryWriter bw = new BinaryWriter(ms);
            bw.Write((int)MsgType.MSG_FC_ADDLN);
            bw.Write(no);            
            bw.Close();
            ms.Close();
            return SendPacket(packet);
        }
        bool SendPacket(byte[] packet)
        {
            try
            {
                Socket sock = new Socket(AddressFamily.InterNetwork,
                    SocketType.Stream, ProtocolType.Tcp);
                IPEndPoint ep = new IPEndPoint(cip, cport);
                sock.Connect(ep);
                sock.Send(packet);
                sock.Close();
                return true;
            }
            catch
            {
                return false;
            }
        }
    }
}

5. ControlServer 구현

중앙 관제에서 공장의 상태 변화를 수신하는 ControlServer를 구현합시다.

먼저 WaferLineCommLib 프로젝트에 ControlServer 클래스를 추가하세요.

생성자에서는 IP 주소와 포트 정보를 입력받아 멤버 필드를 설정할게요.

namespace WaferLineCommLib
{
    public class ControlServer
    {
        string ip;
        int port;
        public ControlServer(string ip,int port)
        {
            this.ip = ip;
            this.port = port;
        }

서버를 가동하는 메서드를 정의합시다.

TCP 서버의 절차는 소켓 생성=>네트워크 인터페이스와 결합=>백로그 큐 크기 결정=>연결 요청 대기 및 수락 Loop입니다.

연결 후에 클라이언트와 통신하는 부분은 DoIt 메서드를 만들어서 구현하기로 할게요.

        public void Start()
        {
            //소켓 생성
            Socket sock = new Socket(AddressFamily.InterNetwork,
                SocketType.Stream, ProtocolType.Tcp);
            //소켓을 네트워크 인터페이스와 결합
            IPAddress addr = IPAddress.Parse(ip);
            IPEndPoint ep = new IPEndPoint(addr, port);
            sock.Bind(ep);
            //백로그 큐 크기 설정
            sock.Listen(5);
            //클라이언트 연결 요청 대기 및 수락 Loop
            while(true)
            {
                Socket dosock = sock.Accept();
                DoIt(dosock);
            }
        }
        private void DoIt(Socket dosock)
        {
        }

그런데 ControlServer를 사용하는 곳에서 Start 메서드를 호출하면 Loop에서 Blocking상태에 빠집니다.

비동기로 Start를 수행할 수 있게 비동기 대리자를 사용할게요.

        delegate void StartDele();
        public void AsyncStart()
        {
            StartDele dele = Start;
            dele.BeginInvoke(null, null);
        }

이제 DoIt 메서드를 구현합시다.

먼저 byte배열에 메시지를 수신합니다.

수신함 메시지 종류를 먼저 확인하여 메시지 종류에 따라 적절한 메서드를 호출하는 구조로 구현합시다.

수신한 byte 배열의 내용을 읽어오는 부분은 MemoryStream과 BinaryReader를 사용할게요.

        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_FC_ADDLN: AddLineProc(br); break;
                case MsgType.MSG_FC_ADDWF: AddWaferProc(br); break;
            }
            br.Close();
            ms.Close();
            dosock.Close();
        }

        private void AddWaferProc(BinaryReader br)
        {
        }

        private void AddLineProc(BinaryReader br)
        {         
        }

AddWaferProc과 AddLineProc에서는 정보를 수신하여 이벤트를 전송합니다.

이를 위해 이벤트 멤버도 추가합니다.

        public event AddLineEventHandler AddedLine;
        public event AddWaferEventHandler AddedWafer;

        private void AddWaferProc(BinaryReader br)
        {
            int no = br.ReadInt32();
            int bwcnt = br.ReadInt32();
            if(AddedWafer != null)
            {
                AddedWafer(this, new AddWaferEventArgs(no, bwcnt));
            }
        }

        private void AddLineProc(BinaryReader br)
        {
            int no = br.ReadInt32();
            if(AddedLine != null)
            {
                AddedLine(this, new AddLineEventArgs(no));
            }
        }

현재까지 작성한 ControlServer.cs 소스 코드입니다.

using System.IO;
using System.Net;
using System.Net.Sockets;
using WaferLineLib;

namespace WaferLineCommLib
{
    public class ControlServer
    {
        public event AddLineEventHandler AddedLine;
        public event AddWaferEventHandler AddedWafer;

        string ip;
        int port;
        public ControlServer(string ip,int port)
        {
            this.ip = ip;
            this.port = port;
        }

        delegate void StartDele();
        public void AsyncStart()
        {
            StartDele dele = Start;
            dele.BeginInvoke(null, null);
        }
        public void Start()
        {
            //소켓 생성
            Socket sock = new Socket(AddressFamily.InterNetwork,
                SocketType.Stream, ProtocolType.Tcp);
            //소켓을 네트워크 인터페이스와 결합
            IPAddress addr = IPAddress.Parse(ip);
            IPEndPoint ep = new IPEndPoint(addr, port);
            sock.Bind(ep);
            //백로그 큐 크기 설정
            sock.Listen(5);
            //클라이언트 연결 요청 대기 및 수락 Loop
            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_FC_ADDLN: AddLineProc(br); break;
                case MsgType.MSG_FC_ADDWF: AddWaferProc(br); break;         
            }
            br.Close();
            ms.Close();
            dosock.Close();
        }

        private void AddWaferProc(BinaryReader br)
        {
            int no = br.ReadInt32();
            int bwcnt = br.ReadInt32();
            if(AddedWafer != null)
            {
                AddedWafer(this, new AddWaferEventArgs(no, bwcnt));
            }
        }

        private void AddLineProc(BinaryReader br)
        {
            int no = br.ReadInt32();
            if(AddedLine != null)
            {
                AddedLine(this, new AddLineEventArgs(no));
            }
        }
    }
}

6. WaferLine 공장 MainForm 구현

중앙 관제 연결 정보를 수신한 이벤트 핸들러를 수정합시다.

소켓 통신은 비동기로 수행하고 있어서 폼을 소유하지 않은 스레드에서 이벤트 핸들러가 수행할 수 있어요.

Windows Forms에서 폼과 컨트롤을 생성한 스레드가 아닌 스레드에서 폼(혹은 컨트롤)을 사용할 때 크로스 스레드 문제가 발생합니다.

이러한 문제는 디버그 모드에서 발생하며 개발자에게 이에 관한 적절한 처리할 것을 알려줍니다.

크로스 스레드 문제를 해결하는 방법 중에 Windows Forms에서 제시하는 방법은 다음과 같습니다.

현재 수행하는 스레드가 폼(혹은 컨트롤)을 생성한 스레드인지 판별하는 InvokeRequired 멤버 속성이 있습니다.

만약 참이면 다른 스레드이며 크로스 스레드 문제가 발생할 수 있음을 의미합니다.

이 때 Invoke 메서드에 대리자와 인자를 전달하면 .NET Framework에 의해 폼(혹은 컨트롤)을 생성한 스레드가 수행할 수 있게 해 줍니다.

        private void Manager_RecvStsEndPoint(object sender, RecvStsEndPtEventArgs e)
        {
            if (this.InvokeRequired)
            {
                RecvStsEndPtEventHandler dele = Manager_RecvStsEndPoint;
                this.Invoke(dele, new object[] { sender, e });
            }
            else
            {
                ts_lb.Text = string.Format("{0}:{1} 연결", e.IPAddress, e.Port);
            }
        }

그리고 Manager를 통해 현재 생산 라인 정보와 생산 라인의 코팅할 Wafer 개수 정보를 전송합니다.

        private void Manager_RecvStsEndPoint(object sender, RecvStsEndPtEventArgs e)
        {
            if (this.InvokeRequired)
            {
                RecvStsEndPtEventHandler dele = Manager_RecvStsEndPoint;
                this.Invoke(dele, new object[] { sender, e });
            }
            else
            {
                Manager manager = Manager.Singleton;
                foreach(ListViewItem lvi in lv_line.Items)
                {
                    int no = int.Parse(lvi.SubItems[0].Text);
                    manager.AddLine(no);
                    manager.AddWafer(no, int.Parse(lvi.SubItems[1].Text));
                }
                ts_lb.Text = string.Format("{0}:{1} 연결", e.IPAddress, e.Port);
            }
        }

7.WaferLine 공장

먼저 중앙 관제 정보를 수신하였을 때 수행하는 Fs_RecvStsEndPoint 메서드를 수정합니다.

중앙 관제 끝점 정보를 이용하여 ControlClient 개체를 생성하는 코드를 추가합니다.

        ControlClient cc;
        private void Fs_RecvStsEndPoint(object sender, RecvStsEndPtEventArgs e)
        {
            IPAddress cip = e.IPAddress;
            int cport = e.Port;
            cc = new ControlClient(cip, cport);
            if(RecvStsEndPoint != null)
            {
                RecvStsEndPoint(this, e);
            }
        }

AddLine 메서드에서는 cc가 null이 아닌지 확인하여 SendAddLine 메서드를 호출합니다.

        public void AddLine(int no)
        {
            if(cc == null)
            {
                return;
            }
            if(cc.SendAddLine(no) == false)
            {
                cc = null;
            }
        }

AddWafer 메서드도 같은 원리입니다.

        public void AddWafer(int no, int bwcnt)
        {
            if (cc == null)
            {
                return;
            }
            if (cc.SendAddWafer(no,bwcnt) == false)
            {
                cc = null;
            }
        }

현재 Manger.cs 소스 코드입니다.

using System.Net;
using WaferLineCommLib;
using WaferLineLib;

namespace WaferLine_공장_시뮬레이션
{
    public class Manager
    {
        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();
        }


        ControlClient cc;
        private void Fs_RecvStsEndPoint(object sender, RecvStsEndPtEventArgs e)
        {
            IPAddress cip = e.IPAddress;
            int cport = e.Port;
            cc = new ControlClient(cip, cport);
            if(RecvStsEndPoint != null)
            {
                RecvStsEndPoint(this, e);
            }
        }

        public void AddLine(int no)
        {
            if(cc == null)
            {
                return;
            }
            if(cc.SendAddLine(no) == false)
            {
                cc = null;
            }
        }

        public void AddWafer(int no, int bwcnt)
        {
            if (cc == null)
            {
                return;
            }
            if (cc.SendAddWafer(no,bwcnt) == false)
            {
                cc = null;
            }
        }
    }
}

8. 중앙 관제 구현

이제 중앙 관제의 CentralForm을 추가 구현합시다.

btn_set의 Click 이벤트 핸들러에서 ControlServer를 생성합니다.

그리고 Line 추가와 Wafer 추가에 관한 이벤트 핸들러를 등록합니다.

그리고 비동기로 서버를 가동합니다.

        private void btn_set_me_Click(object sender, EventArgs e)
        {
            string ip = tbox_me_ip.Text;
            int port = int.Parse(tbox_me_port.Text);
            ControlServer cs = new ControlServer(ip, port);
            cs.AddedLine += Cs_AddedLine;
            cs.AddedWafer += Cs_AddedWafer;
            cs.AsyncStart();
            fc.SendMyInfo(ip, port);
        }

라인 추가 이벤트 핸들러도 크로스 스레드 문제를 고려해야 합니다.

라인 추가하면 ListViewItem 개체를 생성하여 lv_line에 아이템 추가합니다.

ListViewItem은 라인 번호를 Key로 하는 사전 개체에 보관해 놓기로 할게요.

그리고 WaferLine 개체를 생성하여 Tag 속성에 설정할게요.

        Dictionary<int, ListViewItem> lvi_dic = new Dictionary<int, ListViewItem>();
        private void Cs_AddedLine(object sender, AddLineEventArgs e)
        {
            if(this.InvokeRequired)
            {
                AddLineEventHandler dele = Cs_AddedLine;
                this.Invoke(dele, new object[] { sender, e });
            }
            else
            {
                string[] sitems = new string[] { e.No.ToString(), "0" };
                ListViewItem lvi = new ListViewItem(sitems);
                lv_line.Items.Add(lvi);
                lvi_dic[e.No] = lvi;
                WaferLine wl = new WaferLine(e.No);
                lvi.Tag = wl;
            }
        }

Wafer 추가 이벤트 핸들러도 크로스 스레드 문제를 고려합니다.

라인 번호에 해당하는 ListViewItem을 사전 개체에서 참조합니다.

그리고 코팅 전 Wafer 개수를 설정하고 WaferLine에 Wafer를 추가 요청합니다.

여기에 있는 WaferLine은 공장의 WaferLine과 쌍둥이 개체로 생각할 수 있습니다.

        private void Cs_AddedWafer(object sender, AddWaferEventArgs e)
        {
            if(this.InvokeRequired)
            {
                AddWaferEventHandler dele = Cs_AddedWafer;
                this.Invoke(dele, new object[] { sender, e });
            }
            else
            {
                ListViewItem lvi = lvi_dic[e.No];
                lvi.SubItems[1].Text = e.BWCnt.ToString();
                WaferLine wl = lvi.Tag as WaferLine;
                int gap = e.BWCnt - wl.BWCnt;
                wl.InWafer(gap);
            }
        }