본문 바로가기

C#, C++

c#을 이용한 TCP/IP 서버-클라이언트 소켓(Socket)통신

2개의 컴퓨터에서 데이터를 주고받을때 가장 자주 사용되는 방식이 소켓을 이용한 방식이다.

 

C#에서는 다른 플랫폼과 마찬가지로 소켓을 사용하기 위한 간편한 라이브러리를 제공한다.

 

우리는 서버쪽 s_socket과 클라이언트쪽 c_socket을 만들 것이다.

 

서버는 계속 켜져있는 컴퓨터라고 생각하면 쉽다. 계속 켜져있는 채로 클라이언트 PC의 접속을 대기(Listen)한다.

 

클라이언트는 항상 켜져있을 필요는 없다. 필요할 때 서버쪽으로 Connect신호를 보내면, 서버쪽에서

 

이를 받아들이고 클라이언트와의 연결을 성립시킨다.(Accept) 연결이 성립되었다면 우리는 본격적으로 데이터를 주고 

 

받을 수 있다.

 

먼저, 서버쪽 소켓의 Start() 와 Close()를 만들어보자.

 

Start()는 Server 메인소켓을 만들고 연결을 받아들이기 시작하는 메소드 이다.

 

Close()는 만들어진 메인소켓을 해제, Dispose하고 연결되어있던 모든 Connectedclients를 해제하는 작업이다.

 

public class Server
{
	Socket mainsock;
	List<Socket> connectedClients = new List<Socket>();
    int m_port = 5000;
    
	public void Start()
    {
    	try
        {
        	//소켓을 생성한다
        	mainsock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.TCP);
            //목적지를 정해준다
            IPEndPoint serverEP = new IPEndPoint(IPAdress.Any, m_port);
            //목적지와 묶어준다
            mainSock.Bind(serverEP);
            //받아들이기 시작한다 (최대 10개의 Client까지)
            mainSock.Listen(10);
            //연결시도가 감지되면 AcceptCallBack으로 이동하게 설정
            mainSock.BeginAccept(AcceptCallBack, null);
        }
        catch(Exception e)
        {
        }
    }
    
    public void Close()
    {
    	//메인소켓 해제
    	if(mainSock !=null)
        {
        	mainSock.Close();
            mainSock.Dispose();
        }
        //서버->어딘가를 향하는 소켓은 여러개가 있고 목적지가 모두 다릅니다.
        //이 소켓들은 connectedClients에 저장되어 있으므로 모두 해제해줍니다.
        foreach(Socket socket in connectedClients)
        {
        	socket.Close();
            socket.Dispose();
        }
        connectedClients.Clear();
    }

 

이제 다음으로 할 일은 AcceptedCallBack을 만드는 일이다. 클라이언트에서 연결을 시도하면, 무슨 행동을 해야할지

 

정하는 부분이다. 

 

public class AsyncObject
{
    public byte[] Buffer;
    public Socket WorkingSocket;
    public readonly int BufferSize;
    public AsyncObject(int bufferSize)
    {
        BufferSize = bufferSize;
        Buffer = new byte[(long)BufferSize];
    }

    public void ClearBuffer()
    {
        Array.Clear(Buffer, 0, BufferSize);
    }
}

void AcceptCallback(IAsyncResult ar)
{
    try
    {
    	Socket client = mainSock.EndAccept(ar);
        AsyncObject obj = new AsyncObject(1920 * 1080 * 3);
        obj.WorkingSocket = client;
        connectedClients.Add(client);
        client.BeginReceive(obj.Buffer, 0, 1920 * 1080 * 3, 0, DataReceived, obj);
        
        mainSock.BeginAccept(AcceptCallback, null);
    }
    catch(Exception e)
    {
    }
}

받아들인 소켓의 정보들은 IASyncResult ar에 담겨있다.

mainSock.EndAccept(ar);을 호출하면 메인소켓이 더이상 Client를 받아들이지 않고, 현재 받아들인 소켓의 정보를

client라는 소켓에 저장하게 된다.

이제 client와 연결이 완료되었고, client가 서버에 데이터를 보낼때마다 DataReceived라는 콜백함수로 넘어가서 작업을

진행하게 된다. DataReceived라는 함수는 본인이 자유롭게 짜면 된다.

간단하게 틀만 만들어보겠다. 

 

void DataReceived(IAsyncResult ar)
{
	AsyncObject obj = (AsyncObject)ar.AsyncState;
    
    int received = obj.WorkingSocket.EndReceive(ar);
    
    byte[] buffer = new byte[received];
    
    Array.Copy(obj.Buffer, 0 , buffer, 0, received);
}

 

obj.Buffer는 1920 * 1080 * 3으로 지정해준 byte[] 배열이다.

이곳에 클라이언트에서 보낸 데이터가 입력되는데, byte[1000]의 크기로 보냈다면, 1000개만 꺼내와서

buffer라는 배열에 따로 저장했다. 이 buffer를 사용해서 무슨 작업을 할지는 자유롭게 결정하면 된다.

 

Server클래스의 전체적인 코드는 아래와 같다.

 

public class Server
{
    Socket mainSock;
    List<Socket> connectedClients = new List<Socket>();
    int m_port = 5000;
    public void Start()
    {
        try
        {
            mainSock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            IPEndPoint serverEP = new IPEndPoint(IPAddress.Any, m_port);
            mainSock.Bind(serverEP);
            mainSock.Listen(10);
            mainSock.BeginAccept(AcceptCallback, null);
        }
        catch (Exception e)
        {
        }
    }

    public void Close()
    {
        if (mainSock != null)
        {
            mainSock.Close();
            mainSock.Dispose();
        }

        foreach (Socket socket in connectedClients)
        {
            socket.Close();
            socket.Dispose();
        }
        connectedClients.Clear();

        //mainSock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.IP);
    }

    public class AsyncObject
    {
        public byte[] Buffer;
        public Socket WorkingSocket;
        public readonly int BufferSize;
        public AsyncObject(int bufferSize)
        {
            BufferSize = bufferSize;
            Buffer = new byte[(long)BufferSize];
        }

        public void ClearBuffer()
        {
            Array.Clear(Buffer, 0, BufferSize);
        }
    }

    void AcceptCallback(IAsyncResult ar)
    {
        try
        {
            Socket client = mainSock.EndAccept(ar);
            AsyncObject obj = new AsyncObject(1920 * 1080 * 3);
            obj.WorkingSocket = client;
            connectedClients.Add(client);
            client.BeginReceive(obj.Buffer, 0, 1920 * 1080 * 3, 0, DataReceived, obj);

            mainSock.BeginAccept(AcceptCallback, null);
        }
        catch(Exception e)
        { }
    }

    void DataReceived(IAsyncResult ar)
    {
        AsyncObject obj = (AsyncObject)ar.AsyncState;

        int received = obj.WorkingSocket.EndReceive(ar);

        byte[] buffer = new byte[received];

        Array.Copy(obj.Buffer, 0, buffer, 0, received);
    }


}

 

이제 클라이언트쪽 클래스를 만들어보자.

 

클라이언트도 서버와 비슷하지만, 약간의 차이가 있다.

 

일단 서버에서 Start() -> 연결 받아들이기 시작   

 

이라면

 

클라이언트는 이 Start()가 Connect() -> 서버로 연결시도   

 

로 바뀐다.

 

public class Client
{
    Socket mainSock;
    int m_port = 5000;
    public void Connect()
    {
        mainSock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        IPAddress serverAddr = IPAddress.Parse("10.0.0.10");
        IPEndPoint clientEP = new IPEndPoint(serverAddr, m_port);
        mainSock.BeginConnect(clientEP, new AsyncCallback(ConnectCallback), mainSock);
    }
    public void Close()
    {
        if (mainSock != null)
        {
            mainSock.Close();
            mainSock.Dispose();
    }

서버와는 다르게, 클라이언트는 목적지가 반드시 서버쪽 IP여야 한다. (서버쪽은 IPEndPoint의 목적지가 IPAddress.Any였지만, 클라이언트는 목적지를 지정해줘야 한다.)

정상적으로 받아들여진다면, ConnectCallback으로 접근한다.

 

public class AsyncObject
{
    public byte[] Buffer;
    public Socket WorkingSocket;
    public readonly int BufferSize;
    public AsyncObject(int bufferSize)
    {
        BufferSize = bufferSize;
        Buffer = new byte[(long)BufferSize];
    }

    public void ClearBuffer()
    {
        Array.Clear(Buffer, 0, BufferSize);
    }
}
void ConnectCallback(IAsyncResult ar)
{
        try
        {
            Socket client = (Socket)ar.AsyncState;
            client.EndConnect(ar);
            AsyncObject obj = new AsyncObject(4096);
            obj.WorkingSocket = mainSock;
            mainSock.BeginReceive(obj.Buffer, 0, obj.BufferSize, 0, DataReceived, obj);
        }
        catch (Exception e)
        {
        }
}

 

서버와 마찬가지로, Connect가 완료된다면, 계속해서 데이터를 주고받을 것이다. 이때마다 DataReceived로 이동해서

서버에서 보낸 데이터를 처리한다. 역시 마찬가지로, DataReceived를 어떻게 구성할지는 만드는 사람의 자유이다.

 

void DataReceived(IAsyncResult ar)
{
    AsyncObject obj = (AsyncObject)ar.AsyncState;

    int received = obj.WorkingSocket.EndReceive(ar);

    byte[] buffer = new byte[received];

    Array.Copy(obj.Buffer, 0, buffer, 0, received);
}

 

그리고 가장 중요한걸 깜빡했는데, 당연히 데이터를 보내줄 Send함수를 서버쪽과 클라이언트쪽에 추가해주자.

public void Send(byte[] msg)
{
    mainSock.Send(msg);
}

 

전체적인 코드는 아래와 같다.

 

using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;

namespace study
{
    class Program
    {
        public class Server
        {
            Socket mainSock;
            List<Socket> connectedClients = new List<Socket>();
            int m_port = 5000;
            public void Start()
            {
                try
                {
                    mainSock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
                    IPEndPoint serverEP = new IPEndPoint(IPAddress.Any, m_port);
                    mainSock.Bind(serverEP);
                    mainSock.Listen(10);
                    mainSock.BeginAccept(AcceptCallback, null);
                }
                catch (Exception e)
                {
                }
            }

            public void Close()
            {
                if (mainSock != null)
                {
                    mainSock.Close();
                    mainSock.Dispose();
                }

                foreach (Socket socket in connectedClients)
                {
                    socket.Close();
                    socket.Dispose();
                }
                connectedClients.Clear();

                //mainSock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.IP);
            }

            public class AsyncObject
            {
                public byte[] Buffer;
                public Socket WorkingSocket;
                public readonly int BufferSize;
                public AsyncObject(int bufferSize)
                {
                    BufferSize = bufferSize;
                    Buffer = new byte[(long)BufferSize];
                }

                public void ClearBuffer()
                {
                    Array.Clear(Buffer, 0, BufferSize);
                }
            }

            void AcceptCallback(IAsyncResult ar)
            {
                try
                {
                    Socket client = mainSock.EndAccept(ar);
                    AsyncObject obj = new AsyncObject(1920 * 1080 * 3);
                    obj.WorkingSocket = client;
                    connectedClients.Add(client);
                    client.BeginReceive(obj.Buffer, 0, 1920 * 1080 * 3, 0, DataReceived, obj);

                    mainSock.BeginAccept(AcceptCallback, null);
                }
                catch(Exception e)
                { }
            }

            void DataReceived(IAsyncResult ar)
            {
                AsyncObject obj = (AsyncObject)ar.AsyncState;

                int received = obj.WorkingSocket.EndReceive(ar);

                byte[] buffer = new byte[received];

                Array.Copy(obj.Buffer, 0, buffer, 0, received);
            }

            public void Send(byte[] msg)
            {
                mainSock.Send(msg);
            }

        }

        public class Client
        {
            Socket mainSock;
            int m_port = 5000;
            public void Connect()
            {
                mainSock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
                IPAddress serverAddr = IPAddress.Parse("10.0.0.10");
                IPEndPoint clientEP = new IPEndPoint(serverAddr, m_port);
                mainSock.BeginConnect(clientEP, new AsyncCallback(ConnectCallback), mainSock);
            }
            public void Close()
            {
                if (mainSock != null)
                {
                    mainSock.Close();
                    mainSock.Dispose();
                }
            }
            public class AsyncObject
            {
                public byte[] Buffer;
                public Socket WorkingSocket;
                public readonly int BufferSize;
                public AsyncObject(int bufferSize)
                {
                    BufferSize = bufferSize;
                    Buffer = new byte[(long)BufferSize];
                }

                public void ClearBuffer()
                {
                    Array.Clear(Buffer, 0, BufferSize);
                }
            }
            void ConnectCallback(IAsyncResult ar)
            {
                    try
                    {
                        Socket client = (Socket)ar.AsyncState;
                        client.EndConnect(ar);
                        AsyncObject obj = new AsyncObject(4096);
                        obj.WorkingSocket = mainSock;
                        mainSock.BeginReceive(obj.Buffer, 0, obj.BufferSize, 0, DataReceived, obj);
                    }
                    catch (Exception e)
                    {
                    }
            }
            void DataReceived(IAsyncResult ar)
            {
                AsyncObject obj = (AsyncObject)ar.AsyncState;

                int received = obj.WorkingSocket.EndReceive(ar);

                byte[] buffer = new byte[received];

                Array.Copy(obj.Buffer, 0, buffer, 0, received);
            }
            public void Send(byte[] msg)
            {
                mainSock.Send(msg);
            }
        }

        static void Main(string[] args)
        {
            byte[] msg = new byte[8];

            //서버쪽 대기 시작하는코드
            //Server server = new Server();
            //server.Start();


            //클라이언트쪽 연결시도하는 코드
            //Client client = new Client();
            //client.Connect();

            //연결이 완료됐다면 전송하는 코드
            //server.Send(msg);
            //client.Send(msg);

        }
    }

}

 

구조는 참고만하고, 당연히 server와 client는 서로 다른 pc에 존재해야 한다.

server.Start()나 server.send(msg)같은 경우는 서버쪽에서 사용할 pc에서 사용하고

client.Connect()나 client.Send(msg)는 클라이언트쪽 pc에서 사용하자. (같은 코드에 위치해서는 안된다는 의미)

 

그리고, 실제로는 연결이 끊어졌을때 예외처리하는 코드나 예외처리 내부적으로 끊어지고나면 다시 Accept를 하거나

반복해서 Connect신호를 보내게 하거나 다양하게 고려해야할 상황들이 존재한다.

이는 자신의 목적에 맞게끔 적절하게 설정 해 주면 좋을 것 같다.