Skip to content

Instantly share code, notes, and snippets.

@rkttu
Created August 8, 2024 15:02
Show Gist options
  • Save rkttu/67175fe8ef8d1bfb1972b909f5329bba to your computer and use it in GitHub Desktop.
Save rkttu/67175fe8ef8d1bfb1972b909f5329bba to your computer and use it in GitHub Desktop.
Garnet Technical Demo
// Start Redis Server
var redisPassword = Guid.NewGuid().ToString("n");
Console.Out.WriteLine($"Redis Password: {redisPassword}");
var options = new GarnetServerOptions()
{
Address = "127.0.0.1",
Port = 6379,
MemorySize = "128m",
AuthSettings = new PasswordAuthenticationSettings(redisPassword),
};
using var server = new GarnetServer(options);
server.Start();
// Start ASP.NET Core Server
var builder = WebApplication.CreateBuilder(
Environment.GetCommandLineArgs().Skip(1).ToArray());
// Add Redis
builder.Services.AddSingleton<IConnectionMultiplexer>(sp =>
ConnectionMultiplexer.Connect($"{options.Address}:{options.Port},password={redisPassword}")
);
var app = builder.Build();
// API Endpoints
app.MapPost("/send", async (ChatMessage message, IConnectionMultiplexer redis) =>
{
var db = redis.GetDatabase();
await db.ListRightPushAsync("messages", message.ToString());
return Results.Ok();
});
app.MapGet("/messages", async (IConnectionMultiplexer redis) =>
{
var db = redis.GetDatabase();
var messages = await db.ListRangeAsync("messages");
return Results.Ok(messages.Select(m => m.ToString()));
});
// Serve HTML page dynamically
app.MapGet("/", () => Results.Content("""
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>가넷으로 구동되는 채팅 애플리케이션</title>
<style type="text/css" media="all">
body { font-family: Arial, sans-serif; }
#chat { max-width: 600px; margin: 0 auto; padding: 20px; border: 1px solid #ccc; }
#messages { list-style-type: none; padding: 0; }
#messages li { padding: 8px; border-bottom: 1px solid #eee; }
#sendMessage { display: flex; gap: 10px; margin-top: 10px; }
#messageInput { flex: 1; padding: 8px; }
#sendButton { padding: 8px; }
#changeNickname { padding: 8px; margin-top: 10px; }
.modal {
display: none;
position: fixed;
z-index: 1;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: rgb(0,0,0);
background-color: rgba(0,0,0,0.4);
padding-top: 60px;
}
.modal-content {
background-color: #fefefe;
margin: 5% auto;
padding: 20px;
border: 1px solid #888;
width: 80%;
max-width: 300px;
}
.modal input[type="text"] {
width: 100%;
padding: 10px;
margin: 10px 0;
box-sizing: border-box;
}
.modal button {
padding: 10px;
width: 100%;
}
</style>
</head>
<body>
<div id="chat">
<ul id="messages"></ul>
<div id="sendMessage">
<input type="text" id="messageInput" placeholder="메시지를 입력하세요..." />
<button id="sendButton">보내기</button>
</div>
<button id="changeNickname">닉네임 변경</button>
</div>
<div id="nicknameModal" class="modal">
<div class="modal-content">
<h2>닉네임 설정</h2>
<input type="text" id="nicknameInput" placeholder="닉네임을 입력하세요..." />
<button id="saveNicknameButton">저장</button>
</div>
</div>
<script type="text/javascript">
const messagesList = document.getElementById('messages');
const messageInput = document.getElementById('messageInput');
const sendButton = document.getElementById('sendButton');
const changeNicknameButton = document.getElementById('changeNickname');
const nicknameModal = document.getElementById('nicknameModal');
const nicknameInput = document.getElementById('nicknameInput');
const saveNicknameButton = document.getElementById('saveNicknameButton');
let nickname = '';
function openNicknameModal() {
nicknameModal.style.display = 'block';
}
function closeNicknameModal() {
nicknameModal.style.display = 'none';
}
async function fetchMessages() {
try {
const response = await fetch('/messages');
const messages = await response.json();
messagesList.innerHTML = messages.map(msg => `<li>${msg}</li>`).join('');
} catch (error) {
console.error('메시지 가져오기 오류: ', error);
}
}
async function sendMessage(nickname, message) {
try {
await fetch('/send', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ nickname, text: message })
});
messageInput.value = '';
fetchMessages(); // 메시지 전송 후 갱신
} catch (error) {
console.error('메시지 전송 오류:', error);
}
}
saveNicknameButton.addEventListener('click', () => {
const newNickname = nicknameInput.value.trim();
if (newNickname) {
nickname = newNickname;
closeNicknameModal();
}
});
changeNicknameButton.addEventListener('click', openNicknameModal);
sendButton.addEventListener('click', () => {
const message = messageInput.value.trim();
if (nickname && message) {
sendMessage(nickname, message);
}
});
// 닉네임이 설정되지 않은 경우 모달 열기
if (!nickname) {
openNicknameModal();
}
// 페이지 로드 시 메시지 가져오고, 자동 갱신 설정
fetchMessages();
setInterval(fetchMessages, 1000); // 1초마다 갱신
// 모달이 닫혀 있으면 열기
window.onclick = function(event) {
if (event.target == nicknameModal) {
closeNicknameModal();
}
}
</script>
</body>
</html>
""", "text/html"));
await app.RunAsync();
public sealed class ChatMessage
{
public string Nickname { get; set; } = string.Empty;
public string Text { get; set; } = string.Empty;
public override string ToString()
=> $"{Nickname}: {Text}";
}
public static class Program
{
internal static readonly Lazy<Encoding> _utf8WithoutBomEncoding =
new Lazy<Encoding>(() => new UTF8Encoding(false), LazyThreadSafetyMode.None);
public static void Main(string[] args)
{
var redisPassword = Guid.NewGuid().ToString("n").Dump();
Console.Out.WriteLine($"Redis Password: {redisPassword}");
var options = new GarnetServerOptions()
{
Address = "127.0.0.1",
Port = 6379,
MemorySize = "128m",
AuthSettings = new PasswordAuthenticationSettings(redisPassword),
};
using var server = new GarnetServer(options);
server.Start();
// Start ASP.NET Core Server
var builder = WebApplication.CreateBuilder(
Environment.GetCommandLineArgs().Skip(1).ToArray());
builder.Services.AddDistributedMemoryCache();
builder.Services.AddSession(options =>
{
options.IdleTimeout = TimeSpan.FromMinutes(30);
options.Cookie.HttpOnly = true;
options.Cookie.IsEssential = true;
});
// Add Redis
builder.Services.AddSingleton<IConnectionMultiplexer>(sp =>
ConnectionMultiplexer.Connect($"{options.Address}:{options.Port},password={redisPassword}")
);
builder.Services.AddSingleton<QueueService>();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseRouting();
app.UseSession();
app.UseWhen(context => context.Request.Path.StartsWithSegments("/resource"), appBuilder =>
{
appBuilder.UseMiddleware<QueueMiddleware>();
});
app.MapGet("/", async context =>
{
await context.Response.WriteAsync(HtmlPages.IndexPage(), _utf8WithoutBomEncoding.Value);
});
app.MapGet("/queue", async context =>
{
var userId = context.Request.Query["userId"].ToString();
var queueService = context.RequestServices.GetRequiredService<QueueService>();
await queueService.EnqueueAsync(userId);
int position = await queueService.GetQueuePositionAsync(userId);
if (position > 1)
await QueueMiddleware.HandleQueueAsync(context, queueService, userId, position);
else
context.Response.Redirect("/resource");
});
app.MapGet("/resource", async context =>
{
await context.Response.WriteAsync(HtmlPages.ResourcePage(), _utf8WithoutBomEncoding.Value);
});
// 시뮬레이션: 사용자를 대기열에 추가
var queueService = app.Services.GetRequiredService<QueueService>();
var count = 30;
for (int i = 0; i < 30; i++)
{
var userId = Guid.NewGuid().ToString();
queueService.EnqueueAsync(userId).Wait();
}
Console.WriteLine($"Total {count} user(s) added to the queue.");
// 시뮬레이션: 대기열에서 주기적으로 사용자 처리
Task.Run(async () =>
{
var queueService = app.Services.GetRequiredService<QueueService>();
while (true)
{
var userId = await queueService.DequeueAsync();
if (userId != null)
Console.WriteLine($"User {userId} entered.");
await Task.Delay(1000); // 5초마다 사용자 처리
}
});
app.Run();
}
}
public class QueueService
{
private readonly IConnectionMultiplexer _redis;
private readonly IDatabase _db;
private const string QueueKey = "queue";
public QueueService(IConnectionMultiplexer redis)
{
_redis = redis;
_db = _redis.GetDatabase();
}
public async Task EnqueueAsync(string userId)
{
await _db.ListRightPushAsync(QueueKey, userId);
}
public async Task<string> DequeueAsync()
{
return await _db.ListLeftPopAsync(QueueKey);
}
public async Task<int> GetQueuePositionAsync(string userId)
{
var queue = await _db.ListRangeAsync(QueueKey);
for (int i = 0; i < queue.Length; i++)
{
if (queue[i] == userId)
{
return i + 1;
}
}
return -1;
}
public async Task<int> GetQueueLengthAsync()
{
return (int)await _db.ListLengthAsync(QueueKey);
}
}
public class QueueMiddleware
{
private readonly RequestDelegate _next;
private readonly QueueService _queueService;
private const int MaxActiveUsers = 5; // 최대 활성 사용자 수
public QueueMiddleware(RequestDelegate next, QueueService queueService)
{
_next = next;
_queueService = queueService;
}
public async Task InvokeAsync(HttpContext context)
{
if (!context.Session.TryGetValue("UserId", out var userId))
{
userId = Guid.NewGuid().ToByteArray();
context.Session.Set("UserId", userId);
await _queueService.EnqueueAsync(Convert.ToBase64String(userId));
}
string userIdString = Convert.ToBase64String(userId);
int activeUsersCount = await _queueService.GetQueueLengthAsync();
if (activeUsersCount >= MaxActiveUsers)
{
int position = await _queueService.GetQueuePositionAsync(userIdString);
await HandleQueueAsync(context, _queueService, userIdString, position);
}
else
{
await _next(context);
}
}
internal static async Task HandleQueueAsync(HttpContext context, QueueService queueService, string userId, int position)
{
context.Response.Headers.CacheControl = "no-cache, no-store, must-revalidate";
context.Response.Headers.Pragma = "no-cache";
context.Response.Headers.Expires = "0";
if (position > 1)
{
int estimatedWaitTimeInSeconds = (position - 1) * 10; // 사용자당 처리 시간(초)
await context.Response.WriteAsync(HtmlPages.WaitingPage(position, estimatedWaitTimeInSeconds, userId), Program._utf8WithoutBomEncoding.Value);
position = await queueService.GetQueuePositionAsync(userId);
}
else
{
await queueService.DequeueAsync();
context.Response.Redirect("/resource");
}
}
}
internal static class HtmlPages
{
public static string IndexPage()
{
return @"
<!DOCTYPE html>
<html>
<head>
<title>대기열 서비스 샘플</title>
<meta charset='UTF-8' />
</head>
<body>
<div class='text-center'>
<h1 class='display-4'>환영합니다.</h1>
<p>대기열 서비스 샘플 웹 사이트입니다.</p>
<a href='./resource'>사이트 접속하기</a>
</div>
</body>
</html>";
}
public static string ResourcePage()
{
return @"
<!DOCTYPE html>
<html>
<head>
<title>안녕하세요.</title>
<meta charset='UTF-8' />
</head>
<body>
<div class='text-center'>
<h1 class='display-4'>안녕하세요.</h1>
<iframe width='400' height='225' src='https://www.youtube.com/embed/jNQXAC9IVRw' title='Me at the zoo' frameborder='0' allow='accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share' referrerpolicy='strict-origin-when-cross-origin' allowfullscreen></iframe>
</div>
</body>
</html>";
}
public static string WaitingPage(int position, int estimatedWaitTime, string userId)
{
double estimatedWaitTimeInMinutes = Math.Ceiling((double)estimatedWaitTime / 60);
return $@"
<!DOCTYPE html>
<html>
<head>
<title>대기실</title>
<meta charset='UTF-8' />
<script>
setTimeout(function(){{
document.getElementById('waiting_form').submit();
}}, 5000); // 5초마다 새로 고침
</script>
</head>
<body>
<div class='text-center'>
<h1 class='display-4'>대기실</h1>
<p>현재 대기열에서 귀하의 위치: <strong>{position}</strong></p>
<p>예상 대기 시간: <strong>{estimatedWaitTimeInMinutes}</strong> 분</p>
<p>잠시만 기다려 주십시오. 귀하의 순서가 되면 자동으로 이동합니다.</p>
<form type='get' action='./queue' id='waiting_form'>
<input type='hidden' name='userId' value='{userId}' />
</form>
</div>
</body>
</html>";
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment