Boost Asio - Przesyłanie blokuje się za 10/15 razem

0

Witam. Mam problem z serverem/clientem. Client łączy się z serverem, przesyła dane pierwsze 10/15 razy, później otrzymuję komunikat - Nawiązane połączenie zostało przerwane przez oprogramowanie zainstalowane w komputerze - hoście.
Ktoś wie o co może chodzić? Antywirus, firewall etc. powyłączane.

Client:
try
{
    boost::asio::io_service IO_Service;
    boost::asio::ip::tcp::socket Socket(IO_Service);
    boost::asio::ip::tcp::resolver Resolver(IO_Service);
 
    boost::asio::connect(Socket, Resolver.resolve({ SERVER_IP, SERVER_PORT }));
 
    while (true)
    {
        if (!Buffer.empty())
        {
            std::string BufferToSend(Buffer.front());
            boost::asio::write(Socket, boost::asio::buffer(BufferToSend, BufferToSend.length()));
            Buffer.pop();
 
        }
    }
}
catch (std::exception& Error)
{
    std::cout << Error.what() << std::endl;
}
Server:
try
{
    boost::asio::io_service IO_Service;
    tcp::acceptor Acceptor(IO_Service, tcp::endpoint(tcp::v4(), SERVER_PORT));
 
    while (true)
    {
        tcp::socket Socket(IO_Service);
        Acceptor.accept(Socket);
 
        char Buffer[65536] = { 0 };
        boost::system::error_code Error;
 
        size_t Lenght = Socket.read_some(boost::asio::buffer(Buffer), Error);
    }
}
catch (std::exception& Error)
{
    std::cout << Error.what() << std::endl;
}
0

https://forum.muve.pl/index.php?/topic/300-dziwny-b%C5%82%C4%85d-w-trakcie-gry-b%C5%82%C4%85d10053/

postanowiłem troszkę poprzeszukiwać zasoby Internetu i porozmawiać ze znajomym z branży informatycznej, więc napiszę tutaj co się dowiedziałem. Problem jest dość często spotykany, lecz brak dla niego dokładnych rozwiązań. Występuje także pod nazwą "629 połączenie zostało zamknięte przez zdalny komputer". Zezwól aplikacji na dostęp przez Zaporę Ogniową (możesz wyłączyć Firewall'a całkowicie), upewnij się, że nic nie blokuje Twoje łącza - pakiety zabezpieczające (antywirus itp.). Kolejną solucją może okazać się odblokowanie odpowiednie portów TCP 8080 i 4254, aby wszystko funkcjonowało prawidłowo. Wynikać to może z błędnej konfiguracji Zapory Ogniowej, bądź routera. Przypuszczalne jest, że Twój dostawca usług internetowych (np. UPC Polska) blokuje pewien ruch sieciowy - w tym celu skontaktuj się z nim.


Nigdy mi się takie coś nie zdarzyło ale to pierwsze co znalazłem w google.

0
kacper546 napisał(a):

https://forum.muve.pl/index.php?/topic/300-dziwny-b%C5%82%C4%85d-w-trakcie-gry-b%C5%82%C4%85d10053/

postanowiłem troszkę poprzeszukiwać zasoby Internetu i porozmawiać ze znajomym z branży informatycznej, więc napiszę tutaj co się dowiedziałem. Problem jest dość często spotykany, lecz brak dla niego dokładnych rozwiązań. Występuje także pod nazwą "629 połączenie zostało zamknięte przez zdalny komputer". Zezwól aplikacji na dostęp przez Zaporę Ogniową (możesz wyłączyć Firewall'a całkowicie), upewnij się, że nic nie blokuje Twoje łącza - pakiety zabezpieczające (antywirus itp.). Kolejną solucją może okazać się odblokowanie odpowiednie portów TCP 8080 i 4254, aby wszystko funkcjonowało prawidłowo. Wynikać to może z błędnej konfiguracji Zapory Ogniowej, bądź routera. Przypuszczalne jest, że Twój dostawca usług internetowych (np. UPC Polska) blokuje pewien ruch sieciowy - w tym celu skontaktuj się z nim.


Nigdy mi się takie coś nie zdarzyło ale to pierwsze co znalazłem w google.

Wydaje mi się że raczej problem tkwi w kodzie. Przykłady z dokumetacji asio działają, niestety nie potrafię zlokalizować błędu w moim kodzie.

2

Cześć,
Tak jak już napisał autor tematu problem nie jest związany w żaden sposób z działaniem antywirusa, firewalla itp. ponieważ jestem w stanie zreprodukować to u siebie na Ubuntu oraz na Archu.
Na pierwszy rzut oka wszystko wygląda OK, ale jak wiadomo w programowaniu diabeł tkwi w szczegółach, a szczególnie w programowaniu sieciowym.
Od razu zaznaczam, że jest niedziela i mi się nudzi więc post będzie długi i szczegółowy. Jeśli kogoś interesuje TYLKO rozwiązanie bez zbędnych ceregieli to niech przewinie na sam dół do sekcji 4.

1. Jak powinno być?

Abstrahując na chwilę od Boost.Asio przypomnijmy sobie jak koncepcyjnie wygląda komunikacja w ramach patternu klient-serwer. Mamy sobie 2 kolesi - serwer i klient (tak wiem klientów zazwyczaj jest wielu, ale załóżmy dla uproszczenia, że jest tylko jeden). Serwer nasłuchuje sobie na ustalonym porcie. Klient w pewnym momencie nawiązuje połączenie TCP, które zostaje zaakceptowane przez serwer.
Teraz w ramach tego połączenia TCP serwer i klient rozmawiają ze sobą aż w końcu któremuś kolesiowi się znudzi (najcześciej klientowi) i wtedy ten koleś zrywa połączenie.
W tym momencie nie ma już połączenia. Strony nie mogą ze sobą rozmawiać a każda próba rozmowy (czyli send-a z którejś strony) zakończy się błędem i zwróceniem error code-a != 0 i/lub rzuceniem wyjątku.

2. Jak jest?

Jest sobie serwer:

 
void original_server()
{
	try
	{
		boost::asio::io_service IO_Service;
		tcp::acceptor Acceptor(IO_Service, tcp::endpoint(tcp::v4(), SERVER_PORT));

		while (true)
		{
			tcp::socket Socket(IO_Service);
			Acceptor.accept(Socket);

			char Buffer[65536] = { 0 };
			boost::system::error_code Error;

			size_t Lenght = Socket.read_some(boost::asio::buffer(Buffer), Error);
		}
	}
	catch (std::exception& Error)
	{
		std::cout << Error.what() << std::endl;
	}
}

Jest sobie klient:

 
void original_client()
{
try
{
	boost::asio::io_service IO_Service;
	boost::asio::ip::tcp::socket Socket(IO_Service);
	boost::asio::ip::tcp::resolver Resolver(IO_Service);

	boost::asio::connect(Socket, Resolver.resolve({ SERVER_IP, SERVER_PORT }));

	while (true)
	{
		if (!Buffer.empty())
		{
			std::string BufferToSend(Buffer.front());
			boost::asio::write(Socket, boost::asio::buffer(BufferToSend, BufferToSend.length()));
			Buffer.pop();

		}
	}
}
catch (std::exception& Error)
{
	std::cout << Error.what() << std::endl;
}
}

Prześledźmy flow w obu procesach i porównajmy z tym co napisałem w sekcji wyżej.

  • Serwer nasłuchuje sobie na ustalonym porcie
 
Acceptor.accept(Socket);

accept() blokuje tak długo aż po drugiej stronie nastąpi connect() lub wystąpi błąd.

  • Klient w pewnym momencie nawiązuje połączenie TCP...
 
boost::asio::connect(Socket, Resolver.resolve({ SERVER_IP, SERVER_PORT }));
  • ...które zostaje zaakceptowane przez serwer
Acceptor.accept(Socket);

Zatem accept przestaje blokować i kończy się sukcesem

  • Teraz w ramach tego połączenia TCP serwer i klient rozmawiają ze sobą

Flow serwera zatrzymuje się na read_some.

size_t Lenght = Socket.read_some(boost::asio::buffer(Buffer), Error);

Wg dokumentacji read_some blokuje aż wczyta przynajmniej jeden bajt lub wystąpi błąd (np. klient się rozłączy).
Zwróćcie uwagę na na PRZYNAJMNIEJ. Tzn. może wystąpić taka sytuacja (i często występuje) że klient wyśle np. 128B ale read_some wczyta tylko jeden i zakończy się sukcesem!
Co z pozostałymi 127? Pozostałe 127B czekają sobie grzecznie w buforze po stronie kernela na serwerze. Czekają na kolejne read_some-y (jeden lub wiele) aż wszystko zostanie wczytane.
To ile wczytaliśmy do tej pory możemy pamiętać sumując poszczególne Lenght-y.

Flow z klienta zatrzymuje się na write:

boost::asio::write(Socket, boost::asio::buffer(BufferToSend, BufferToSend.length()));

Wg dokumentacji write blokuje aż wyśle wszystkie bajty z Buffer-a lub wystąpi błąd. Tym razem WSZYSTKIE, a więc inaczej niż w przypadku read_some.
<offtop>
Tak na marginesie nawet jeśli wartość zwrócona przez write powiedzmy send_bytes == BufferToSend.length() to wcale nie oznacza że klient otrzymał send_bytes bajtów. Nie oznacza to nawet, że
send_bytes bajtów zostało fizycznie wysłanych! Oznacza to tylko, że send_bytes zostało skopiowane do stosu TCP/IP w kernelu.
Czemu tak? Czemu write nie zwraca "prawdziwej" ilości wysłanych danych? Cały podsystem sieci jądra działa asynchronicznie, skopiowane dane muszą jeszcze zostać "obrobione" w jądrze,
następnie podzielone na strumień niwielkich datagramów i dopiero wysłane być może na drugi koniec świata. Cały proces jest na tyle czasochłonny, że czekanie we write
np. na ACK-i i wtedy dopiero zwracanie zabiłoby wydajność.
</offtop>

  • Wcześniej napisałem że to klient najczęściej zrywa połączenia. No ale tutaj robi to serwer:) I to jeszcze jak! Tak wygląda kod zrywający połącznie:
}

Serio. Destruktor tcp::~socket lokalnego obiektu Socket, który wywołuje się po wyjściu ze scope-a niszczy socket, a zatem zrywa połączenie z klientem.
W tym momencie pętla się przekręca, koło się zamyka i flow przechodzi z powrotem do Acceptor.accept(Socket);.

3. Gdzie jest problem?

Rozważmy taki scenariusz:

  • Serwer sobie nasłuchuje, klient się łączy.
  • Klient w pętli wysyła stringi z Buffer-a, powiedzmy wykonuje write("1"), write("2")
  • kilkanaście us później Serwer robi read_some() i dostaje np. "12" (patrz wyżej czemu)
  • Serwer zrywa połączenie (socket jest niszczony, patrz wyżej czemu), pętla się przekręca i Serwer czeka na kolejne połączenie
  • kilka us później klient robi write("3") i klops! W tym momencie klient nie jest już połączony więc dostaje "Connection refused" (error code = 111).

4. Jak to naprawić?

Serwer:

  • przede wszystkim serwer ma się NIE rozłączać. To klient inicjuje połączenie i klient je zamyka. Wyrzucić w serwerze
tcp::socket Socket(IO_Service);
Acceptor.accept(Socket);

poza pętle.

  • sprawdzać czy accept się udał, jest przeciążona wersja accept-a biorąca error_code& + assert.
  • logować poszczególne kroki
  • sprawdzać czy read_some się udał
  • logować ilość wczytanych danych ZAWSZE, nie zaszkodzi też robić dump-a Buffer-a w prostych programach jak ten.

Po poprawkach serwer:

void server()
{
	try
	{
		boost::asio::io_service IO_Service;
		boost::asio::ip::tcp::acceptor Acceptor(IO_Service,
							boost::asio::ip::tcp::endpoint(boost::asio::ip::tcp::v4(), SERVER_PORT));
		boost::asio::ip::tcp::socket Socket(IO_Service);
		boost::system::error_code Error;
		Acceptor.accept(Socket, Error);
		assert(!Error);
		std::cout << "Accepted connection\n";

		while (true)
		{
			char Buffer[65536] = { 0 };
			size_t Lenght = Socket.read_some(boost::asio::buffer(Buffer), Error);
			assert(!Error);
			assert(Lenght > 0);
			std::cout << "Read some " << Lenght << "bytes\n";
			std::cout << "Content: " << Buffer << "\n";
		}
	}
	catch (std::exception& Error)
	{
		std::cout << "Exception: " << Error.what() << std::endl;
	}
}

Klient:

  • sprawdzać czy connect się udał, jest przeciążona wersja connect-a biorąca error_code& + assert
  • sprawdzać ile danych wysłał write, logować ilość wysłanych danych ZAWSZE

Po poprawkach:

void client()
{
	std::queue<std::string> Buffer;
	Buffer.push("Jeden");
	Buffer.push("Dwa");
	Buffer.push("Trzy");

	try
	{
		boost::asio::io_service IO_Service;
		boost::asio::ip::tcp::socket Socket(IO_Service);
		boost::asio::ip::tcp::resolver Resolver(IO_Service);

		boost::system::error_code error;
		boost::asio::connect(Socket, Resolver.resolve({ SERVER_IP, std::to_string(SERVER_PORT) }), error);
		assert(!error);

		while (true)
		{
			if (!Buffer.empty())
			{
				std::string BufferToSend(Buffer.front());
				size_t send_bytes = boost::asio::write(Socket, boost::asio::buffer(BufferToSend, BufferToSend.length()));
				assert(send_bytes > 0);
				std::cout << "Write some " << send_bytes << "bytes\n";
				Buffer.pop();
			}
		}
	}
	catch (std::exception& Error)
	{
		std::cout << "Exception: ";
		std::cout << Error.what() << std::endl;
	}
}

Na zakończenie. Żeby nie było wątpliwości, powyższe kody źródłowe NIE są idealne, być może są scenariusze w których coś nie pyknie.
W programowaniu sieciowym b. wiele rzeczy może pójść nie tak - patrz http://stackoverflow.com/questions/3857272/boost-error-codes-reference
Na szczęście wszytkie faile komunikacji powinny zostać tutaj wyłapane przez asserty lub zalogowane przez co o wiele łatwiej będzie znaleźć przyczynę problemu.

1 użytkowników online, w tym zalogowanych: 0, gości: 1