UNITY/네트워크

[네트워크] TCP/IP - 동기형 방식의 연결 (1)TCP 동기형 서버

램플릿 2025. 5. 30. 22:29

  TCPServerSync.cs (서버)   

  1.TcpListener를 사용하여 클라이언트 연결을 수신합니다.
  2.클라이언트로부터 메시지를 받고, 동일한 메시지를 다시 보냅니다.
  3.별도의 스레드에서 실행됩니다. (메인 스레드 블로킹을 완전히 피할 수는 없지만, 최소화합니다.)

 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;

public class TcpServerSync : MonoBehaviour //Unity 컴포넌트로 동작하기 위해 MonoBehavior를 상속받음
{
    [SerializeField] private int port = 8888;
    private TcpListener tcpListener;    
    private Thread serverThread;    //연결 수신을 처리하기 위한 별도의 스레드. 
    private TcpClient connectedClient;  


    // Start is called before the first frame update
    void Start()
    {
        serverThread = new Thread(RunServer); //RunServer메소드를 실행하는 serverThread를 생성. 메인 스레드(Unity의 주 스레드)가 멈추는 것을 최소화하기 위해 별도의 스레드에서 처리.
        serverThread.IsBackground = true;   //백그라운드 스레드로 설정하면 메인 스레드가 종료될 때 자동으로 함께 종료된다.
        serverThread.Start();   //serverThread를 시작.
    }

    private void RunServer()
    {
        try
        {
            tcpListener = new TcpListener(IPAddress.Any, port); //TCP 연결을 수신하는 리스너 객체를 생성
            tcpListener.Start();    // 클라이언트 연결 수신을 시작.
            Debug.Log("Server Start!:"+port);

            while(true) //동기 서버이므로 while 루프를 돌며 항상 대기하는 상태를 유지한다. 
            {   
                //클라이언트 연결 요청이 올 때까지 블로킹 발생(대기). 연결을 수락한 후 연결된 TcpClient 객체를 반환.
                connectedClient = tcpListener.AcceptTcpClient();    //tcpListener.Stop() 호출로 서버 소켓이 강제로 닫혔다면 루프를 중단하고 catch-finally로 가서 자원을 정리한다.
                Debug.Log("Client connected");

                HandleClient(connectedClient);
            }
        }
        catch (Exception e)
        {
            Debug.LogError("Server Err"+e.Message);
        }
        finally
        {
            if(tcpListener != null)
            {
                tcpListener.Stop();
            }
        }
    }
    
    
    private void HandleClient(TcpClient client)
    {
       //using 에서 선언된 NetworkStream 객체 stream은, 블록이 끝날 때 자동으로 stream.Dispose()가 호출되어 자원을 정리한다.
      using(NetworkStream stream = client.GetStream())
      {
         byte[] buffer = new byte[1024];  //한 번의 통신에서 가져올 수 있는 데이터의 크기 (1KB)
         int bytesRead;

         while(true)
         {
            try
            {
                //.Read() : buffer.Length만큼 데이터를 읽어서 배열(buffer)에 인덱스 0번부터 채워 넣는다. 그리고 실제로 읽힌 바이트 수(읽은 데이터의 길이)를 반환
                bytesRead = stream.Read(buffer, 0, buffer.Length);

                //상대가 정상적으로 소켓을 닫은 상태. 더 이상 받을 데이터가 없음(연결 종료 신호)
                if(bytesRead ==0)
                {
                    Debug.Log("Client Disconnected");
                    break;
                }
                
                string message = Encoding.UTF8.GetString(buffer, 0, bytesRead); //바이트 배열 buffer의 데이터를 문자열로 변환
                Debug.Log("Received :" + message);

                stream.Write(buffer, 0, bytesRead);  //server to client (echo)
            }
            catch(Exception e)
            {
                if (e is SocketException || e is ObjectDisposedException)
                {
                    Debug.Log("Client Disconnected");
                }
                else
                {
                    Debug.LogError("Client conn error");
                }
                break; //예외가 발생하였으므로 while 루프를 빠져나간다.
            }
         }
      }
     client.Close();
    }

    private void OnApplicationQuit() 
    {
        /*
        if(serverThread != null && serverThread.IsAlive)
        {
            serverThread.Abort(); //스레드 강제종료
        }   
        */

        if(tcpListener != null)
        {
            tcpListener.Stop();	//블로킹 해제를 위해 내부적으로 소켓을 닫고 SocketException 을 발생시켜 루프에서 빠져나옴
        }

        if(connectedClient != null)
        {
            connectedClient.Close();	// 클라이언트 연결을 명시적으로 종료
        }
    }

}

 


 

RunServer()

 

  tcpListener = new TcpListener(IPAddress.Any, port)   

  • new TcpListener(...) : TCP 연결을 수신할 수 있는 리스너(서버 소켓)를 생성
  • IPAddress.Any : 이 서버가 가진 모든 IP 주소(=모든 NIC, 네트워크 카드)로의 연결 수신을 허용하겠다는 뜻

보안상 서버가 특정 네트워크 인터페이스에서만 연결을 수락하게 만들고 싶다면 리스너에서 의도적으로 tcpListener = new TcpListener(IPAddress.Parse("192.168.0.10"), port) 처럼 명시해 수신을 제한할 수 있다.

IPAddress.Any 모든 IP에서 들어오는 연결 수락 (0.0.0.0)
IPAddress.Parse("192.168.0.10") 오직 이 IP에서 들어오는 연결만 수락 (예: 내부 네트워크만)
IPAddress.Loopback or 127.0.0.1 자기 자신만 접속 가능 (외부 접속 불가)

 

또는, 모든 연결을 받은 후  client.Client.RemoteEndPoint 등으로 허용된 클라이언트 IP인지 검사해서 연결할지 거부할지 결정하는 필터링 방식으로 구현하기도 한다.

 아주 민감한 시스템이라면 더 복잡한 인증 방식이나 방화벽 설정으로 사전 차단하는 것이 더 좋을 수도 있다.

 

  IPAddress.Any  

하나의 컴퓨터(서버)는 여러 네트워크 인터페이스(IP주소)를 가질 수 있다.

IP 주소 용도 특징

127.0.0.1 루프백(Loopback), 자기 자신 오직 자기 자신과의 통신용, 네트워크 장비를 거치지 않음
192.168.x.x 로컬 네트워크 (LAN) 실제로 같은 네트워크에 있는 다른 기기들과 통신 가능

 

✅ 실제 동작

서버가 TcpListener(IPAddress.Any, 8888)으로 수신 대기하면:

  • 로컬에서 접속: 127.0.0.1:8888 (외부 접속 절대 불가)
  • 다른 PC에서 접속: 192.168.0.10:8888

👉 둘 다 같은 애플리케이션(서버)에 도달하지만, 다른 네트워크 경로를 통해 접근하는 것이다.

 

  .AcceptTcpClient( )   

tcpListener.AcceptTcpClient() 메소드는 클라이언트가 연결 요청을 보낼 때까지 대기하는 동기방식 메소드이다.

클라이언트가 연결 요청을 보내지 않으면 연결이 올 때까지 프로그램이 멈춘 채 다음 줄로 넘어가지 않는 블로킹 blocking이 발생한다.

연결이 수신되면 연결된 TcpClient 객체를 반환하고, 이렇게 얻어진 TcpClient에서 NetworkStream을 얻어 데이터를 읽고 쓸 수 있게 된다.

 

  try-catch-finally   

네트워크 통신에서 발생하는 다양한 예외를 처리하기 위해 꼭 필요한 try-catch 구문

다양한 환경에서 발생하는 예외 사항으로 갑작스레 종료되는 것을 방지하기 위한 예외 처리 구문이다.

 

무조건 실행되는 finally 블록으로 리소스를 정리

finally
{
    if(tcpListener != null)
    {
        tcpListener.Stop();
    }
}
  • 예외 발생 여부와 관계없이 안전하게 리소스를 정리하기 위한 방어적인 프로그래밍 기법으로 finally를 사용하였다. tcpListener가 정상적으로 생성되지 않아 catch문으로 넘어가더라도 finally 블록은 항상 실행되기 때문이다.
  • if(tcpListener != null) 조건문 :  tcpListener가 null인 상태에서 .Stop()을 호출하려 하면 NullReferenceException이 발생할 수 있으므로, 이를 방지하기 위해 null 체크를 한다.

💡 tcpListener가 null인 경우 정리 : 

  1. new TcpListener(...)에서 예외 발생
    • 포트 충돌 (Address already in use)
    • 잘못된 IP 주소
    • 네트워크 리소스 부족 등
  2. .Start()에서 예외 발생
    • OS에서 포트를 바인딩할 수 없는 경우
    • 권한 부족 (특정 포트는 관리자 권한 필요)

이 경우엔 tcpListener 객체가 생성되지 않았을 수 있으므로 null 체크가 필요하다.


 

HandleClient(TcpClient client)

 

  using   

코드에서의 이 부분은 NetworkStream 객체를 자동으로 해제(dispose)하기 위한 구조이다.

using 키워드는 자원을 자동으로 정리하는 데 사용되며, 특히 스트림, 파일, 소켓 등 외부 자원을 사용할 때 매우 중요하다.

using (NetworkStream stream = client.GetStream())

 


🔍 의미 정리:

  • client.GetStream()은 TCP 연결에서 데이터를 송수신할 수 있는 NetworkStream 객체를 반환한다.
  • using (...) 블록 안에서 선언된 stream은, 블록이 끝날 때 자동으로 .Dispose()가 호출되어 자원을 정리한다.
  • 즉, 스트림이 자동으로 닫히고, 내부의 unmanaged 리소스(파일 핸들, 소켓 등)가 해제된다.
  • 예외가 발생하더라도 using 블록은 finally처럼 끝까지 실행되어 안전하게 정리된다.

예를 들어서 설명하자면:

using (var stream = client.GetStream())
{
    // 데이터를 읽고 쓰는 작업
} // 여기서 stream.Dispose()가 자동으로 호출됨

이 코드는 아래와 동일한 효과이다:

NetworkStream stream = client.GetStream();
try
{
    // 작업
}
finally
{
    if (stream != null)
        stream.Dispose();
}

왜 중요한가요?

  • 리소스 누수 방지: 스트림을 닫지 않으면 메모리나 시스템 자원이 낭비될 수 있다.
  • 안정성 향상: 예외가 발생해도 자원이 정리되므로, 시스템이 불안정해지는 것을 막는다.

 

  .Read(buffer, 0, buffer.Length)   

stream.Read(buffer, 0, buffer.Length)는 buffer.Length만큼 데이터를 읽어서 배열(buffer)에 인덱스 0번부터 채워 넣는다. 그리고 실제로 읽힌 바이트 수(읽은 데이터의 길이)를 반환한다.

* 상대방이 정상적으로 소켓을 닫은 상태에서 Read()를 호출하면, 예외는 발생하지 않고, 대신 더 이상 받을 데이터가 없다는 의미로 0을 반환한다. 이 조건을 이용해 서버는 클라이언트 연결 종료를 감지하고 break하여 루프를 빠져나갈 수 있다.

 

  Encoding.UTF8.GetString(buffer, 0, bytesRead)   

string message = Encoding.UTF8.GetString(buffer, 0, bytesRead) : 바이트 배열(buffer)에 저장된 데이터를 0부터 bytesRead(바이트 수)만큼 읽어 UTF-8 인코딩을 사용해 문자열로 변환한다.

Encoding.UTF8.GetString()는 입력받은 바이트를 문자열로 변환한다. 네트워크 통신으로 데이터를 주고받을 때에는 바이트 배열로 데이터를 가져오기 때문에 문자열 처리를 위해서는 Encoding을 통해 데이터를 문자열로 변환해야한다. 

 

  .Write(buffer, 0, bytesRead)   

stream.Write(buffer, 0, bytesRead) : 클라이언트에 응답 메시지를 보냅니다. buffer 배열의 인덱스 0부터 bytesRead 바이트만큼의 데이터를 스트림을 통해 전송한다.

 

  SocketException   

SocketException : .NET에서 소켓 작업 중 네트워크 오류가 발생했을 때 자동으로 발생하는 예외
주로 TCP/IP 통신을 할 때, 네트워크 상태가 나쁘거나 연결이 끊어졌을 때 발생한다.

 

주요 발생 상황 설명

서버가 꺼져 있는 상태에서 연결 시도 클라이언트에서 SocketException 발생
연결된 클라이언트가 갑자기 연결 종료 서버에서 읽기 시도 시 SocketException 발생
방화벽 또는 네트워크 오류 패킷이 차단되어 통신 실패
포트가 이미 사용 중 Bind() 호출 시 실패
너무 많은 연결 시도 포트 또는 백로그 제한 초과

🧾 예제

try
{
    TcpClient client = new TcpClient("127.0.0.1", 12345);
}
catch (SocketException ex)
{
    Console.WriteLine("Socket error occurred: " + ex.Message);
    Console.WriteLine("Error Code: " + ex.ErrorCode);  // OS별 에러 코드 확인 가능
}

🔍 중요한 속성

오류 원인을 파악하려면 ErrorCode 또는 SocketErrorCode 속성을 확인하는 것이 좋다.

Message 오류 메시지 (사람이 읽기 쉬움)
ErrorCode 운영체제 수준의 네트워크 에러 코드
SocketErrorCode .NET이 정의한 열거형 SocketError

 

예시 :

catch (SocketException ex)
{
    if (ex.SocketErrorCode == SocketError.ConnectionReset)
    {
        Console.WriteLine("상대방이 연결을 강제로 종료했습니다.");
    }
}

 

  ObjectDisposedException   

ObjectDisposedException :  .NET에서 더 이상 사용할 수 없는 객체(이미 Dispose()되거나 Close()된 객체)를 사용하려 할 때 발생하는 예외.

특히 네트워크, 파일, 스트림 관련 프로그래밍에서 나타난다.


📁 예 1: 파일 스트림

FileStream fs = new FileStream("data.txt", FileMode.Open);
fs.Close(); // 또는 fs.Dispose()
fs.ReadByte(); // ❌ ObjectDisposedException 발생

 

🌐 예 2: NetworkStream

NetworkStream stream = client.GetStream();
stream.Close();  // 연결 종료
stream.Read(buffer, 0, 1024);  // ❌ 이미 닫힌 스트림 → 예외 발생

 

이 예외는 사용자 실수나 정상적인 종료 흐름 중에도 발생할 수 있기 때문에, catch문으로 감싸 안전하게 무시하거나 로그 처리하는 것이 일반적이다.

try
{
    stream.Read(buffer, 0, buffer.Length);
}
catch (ObjectDisposedException)
{
    Debug.Log("클라이언트 스트림이 이미 닫혔습니다.");
}

🔒 왜 중요한가?

  • 닫힌 리소스를 다시 사용하는 건 리소스 낭비 혹은 예기치 않은 동작을 유발한다.
  • .NET은 이를 방지하기 위해 ObjectDisposedException을 발생시켜 개발자에게 경고하는 것이다.

 

  IOException   

IOException : C#에서 입출력 작업 중 예기치 못한 오류가 발생했을 때 던져지는 기본 예외 클래스
파일, 네트워크, 스트림 등 다양한 I/O 작업에서 공통적으로 사용된다.


 

📦 대표적인 발생 상황

📁 파일 시스템 오류 파일이 없거나, 읽기/쓰기 권한이 없거나, 디스크 공간 부족
🌐 네트워크 오류 NetworkStream에서 읽기/쓰기 중 연결이 끊김
🔒 리소스 사용 중 다른 프로세스가 파일을 잠궈서 접근할 수 없음
💾 장치 문제 드라이브가 제거됨, 미디어가 손상됨 등

🧪 예제: 파일 입출력에서의 IOException

try
{
    using (var reader = new StreamReader("nonexistent.txt"))
    {
        string content = reader.ReadToEnd();
    }
}
catch (IOException ex)
{
    Console.WriteLine("I/O 오류 발생: " + ex.Message);
}

🧪 예제: 네트워크 스트림에서의 IOException

try
{
    int bytesRead = stream.Read(buffer, 0, buffer.Length);
}
catch (IOException ex)
{
    Console.WriteLine("네트워크 읽기 실패: " + ex.Message);
}

※ 이 경우 내부적으로 SocketException이 원인일 수도 있지만, 외부에선 IOException으로 감싸져서 던져질 수 있습니다.


📌 예외 계층 구조

System.Exception
  └── System.SystemException
       └── System.IO.IOException
            ├── DirectoryNotFoundException
            ├── FileNotFoundException
            ├── EndOfStreamException
            └── PathTooLongException

 

✔️ 즉, IOException은 여러 입출력 관련 예외들의 부모 클래스이다.

 

try-catch문에서 더 세분화된 예외(FileNotFoundException, EndOfStreamException 등) 처리가 필요하면 하위 클래스를 먼저 catch하고, 그 외는 IOException에서 잡는 것이 좋다.

 

  개선할 점   

SocketException 네트워크 오류 (연결 강제 종료, 전송 실패 등) → 적절히 감지함
ObjectDisposedException 스트림이 이미 닫힌 경우 → 흔한 시나리오, 잘 처리
Debug.Log() 사용자에게 로그로 상황 전달 → 좋음
break 오류 시 루프 탈출 → 적절함

 

 

⚠️ 개선 포인트

  1. IOException을 별도로 처리하면 명확함
    • NetworkStream.Read()는 내부적으로 IOException도 던질 수 있음.
    • 이걸 SocketException과 구분해서 처리할 수 있으면 더 정밀한 제어가 가능.
  2. 디버그 로그를 구체화하면 유지보수에 도움
    • 예외 메시지를 로그에 출력하면 디버깅이 쉬움:
    • Debug.LogWarning("Client disconnected: " + e.Message);
  3. client.Close()는 finally에 넣는 게 더 안전
    • 예외가 발생했든 안 했든 반드시 닫혀야 하는 자원이기 때문에 finally에 두는 게 명확하고 안전하다.

✅ 개선 예시

private void HandleClient(TcpClient client)
{
    try
    {
        using (NetworkStream stream = client.GetStream())
        {
            byte[] buffer = new byte[1024];
            int bytesRead;

            while (true)
            {
                try
                {
                    bytesRead = stream.Read(buffer, 0, buffer.Length);

                    if (bytesRead == 0)
                    {
                        Debug.Log("Client Disconnected");
                        break;
                    }

                    string message = Encoding.UTF8.GetString(buffer, 0, bytesRead);
                    Debug.Log("Received: " + message);

                    stream.Write(buffer, 0, bytesRead);
                }
                catch (SocketException ex)
                {
                    Debug.LogWarning("SocketException: " + ex.Message);
                    break;
                }
                catch (ObjectDisposedException ex)
                {
                    Debug.LogWarning("Stream closed: " + ex.Message);
                    break;
                }
                catch (IOException ex)
                {
                    Debug.LogWarning("IO error: " + ex.Message);
                    break;
                }
                catch (Exception ex)
                {
                    Debug.LogError("Unexpected error: " + ex.Message);
                    break;
                }
            }
        }
    }
    finally
    {
        client.Close();
        Debug.Log("Client connection closed");
    }
}

 


 

유니티 이벤트 함수 OnApplicationQuit()

 

 

  OnApplicationQuit()   

OnApplicationQuit() : Unity 애플리케이션이 종료될 때 호출된다.

  • tcpListener.Stop() : 리스너를 종료한다.
  • connectClient.Close() : 클라이언트 연결을 종료한다.
  • serverThread.Abort() : 스레드를 강제 종료한다. (Abort()는 스레드를 즉시 중지시키지만, 리소스 정리, 파일/소켓 등에 문제가 발생할 수 있으므로, 더 안전한 방법을 사용하는 것이 좋다. 보통은 while (isRunning) 같은 플래그를 사용해서 스레드에게 "스스로 종료하도록 신호"를 주는 방식이 더 안전하다.

 

  Abort()보다 더 안전한 대안   

: isRunning 플래그로 스레드에게 정상 종료 요청 후 Join()으로 기다리기

 

🔧 1. 클래스 수준에 플래그와 스레드 선언 추가

private Thread serverThread;
private volatile bool isRunning = true; // 스레드 종료용 플래그

volatile 변수는 항상 메인 메모리에서 직접 읽고 쓴다. (최신 값 반영 보장, 동기화 문제 줄어듦)
용도 : 멀티스레드 환경에서 플래그 상태 공유할 때 사용.


🔧 2. 서버 루프 내부 변경 (예: RunServer 메서드)

private void RunServer()
{
    tcpListener = new TcpListener(IPAddress.Any, port);
    tcpListener.Start();
    Debug.Log("Server Started on port " + port);

    while (isRunning)
    {
        if (!tcpListener.Pending())
        {
            Thread.Sleep(100); // CPU 낭비 방지 (0.1초 후 재시도)
            continue;
        }

        TcpClient client = tcpListener.AcceptTcpClient();
        Debug.Log("Client Connected");
        HandleClient(client);
    }

    tcpListener.Stop(); // 루프 종료 후 안전하게 listener 종료
}

 

AcceptTcpClient()가 블로킹되면 서버 종료 신호가 잘 작동하지 않을 수 있음.
따라서, 들어온 연결이 있을 때만 true를 반환하는 "tcpListener.Pending()"를 사용하여 블로킹을 피한다.


🔧 3. OnApplicationQuit()에서 안전하게 종료

private void OnApplicationQuit()
{
    // 먼저 종료 신호 전송
    isRunning = false;

    // 서버 스레드가 안전하게 종료될 때까지 대기
    if (serverThread != null && serverThread.IsAlive)
    {
        serverThread.Join();
    }

    if (tcpListener != null)
    {
        tcpListener.Stop();
    }

    if (connectedClient != null)
    {
        connectedClient.Close();
    }

    Debug.Log("서버 종료 완료");
}

✅ 요약

스레드 종료 방식 Thread.Abort() (강제 종료, 위험) isRunning = false로 플래그 설정 후 Join() 대기
장점 - 안전하게 리소스 정리, 예외 없음, 유지보수 쉬움

 


  서버를 종료할 때의 순서   

 스레드 → 리스너 → 클라이언트 연결 순서가 대체로 맞고 안전한 순서이다.
하지만 상황에 따라 약간의 조정이 필요할 수 있어서, 각각의 리소스가 어떤 역할을 하는지 정확히 이해한 뒤 순서를 결정하는 것이 중요하다.


🔁 각각의 요소 역할

스레드 서버가 while 루프 등으로 실행 중인 백그라운드 작업
리스너 (TcpListener) 새로운 클라이언트 연결 요청을 수신
클라이언트 연결 (TcpClient) 이미 연결된 클라이언트와의 통신 담당

✅ 일반적인 종료 순서: 스레드 → 리스너 → 클라이언트

1. 스레드 중지 요청 (isRunning = false)

  • 스레드가 계속 루프를 돌고 있다면 종료할 수 없다.
  • 먼저 isRunning = false 같은 플래그로 종료 신호를 준다.

2. 리스너 종료 (tcpListener.Stop())

  • TcpListener.AcceptTcpClient()는 블로킹되므로, 리스너를 먼저 닫아야 스레드가 빠져나온다.
  • Stop()을 호출하면 AcceptTcpClient()에서 예외가 발생하거나 빠져나온다.

3. 스레드 Join

  • 이제 스레드가 자연스럽게 종료될 수 있으니 serverThread.Join()으로 기다릴 수 있다.

4. 기존 클라이언트 연결 닫기 (TcpClient.Close())

  • 기존에 이미 연결된 클라이언트들과의 통신을 정리합니다.

❗ tcpListener.AcceptTcpClient()에서 블로킹 된 상태에서는 tcpListener.Stop()을 호출할 수 없기 때문에 먼저 isRunning=false 플래그로 종료 신호를 주어야 한다. (그렇지 않으면 무한대기에 빠짐)


✅ 순서 이유

스레드 (종료 요청) 루프 중단 플래그로 종료 유도
리스너 (Stop()) 블로킹 I/O를 해제하여 스레드 탈출 가능하게 함
스레드 Join() 스레드가 안전하게 종료될 때까지 대기
클라이언트 연결 종료 이미 연결된 클라이언트 정리

 

 


RunServer 메소드에서 발생할 수 있는 예외의 종류

1. tcpListener = new TcpListener(IPAddress.Any, port);

  • ArgumentOutOfRangeException: 포트 번호가 0보다 작거나 65535보다 큰 경우.
  • SocketException: 내부적으로 IP 주소나 포트를 바인딩할 수 없는 경우.

2. tcpListener.Start();

  • SocketException:
    • 이미 해당 포트에 바인딩된 다른 소켓이 있을 경우.
    • 시스템 리소스 부족 (예: 포트가 너무 많이 열려 있는 경우).

3. tcpListener.AcceptTcpClient();

  • SocketException:
    • 수신 대기 중 문제가 생긴 경우 (예: Listener가 예기치 않게 중지됨).
    • 클라이언트 연결 과정에서 네트워크 오류가 발생한 경우.
  • ObjectDisposedException:
    • Listener가 이미 Stop()되어서 더 이상 연결을 받을 수 없는 상태인 경우.

4. HandleClient(connectedClient);

  • 여기서 발생할 예외는 HandleClient 함수 내 구현에 따라 다릅니다. 예를 들어:
    • NullReferenceException: 클라이언트가 null일 경우.
    • IOException: 스트림 처리 중 오류 발생.
    • CustomException: 사용자 정의 예외 등.

그 외 일반적으로 catch되는 예외

  • Exception (상위 예외): 위에서 언급된 모든 예외를 포함하는 일반 예외.
  • 만약 ThreadAbortException 같은 특별한 시스템 예외가 발생한다면, 그에 따라 catch되지 않을 수도 있습니다.

결론

이 try 블록에서 발생 가능한 주요 예외는 다음과 같습니다:

  • SocketException
  • ArgumentOutOfRangeException
  • ObjectDisposedException
  • IOException (간접적으로)
  • NullReferenceException
  • 그리고 기타 예상하지 못한 Exception

필요하다면 예외를 좀 더 구체적으로 나눠서 catch (SocketException e) 등으로 핸들링하는 것이 좋습니다. 이는 디버깅과 유지보수에 큰 도움이 됩니다.

 

 

 


HandleClient 메소드에서 발생할 수 있는 예외의 종류

 

이 HandleClient 메서드는 클라이언트와의 통신을 처리하며, 여기서 발생할 수 있는 예외는 주로 네트워크 통신, 스트림 처리, 그리고 버퍼 처리와 관련된 것입니다. 아래에서 코드 흐름을 따라 발생 가능한 예외들을 설명해드릴게요.


📌 주요 예외 발생 가능 지점 및 예외 설명


🔹 NetworkStream stream = client.GetStream();

  • InvalidOperationException
    • TcpClient가 아직 연결되지 않았거나 이미 닫힌 경우.
  • ObjectDisposedException
    • TcpClient가 이미 Close()된 상태일 경우.

🔹 stream.Read(buffer, 0, buffer.Length);

  • IOException
    • 네트워크 문제로 읽기를 실패한 경우. 예: 클라이언트가 강제 종료되었을 때.
    • 내부적으로 SocketException이 발생하여 포장되어 전달될 수 있음.
  • ObjectDisposedException
    • 스트림이 이미 닫혀 있을 경우.
  • ArgumentOutOfRangeException / ArgumentException
    • 인자가 잘못된 경우 (이 코드는 정상 값이므로 가능성 낮음).

🔹 Encoding.UTF8.GetString(buffer, 0, bytesRead);

  • ArgumentOutOfRangeException
    • bytesRead가 음수이거나 범위를 벗어난 경우. (정상 작동 시 발생하지 않음)
  • DecoderFallbackException
    • 받은 바이트가 유효한 UTF-8 문자열이 아닌 경우.
    • 예: 깨진 바이트 시퀀스를 UTF-8로 디코딩할 수 없을 때.

🔹 stream.Write(buffer, 0, bytesRead);

  • IOException
    • 네트워크 오류로 인해 쓰기 실패.
    • 예: 클라이언트 연결이 끊어졌을 때.
  • ObjectDisposedException
    • 스트림이 이미 닫혀 있을 경우.
  • ArgumentOutOfRangeException / ArgumentException
    • bytesRead 값이 이상한 경우 (이 코드에선 발생 가능성 거의 없음).

🔹 client.Close();

  • 일반적으로 Close()는 안전하지만, 내부 자원 정리 중 ObjectDisposedException이 드물게 발생할 수도 있습니다 (거의 예외 수준의 경우).

✅ 정리: 주요 발생 예외 목록

예외 타입 설명

IOException 네트워크 중단, 연결 종료 등
SocketException 네트워크 레벨 예외, 주로 IOException으로 래핑됨
ObjectDisposedException 스트림 또는 TcpClient가 이미 닫힌 경우
InvalidOperationException 스트림을 사용할 수 없는 상태에서 호출
DecoderFallbackException 유효하지 않은 UTF-8 바이트 해석 시
ArgumentException, ArgumentOutOfRangeException 버퍼 관련 잘못된 인자 전달 시 (코드 상 거의 발생 안함)

🔧 개선 팁

  • DecoderFallbackException을 별도로 catch하거나 UTF-8 디코딩에 DecoderFallback 설정을 고려해도 좋습니다.
  • catch (IOException ex) when (ex.InnerException is SocketException) 식의 고급 예외 필터링도 가능합니다 (C# 6.0 이상).

궁금한 예외 케이스나 디버깅 메시지를 더 구체화하고 싶으시면, 로그 구문이나 e.ToString()으로 전체 스택트레이스를 출력하는 것도 좋은 방법입니다.