C# 네트워크 프로그래밍

2025. 3. 28. 00:06개발/C#

이 글은 박상현 님의 '이것이 C#이다 개정판'을 참고하여 공부한 내용입니다.


서버는 정확히 무엇일까요. 시스템 엔지니어로 일할 때 서버들을 유지보수하며 생각했었습니다. apache, mariadb와 같은 서버 프로그램을 구동하고 있는 워크스테이션을 서버로 부를 수 있는 것인가. 그러면 Dell R750과 같은 서버 하드웨어에 단순하게 ssh를 통해 접속해 OpenFOAM과 같은 전산유체역학 소프트웨어를 실행하여 개인 컴퓨터로 사용한다면 이 것은 서버일까. 같은 생각을 말이죠.

 

TcpListener / TcpClient

System.Net.Sockets 네임스페이스에 정의되어 있는 클래스로 소켓기반의 서버와 클라이언트 역할을 합니다.

 

Server.cs

using System;
using System.Net;
using System.Net.Sockets;
using System.Text;

class Program
{
    static void Main()
    {
        int port = 12345;
        TcpListener server = new TcpListener(IPAddress.Any, port);

        server.Start();
        Console.WriteLine($"Server starts! Waiting at {port}...");

        TcpClient client = server.AcceptTcpClient();
        Console.WriteLine("Conneted client!");

        NetworkStream stream = client.GetStream();
        byte[] buffer = new byte[1024];
        int bytesRead = stream.Read(buffer, 0, buffer.Length);

        string receivedMessage = Encoding.UTF8.GetString(buffer, 0, bytesRead);
        Console.WriteLine($"Received message: {receivedMessage}");

        string response = "Hello, Client!";
        byte[] responseData = Encoding.UTF8.GetBytes(response);
        stream.Write(responseData, 0, responseData.Length);

        client.Close();
        server.Stop();
        Console.WriteLine("Server exit");
        Console.ReadLine();
    }
}

 

Client.cs

using System;
using System.Net.Sockets;
using System.Text;

class Program
{
    static void Main()
    {
        string serverIp = "127.0.0.1";
        int port = 12345;

        TcpClient client = new TcpClient();
        client.Connect(serverIp, port);
        Console.WriteLine("Connected to server");

        NetworkStream stream = client.GetStream();

        string message = "Hello, Server!";
        byte[] data = Encoding.UTF8.GetBytes(message);
        stream.Write(data, 0, data.Length);

        byte[] buffer = new byte[1024];
        int bytesRead = stream.Read(buffer, 0, buffer.Length);
        string response = Encoding.UTF8.GetString(buffer, 0, bytesRead);
        Console.WriteLine($"Server response: {response}");

        client.Close();
        Console.WriteLine("Client exit");
        Console.ReadLine();
    }
}

 

Server

Server starts! Waiting at 12345...
Conneted client!
Received message: Hello, Server!
Server exit

 

Client

Connected to server
Server response: Hello, Client!
Client exit

 

간단한 서버와 클라이언트 모델을 작성하여 문자열을 한 번씩 주고받는 코드입니다.

TcpListener / TcpClient Socket

 

우선 서버 객체를 생성합니다. 생성자에게 전달할 수 있는 인자는 위에서 사용한 (IPAddress, Int32) 혹은 IPEndPoint 객체 두 가지를 받습니다. 위에서 IPAddress.Any는 0.0.0.0과 동일합니다. 즉, 서버의 어떤 IP로든 접근이 가능합니다.

 

이후 서버는 Start 메서드를 사용하여 소켓을 EndPoint와 바인딩하고 클라이언트의 연결 요청을 대기합니다. 이때 Start 메서드에 정수형 인자를 사용하게 되면 보류 중인 클라이언트의 요청 수를 지정할 수 있습니다. 지정된 수가 넘어가면 클라이언트에서는 예외가 발생합니다.

 

그다음 AcceptTcpClinet 메서드를 사용하여 보류 중인 클라이언트의 요청을 수락합니다. 그리고 수락한 클라이언트에 연결된 TcpClient를 반환하게 됩니다.

 

반환된 TcpClient를 사용하여  NetworkStream을 가져옵니다. 이제 NetworkStream을 통해 데이터를 주고받을 수 있습니다. NetworkStream은 Write와 Read를 문자열로 할 수 없고 바이트 단위로만 가능하기 때문에 문자열을 바이트로 변환해야 합니다.

 

클라이언트에서 보낸 메시지를 Encoding.UTF8.GetString을 통해 문자열로 변환하여 출력합니다. 이번에는 반대로 클라이언트에게 Encoding.UTF8.GetBytes를 사용하여 보낼 문자열을 바이트로 변환하여 Write 합니다. 이제 사용했던 소켓을 닫기 위해 TcpClient.Close 메서드를 호출합니다.

 

마지막으로 TcpListener.Stop 메서드를 호출하여 클라이언트의 연결 요청을 닫습니다. 이때 보류 중인 요청들은 모두 소실되어 클라이언트에서 예외가 발생합니다.

 

Socket Buffer

Socket Buffer

 

NetworkStream을 사용하여 서버와 클라이언트는 바이트를 교환합니다. 이때 주의해야 할 점은 소켓은 버퍼를 사용한다는 것입니다. 버퍼는 메모리의 커널 영역에 있으며, 커널에서 관리됩니다. 그래서 사용자가 Write를 하여도 커널에서 데이터를 즉시 보내지 않을 수도 있습니다. 보낸다고 해도 일부 바이트만 보내게 될 수도 있습니다.

 

이런 경우 수신측은 데이터 해석이 어려울 수 있습니다. 현재 읽은 데이터가 일부인지 전체인지 판단해야 하며, 일부라면 앞으로 데이터를 얼마나 읽어야 하는지도 알아야 합니다. 그래서 네트워크 통신을 원활히 하려면 프로토콜을 사용합니다. 왜 프로토콜을 사용해야 하는지 HTTP를 예시로 들어 설명드리겠습니다.

 

HTTP Request

GET / HTTP/1.1
Host: bohlee.tistory.com
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-encoding: gzip, deflate, br, zstd
Accept-language: ko,en-US;q=0.9,en;q=0.8,zh-CN;q=0.7,zh;q=0.6
...

 

HTTP Response

HTTP/1.1 200 OK
Date: Fri, 28 Mar 2025 13:37:07 GMT
Cache-control: no-cache, no-store, max-age=0, must-revalidate
Content-length: 46469
Content-type: text/html;charset=UTF-8
...

<!DOCTYPE html>
<html lang="ko">
...

 

HTTP는 텍스트 메시지를 사용하여 요청과 응답을 송수신합니다. 웹 브라우저는 이 텍스트 문서를 해석하여 웹 페이지를 보여주는 것입니다. 중요한 건 HTTP의 내용이 아닌 규칙입니다.

 

HTTP는 기본적으로 CRLF 개행을 기준으로 한줄씩 데이터를 해석합니다. 그렇기 때문에 수신 측은 개행이 나오기 전까지 데이터를 계속해서 읽습니다. 물론 악의적인 데이터 공격을 대비하여 최대 길이를 상정하여 데이터를 수신합니다. 참고로 본문의 경우에는 헤더에서 길이를 미리 알려줍니다.

 

그리고 한 줄씩 해석한 데이터는 Start Line, Header, Empty Line, Body 총 4가지 범위로 분류됩니다. 가장 첫 번째 줄은 Start Line이며 데이터가 없는 빈 줄, Empty Line이 나오기 전 까지는 Header, 그리고 Empty Line 이후에는 Body로 해석됩니다. 그래서 수신 측은 Body를 모두 읽거나, Body가 없는 경우 Empty Line까지 읽으면 하나의 요청/응답으로 인식합니다.

 

HTTP Request Reading

 

그래서 데이터가 나누어 수신되어도, 규칙을 알고 있기때문에 원활한 통신이 가능한 것입니다. HTTP에 대해 더 자세한 내용을 알고 싶으시다면 RFC 7230 혹은 RFC 7540을 읽어 보시길 추천드립니다. 저도 HTTP 서버개발을 하며 필요한 부분만 찾아서 읽어 일부 틀린 내용이 있을 수 있습니다. 그러니 편하게 지적해 주시면 감사드립니다.

'개발 > C#' 카테고리의 다른 글

C# Garbage Collection  (1) 2025.04.01
C# 스레드와 태스크  (0) 2025.03.19
C# 입출력 작업  (0) 2025.03.09
C# dynamic  (0) 2025.03.05
C# 애트리뷰트  (0) 2025.02.25