유튜브에 c# tcp소켓통신 채팅 관련 영상이 있었다 그 영상의 코드를 공부해봤다.

 

먼저 ServerClinet 

public class ServerClient
{
    public TcpClient tcp;
    public string clientName;

    public ServerClient(TcpClient clientSocket)
    {
        clientName = "Guest";
        tcp = clientSocket;
    }
}

TcpClient : TCP 네트워크 서비스에 대한 클라이언트 연결을 제공한다.

 

ServerCreate

    public void ServerCreate()
    {
        _listClients = new List<ServerClient>();
        _listDisconnect = new List<ServerClient>();

        try
        {
            int port = ins_PortInput.text == "" ? 9999 : int.Parse(ins_PortInput.text);
            _server = new TcpListener(IPAddress.Any, port);
            _server.Start(); //바인드 처리.

            StartListening();
            _bserverStarted = true;
            Chat.instance.ShowMessage($"서버가 {port}에서 시작되었습니다.");
        }
        catch (Exception e)
        {
            Chat.instance.ShowMessage($"Socket error: {e.Message}");
        }
    }

port로 포트번호를 만들어준다. (현재 사용하지 않는 포트번호로 지정)

IPAddress.Any : 모든 클라이언트에서 오는 요청을 받겠다는 의미

TcpListener.Start : 들어오는 연결 요청의 수신을 시작. (바인드)

 

StartListening

    private void StartListening()
    {
        _server.BeginAcceptTcpClient(AcceptTcpClient, _server);
    }

BeginAcceptTcpClient(AsyncCallback? callback, object? state) : 들어오는 연결 시도를 받아들이는 비동기 작업을 시작한다.

AsyncCallback은 작업이 완료됐을때 호출한다.

state는 연결을 받아들이는 작업자에 대한 정보가 들어 있는 정의 개체이다. 작업이 완료되면 callback대리자에게 전달한다.

 

AcceptTcpClient

    private void AcceptTcpClient(IAsyncResult ar)
    {
        TcpListener listener = (TcpListener)ar.AsyncState;
        _listClients.Add(new ServerClient(listener.EndAcceptTcpClient(ar)));
        StartListening();

        // 메시지를 연결된 모두에게 보냄
        Broadcast("%NAME", new List<ServerClient>() { _listClients[_listClients.Count - 1] });
    }

매개변수

IAsyncResult : 비동기 작업의 상태를 나타낸다.

IAsyncResult 

TcpListener.EndAcceptTcpClient() : 들어오는 연결 시도를 비동기적으로 받아들이고 원격 호스트 통신을 처리할 새로운 TcpClient을 만든다. (리턴 TcpListener)

 

AcceptTcpClient이 실행 된후에 다시 StartListening()를 실행시켜 새로운 Client를 받을 수 있도록 처리한다.

 

Update()

	priavate void Update()
    {
        if (!_bserverStarted)
        { 
            return;
        }

        foreach (ServerClient c in _listClients)
        {
            // 클라이언트가 여전히 연결되있나?
            if (!IsConnected(c.tcp))
            {
                c.tcp.Close();
                _listDisconnect.Add(c);
                continue;
            }
            // 클라이언트로부터 체크 메시지를 받는다
            else
            {
                NetworkStream s = c.tcp.GetStream();
                if (s.DataAvailable)
                {
                    string data = new StreamReader(s, true).ReadLine();
                    if (data != null)
                        OnIncomingData(c, data);
                }
            }
        }

        for (int i = 0; i < _listDisconnect.Count - 1; i++)
        {
            Broadcast($"{_listDisconnect[i].clientName} 연결이 끊어졌습니다", _listClients);

            _listClients.Remove(_listDisconnect[i]);
            _listDisconnect.RemoveAt(i);
        }
    }

Update에서 클라이언트들의 접속여부와 채팅 내용을 체크하도록 한다.

 

IsConnected

   bool IsConnected(TcpClient c)
    {
        try
        {
            if (c != null && c.Client != null && c.Client.Connected)
            {
                if (c.Client.Poll(0, SelectMode.SelectRead))
                    return !(c.Client.Receive(new byte[1], SocketFlags.Peek) == 0);

                return true;
            }
            else
                return false;
        }
        catch
        {
            return false;
        }
    }

 

if(c.Client.Poll(0, SelectMode.SelectRead))

TcpClient.Poll(int microseconds, SelectMode mode); : 메소드는 간단히 말하자면 하려는 행동이 완료할 수 있는 상태면 true를 리턴한다. 

매개변수

microseconds : 응답을 기다리는 시간

SelectMode : 오류상태모드, 읽기 상태 모드, 쓰기 상태 모드를 선택한다.

=> 데이터를 읽을 수 있다면 true를 반환한다.

 

!(c.Client.Receive(new byte[1], SocketFlags.Peek) == 0);

socket.Receive(Byte [], Int32, Int32, SocketFlags, SocketError)

매개변수

byte[] 수신된 데이터에 대한 스토리지 위치인 Byte형식의 배열

SoketFlags.Peek : 소켓 전송 및 수신 동작을 지정(Peek:들어오는 메시지를 미리 본다)

=> 1바이트를 보내고 실제 수신된 바이트를 확인하여 연결여부를 확인한다.

 

다시 위쪽에 else부분을 살펴보면,

            else
            {
                NetworkStream s = c.tcp.GetStream();
                if (s.DataAvailable)
                {
                    string data = new StreamReader(s, true).ReadLine();
                    if (data != null)
                        OnIncomingData(c, data);
                }
            }

TcpClient.GetStream() : 데이터를 보내고 받는 데 사용되는 NetworkStream을 반환한다.

NetworkStream.DataAvailable() : 데이터를 읽을 수 있는지 여부를 나타내는 값을 가져온다.( 읽을 수 있으면 true, 그렇지 않으면 false)

 

이제 NetworkStream.DataAvailable() 까지 성공했다면 밑에 함수를 통해

    void OnIncomingData(ServerClient c, string data)
    {
        if (data.Contains("&NAME"))
        {
            c.clientName = data.Split('|')[1];
            Broadcast($"{c.clientName}이 연결되었습니다", _listClients);
            return;
        }

        Broadcast($"{c.clientName} : {data}", _listClients);
    }

 

Broadcast()

    void Broadcast(string data, List<ServerClient> cl)
    {
        foreach (var c in cl)
        {
            try
            {
                StreamWriter writer = new StreamWriter(c.tcp.GetStream());
                writer.WriteLine(data);
                writer.Flush();
            }
            catch (Exception e)
            {
                Chat.instance.ShowMessage($"쓰기 에러 : {e.Message}를 클라이언트에게 {c.clientName}");
            }
        }
    }

리스트로 받은 모두 ServerClient에 메세지를 전달하는 과정이다.

StreamWriter.Flush : 현재writer의 모든 버퍼를 지우면 버퍼링된 모든 데이터가 내부 스트림에 쓰여진다.

 

참고

https://www.youtube.com/watch?v=y3FU6d_BpjI 

https://docs.microsoft.com/ko-kr/dotnet/api/system.net.sockets

+ Recent posts