Dobra, po pierwsze, jeśli coś masz i działa, zazwyczaj dobrym pomysłem jest pozostawienie tego w ten sposób. Po co naprawiać to, co nie jest zepsute?
Ale jeśli masz problemy i naprawdę chcesz przepisać kod sieci, myślę, że masz cztery główne opcje:
- Wielowątkowy kod blokujący (co teraz robisz)
- Gniazda nieblokujące z powiadomieniem wyzwalanym poziomem
- Gniazda nieblokujące z powiadomieniem o zmianie gotowości
- Gniazda asynchroniczne
Po napisaniu wielu klientów i serwerów dla wielu graczy (od peer-to-peer do masowo multiplayer) lubię myśleć, że opcja 2 prowadzi do najmniejszej złożoności, z całkiem dobrą wydajnością, zarówno dla części serwerowej, jak i klienckiej. W drugiej chwili wybrałbym opcję 4, ale zwykle wymaga to ponownego przemyślenia całego programu, i w większości uważam, że jest to przydatne dla serwerów, a nie klientów.
W szczególności chciałbym odradzać blokowanie gniazd w środowisku wielowątkowym, ponieważ zazwyczaj prowadzi to do blokowania i innych funkcji synchronizacji, które nie tylko znacznie zwiększają złożoność kodu, ale mogą również obniżać jego wydajność, ponieważ niektóre wątki czekają na inni
Ale przede wszystkim większość implementacji gniazd (a przy tym większość implementacji I / O) nie blokuje na najniższym poziomie. Operacje blokujące są po prostu przewidziane, aby uprościć tworzenie trywialnych programów. Gdy gniazdo blokuje, procesor w tym wątku jest całkowicie bezczynny, więc po co budować abstrakcję nieblokującą nad abstrakcją blokującą nad zadaniem już nieblokującym?
Nieblokujące programowanie gniazd jest nieco zniechęcające, jeśli jeszcze tego nie próbowałeś, ale okazuje się, że jest dość proste, a jeśli już odpytujesz dane wejściowe, masz już nastawienie do wykonywania gniazd nieblokujących.
Pierwszą rzeczą, którą chcesz zrobić, to ustawić gniazdo na nieblokujące. Robisz to z fcntl()
.
Po tym, zanim to zrobisz send()
, recv()
, sendto()
, recvfrom()
, accept()
( connect()
jest to inny bit) lub innych połączeń, które mogłyby zablokować wątek, zadzwonić select()
na gniazda. select()
informuje, czy na gnieździe można wykonać kolejną operację odczytu lub zapisu bez jego blokowania. W takim przypadku możesz bezpiecznie wykonać żądaną operację, a gniazdo się nie zablokuje.
Włączenie tego do gry jest dość proste. Jeśli masz już pętlę gry, na przykład taką:
while game_is_running do
poll_input()
update_world()
do_sounds()
draw_world()
end
możesz to zmienić, aby wyglądać tak:
while game_is_running do
poll_input()
read_network()
update_world()
do_sounds()
write_network()
draw_world()
end
gdzie
function read_network()
while select(socket, READ) do
game.net_input.enqueue(recv(socket))
end
end
i
function write_network()
while not game.net_output.empty and select(socket, WRITE) do
send(socket, game.net_output.dequeue())
end
end
Jeśli chodzi o zasoby, jedyną książką, którą, jak sądzę, każdy musi mieć na półkach, nawet jeśli jest to jedyna książka, jaką mają, jest „ Unix Network Programming, tom 1 ” zmarłego Richarda Stevensa. Nie ma znaczenia, czy wykonujesz programowanie w systemie Windows, innym systemie operacyjnym lub języku. Nie myśl, że rozumiesz gniazda, dopóki nie przeczytasz tej książki.
Innym źródłem, w którym można znaleźć ogólny przegląd dostępnych rozwiązań w zakresie programowania z wieloma gniazdami (głównie istotne dla programowania serwera), jest ta strona .
Nie napisałeś, którego języka programowania używasz, ale większość języków zapewnia dobre asynchroniczne struktury gniazd, które często wykorzystują funkcje podstawowego systemu operacyjnego.
Kiedy piszesz własną implementację wątkową opartą na blokujących gniazdach (pamiętaj: gdy masz wielu klientów, potrzebujesz wątku dla każdego gniazda), po prostu odkryjesz na nowo to, co już oferuje asynchroniczna struktura gniazd.
Polecam więc korzystanie z gniazd asynchronicznych.
źródło
N
klient obsługuje klientów? Jeśli blokujesz i chcesz mieć pewność, że każde gniazdo zostanie przetworzone, niezależnie od stanu pozostałych gniazd, musisz mieć jeden wątek na gniazdo. To lub nie używaj gniazd blokujących.