배경
수천개의 소켓을 열고 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}");
}
}
}