Praca na socketach i wątkach - Bad File Descriptor

Praca na socketach i wątkach - Bad File Descriptor
Shizzer
  • Rejestracja:prawie 8 lat
  • Ostatnio:4 miesiące
  • Postów:231
0

Witajcie

Mam dość trudny do rozwiązania problem (przynajmniej dla mnie). Otóż piszę aktualnie projekt serwera FTP i klienta w oparciu o niskopoziomowe sockety oraz protokół TCP. Serwer oparty jest o wątki z racji tego, że musi obsługiwać x klientów, a nie tylko jednego. Tutaj pojawia się problem.

Z dokumentu RFC serwera FTP (https://tools.ietf.org/html/rfc959) wynika, że komenda quit powoduje zamknięcie socketu kontrolnego (tego, który odpowiada za przesył odpowiedzi na komendy ze strony klienta, socket ten nie obsługuje przesyłania danych takich jak np. pliki). Po zamknięciu socketu kontrolnego serwer oczywiście musi działać dalej, ale niestety następuje błąd Bad file descriptor przy instrukcji self.sock.listen(1) co oznacza, że serwer próbuje nasłuchiwać na już zamkniętym sockecie. Logicznie rzecz biorąc wystarczyłoby napisać coś takiego:

Kopiuj
import socket

try:
    self.control_sock.listen(1)
except OSError:
    self.control_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    self.HOST = socket.gethostbyname(socket.gethostname())
    self.PORT = 5000
    self.control_sock.bind((self.HOST, self.PORT))
    self.control_sock.listen(1)

Ale ten kod również nie rozwiązuje problemu, ponieważ wyrzuca exception Adresses already in use właściwie nie wiem z jakiego powodu, ponieważ po komendzie quit sockety po stronie serwera i po stronie klienta są zamykane toteż host i port powinny się "zwolnić".

Oto kody:

SERWER

Kopiuj
#!/usr/bin/python3

from threading import Thread
import getpass
import socket
import platform
import os
from os import path
import sys

class waitForClients(Thread):
	def __init__(self):
		Thread.__init__(self)
		self.control_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
		self.HOST = socket.gethostbyname(socket.gethostname())
		self.PORT = 5000
		self.control_sock.bind((self.HOST, self.PORT))

	def run(self):
		while True:
			try:
				self.control_sock.listen(1)
			except OSError:
				self.__init__()
				self.control_sock.listen(1)
			talk_with_client = receiveCommandsFromClient(self.control_sock, self.control_sock.accept())
			talk_with_client.start()

class createDataConnection():
	def __init__(self):
		self.data_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
		self.data_HOST = '127.0.1.1'
		self.data_PORT = 5010
		self.data_sock.connect((self.data_HOST, self.data_PORT))
		print("Polaczony!")

class receiveCommandsFromClient(Thread):
	def __init__(self, sock, connection_tuple):
		Thread.__init__(self)
		self.control_sock = sock
		self.conn, self.addr = connection_tuple
		self.logged_in = False
		self.mode = 'ascii'
		self.username = ''
		self.home_directory = os.path.abspath('/')				# katalogiem domowym jest /
		self.current_working_directory = os.path.abspath('/')	# poczatkowo katalogiem roboczym jest /
		self.passive_mode = False
		self.commands =	{
							"type": self.TYPE,
				 	  		"user": self.USER,
							"cd":	self.CHANGE_DIR,
							"quit": self.QUIT
						}

	def run(self):
		self.welcome_msg()
		self.data_connection = createDataConnection()
		self.login()
		self.logged_in = self.check_identification() 
		while True:
			self.data = self.receive_data()
			self.command = self.data.split(" ")[0]
			if not self.data:
				break
			if self.logged_in:
				try:
					self.commands.get(self.command)()
				except TypeError:
					self.conn.send("Invalid command.".encode())
			else:
				if self.command == 'user':
					self.commands.get(self.command)()
				else:
					self.conn.send("530 Please Login with USER and PASS.\nLogin failed.".encode())
			
	def welcome_msg(self):
		self.conn.send("Connected to the server.\nServer is ready.\n".encode())

	def login(self):
		self.username = self.conn.recv(1024).decode()
		self.conn.send("331 Password required for USER.\n".encode())

	def required_msg(self):
		self.conn.send("331 Password required for USER.\n".encode())

	def correct_ident_msg(self):
		self.conn.send(bytes('230- ----------------------------------------------------------\n' 
			      '230- FTP server by Kamil Szpakowski\n' 				 
			      '230- ----------------------------------------------------------\n' 
			      'Remote system type is ' + platform.system() + '.', "utf-8"))

	def incorrect_ident_msg(self):
		self.conn.send("530 Please Login with USER and PASS.\nLogin failed.".encode())

	
	def check_identification(self):
		if self.username == 'anonymous':
			self.correct_ident_msg()
			return True
		else:
			self.incorrect_ident_msg()
			return False


	def receive_data(self):
		try:
			data = self.conn.recv(1024).decode()
			return data
		except ConnectionResetError:
			self.data_connection.data_sock.close()

	def take_parameter(self):
		try:
			self.param = self.data.split(" ")[1]
		except IndexError:
			self.param = ''

	def TYPE(self):
		self.take_parameter()
		if self.param == 'ascii':
			self.mode = self.param
			self.conn.send("200 TYPE set to A.".encode())
		elif self.param == 'binary':
			self.mode = self.param
			self.conn.send("200 TYPE set to I.".encode())
		elif self.param == '' and self.mode == 'ascii':
			self.conn.send(bytes('Using ' + self.mode + ' mode to transfer files.', 'utf-8'))
		elif self.param == '' and self.mode == 'binary':
			self.conn.send(bytes('Using ' + self.mode + ' mode to transfer files.', 'utf-8'))

	def USER(self):
		self.take_parameter()
		if self.logged_in:
			self.conn.send("503 You are already logged in!\nLogin failed.".encode())
		else:
			if not self.param:
				self.conn.send('usage: user <login>'.encode())
			else:
				self.username = self.param
				self.required_msg()
				self.logged_in = self.check_identification()

	def CHANGE_DIR(self):
		self.take_parameter()
		new_path = self.param
		first_char_of_new_path = new_path[0]
		if first_char_of_new_path == '/' and os.path.isdir(new_path):
			os.chdir(new_path)
			self.current_working_directory = os.path.join(new_path)
			self.conn.send("250 CWD command successful.".encode())
		elif first_char_of_new_path != '/' and os.path.isdir(new_path):
			os.chdir(os.path.join(self.current_working_directory, new_path))
			self.current_working_directory = os.path.join(self.current_working_directory, new_path)
			self.conn.send("250 CWD command successful.".encode())
		elif not os.path.isdir(new_path):
			self.conn.send(bytes("550 " + new_path + ": No such file or directory.", "utf-8"))

	def QUIT(self):
		self.control_sock.close()
		self.data_connection.data_sock.close()
	
if __name__ == "__main__":
	th = waitForClients()
	th.start()

KLIENT

Kopiuj
#!/usr/bin/python3
import socket
import getpass
import threading
import sys

class talkWithTheServer(threading.Thread):
	def __init__(self, control_sock, data_sock):
		threading.Thread.__init__(self)
		self.control_sock = control_sock
		self.data_sock = data_sock
		self.login = ''
		self.client_side_effects = {	
										"quit": self.QUIT,
										"user": self.rest_of_authorization_processes
								   };

	def receive_first_msg(self):
		welcome_msg = self.control_sock.recv(1024)
		print(welcome_msg.decode())
	
	def login_auth_to_server(self):
		self.login = input("Name: ")
		self.control_sock.send(self.login.encode())

	def password_auth_to_server(self):
		getpass.getpass()
	
	def receive_required_auth_data(self):
		user_auth_data = self.control_sock.recv(30)
		print(user_auth_data.decode())

	def receive_incorrect_auth_data(self):
		incorrect_auth_data = self.control_sock.recv(55).decode()
		incorrect_auth_data = incorrect_auth_data.split("\n")[1:]
		print('\n'.join(incorrect_auth_data))

	def receive_correct_auth_data(self):
		correct_auth_data = self.control_sock.recv(1024)
		print(correct_auth_data.decode())

	def send_data(self):
		self.data_to_send = input('ftp> ')
		try:
			self.command = self.data_to_send.split(" ")[0]
		except IndexError:
			self.command = ''
		self.control_sock.send(self.data_to_send.encode())
		
	def receive_and_send_data(self):
		while True:
			data = self.control_sock.recv(1024)
			print(data.decode())
			self.send_data()
			self.check_for_client_side_command()
			if not data:
				break

	def receive_data(self):
		self.control_sock.send(self.data_to_send.encode())
		data = self.control_sock.recv(1024)
		print(data.decode())

	def check_for_client_side_command(self):
		try:
			self.client_side_effects.get(self.command)()
		except TypeError:
			pass

	def first_authorization_process(self):
		self.login_auth_to_server()
		self.receive_required_auth_data()	
		self.password_auth_to_server()									
		if self.login == 'anonymous':									
			self.receive_correct_auth_data()
			self.send_data()
			self.check_for_client_side_command()
		else:
			self.receive_incorrect_auth_data()
			self.send_data()

	def rest_of_authorization_processes(self):
		try:
			self.param = self.data_to_send.split(" ")[1]
		except IndexError:
			self.param = ''
		if self.login == 'anonymous':
			self.receive_correct_auth_data()
			self.send_data()
		elif self.param == 'anonymous':
			self.receive_required_auth_data()
			self.password_auth_to_server()
			self.receive_correct_auth_data()
			self.login = self.param
			self.send_data()
		else:
			self.receive_required_auth_data()
			self.password_auth_to_server()
			self.receive_incorrect_auth_data()
			self.send_data()

	def QUIT(self):
		self.control_sock.close()
		self.data_sock.close()
		sys.exit()


	def run(self):
		self.receive_first_msg()
		self.first_authorization_process()
		self.receive_and_send_data()
	
class createControlConnection():
	def __init__(self):
		self.control_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
		self.control_HOST = '127.0.1.1'
		self.control_PORT = 5000
		self.control_sock.connect((self.control_HOST, self.control_PORT))

class createDataConnection(createControlConnection, threading.Thread):
	def __init__(self, control_sock):
		threading.Thread.__init__(self)
		self.control_sock = control_sock
		self.data_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
		self.data_HOST = ''
		self.data_PORT = 5010
		self.data_sock.bind((self.data_HOST, self.data_PORT))
		self.data_sock.listen(1)
		self.data_conn, self.data_addr = self.data_sock.accept()
			
		
if __name__ == "__main__":
	new_control_connection = createControlConnection()
	data_connection = createDataConnection(new_control_connection.control_sock)
	talk_with_the_server = talkWithTheServer(new_control_connection.control_sock, data_connection.data_sock)
	talk_with_the_server.start()

Staram się jak najmniej prosić o pomoc, ale ten problem mnie przerósł. Byłbym wdzięczny za jakąkolwiek pomoc.


Shizzer
  • Rejestracja:prawie 8 lat
  • Ostatnio:4 miesiące
  • Postów:231
0

Problem został przeze mnie rozwiązany. Jeśli ktoś miałby podobny to oto rozwiązanie. Nie wiem na ile ono jest optymalne, aczkolwiek działa prawidłowo i dostatecznie szybko. Zastanawiałem się jak można to zrobić inaczej, ale ostatecznie wybrałem najprostsze rozwiązanie tego problemu.

Kopiuj
class waitForClients(Thread):
    def __init__(self):
        Thread.__init__(self)
        self.control_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.HOST = socket.gethostbyname(socket.gethostname())
        self.PORT = 5000
        self.control_sock.bind((self.HOST, self.PORT))
 
    def run(self):
        while True:
            try:
                self.control_sock.listen(1)
            except OSError:
                self.__init__()
                self.control_sock.listen(1)
            talk_with_client = receiveCommandsFromClient(self.control_sock, self.control_sock.accept())
            talk_with_client.start()

Metodę run zmodyfikowałem do takiej postaci:

Kopiuj

	def run(self):
		while True:
			try:
				self.control_sock.listen(1)
			except OSError:
				while True:
					try:
						self.__init__()
						break
					except:
						continue
			    self.control_sock.listen(1)
			talk_with_client = receiveCommandsFromClient(self.control_sock, self.control_sock.accept())
			talk_with_client.start()

Gdy program wyrzuci wyjątek Bad file descriptor to serwer stara się stworzyć nowy socket. Jeśli nie uda mu się tego zrobić z powodu wyjątku Adresses already in use to tworzy sockety dopóki wyjątek przestanie istnieć. Gdy uda mu się sockety stworzyć ponownie na nich nasłuchuje.


siloam
"tworzy sockety dopóki wyjątek przestanie istnieć." Bardzo niebezpieczne rozwiązanie. Możesz szybko zapchać tym pamięć. Wątki są drogie (jeden potrafi zająć ok 8 MB, a tam nie masz nawet Thread Poola, cały czas tworzysz nowe). Wypróbuj asyncio i sockety asynchronicznie, albo jakiś asynchroniczny framework typu Twisted (masz tam ogranięty protokół FTP) lub Circuits.
Shizzer
Dzięki za zwrócenie uwagi. Nie wiedziałem o tym. Spróbuję rozwiązać to inaczej w takim razie
siloam
Asyncio też za bardzo intuicyjne nie jest, ale jeżeli nie chcesz korzystać z gotowych frameworków takich jak Twisted to może greenlety pomogą. Alokują znaczenie mniej pamięci i pozwalają na łatwą asynchroniczność. https://greenlet.readthedocs.io/en/latest/
Shizzer
Chciałbym to zrobić jak najbardziej niskopoziomowo żeby jak najlepiej zrozukieć komunikację między serwerem a klientem.
Shizzer
Tylko mam taki problem, że kodu jest dużo i ciężko mi będzie teraz ten kod zaktualizować do postaci programu obsługującego asynchroniczność. Zależy mi w sumie tylko na tym żeby zrozumieć działanie FTP. Jak będę pisał usługę serwera HTTP to użyję puli wątków i asynchronizacji.
Kliknij, aby dodać treść...

Pomoc 1.18.8

Typografia

Edytor obsługuje składnie Markdown, w której pojedynczy akcent *kursywa* oraz _kursywa_ to pochylenie. Z kolei podwójny akcent **pogrubienie** oraz __pogrubienie__ to pogrubienie. Dodanie znaczników ~~strike~~ to przekreślenie.

Możesz dodać formatowanie komendami , , oraz .

Ponieważ dekoracja podkreślenia jest przeznaczona na linki, markdown nie zawiera specjalnej składni dla podkreślenia. Dlatego by dodać podkreślenie, użyj <u>underline</u>.

Komendy formatujące reagują na skróty klawiszowe: Ctrl+B, Ctrl+I, Ctrl+U oraz Ctrl+S.

Linki

By dodać link w edytorze użyj komendy lub użyj składni [title](link). URL umieszczony w linku lub nawet URL umieszczony bezpośrednio w tekście będzie aktywny i klikalny.

Jeżeli chcesz, możesz samodzielnie dodać link: <a href="link">title</a>.

Wewnętrzne odnośniki

Możesz umieścić odnośnik do wewnętrznej podstrony, używając następującej składni: [[Delphi/Kompendium]] lub [[Delphi/Kompendium|kliknij, aby przejść do kompendium]]. Odnośniki mogą prowadzić do Forum 4programmers.net lub np. do Kompendium.

Wspomnienia użytkowników

By wspomnieć użytkownika forum, wpisz w formularzu znak @. Zobaczysz okienko samouzupełniające nazwy użytkowników. Samouzupełnienie dobierze odpowiedni format wspomnienia, zależnie od tego czy w nazwie użytkownika znajduje się spacja.

Znaczniki HTML

Dozwolone jest używanie niektórych znaczników HTML: <a>, <b>, <i>, <kbd>, <del>, <strong>, <dfn>, <pre>, <blockquote>, <hr/>, <sub>, <sup> oraz <img/>.

Skróty klawiszowe

Dodaj kombinację klawiszy komendą notacji klawiszy lub skrótem klawiszowym Alt+K.

Reprezentuj kombinacje klawiszowe używając taga <kbd>. Oddziel od siebie klawisze znakiem plus, np <kbd>Alt+Tab</kbd>.

Indeks górny oraz dolny

Przykład: wpisując H<sub>2</sub>O i m<sup>2</sup> otrzymasz: H2O i m2.

Składnia Tex

By precyzyjnie wyrazić działanie matematyczne, użyj składni Tex.

<tex>arcctg(x) = argtan(\frac{1}{x}) = arcsin(\frac{1}{\sqrt{1+x^2}})</tex>

Kod źródłowy

Krótkie fragmenty kodu

Wszelkie jednolinijkowe instrukcje języka programowania powinny być zawarte pomiędzy obróconymi apostrofami: `kod instrukcji` lub ``console.log(`string`);``.

Kod wielolinijkowy

Dodaj fragment kodu komendą . Fragmenty kodu zajmujące całą lub więcej linijek powinny być umieszczone w wielolinijkowym fragmencie kodu. Znaczniki ``` lub ~~~ umożliwiają kolorowanie różnych języków programowania. Możemy nadać nazwę języka programowania używając auto-uzupełnienia, kod został pokolorowany używając konkretnych ustawień kolorowania składni:

```javascript
document.write('Hello World');
```

Możesz zaznaczyć również już wklejony kod w edytorze, i użyć komendy  by zamienić go w kod. Użyj kombinacji Ctrl+`, by dodać fragment kodu bez oznaczników języka.

Tabelki

Dodaj przykładową tabelkę używając komendy . Przykładowa tabelka składa się z dwóch kolumn, nagłówka i jednego wiersza.

Wygeneruj tabelkę na podstawie szablonu. Oddziel komórki separatorem ; lub |, a następnie zaznacz szablonu.

nazwisko;dziedzina;odkrycie
Pitagoras;mathematics;Pythagorean Theorem
Albert Einstein;physics;General Relativity
Marie Curie, Pierre Curie;chemistry;Radium, Polonium

Użyj komendy by zamienić zaznaczony szablon na tabelkę Markdown.

Lista uporządkowana i nieuporządkowana

Możliwe jest tworzenie listy numerowanych oraz wypunktowanych. Wystarczy, że pierwszym znakiem linii będzie * lub - dla listy nieuporządkowanej oraz 1. dla listy uporządkowanej.

Użyj komendy by dodać listę uporządkowaną.

1. Lista numerowana
2. Lista numerowana

Użyj komendy by dodać listę nieuporządkowaną.

* Lista wypunktowana
* Lista wypunktowana
** Lista wypunktowana (drugi poziom)

Składnia Markdown

Edytor obsługuje składnię Markdown, która składa się ze znaków specjalnych. Dostępne komendy, jak formatowanie , dodanie tabelki lub fragmentu kodu są w pewnym sensie świadome otaczającej jej składni, i postarają się unikać uszkodzenia jej.

Dla przykładu, używając tylko dostępnych komend, nie możemy dodać formatowania pogrubienia do kodu wielolinijkowego, albo dodać listy do tabelki - mogłoby to doprowadzić do uszkodzenia składni.

W pewnych odosobnionych przypadkach brak nowej linii przed elementami markdown również mógłby uszkodzić składnie, dlatego edytor dodaje brakujące nowe linie. Dla przykładu, dodanie formatowania pochylenia zaraz po tabelce, mogłoby zostać błędne zinterpretowane, więc edytor doda oddzielającą nową linię pomiędzy tabelką, a pochyleniem.

Skróty klawiszowe

Skróty formatujące, kiedy w edytorze znajduje się pojedynczy kursor, wstawiają sformatowany tekst przykładowy. Jeśli w edytorze znajduje się zaznaczenie (słowo, linijka, paragraf), wtedy zaznaczenie zostaje sformatowane.

  • Ctrl+B - dodaj pogrubienie lub pogrub zaznaczenie
  • Ctrl+I - dodaj pochylenie lub pochyl zaznaczenie
  • Ctrl+U - dodaj podkreślenie lub podkreśl zaznaczenie
  • Ctrl+S - dodaj przekreślenie lub przekreśl zaznaczenie

Notacja Klawiszy

  • Alt+K - dodaj notację klawiszy

Fragment kodu bez oznacznika

  • Alt+C - dodaj pusty fragment kodu

Skróty operujące na kodzie i linijkach:

  • Alt+L - zaznaczenie całej linii
  • Alt+, Alt+ - przeniesienie linijki w której znajduje się kursor w górę/dół.
  • Tab/⌘+] - dodaj wcięcie (wcięcie w prawo)
  • Shit+Tab/⌘+[ - usunięcie wcięcia (wycięcie w lewo)

Dodawanie postów:

  • Ctrl+Enter - dodaj post
  • ⌘+Enter - dodaj post (MacOS)