카테고리:

4 분 소요

서버의 종류

서버를 구현하는 방식에는 크게 단일 스레드 기반 서버와 다중 스레드 기반 서버로 나뉜다.

다중 스레드 기반 서버의 경우, 각각의 스레드가 연결을 담당하여 통신하므로 이해가 쉽다. 하지만, 단일 스레드인데 어떻게 여러 클라이언트의 접속을 해결하느냐는 의문이 든다. Node.js의 libuv 구현체의 Event Loop를 사용하여 구현한 서버를 보면 이해할 수 있다.

C#의 SocketAsyncEventArgs에서도 운영체제에서 제공하는 IOCP(I/O Completion Port)와 같은 메커니즘을 ThreadPool을 사용하여 비동기 작업을 처리한다.

소스 코드

아래는 SocketAsyncEventArgs를 사용하여 비동기 이벤트 기반의 에코 서버를 구현하는 코드이다.

  • AsyncEchoServer.cs
using System;
using System.Net;
using System.Net.Sockets;

class AsyncEchoServer {
    private Socket listenerSocket;

    public void StartServer() {
        listenerSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        listenerSocket.Bind(new IPEndPoint(IPAddress.Any, 12345));
        listenerSocket.Listen(10);

        Console.WriteLine("Server started. Waiting for connections...");

        StartAccept();
        Console.ReadLine(); // Keep the application running
    }

    private void StartAccept() {
        SocketAsyncEventArgs arg = new();
        arg.Completed += (_, e) => AcceptCompleted(e);

        RegisterAccept(arg);
    }

    private void RegisterAccept(SocketAsyncEventArgs e) {
        e.AcceptSocket = null;

        bool pending = listenerSocket.AcceptAsync(e);
        if (pending == false) {
            AcceptCompleted(e);
        }
    }

    private void AcceptCompleted(SocketAsyncEventArgs e) {
        Console.WriteLine($"Client connected: {e.AcceptSocket.RemoteEndPoint}");

        SocketAsyncEventArgs arg = new();

        byte[] buffer = new byte[1024];
        arg.SetBuffer(buffer, 0, buffer.Length);
        arg.Completed += (_, receiveArgs) => RecvCompleted(receiveArgs);
        arg.AcceptSocket = e.AcceptSocket;
        arg.RemoteEndPoint = e.RemoteEndPoint;

        RegisterRecv(arg);

        RegisterAccept(e);
    }

    private void RegisterRecv(SocketAsyncEventArgs e) {
        bool pending = e.AcceptSocket.ReceiveAsync(e);
        if (pending == false) {
            RecvCompleted(e);
        }
    }

    private void RecvCompleted(SocketAsyncEventArgs e) {
        if (e.BytesTransferred > 0 && e.SocketError == SocketError.Success) {
            byte[] receivedData = new byte[e.BytesTransferred];
            Array.Copy(e.Buffer, receivedData, e.BytesTransferred);

            Console.WriteLine($"Received from {e.AcceptSocket.RemoteEndPoint}: {string.Join(" ", receivedData)}");

            // Echo back to the client
            e.AcceptSocket.Send(receivedData);

            // Continue receiving
            RegisterRecv(e);
        } else {
            Console.WriteLine($"Client disconnected: {e.AcceptSocket.RemoteEndPoint}");
            e.AcceptSocket.Close();
        }
    }
}
  • MainClass.cs
class MainClass {
    public static void Main(string[] args) {
        AsyncEchoServer server = new();
        server.StartServer();
    }
}

중요한 내용

MSDN에 따르면 AcceptAsync 함수와 ReceiveAsync 함수, SendAsync 함수는 pending을 검사하여야 한다.

반환형은 Boolean으로 I/O 작업이 보류 중인 경우 true입니다.

작업이 완료되면 e 매개 변수에 대한 Completed 이벤트가 발생합니다. I/O 작업이 동기적으로 완료된 경우 false입니다. 이 경우에는 e 매개 변수에서 Completed 이벤트가 발생하지 않으며, 메서드 호출이 반환된 직후 매개 변수로 전달된 e 개체를 검사하여 작업 결과를 검색할 수 있습니다.

참고

https://velog.io/@yearsalary/libuv-event-loop-in-Node-js
https://www.inflearn.com/questions/524822/socketasynceventargs-%EC%9D%98-eventhandler%EC%99%80-threadpool
https://blog.naver.com/hgp33/221146608982
https://learn.microsoft.com/ko-kr/dotnet/api/system.net.sockets.socketasynceventargs?view=net-8.0
https://siku314.tistory.com/75

태그: AcceptAsync, async, Echo, eventhandler, pending, Pool, ReceiveAsync, Server, Socket, SocketAsyncEventArgs, ThreadPool, 비동기, 서버, 스레드, 쓰레드, 이벤트

업데이트: