နိဒါန်း
Node.js TR ဂီယာ ခရိုမီယမ် မှတ်ထားသည် - ...
ဤဆောင်းပါးတွင်၊ ကျွန်ုပ်တို့သည် I/O ဓာတ်ပေါင်းဖိုတစ်ခု၏ အဝင်အထွက်များကို ကြည့်ရှုပြီး ၎င်းသည် မည်သို့အလုပ်လုပ်ကြောင်း၊ ကုဒ်စာကြောင်း 200 ထက်နည်းသော အကောင်အထည်ဖော်မှုကို ရေးသားကာ ရိုးရှင်းသော HTTP ဆာဗာကို တောင်းဆိုမှု/မိနစ် 40 သန်းကျော် တောင်းဆိုမှုတစ်ခု ပြုလုပ်ပါမည်။
စကားချီး
- I/O ဓာတ်ပေါင်းဖို၏ လုပ်ဆောင်ချက်ကို နားလည်စေရန် ဆောင်းပါးကို ရေးသားခဲ့ခြင်းဖြစ်ပြီး ၎င်းကို အသုံးပြုသည့်အခါ အန္တရာယ်များကို နားလည်စေပါသည်။
- ဆောင်းပါးကို နားလည်ရန် အခြေခံ ဗဟုသုတ လိုအပ်ပါသည်။
C ဘာသာစကား နှင့် network application development တွင် အတွေ့အကြုံအချို့။ - ကုဒ်အားလုံးကို C ဘာသာစကားဖြင့် တိကျစွာရေးထားသည် ((သတိပြုရန်- ရှည်လျားသော PDF)
C11 စံနှုန်းသို့ Linux အတွက် နှင့် တွင် ရနိုင်ပါသည်။GitHub .
အဘယျကွောငျ့မဟုတျလော
အင်တာနက်၏ရေပန်းစားလာသည်နှင့်အမျှ၊ ဝဘ်ဆာဗာများသည် ချိတ်ဆက်မှုအများအပြားကို တစ်ပြိုင်နက်တည်း ကိုင်တွယ်ရန် လိုအပ်လာသောကြောင့် နည်းလမ်းနှစ်သွယ်ကို ကြိုးပမ်းခဲ့သည်- OS thread အများအပြားတွင် I/O ကို ပိတ်ဆို့ခြင်းနှင့် I/O နှင့် ပေါင်းစပ်ခြင်းမဟုတ်သော ပိတ်ဆို့ခြင်း “system selector” လို့လည်း ခေါ်တဲ့ ပွဲသတိပေးချက်စနစ် (
ပထမချဉ်းကပ်မှုတွင် အဝင်ချိတ်ဆက်မှုတစ်ခုစီအတွက် OS thread အသစ်တစ်ခုဖန်တီးခြင်း ပါဝင်သည်။ ၎င်း၏အားနည်းချက်မှာ scalability ညံ့ဖျင်းခြင်းဖြစ်သည်- လည်ပတ်မှုစနစ်သည် များစွာအကောင်အထည်ဖော်ရမည်ဖြစ်ပါသည်။
ပြုပြင်ထားသောဗားရှင်းကို မီးမောင်းထိုးပြသည်။
ဒုတိယနည်းလမ်းကိုအသုံးပြုသည်။
ဤနည်းလမ်းများအကြား ခြားနားချက်မှာ အောက်ပါအတိုင်းဖြစ်သည်။
- I/O လုပ်ဆောင်ချက်များကို ပိတ်ဆို့ခြင်း။ ဆိုင်းငံ့ အသုံးပြုသူစီးဆင်းမှု သည်အထိOS မှန်ကန်သည်အထိ
အပိုင်းအစများ ဝင်လာIP လုပ်တာပါ။ byte stream သို့ (သည် TCP ဒေတာလက်ခံရရှိခြင်း) သို့မဟုတ် နောက်ဆက်တွဲပေးပို့ခြင်းအတွက် အတွင်းပိုင်း စာရေးကြားခံများတွင် နေရာအလုံအလောက်ရှိမည်မဟုတ်ပါ။NIC (ဒေတာပေးပို့ခြင်း)။ - စနစ်ရွေးချယ်မှု အချိန်ပို OS က ပရိုဂရမ်ကို အကြောင်းကြားတယ်။ ပြီးပြီ defragmented IP packets (TCP၊ data reception) သို့မဟုတ် internal write buffers တွင် နေရာအလုံအလောက်ရှိသည်။ ပြီးပြီ ရနိုင်သည် (ဒေတာပေးပို့ခြင်း)။
နိဂုံးချုပ်ရလျှင် I/O တစ်ခုစီအတွက် OS thread တစ်ခုကို သိမ်းဆည်းခြင်းသည် ကွန်ပြူတာ ပါဝါကို ဖြုန်းတီးခြင်း ဖြစ်သည်၊ အကြောင်းမှာ အမှန်တကယ်တွင်၊ thread များသည် အသုံးဝင်သော အလုပ်များကို မလုပ်နိုင်သောကြောင့် ဖြစ်သည်။
I/O ဓာတ်ပေါင်းဖို မော်ဒယ်
I/O ဓာတ်ပေါင်းဖိုသည် စနစ်ရွေးချယ်သူနှင့် အသုံးပြုသူကုဒ်ကြားရှိ အလွှာတစ်ခုအဖြစ် လုပ်ဆောင်သည်။ ၎င်း၏လုပ်ဆောင်မှုနိယာမကို အောက်ပါဘလောက်ပုံကြမ်းဖြင့် ဖော်ပြထားပါသည်။
- အချို့သော socket သည် ပိတ်ဆို့ခြင်းမဟုတ်သော I/O လုပ်ဆောင်ချက်ကို လုပ်ဆောင်နိုင်သည်ဟု ဖြစ်ရပ်တစ်ခုသည် သတိပေးချက်တစ်ခုဖြစ်သည်။
- Event handler သည် အဖြစ်အပျက်တစ်ခုကို လက်ခံရရှိသောအခါတွင် I/O ဓာတ်ပေါင်းဖိုမှ ခေါ်ဝေါ်သည့် လုပ်ဆောင်ချက်ဖြစ်ပြီး၊ ထို့နောက် ပိတ်ဆို့ခြင်းမရှိသော I/O လုပ်ဆောင်ချက်ကို လုပ်ဆောင်သည်။
I/O ဓာတ်ပေါင်းဖိုသည် အဓိပ္ပါယ်ဖွင့်ဆိုချက်အားဖြင့် single-threaded ဖြစ်သည်၊ သို့သော် 1 thread: 1 reactor အချိုးဖြင့် သဘောတရားကို multi-threaded ပတ်၀န်းကျင်တွင် အသုံးပြုခြင်းမှ ရပ်တန့်ခြင်းမရှိဘဲ CPU cores အားလုံးကို ပြန်လည်အသုံးပြုနိုင်ပါသည်။
အကောင်အထည်ဖော်မှု
အများသူငှာ interface ကို ဖိုင်တစ်ခုတွင် ထားရှိပါမည်။ reactor.h
reactor.c
reactor.h
အောက်ပါ ကြေညာချက်များ ပါဝင်ပါမည်။
reactor.h တွင် ကြေငြာချက်များကို ပြပါ။
typedef struct reactor Reactor;
/*
* Указатель на функцию, которая будет вызываться I/O реактором при поступлении
* события от системного селектора.
*/
typedef void (*Callback)(void *arg, int fd, uint32_t events);
/*
* Возвращает `NULL` в случае ошибки, не-`NULL` указатель на `Reactor` в
* противном случае.
*/
Reactor *reactor_new(void);
/*
* Освобождает системный селектор, все зарегистрированные сокеты в данный момент
* времени и сам I/O реактор.
*
* Следующие функции возвращают -1 в случае ошибки, 0 в случае успеха.
*/
int reactor_destroy(Reactor *reactor);
int reactor_register(const Reactor *reactor, int fd, uint32_t interest,
Callback callback, void *callback_arg);
int reactor_deregister(const Reactor *reactor, int fd);
int reactor_reregister(const Reactor *reactor, int fd, uint32_t interest,
Callback callback, void *callback_arg);
/*
* Запускает цикл событий с тайм-аутом `timeout`.
*
* Эта функция передаст управление вызывающему коду если отведённое время вышло
* или/и при отсутствии зарегистрированных сокетов.
*/
int reactor_run(const Reactor *reactor, time_t timeout);
I/O ဓာတ်ပေါင်းဖိုတည်ဆောက်ပုံ ပါဝင်သည်။ GHashTable
CallbackData
(ဖြစ်ရပ်ကိုင်တွယ်သူ၏ဖွဲ့စည်းပုံနှင့်၎င်းအတွက်အသုံးပြုသူအငြင်းအခုံ)။
ဓာတ်ပေါင်းဖိုနှင့် CallbackData ပြပါ။
struct reactor {
int epoll_fd;
GHashTable *table; // (int, CallbackData)
};
typedef struct {
Callback callback;
void *arg;
} CallbackData;
ကျွန်ုပ်တို့သည် ကိုင်တွယ်နိုင်စွမ်းကို ဖွင့်ထားကြောင်း သတိပြုပါ။ reactor.h
ဖွဲ့စည်းပုံကို ကျနော်တို့ ကြေညာတယ်။ reactor
နှင့် reactor.c
ကျွန်ုပ်တို့သည် ၎င်းကိုသတ်မှတ်ထားသောကြောင့် သုံးစွဲသူအား ၎င်း၏နယ်ပယ်များကို ပြတ်သားစွာပြောင်းလဲခြင်းမှ ကာကွယ်ပေးပါသည်။ ဒါက ပုံစံတွေထဲက တစ်ခုပါ။
လုပ်ငန်းဆောင်တာ reactor_register
, reactor_deregister
и reactor_reregister
စနစ်ရွေးချယ်သည့်စနစ်နှင့် hash ဇယားရှိ စိတ်ဝင်စားဖွယ်ခြေစွပ်များစာရင်းနှင့် သက်ဆိုင်သည့်ဖြစ်ရပ်ကိုင်တွယ်သူများကို အပ်ဒိတ်လုပ်ပါ။
မှတ်ပုံတင်ခြင်းလုပ်ဆောင်ချက်များကိုပြသပါ။
#define REACTOR_CTL(reactor, op, fd, interest)
if (epoll_ctl(reactor->epoll_fd, op, fd,
&(struct epoll_event){.events = interest,
.data = {.fd = fd}}) == -1) {
perror("epoll_ctl");
return -1;
}
int reactor_register(const Reactor *reactor, int fd, uint32_t interest,
Callback callback, void *callback_arg) {
REACTOR_CTL(reactor, EPOLL_CTL_ADD, fd, interest)
g_hash_table_insert(reactor->table, int_in_heap(fd),
callback_data_new(callback, callback_arg));
return 0;
}
int reactor_deregister(const Reactor *reactor, int fd) {
REACTOR_CTL(reactor, EPOLL_CTL_DEL, fd, 0)
g_hash_table_remove(reactor->table, &fd);
return 0;
}
int reactor_reregister(const Reactor *reactor, int fd, uint32_t interest,
Callback callback, void *callback_arg) {
REACTOR_CTL(reactor, EPOLL_CTL_MOD, fd, interest)
g_hash_table_insert(reactor->table, int_in_heap(fd),
callback_data_new(callback, callback_arg));
return 0;
}
I/O ဓာတ်ပေါင်းဖိုသည် အဖြစ်အပျက်ကို ဖော်ပြချက်ဖြင့် ကြားဖြတ်ပြီးနောက် fd
၎င်းသည် ဖြတ်သန်းသွားသော သက်ဆိုင်ရာ event handler ကို ခေါ်သည်။ fd
, void
.
reactor_run() လုပ်ဆောင်ချက်ကို ပြပါ။
int reactor_run(const Reactor *reactor, time_t timeout) {
int result;
struct epoll_event *events;
if ((events = calloc(MAX_EVENTS, sizeof(*events))) == NULL)
abort();
time_t start = time(NULL);
while (true) {
time_t passed = time(NULL) - start;
int nfds =
epoll_wait(reactor->epoll_fd, events, MAX_EVENTS, timeout - passed);
switch (nfds) {
// Ошибка
case -1:
perror("epoll_wait");
result = -1;
goto cleanup;
// Время вышло
case 0:
result = 0;
goto cleanup;
// Успешная операция
default:
// Вызвать обработчиков событий
for (int i = 0; i < nfds; i++) {
int fd = events[i].data.fd;
CallbackData *callback =
g_hash_table_lookup(reactor->table, &fd);
callback->callback(callback->arg, fd, events[i].events);
}
}
}
cleanup:
free(events);
return result;
}
အနှစ်ချုပ်ရန်၊ အသုံးပြုသူကုဒ်ရှိ လုပ်ဆောင်ချက်ခေါ်ဆိုမှုများ၏ကွင်းဆက်သည် အောက်ပါပုံစံအတိုင်း ဖြစ်လိမ့်မည်-
Single threaded server
မြင့်မားသောဝန်အောက်ရှိ I/O ဓာတ်ပေါင်းဖိုကို စမ်းသပ်ရန်အတွက်၊ ပုံတစ်ခုနှင့် မည်သည့်တောင်းဆိုမှုကိုမဆို တုံ့ပြန်သည့် ရိုးရှင်းသော HTTP ဝဘ်ဆာဗာကို ကျွန်ုပ်တို့ ရေးသားပါမည်။
HTTP ပရိုတိုကောကို အမြန်ကိုးကား
HTTP ကို အလွယ်တကူ အသုံးပြုနိုင်သည်။
တောင်းဆိုမှုပုံစံ
<КОМАНДА> <URI> <ВЕРСИЯ HTTP>CRLF
<ЗАГОЛОВОК 1>CRLF
<ЗАГОЛОВОК 2>CRLF
<ЗАГОЛОВОК N>CRLF CRLF
<ДАННЫЕ>
CRLF
ဇာတ်ကောင်နှစ်ခု၏ အတွဲတစ်ခုဖြစ်ပါသည်r
иn
တောင်းဆိုချက်၏ ပထမစာကြောင်း၊ ခေါင်းစီးများနှင့် ဒေတာကို ပိုင်းခြားထားသည်။<КОМАНДА>
- တယောCONNECT
,DELETE
,GET
,HEAD
,OPTIONS
,PATCH
,POST
,PUT
,TRACE
. ဘရောက်ဆာသည် ကျွန်ုပ်တို့၏ဆာဗာသို့ အမိန့်တစ်ခုပေးပို့လိမ့်မည်။GET
အဓိပ္ပါယ်မှာ "ဖိုင်၏ အကြောင်းအရာများကို ကျွန်ုပ်ထံ ပေးပို့ပါ။"<URI>
-တူညီသောအရင်းအမြစ်သတ်မှတ်မှု . ဥပမာ URI = ဆိုရင်/index.html
ထို့နောက် ဖောက်သည်သည် ဆိုက်၏ ပင်မစာမျက်နှာကို တောင်းဆိုသည်။<ВЕРСИЯ HTTP>
— ဖော်မတ်ရှိ HTTP ပရိုတိုကော၏ဗားရှင်းHTTP/X.Y
. ယနေ့ခေတ်တွင် အသုံးအများဆုံး ဗားရှင်းဖြစ်သည်။HTTP/1.1
.<ЗАГОЛОВОК N>
ဖော်မတ်ရှိ သော့တန်ဖိုးအတွဲတစ်ခုဖြစ်သည်။<КЛЮЧ>: <ЗНАЧЕНИЕ>
နောက်ထပ်ခွဲခြမ်းစိတ်ဖြာမှုအတွက် ဆာဗာသို့ ပေးပို့ခဲ့သည်။<ДАННЫЕ>
- လုပ်ဆောင်ချက်ကို လုပ်ဆောင်ရန်အတွက် ဆာဗာမှ လိုအပ်သော အချက်အလက်။ မကြာခဏ ရိုးရှင်းပါသည်။JSON သို့မဟုတ် အခြားပုံစံ။
တုံ့ပြန်မှုပုံစံ
<ВЕРСИЯ HTTP> <КОД СТАТУСА> <ОПИСАНИЕ СТАТУСА>CRLF
<ЗАГОЛОВОК 1>CRLF
<ЗАГОЛОВОК 2>CRLF
<ЗАГОЛОВОК N>CRLF CRLF
<ДАННЫЕ>
<КОД СТАТУСА>
လုပ်ဆောင်ချက်၏ ရလဒ်ကို ကိုယ်စားပြုသော နံပါတ်တစ်ခုဖြစ်သည်။ ကျွန်ုပ်တို့၏ဆာဗာသည် အခြေအနေ 200 (အောင်မြင်သော လုပ်ဆောင်ချက်) ကို အမြဲတမ်း ပြန်ပေးပါမည်။<ОПИСАНИЕ СТАТУСА>
— အခြေအနေကုဒ်၏ စာကြောင်းကို ကိုယ်စားပြုခြင်း။ အခြေအနေကုဒ် 200 အတွက် ဒါကOK
.<ЗАГОЛОВОК N>
- တောင်းဆိုချက်တွင် ပါရှိသည့် ဖော်မတ်၏ ခေါင်းစီး။ ခေါင်းစဉ်တွေ ပြန်ပေးမယ်။Content-Length
(ဖိုင်အရွယ်အစား) နှင့်Content-Type: text/html
(ဒေတာအမျိုးအစားကို ပြန်ပေးသည်)။<ДАННЫЕ>
- အသုံးပြုသူမှတောင်းဆိုထားသောဒေတာ။ ကျွန်ုပ်တို့၏အခြေအနေတွင်၊ ဤအရာသည် ပုံသဏ္ဌာန်အတွက် လမ်းကြောင်းဖြစ်သည်။HTML ကို .
ဖိုင် http_server.c
common.h
common.h တွင် လုပ်ဆောင်ချက် ရှေ့ပြေးပုံစံများကို ပြပါ။
/*
* Обработчик событий, который вызовется после того, как сокет будет
* готов принять новое соединение.
*/
static void on_accept(void *arg, int fd, uint32_t events);
/*
* Обработчик событий, который вызовется после того, как сокет будет
* готов отправить HTTP ответ.
*/
static void on_send(void *arg, int fd, uint32_t events);
/*
* Обработчик событий, который вызовется после того, как сокет будет
* готов принять часть HTTP запроса.
*/
static void on_recv(void *arg, int fd, uint32_t events);
/*
* Переводит входящее соединение в неблокирующий режим.
*/
static void set_nonblocking(int fd);
/*
* Печатает переданные аргументы в stderr и выходит из процесса с
* кодом `EXIT_FAILURE`.
*/
static noreturn void fail(const char *format, ...);
/*
* Возвращает файловый дескриптор сокета, способного принимать новые
* TCP соединения.
*/
static int new_server(bool reuse_port);
functional macro ကိုလည်း ဖော်ပြထားတယ်။ SAFE_CALL()
နှင့် function ကိုသတ်မှတ်ထားသည်။ fail()
. မက်ခရိုသည် စကားရပ်၏တန်ဖိုးကို အမှားနှင့် နှိုင်းယှဉ်ပြီး အခြေအနေမှန်ပါက လုပ်ဆောင်ချက်ကို ခေါ်သည် fail()
:
#define SAFE_CALL(call, error)
do {
if ((call) == error) {
fail("%s", #call);
}
} while (false)
လုပ်ဆောင်ချက် fail()
terminal သို့ ဖြတ်သွားသော အကြောင်းပြချက်များကို print ထုတ်သည် (ကဲ့သို့သော printf()
EXIT_FAILURE
:
static noreturn void fail(const char *format, ...) {
va_list args;
va_start(args, format);
vfprintf(stderr, format, args);
va_end(args);
fprintf(stderr, ": %sn", strerror(errno));
exit(EXIT_FAILURE);
}
လုပ်ဆောင်ချက် new_server()
စနစ်ခေါ်ဆိုမှုများဖြင့် ဖန်တီးထားသော "ဆာဗာ" ဆော့ဖ်ဝဲ၏ ဖိုင်ဖော်ပြချက်အား ပြန်ပေးသည်။ socket()
bind()
listen()
new_server() လုပ်ဆောင်ချက်ကို ပြပါ။
static int new_server(bool reuse_port) {
int fd;
SAFE_CALL((fd = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, IPPROTO_TCP)),
-1);
if (reuse_port) {
SAFE_CALL(
setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &(int){1}, sizeof(int)),
-1);
}
struct sockaddr_in addr = {.sin_family = AF_INET,
.sin_port = htons(SERVER_PORT),
.sin_addr = {.s_addr = inet_addr(SERVER_IPV4)},
.sin_zero = {0}};
SAFE_CALL(bind(fd, (struct sockaddr *)&addr, sizeof(addr)), -1);
SAFE_CALL(listen(fd, SERVER_BACKLOG), -1);
return fd;
}
- အလံကို အသုံးပြု၍ ပိတ်ဆို့ခြင်းမဟုတ်သောမုဒ်တွင် socket ကို အစပိုင်းတွင် ဖန်တီးထားကြောင်း သတိပြုပါ။
SOCK_NONBLOCK
ဒါမှ function ထဲမှာon_accept()
(အပြည့်အစုံဖတ်ရန်) စနစ်ခေါ်ဆိုမှုaccept()
thread execution ကို မရပ်တန့်ခဲ့ပါ။ - လျှင်
reuse_port
ညီမျှသည်true
ထို့နောက် ဤလုပ်ဆောင်ချက်သည် ရွေးချယ်စရာဖြင့် socket ကို configure လုပ်မည်ဖြစ်သည်။ ဖြတ်.SO_REUSEPORT
Multi-threaded ပတ်၀န်းကျင်တွင် တူညီသော port ကိုအသုံးပြုရန် (ကဏ္ဍ “Multi-threaded server” ကိုကြည့်ပါ)။setsockopt()
ပွဲကိုင်တွယ်သူ on_accept()
OS သည် ဖြစ်ရပ်တစ်ခုကို ထုတ်ပေးပြီးနောက် ခေါ်သည်။ EPOLLIN
ဤကိစ္စတွင်၊ ချိတ်ဆက်မှုအသစ်ကို လက်ခံနိုင်သည်ဟု ဆိုလိုသည်။ on_accept()
ချိတ်ဆက်မှုအသစ်ကို လက်ခံသည်၊ ၎င်းကို ပိတ်ဆို့ခြင်းမပြုသည့်မုဒ်သို့ ပြောင်းပြီး ဖြစ်ရပ်ကိုင်တွယ်သူနှင့် စာရင်းသွင်းပါ။ on_recv()
I/O ဓာတ်ပေါင်းဖိုတစ်ခုတွင်။
on_accept() လုပ်ဆောင်ချက်ကို ပြပါ။
static void on_accept(void *arg, int fd, uint32_t events) {
int incoming_conn;
SAFE_CALL((incoming_conn = accept(fd, NULL, NULL)), -1);
set_nonblocking(incoming_conn);
SAFE_CALL(reactor_register(reactor, incoming_conn, EPOLLIN, on_recv,
request_buffer_new()),
-1);
}
ပွဲကိုင်တွယ်သူ on_recv()
OS သည် ဖြစ်ရပ်တစ်ခုကို ထုတ်ပေးပြီးနောက် ခေါ်သည်။ EPOLLIN
၊ ဤကိစ္စတွင် ချိတ်ဆက်မှု မှတ်ပုံတင်သည်ဟု အဓိပ္ပာယ်ရသည်။ on_accept()
ဒေတာလက်ခံရန် အဆင်သင့်ဖြစ်သည်။
on_recv()
HTTP တောင်းဆိုချက်ကို လုံးဝလက်ခံရရှိသည်အထိ ချိတ်ဆက်မှုမှ ဒေတာကို ဖတ်ပြီး ကိုင်တွယ်သူအား မှတ်ပုံတင်သည်။ on_send()
HTTP တုံ့ပြန်မှုပေးပို့ရန်။ client သည် ချိတ်ဆက်မှုကို ဖြတ်တောက်ပါက၊ socket ကို အသုံးပြု၍ မှတ်ပုံတင်ခြင်းကို ဖျက်သိမ်းပြီး ပိတ်သွားမည်ဖြစ်သည်။ close()
on_recv() လုပ်ဆောင်ချက်ကို ပြပါ
static void on_recv(void *arg, int fd, uint32_t events) {
RequestBuffer *buffer = arg;
// Принимаем входные данные до тех пор, что recv возвратит 0 или ошибку
ssize_t nread;
while ((nread = recv(fd, buffer->data + buffer->size,
REQUEST_BUFFER_CAPACITY - buffer->size, 0)) > 0)
buffer->size += nread;
// Клиент оборвал соединение
if (nread == 0) {
SAFE_CALL(reactor_deregister(reactor, fd), -1);
SAFE_CALL(close(fd), -1);
request_buffer_destroy(buffer);
return;
}
// read вернул ошибку, отличную от ошибки, при которой вызов заблокирует
// поток
if (errno != EAGAIN && errno != EWOULDBLOCK) {
request_buffer_destroy(buffer);
fail("read");
}
// Получен полный HTTP запрос от клиента. Теперь регистрируем обработчика
// событий для отправки данных
if (request_buffer_is_complete(buffer)) {
request_buffer_clear(buffer);
SAFE_CALL(reactor_reregister(reactor, fd, EPOLLOUT, on_send, buffer),
-1);
}
}
ပွဲကိုင်တွယ်သူ on_send()
OS သည် ဖြစ်ရပ်တစ်ခုကို ထုတ်ပေးပြီးနောက် ခေါ်သည်။ EPOLLOUT
ဆိုလိုတာက ဆက်သွယ်မှု မှတ်ပုံတင်ထားတယ်။ on_recv()
ဒေတာပေးပို့ရန် အဆင်သင့်ဖြစ်ပါပြီ။ ဤလုပ်ဆောင်ချက်သည် HTML ပါ၀င်သော HTTP တုံ့ပြန်မှုတစ်ခုအား သုံးစွဲသူထံ ပုံတစ်ခုနှင့် ပေးပို့ပြီးနောက် ဖြစ်ရပ်ကိုင်တွယ်သူကို ပြန်ပြောင်းပေးသည်။ on_recv()
.
on_send() လုပ်ဆောင်ချက်ကို ပြပါ။
static void on_send(void *arg, int fd, uint32_t events) {
const char *content = "<img "
"src="https://habrastorage.org/webt/oh/wl/23/"
"ohwl23va3b-dioerobq_mbx4xaw.jpeg">";
char response[1024];
sprintf(response,
"HTTP/1.1 200 OK" CRLF "Content-Length: %zd" CRLF "Content-Type: "
"text/html" DOUBLE_CRLF "%s",
strlen(content), content);
SAFE_CALL(send(fd, response, strlen(response), 0), -1);
SAFE_CALL(reactor_reregister(reactor, fd, EPOLLIN, on_recv, arg), -1);
}
နောက်ဆုံးတော့ ဖိုင်ထဲမှာ http_server.c
function တွင်၊ main()
ကျွန်ုပ်တို့သည် I/O ဓာတ်ပေါင်းဖိုကို အသုံးပြု၍ ဖန်တီးပါသည်။ reactor_new()
ဆာဗာတစ်ခု ဖန်တီးပြီး မှတ်ပုံတင်ပါ၊ ဓာတ်ပေါင်းဖိုကို အသုံးပြု၍ စတင်ပါ။ reactor_run()
တစ်မိနစ်တိတိ၊ ထို့နောက် ကျွန်ုပ်တို့သည် အရင်းအမြစ်များကို ထုတ်လွှတ်ပြီး ပရိုဂရမ်မှ ထွက်သည်။
http_server.c ကိုပြပါ။
#include "reactor.h"
static Reactor *reactor;
#include "common.h"
int main(void) {
SAFE_CALL((reactor = reactor_new()), NULL);
SAFE_CALL(
reactor_register(reactor, new_server(false), EPOLLIN, on_accept, NULL),
-1);
SAFE_CALL(reactor_run(reactor, SERVER_TIMEOUT_MILLIS), -1);
SAFE_CALL(reactor_destroy(reactor), -1);
}
အားလုံးမျှော်လင့်ထားသည့်အတိုင်း အလုပ်ဖြစ်မဖြစ် စစ်ဆေးကြပါစို့။ ပြုစုခြင်း (chmod a+x compile.sh && ./compile.sh
ပရောဂျက် root တွင်) နှင့် ကိုယ်တိုင်ရေးထားသော ဆာဗာကို ဖွင့်ပါ၊ ဖွင့်ပါ။
စွမ်းဆောင်ရည် တိုင်းတာခြင်း။
ကျွန်ုပ်၏ကားသတ်မှတ်ချက်များကို ပြပါ။
$ screenfetch
MMMMMMMMMMMMMMMMMMMMMMMMMmds+. OS: Mint 19.1 tessa
MMm----::-://////////////oymNMd+` Kernel: x86_64 Linux 4.15.0-20-generic
MMd /++ -sNMd: Uptime: 2h 34m
MMNso/` dMM `.::-. .-::.` .hMN: Packages: 2217
ddddMMh dMM :hNMNMNhNMNMNh: `NMm Shell: bash 4.4.20
NMm dMM .NMN/-+MMM+-/NMN` dMM Resolution: 1920x1080
NMm dMM -MMm `MMM dMM. dMM DE: Cinnamon 4.0.10
NMm dMM -MMm `MMM dMM. dMM WM: Muffin
NMm dMM .mmd `mmm yMM. dMM WM Theme: Mint-Y-Dark (Mint-Y)
NMm dMM` ..` ... ydm. dMM GTK Theme: Mint-Y [GTK2/3]
hMM- +MMd/-------...-:sdds dMM Icon Theme: Mint-Y
-NMm- :hNMNNNmdddddddddy/` dMM Font: Noto Sans 9
-dMNs-``-::::-------.`` dMM CPU: Intel Core i7-6700 @ 8x 4GHz [52.0°C]
`/dMNmy+/:-------------:/yMMM GPU: NV136
./ydNMMMMMMMMMMMMMMMMMMMMM RAM: 2544MiB / 7926MiB
.MMMMMMMMMMMMMMMMMMM
single-threaded server ၏ စွမ်းဆောင်ရည်ကို တိုင်းတာကြပါစို့။ Terminal နှစ်ခုကိုဖွင့်ကြပါစို့။ တစ်ခုတွင် ကျွန်ုပ်တို့ လုပ်ဆောင်ပါမည်။ ./http_server
မတူဘူး-၊
$ wrk -c100 -d1m -t8 http://127.0.0.1:18470 -H "Host: 127.0.0.1:18470" -H "Accept-Language: en-US,en;q=0.5" -H "Connection: keep-alive"
Running 1m test @ http://127.0.0.1:18470
8 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 493.52us 76.70us 17.31ms 89.57%
Req/Sec 24.37k 1.81k 29.34k 68.13%
11657769 requests in 1.00m, 1.60GB read
Requests/sec: 193974.70
Transfer/sec: 27.19MB
ကျွန်ုပ်တို့၏ single-threaded server သည် ချိတ်ဆက်မှု 11 မှ အစပြု၍ တစ်မိနစ်လျှင် တောင်းဆိုမှု 100 သန်းကျော်ကို လုပ်ဆောင်နိုင်ခဲ့ပါသည်။ ရလဒ်မကောင်းပေမယ့် ပိုမိုကောင်းမွန်အောင် လုပ်နိုင်ပါ့မလား။
Multithreaded ဆာဗာ
အထက်တွင်ဖော်ပြခဲ့သည့်အတိုင်း I/O ဓာတ်ပေါင်းဖိုအား CPU cores အားလုံးကို အသုံးပြု၍ သီးခြား thread များဖြင့် ဖန်တီးနိုင်သည်။ ဤနည်းလမ်းကို လက်တွေ့ကျင့်သုံးကြပါစို့။
http_server_multithreaded.c ကို ပြပါ။
#include "reactor.h"
static Reactor *reactor;
#pragma omp threadprivate(reactor)
#include "common.h"
int main(void) {
#pragma omp parallel
{
SAFE_CALL((reactor = reactor_new()), NULL);
SAFE_CALL(reactor_register(reactor, new_server(true), EPOLLIN,
on_accept, NULL),
-1);
SAFE_CALL(reactor_run(reactor, SERVER_TIMEOUT_MILLIS), -1);
SAFE_CALL(reactor_destroy(reactor), -1);
}
}
ခုတော့ ကြိုးတိုင်း
static Reactor *reactor;
#pragma omp threadprivate(reactor)
function argument ကို သတိပြုပါ။ new_server()
လုပ်ရပ်များ true
. ဆိုလိုသည်မှာကျွန်ုပ်တို့သည်ဆာဗာ socket တွင်ရွေးချယ်စရာကိုသတ်မှတ်ပေးသည်။ SO_REUSEPORT
ဒုတိယအပြေး
ယခု Multi-threaded ဆာဗာ၏ စွမ်းဆောင်ရည်ကို တိုင်းတာကြပါစို့။
$ wrk -c100 -d1m -t8 http://127.0.0.1:18470 -H "Host: 127.0.0.1:18470" -H "Accept-Language: en-US,en;q=0.5" -H "Connection: keep-alive"
Running 1m test @ http://127.0.0.1:18470
8 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 1.14ms 2.53ms 40.73ms 89.98%
Req/Sec 79.98k 18.07k 154.64k 78.65%
38208400 requests in 1.00m, 5.23GB read
Requests/sec: 635876.41
Transfer/sec: 89.14MB
1 မိနစ်အတွင်း လုပ်ဆောင်ခဲ့သော တောင်းဆိုမှုအရေအတွက်သည် ~3.28 ဆ တိုးလာသည်။ ဒါပေမယ့် အဝိုင်းနံပါတ်ရဲ့ XNUMX သန်းလောက်သာ ပျက်နေတာကြောင့် အဲဒါကို ပြင်ကြည့်ရအောင်။
ပထမဆုံး ထုတ်ပေးလိုက်တဲ့ စာရင်းဇယားကို ကြည့်ရအောင်
$ sudo perf stat -B -e task-clock,context-switches,cpu-migrations,page-faults,cycles,instructions,branches,branch-misses,cache-misses ./http_server_multithreaded
Performance counter stats for './http_server_multithreaded':
242446,314933 task-clock (msec) # 4,000 CPUs utilized
1 813 074 context-switches # 0,007 M/sec
4 689 cpu-migrations # 0,019 K/sec
254 page-faults # 0,001 K/sec
895 324 830 170 cycles # 3,693 GHz
621 378 066 808 instructions # 0,69 insn per cycle
119 926 709 370 branches # 494,653 M/sec
3 227 095 669 branch-misses # 2,69% of all branches
808 664 cache-misses
60,604330670 seconds time elapsed
-march=native
, MAX_EVENTS
အသုံးပြု EPOLLET
စွမ်းဆောင်ရည် သိသိသာသာ တိုးမလာပါ။ ဒါပေမယ့် သင် တပြိုင်နက်တည်း ချိတ်ဆက်မှု အရေအတွက်ကို တိုးပေးရင် ဘာဖြစ်မလဲ။
352 တပြိုင်နက်ချိတ်ဆက်မှုများအတွက်စာရင်းအင်းများ
$ wrk -c352 -d1m -t8 http://127.0.0.1:18470 -H "Host: 127.0.0.1:18470" -H "Accept-Language: en-US,en;q=0.5" -H "Connection: keep-alive"
Running 1m test @ http://127.0.0.1:18470
8 threads and 352 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 2.12ms 3.79ms 68.23ms 87.49%
Req/Sec 83.78k 12.69k 169.81k 83.59%
40006142 requests in 1.00m, 5.48GB read
Requests/sec: 665789.26
Transfer/sec: 93.34MB
လိုချင်သောရလဒ်ကို ရရှိခဲ့ပြီး ၎င်းနှင့်အတူ ချိတ်ဆက်မှုအရေအတွက်အပေါ် 1 မိနစ်အတွင်း လုပ်ဆောင်ပြီးသော တောင်းဆိုမှုအရေအတွက်၏ မှီခိုမှုကို ပြသသည့် စိတ်ဝင်စားဖွယ်ဂရပ်ဖစ်သည်-
ချိတ်ဆက်မှု နှစ်ရာကျော်ပြီးနောက်၊ ဆာဗာနှစ်ခုလုံးအတွက် စီမံဆောင်ရွက်ထားသည့် တောင်းဆိုချက်အရေအတွက်သည် သိသိသာသာ ကျဆင်းသွားသည်ကို ကျွန်ုပ်တို့တွေ့မြင်ရသည် (ကဏ္ဍပေါင်းစုံဗားရှင်းတွင် ၎င်းသည် ပို၍သိသာသည်)။ ၎င်းသည် Linux TCP/IP stack အကောင်အထည်ဖော်မှုနှင့် ဆက်စပ်မှုရှိပါသလား။ ဂရပ်၏ ဤအပြုအမူနှင့် ပတ်သက်သော သင့်ယူဆချက်များကို မှတ်ချက်များတွင် ကြိုးမျိုးစုံနှင့် ကြိုးတန်းတစ်ခုတည်း ရွေးချယ်စရာများအတွက် အကောင်းဆုံးဖြစ်အောင် ရေးပါ။
ဘယ်လို
I/O ဓာတ်ပေါင်းဖို၏ အားနည်းချက်များ
I/O ဓာတ်ပေါင်းဖိုသည် ၎င်း၏ အားနည်းချက်များ မရှိဘဲ မဟုတ်ကြောင်း နားလည်ထားရန် လိုအပ်သည်-
- Multi-threaded ပတ်၀န်းကျင်တွင် I/O ဓာတ်ပေါင်းဖိုကို အသုံးပြုခြင်းသည် အတန်ငယ် ပိုခက်ခဲသောကြောင့် ဖြစ်သည်။ စီးဆင်းမှုများကို သင်ကိုယ်တိုင် စီမံခန့်ခွဲရမည်ဖြစ်သည်။
- လေ့ကျင့်ခန်းအများစုတွင် ဝန်သည် ယူနီဖောင်းမဟုတ်သောကြောင့် ကြိုးတစ်ခုအား အလုပ်ရှုပ်နေချိန်တွင် ကြိုးတစ်ချောင်းကို ဖြတ်တောက်သွားစေနိုင်သည် ။
- ဖြစ်ရပ်ကိုင်တွယ်သူသည် စာတွဲတစ်ခုအား ပိတ်ဆို့ပါက၊ စနစ်ရွေးချယ်သူကိုယ်တိုင်လည်း ပိတ်ဆို့သွားမည်ဖြစ်ပြီး၊ ရှာရခက်သော ချွတ်ယွင်းချက်များကို ဖြစ်ပေါ်စေနိုင်သည်။
ဒီပြဿနာတွေကို ဖြေရှင်းပေးတယ်။
ကောက်ချက်
ဤနေရာတွင် ကျွန်ုပ်တို့၏ သီအိုရီမှ ပရိုဖိုင်းအိတ်ဇောသို့ တည့်တည့်ခရီး ပြီးဆုံးသွားပါသည်။
ကွန်ရက်ဆော့ဖ်ဝဲလ်ကို အဆင်ပြေပြေ နှင့် မြန်နှုန်းအမျိုးမျိုးဖြင့် ရေးသားခြင်းအတွက် အခြားအညီအမျှ စိတ်ဝင်စားစရာကောင်းသော နည်းလမ်းများစွာရှိသောကြောင့် ၎င်းကို သင်မနေသင့်ပါ။ စိတ်ဝင်စားစရာကောင်းတာက ကျွန်တော့်အမြင်အရတော့ လင့်ခ်တွေကို အောက်မှာပေးထားပါတယ်။
လာမယ့်အချိန်အထိ!
စိတ်ဝင်စားစရာစီမံကိန်းများ
တခြားဘာတွေဖတ်ရမလဲ။
https://linux.die.net/man/7/socket https://stackoverflow.com/questions/1050222/what-is-the-difference-between-concurrency-and-parallelism http://www.kegel.com/c10k.html https://kernel.dk/io_uring.pdf https://aturon.github.io/blog/2016/09/07/futures-design/ https://tokio.rs/blog/2019-10-scheduler/ https://www.artima.com/articles/io_design_patterns.html https://habr.com/en/post/183832/
source: www.habr.com