Funktionierender Prototyp des Serious Games zur Vermittlung von Wissen zu Software-Engineering-Arbeitsmodellen.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

client.py 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328
  1. from __future__ import annotations
  2. import socket
  3. import ssl
  4. import threading
  5. from typing import Any, Optional, Sequence, Type
  6. from ..client import ClientProtocol
  7. from ..datastructures import HeadersLike
  8. from ..extensions.base import ClientExtensionFactory
  9. from ..extensions.permessage_deflate import enable_client_permessage_deflate
  10. from ..headers import validate_subprotocols
  11. from ..http import USER_AGENT
  12. from ..http11 import Response
  13. from ..protocol import CONNECTING, OPEN, Event
  14. from ..typing import LoggerLike, Origin, Subprotocol
  15. from ..uri import parse_uri
  16. from .connection import Connection
  17. from .utils import Deadline
  18. __all__ = ["connect", "unix_connect", "ClientConnection"]
  19. class ClientConnection(Connection):
  20. """
  21. Threaded implementation of a WebSocket client connection.
  22. :class:`ClientConnection` provides :meth:`recv` and :meth:`send` methods for
  23. receiving and sending messages.
  24. It supports iteration to receive messages::
  25. for message in websocket:
  26. process(message)
  27. The iterator exits normally when the connection is closed with close code
  28. 1000 (OK) or 1001 (going away) or without a close code. It raises a
  29. :exc:`~websockets.exceptions.ConnectionClosedError` when the connection is
  30. closed with any other code.
  31. Args:
  32. socket: Socket connected to a WebSocket server.
  33. protocol: Sans-I/O connection.
  34. close_timeout: Timeout for closing the connection in seconds.
  35. """
  36. def __init__(
  37. self,
  38. socket: socket.socket,
  39. protocol: ClientProtocol,
  40. *,
  41. close_timeout: Optional[float] = 10,
  42. ) -> None:
  43. self.protocol: ClientProtocol
  44. self.response_rcvd = threading.Event()
  45. super().__init__(
  46. socket,
  47. protocol,
  48. close_timeout=close_timeout,
  49. )
  50. def handshake(
  51. self,
  52. additional_headers: Optional[HeadersLike] = None,
  53. user_agent_header: Optional[str] = USER_AGENT,
  54. timeout: Optional[float] = None,
  55. ) -> None:
  56. """
  57. Perform the opening handshake.
  58. """
  59. with self.send_context(expected_state=CONNECTING):
  60. self.request = self.protocol.connect()
  61. if additional_headers is not None:
  62. self.request.headers.update(additional_headers)
  63. if user_agent_header is not None:
  64. self.request.headers["User-Agent"] = user_agent_header
  65. self.protocol.send_request(self.request)
  66. if not self.response_rcvd.wait(timeout):
  67. self.close_socket()
  68. self.recv_events_thread.join()
  69. raise TimeoutError("timed out during handshake")
  70. if self.response is None:
  71. self.close_socket()
  72. self.recv_events_thread.join()
  73. raise ConnectionError("connection closed during handshake")
  74. if self.protocol.state is not OPEN:
  75. self.recv_events_thread.join(self.close_timeout)
  76. self.close_socket()
  77. self.recv_events_thread.join()
  78. if self.protocol.handshake_exc is not None:
  79. raise self.protocol.handshake_exc
  80. def process_event(self, event: Event) -> None:
  81. """
  82. Process one incoming event.
  83. """
  84. # First event - handshake response.
  85. if self.response is None:
  86. assert isinstance(event, Response)
  87. self.response = event
  88. self.response_rcvd.set()
  89. # Later events - frames.
  90. else:
  91. super().process_event(event)
  92. def recv_events(self) -> None:
  93. """
  94. Read incoming data from the socket and process events.
  95. """
  96. try:
  97. super().recv_events()
  98. finally:
  99. # If the connection is closed during the handshake, unblock it.
  100. self.response_rcvd.set()
  101. def connect(
  102. uri: str,
  103. *,
  104. # TCP/TLS — unix and path are only for unix_connect()
  105. sock: Optional[socket.socket] = None,
  106. ssl_context: Optional[ssl.SSLContext] = None,
  107. server_hostname: Optional[str] = None,
  108. unix: bool = False,
  109. path: Optional[str] = None,
  110. # WebSocket
  111. origin: Optional[Origin] = None,
  112. extensions: Optional[Sequence[ClientExtensionFactory]] = None,
  113. subprotocols: Optional[Sequence[Subprotocol]] = None,
  114. additional_headers: Optional[HeadersLike] = None,
  115. user_agent_header: Optional[str] = USER_AGENT,
  116. compression: Optional[str] = "deflate",
  117. # Timeouts
  118. open_timeout: Optional[float] = 10,
  119. close_timeout: Optional[float] = 10,
  120. # Limits
  121. max_size: Optional[int] = 2**20,
  122. # Logging
  123. logger: Optional[LoggerLike] = None,
  124. # Escape hatch for advanced customization
  125. create_connection: Optional[Type[ClientConnection]] = None,
  126. ) -> ClientConnection:
  127. """
  128. Connect to the WebSocket server at ``uri``.
  129. This function returns a :class:`ClientConnection` instance, which you can
  130. use to send and receive messages.
  131. :func:`connect` may be used as a context manager::
  132. async with websockets.sync.client.connect(...) as websocket:
  133. ...
  134. The connection is closed automatically when exiting the context.
  135. Args:
  136. uri: URI of the WebSocket server.
  137. sock: Preexisting TCP socket. ``sock`` overrides the host and port
  138. from ``uri``. You may call :func:`socket.create_connection` to
  139. create a suitable TCP socket.
  140. ssl_context: Configuration for enabling TLS on the connection.
  141. server_hostname: Host name for the TLS handshake. ``server_hostname``
  142. overrides the host name from ``uri``.
  143. origin: Value of the ``Origin`` header, for servers that require it.
  144. extensions: List of supported extensions, in order in which they
  145. should be negotiated and run.
  146. subprotocols: List of supported subprotocols, in order of decreasing
  147. preference.
  148. additional_headers (HeadersLike | None): Arbitrary HTTP headers to add
  149. to the handshake request.
  150. user_agent_header: Value of the ``User-Agent`` request header.
  151. It defaults to ``"Python/x.y.z websockets/X.Y"``.
  152. Setting it to :obj:`None` removes the header.
  153. compression: The "permessage-deflate" extension is enabled by default.
  154. Set ``compression`` to :obj:`None` to disable it. See the
  155. :doc:`compression guide <../../topics/compression>` for details.
  156. open_timeout: Timeout for opening the connection in seconds.
  157. :obj:`None` disables the timeout.
  158. close_timeout: Timeout for closing the connection in seconds.
  159. :obj:`None` disables the timeout.
  160. max_size: Maximum size of incoming messages in bytes.
  161. :obj:`None` disables the limit.
  162. logger: Logger for this client.
  163. It defaults to ``logging.getLogger("websockets.client")``.
  164. See the :doc:`logging guide <../../topics/logging>` for details.
  165. create_connection: Factory for the :class:`ClientConnection` managing
  166. the connection. Set it to a wrapper or a subclass to customize
  167. connection handling.
  168. Raises:
  169. InvalidURI: If ``uri`` isn't a valid WebSocket URI.
  170. OSError: If the TCP connection fails.
  171. InvalidHandshake: If the opening handshake fails.
  172. TimeoutError: If the opening handshake times out.
  173. """
  174. # Process parameters
  175. wsuri = parse_uri(uri)
  176. if not wsuri.secure and ssl_context is not None:
  177. raise TypeError("ssl_context argument is incompatible with a ws:// URI")
  178. if unix:
  179. if path is None and sock is None:
  180. raise TypeError("missing path argument")
  181. elif path is not None and sock is not None:
  182. raise TypeError("path and sock arguments are incompatible")
  183. else:
  184. assert path is None # private argument, only set by unix_connect()
  185. if subprotocols is not None:
  186. validate_subprotocols(subprotocols)
  187. if compression == "deflate":
  188. extensions = enable_client_permessage_deflate(extensions)
  189. elif compression is not None:
  190. raise ValueError(f"unsupported compression: {compression}")
  191. # Calculate timeouts on the TCP, TLS, and WebSocket handshakes.
  192. # The TCP and TLS timeouts must be set on the socket, then removed
  193. # to avoid conflicting with the WebSocket timeout in handshake().
  194. deadline = Deadline(open_timeout)
  195. if create_connection is None:
  196. create_connection = ClientConnection
  197. try:
  198. # Connect socket
  199. if sock is None:
  200. if unix:
  201. sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
  202. sock.settimeout(deadline.timeout())
  203. assert path is not None # validated above -- this is for mpypy
  204. sock.connect(path)
  205. else:
  206. sock = socket.create_connection(
  207. (wsuri.host, wsuri.port),
  208. deadline.timeout(),
  209. )
  210. sock.settimeout(None)
  211. # Disable Nagle algorithm
  212. if not unix:
  213. sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, True)
  214. # Initialize TLS wrapper and perform TLS handshake
  215. if wsuri.secure:
  216. if ssl_context is None:
  217. ssl_context = ssl.create_default_context()
  218. if server_hostname is None:
  219. server_hostname = wsuri.host
  220. sock.settimeout(deadline.timeout())
  221. sock = ssl_context.wrap_socket(sock, server_hostname=server_hostname)
  222. sock.settimeout(None)
  223. # Initialize WebSocket connection
  224. protocol = ClientProtocol(
  225. wsuri,
  226. origin=origin,
  227. extensions=extensions,
  228. subprotocols=subprotocols,
  229. state=CONNECTING,
  230. max_size=max_size,
  231. logger=logger,
  232. )
  233. # Initialize WebSocket protocol
  234. connection = create_connection(
  235. sock,
  236. protocol,
  237. close_timeout=close_timeout,
  238. )
  239. # On failure, handshake() closes the socket and raises an exception.
  240. connection.handshake(
  241. additional_headers,
  242. user_agent_header,
  243. deadline.timeout(),
  244. )
  245. except Exception:
  246. if sock is not None:
  247. sock.close()
  248. raise
  249. return connection
  250. def unix_connect(
  251. path: Optional[str] = None,
  252. uri: Optional[str] = None,
  253. **kwargs: Any,
  254. ) -> ClientConnection:
  255. """
  256. Connect to a WebSocket server listening on a Unix socket.
  257. This function is identical to :func:`connect`, except for the additional
  258. ``path`` argument. It's only available on Unix.
  259. It's mainly useful for debugging servers listening on Unix sockets.
  260. Args:
  261. path: File system path to the Unix socket.
  262. uri: URI of the WebSocket server. ``uri`` defaults to
  263. ``ws://localhost/`` or, when a ``ssl_context`` is provided, to
  264. ``wss://localhost/``.
  265. """
  266. if uri is None:
  267. if kwargs.get("ssl_context") is None:
  268. uri = "ws://localhost/"
  269. else:
  270. uri = "wss://localhost/"
  271. return connect(uri=uri, unix=True, path=path, **kwargs)