[Network] System.Net.Sockets.Socket과 Linux epoll

배경

수천개의 소켓을 열고 Accept를 Multiplexing 으로 처리해야 합니다.

select를 사용하여 범용적인 코드를 작성할 수 있지만
이벤트를 처리하기 위해 소켓의 개수만큼 선형으로 찾고 매 호출마다
소켓 리스트를 복사해야하는 문제가 있습니다.

이를 해결하기 위해 IO Completion Port 를 사용할 수 있지만
IOCP는 Windows 의 기술이기 때문에 Linux에선 대안을 찾아야합니다.

epoll?

epoll은 확장 가능한 I/O 이벤트 알림 메커니즘을위한 Linux 커널 시스템 호출입니다.

환경

  • .NET 6, C# 10
  • Ubuntu 20.04

구현

epoll_create
epoll_ctl
epoll_wait

위 세개 함수를 사용하기 위해 P/Invoke 코드를 작성합니다.
(Tdms.Linux 라이브러리를 사용해도 됩니다.)

Libc.cs

public static class Libc
{
    public const int EPOLL_CTL_ADD = 0x1;
    public const int EPOLL_CTL_DEL = 0x2;
    public const int EPOLL_CTL_MOD = 0x3;

    [StructLayout(LayoutKind.Explicit)]
    public struct epoll_data
    {
        [FieldOffset(0)]
        public IntPtr ptr;

        [FieldOffset(0)]
        public int fd;

        [FieldOffset(0)]
        public uint u32;

        [FieldOffset(0)]
        public ulong u64;
    };

    [StructLayout(LayoutKind.Explicit)]
    public struct epoll_event
    {
        [FieldOffset(0)]
        public EPOLL_EVENTS events;

        [FieldOffset(4)]
        public epoll_data data;
    };

    [Flags]
    public enum EPOLL_EVENTS : uint
    {
        EPOLLIN = 0x001,
        EPOLLPRI = 0x002,
        EPOLLOUT = 0x004,
        EPOLLRDNORM = 0x040,
        EPOLLRDBAND = 0x080,
        EPOLLWRNORM = 0x100,
        EPOLLWRBAND = 0x200,
        EPOLLMSG = 0x400,
        EPOLLERR = 0x008,
        EPOLLHUP = 0x010,
        EPOLLRDHUP = 0x2000,
        EPOLLEXCLUSIVE = 1u << 28,
        EPOLLWAKEUP = 1u << 29,
        EPOLLONESHOT = 1u << 30,
        EPOLLET = 1u << 31
    }

    [DllImport("libc.so.6")]
    public static extern int epoll_create(int size);

    [DllImport("libc.so.6")]
    public static extern unsafe int epoll_ctl(int epfd, int op, int fd, epoll_data* epevent);

    [DllImport("libc.so.6")]
    public static extern unsafe int epoll_wait(int epfd, epoll_data* events, int maxevents, int timeout);
}

Program.cs

const int portStart = 10000;
const int portEnd = 12000;

// epoll 생성

var epollDescriptor = Libc.epoll_create(1);

if (epollDescriptor < 0)
    throw new IOException($"Call to epoll_create API failed({epollDescriptor})");

var sockets = new Dictionary<int, Socket>();

for (int i = portStart; i <= portEnd; i++)
{
    var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
    socket.Bind(new IPEndPoint(IPAddress.Any, i));
    socket.Listen();
    sockets[socket.Handle.ToInt32()] = socket;

    // epoll 이벤트 등록

    unsafe
    {
        var epEvent = new Libc.epoll_event
        {
            events = Libc.EPOLL_EVENTS.EPOLLIN,
            data = new Libc.epoll_data
            {
                fd = socket.Handle.ToInt32()
            }
        };

        var result = Libc.epoll_ctl(epollDescriptor, Libc.EPOLL_CTL_ADD, epEvent.data.fd, &epEvent);

        if (result != 0)
            throw new IOException($"Call to epoll_ctl(EPOLL_CTL_ADD) API failed({result})");
    }
}

// epoll 이벤트 수신 후 Socket.Accept
unsafe
{
    var epEvent = new Libc.epoll_event();

    while (true)
    {
        var result = Libc.epoll_wait(epollDescriptor, &epEvent, 1, -1);

        if (result > 0)
        {
            var socket = sockets[epEvent.data.fd];
            var client = socket.Accept();
            Console.WriteLine($"[{socket.LocalEndPoint}] Accept from {client.RemoteEndPoint}");
        }
    }
}
7개의 좋아요

IOCP game server에 windows server를 많이 사용하는 이유중 하나죠!

3개의 좋아요

이미 닷넷코어에서는 내부적으로 리눅스에서는 epoll을 사용하고 있습니다. 닷넷코어에서 사용하는 epoll 보다 더 최적화(즉 자신만을 위한)를 하고 싶은 경우에만 직접 epoll API를 다루는 것이 좋습니다.

2개의 좋아요

내부적으로 epoll을 다루고 있지만 대량의 소켓을 하나의 epoll로 컨트롤할 방법은 없는것으로 알고있습니다.

2개의 좋아요