{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<p>\n",
    "<font size='5' face='Georgia, Arial'>IIC-2233 Apunte Programación Avanzada</font><br>\n",
    "<font size='1'> Modificado en 2019-1 al 2026-1 por Equipo Docente IIC2233. </font>\n",
    "</p>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Tabla de contenidos\n",
    "\n",
    "1. [*Networking* con Python](#Networking-con-Python)\n",
    "    1. [`Sockets`](#Sockets)\n",
    "        1. [Comandos básicos de sockets](#comandos-básicos-de-sockets)\n",
    "            1. [Cliente TCP](#Cliente-TCP)\n",
    "            2. [Servidor TCP](#Servidor-TCP)\n",
    "            3. [Ejemplo base](#ejemplo-base)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# *Networking* con Python\n",
    "\n",
    "En este material, veremos cómo implementar programas de **bajo nivel** en Python para que puedan comunicarse con otro programa, el cual puede encontrarse en el mismo computador o en otro."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## *Sockets*\n",
    "\n",
    "Un ***socket*** es un objeto del sistema operativo que permite a un programa transmitir y recibir datos desde y hacia otro programa corriendo en otra máquina, o en la misma máquina pero en otro puerto. Son nuestro \"punto de entrada y salida a/desde la red\" en nuestro código. Para obtener un *socket*, necesitamos (1) especificar qué tipo de dirección IP usaremos (IPv4 o IPv6), y qué protocolo de transporte usaremos (TCP ó UDP).\n",
    "\n",
    "En Python, los *sockets* son provistos por el módulo `socket`. Para crear un *socket*, debemos crear una instancia de la clase `socket(family, type)`, donde `family` es el tipo de dirección IP que usaremos, y `type` es el protocolo de transporte. Algunos de los valores permitidos son:\n",
    "\n",
    "- `family`\n",
    "    - `AF_INET` para direcciones IPv4\n",
    "    - `AF_INET6` para direcciones IPv6\n",
    "- `type`\n",
    "    - `SOCK_STREAM` para TCP\n",
    "    - `SOCK_DGRAM` para UDP\n",
    "\n",
    "Por ejemplo, para crear un *socket* TCP con IPv4, lo hacemos de la siguiente manera."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "import socket\n",
    "\n",
    "sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Comandos básicos de sockets\n",
    "\n",
    "Veamos cómo implementar una arquitectura cliente-servidor, utilizando _sockets_. Para poder implementarlo, utilizaremos varios conceptos del notebook \"2-networking_conceptos_básicos\" de la semana anterior, te recomendamos revisarlo si no recuerdas la materia.\n",
    "\n",
    "Para poder comunicarnos utilizando _sockets_ será necesario seguir una serie de pasos definidos para establecer la comunicación entre el cliente y el servidor, y asegurar que se envíen y reciban correctamente los mensajes.\n",
    "\n",
    "Los pasos que se deben seguir se explican en la siguiente imagen:\n",
    "\n",
    "<img src=\"imgs/socket.jpg\" alt=\"Flujo sockets entre cliente y servidor\" style=\"height: 400px\"/>\n",
    "<p>  </p>\n",
    "\n",
    "Fuente: [**HowDev:** Socket programming in Python](https://how.dev/answers/socket-programming-in-python)\n",
    "\n",
    "> **NOTA:** Por simplicidad, en el siguiente notebook solo enseñaremos cómo implementar un comunicación utilizando el protocolo TCP. Si quieres aprender cómo se utiliza el protocolo UDP, revisa el notebook bonus de esta semana. "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Cliente TCP\n",
    "\n",
    "Para establecer una conexión TCP necesitamos (1) la dirección IP del *host* destinatario, y (2) el puerto del *host* destinatario. Para conectarse a un servidor TCP usamos el método `connect((host, port))` del *socket*. El método recibe una tupla, cuyo primer elemento debe ser una dirección IP acorde a lo que especificamos al momento de crear el *socket*, y el segundo elemento es el puerto al que nos queremos conectar. \n",
    "\n",
    "Opcionalmente, el primer elemento de la tupla puede ser un *string* con el *hostname*, como por ejemplo, `python.org`, `github.com` o `canvas.uc.cl`. En ese caso, el sistema utilizará el protocolo DNS para obtener la  dirección IP correspondiente a ese *hostname*.\n",
    "\n",
    "Por ejemplo, hagamos un cliente que se conecte a `104.18.32.7` en el puerto 80 vía TCP."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "import socket\n",
    "\n",
    "sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n",
    "sock.connect(('104.18.32.7', 80))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Si nos queremos conectar a [stackoverflow.com](https://stackoverflow.com) también lo podemos hacer sin conocer su dirección IP."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [],
   "source": [
    "import socket\n",
    "\n",
    "sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n",
    "sock.connect(('stackoverflow.com', 80))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Llegó la hora de enviar algún mensaje. Los *sockets* sólo transmiten *bytes*, por lo que cualquier cosa que enviemos debemos codificarla. Para enviar los *bytes* de un mensaje, se puede ocupar uno de los siguientes métodos de socket:\n",
    "\n",
    "- `send(bytes)`: Trata de enviar el mensaje almacenado en la variable *bytes*. Sin embargo, puede que envíe solo parte de este en vez del mensaje completo, o el envío resulte en error. Debido a lo anterior, este método retorna la cantidad de *bytes* que fue efectivamente enviada, dejando como responsabilidad al programador el llamar nuevamente `send` para enviar los *bytes* restantes hasta que se haya enviado el mensaje completo. Nos daremos cuenta de esto cuando la suma de la cantidad de bytes enviados en cada llamada al método sea igual al largo del mensaje original.\n",
    "- `sendall(bytes)`: Este método se asegura de enviar todos los *bytes* del mensaje, haciendo uso por debajo del método `send` hasta que el mensaje se haya enviado en su totalidad. Sin embargo, en caso de haber un error en el envío, este método solo lanzará una excepción, por lo que no podremos saber cuántos bytes efectivamente se habían enviado de forma previa a que ocurriera el error.\n",
    "\n",
    "En el siguiente ejemplo, los *bytes* contienen un mensaje en el formato HTTP, y corresponde a una **solicitud HTTP** (*HTTP Request*) que puede ser comprendida por un servidor web. Los enviaremos mediante el método `sendall`:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [],
   "source": [
    "solicitud = 'GET / HTTP/1.1\\nHost: stackoverflow.com\\n\\n'\n",
    "sock.sendall(solicitud.encode('ascii'))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "¿Qué nos habrá respondido el servidor? Para saberlo, debemos ocupar el método `recv(buffer)`, que nos retornará un objeto `bytes`. El parámetro que recibe este método es la cantidad __máxima__* de *bytes* que será leída. La documentación sugiere colocar una potencia de 2 pequeña, como 4096.\n",
    "\n",
    "*Es la cantidad máxima, porque lo que hace es sacar los bytes que hayan llegado al socket en ese instante. Si es que en el socket habían llegado 20 bytes al momento de llamar a `sock.recv(4096)`, entonces el método me entregará solo 20 bytes."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "HTTP/1.1 301 Moved Permanently\n",
      "Date: Thu, 30 Apr 2026 13:05:49 GMT\n",
      "Content-Length: 0\n",
      "Connection: keep-alive\n",
      "Location: https://stackoverflow.com/\n",
      "set-cookie: __cf_bm=5gEAIOlyavrWEu6XJQYROZnp38MjpDH8G2OTr3tzucs-1777554349.3086414-1.0.1.1-UEHbgCMKiv0_4Auo.0UUONgRiJP0ls2H3_axSViTMxxEZmykZehdpph.fuxrAxMishYoS99UE.Hy80xPAlTv82M2FLcpnJ7FYftRTtGCqMSwflY90Sp.OsinYwnT3iRI; HttpOnly; Path=/; Domain=stackoverflow.com; Expires=Thu, 30 Apr 2026 13:35:49 GMT\n",
      "X-DNS-Prefetch-Control: off\n",
      "Server: cloudflare\n",
      "CF-RAY: 9f46c29b29def3b5-SCL\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "data = sock.recv(4096)\n",
    "print(data.decode('ascii'))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "El último paso, pero no el menos importante: **debemos cerrar la conexión**; de lo contrario, si nuestro programa continúa, estará ocupando recursos que no utiliza."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [],
   "source": [
    "sock.close()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Hagamos el ejercicio completo en una sola celda. Además, aprovechemos de manejar las excepciones pues, en *networking*, es muy probable que se produzcan errores. La clase `ConnectionError` es la clase base para distintos errores relacionados con problemas de conexión, por lo que capturando su excepción podremos responder a cualquier problema que ocurra en este sentido."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "HTTP/1.1 301 Moved Permanently\n",
      "Date: Thu, 30 Apr 2026 13:05:49 GMT\n",
      "Content-Length: 0\n",
      "Connection: keep-alive\n",
      "Location: https://stackoverflow.com/\n",
      "set-cookie: __cf_bm=FMwKykhs2QyioCOc1kRrqTbvk_wWOmF8HnI0odsAD88-1777554349.351345-1.0.1.1-wlPprT3opqnOJ0Z7kwTSE7Hj62LyKHhfwEonX7TnNIX7WBmd3f78aBpxOtl0LBZr1aquwwYjAJF5.7E0UheAe6Kdf6_.78kgoVzgL4MgD5FJH.8XIIveN.9SiULy.vVe; HttpOnly; Path=/; Domain=stackoverflow.com; Expires=Thu, 30 Apr 2026 13:35:49 GMT\n",
      "X-DNS-Prefetch-Control: off\n",
      "Server: cloudflare\n",
      "CF-RAY: 9f46c29b7ba8e7ab-SCL\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "import socket\n",
    "\n",
    "sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n",
    "\n",
    "host = \"stackoverflow.com\"\n",
    "port = 80\n",
    "\n",
    "try:\n",
    "    sock.connect((host, port))\n",
    "    sock.sendall('GET / HTTP/1.1\\nHost: stackoverflow.com\\n\\n'.encode('utf-8'))\n",
    "    data = sock.recv(4096)\n",
    "    print(data.decode('utf-8'))\n",
    "except ConnectionError as e:\n",
    "    # ConnectionError es la clase base de BrokenPipeError, ConnectionAbortedError,\n",
    "    # ConnectionRefusedError y ConnectionResetError\n",
    "    print(\"Ocurrió un error.\")\n",
    "finally:\n",
    "    # ¡No olvidemos cerrar la conexión!\n",
    "    sock.close()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "La respuesta que obtenemos del servidor web ubicado en el puerto 80 de `stackoverflow.com` es una **respuesta HTTP** (*HTTP Response*) que contiene a lo más 4096 *bytes*. La respuesta HTTP es una de 301 (moved permanently), donde se indica que para conectarse a StackOverflow hay que usar la nueva dirección indicada, usando HTTPS.\n",
    "\n",
    "¿Qué significa esto? El puerto 80 es usado por los servidores. Se trata un servicio de **texto plano**, esto es, sin encriptación. La mayoría de los servidores web modernos utilizan una versión de HTTP con encriptación, conocida como HTTPS y que funciona en el puerto 443. Conectarse a un servidor web con HTTPS requiere operaciones de encriptación sobre el *socket* que complicarían innecesariamente este ejemplo."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Servidor TCP\n",
    "\n",
    "> **NOTA**: Debido a la naturaleza de este contenido, esta sección contiene porciones de código separados que **no pueden ser ejecutados** con el fin de poder explicarlas de forma ordenada, ya que no es recomendable ejecutarlos en Jupyter Notebook. Si quieres ejecutarlo, en la carpeta `codigo/TCP` encontrarás un código similar al que veremos a continuación.\n",
    "\n",
    "Un servidor debe escuchar las conexiones que le llegan a él en un puerto específico. Para ello, debemos \"enlazar\" o \"asociar\" un *socket* a ese puerto con el método `bind((host, port))`. El primer elemento de la tupla es el *hostname* del *host* en que estamos corriendo el servidor; el segundo elemento es el puerto donde queremos escuchar las conexiones.\n",
    "\n",
    "Supongamos que queremos escuchar en el puerto 9000.\n",
    "\n",
    "```python\n",
    "import socket\n",
    "\n",
    "sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n",
    "\n",
    "# Poner como host 'localhost' indica que el host será nuestro computador\n",
    "host = 'localhost'\n",
    "port = 8998\n",
    "\n",
    "sock.bind((host, port))\n",
    "```\n",
    "\n",
    "En el cliente no es necesario hacer un *bind* entre el *host* y el puerto, ya que el sistema operativo lo hace implícitamente a través del método `connect`, asignando al cliente un puerto aleatorio. Si el servidor al cual nos conectamos exigiera que el puerto de cada cliente esté en un rango específico, tendríamos que \"enlazar\" el cliente a un puerto específico también."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ahora, empecemos a escuchar conexiones con el método `listen(conexiones_pendientes)`. El parámetro `conexiones_pendientes` indica cuántas conexiones **pendientes (sin aceptar)** pueden estar de forma paralela esperando a ser aceptadas por el servidor, antes de que este rechace cualquier conexión nueva sobre ese máximo.\n",
    "\n",
    "```python\n",
    "sock.listen(3) # Como máximo pueden haber hasta 3 clientes esperando ser aceptados al mismo tiempo.\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ahora, empecemos a aceptar conexiones. Para ello, ocupamos el método `accept()`, que nos retornará un _socket_ con el que podremos comunicarnos con ese cliente en específico, y la dirección de ese cliente (en específico, una tupla con su dirección IP y su puerto). El método esperará hasta que haya una conexión entrante; por lo tanto, si nadie se conecta, el programa quedará congelado.\n",
    "\n",
    "```python\n",
    "while True:\n",
    "    socket_cliente, address = sock.accept()\n",
    "    print(\"Conexión aceptada desde\", address)\n",
    "    socket_cliente.sendall(\"Gracias por conectarte\\n\".encode(\"utf-8\"))\n",
    "    socket_cliente.close()\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Cuando queramos terminar el programa servidor, debemos cerrar el *socket* que estaba aceptando las conexiones.\n",
    "```python\n",
    "sock.close()\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Ejemplo base\n",
    "\n",
    "Hagamos el ejercicio completo, ejecuta los códigos y mira cómo se comportan. Puedes encontrar los códigos del servidor y el cliente en la carpeta `codigo/TCP`. Para ejecutarlos, debes abrir múltiples terminales, en una debes ejecutar el archivo del servidor y en las otras el archivo del cliente. \n",
    "\n",
    "Primero revisemos el código del servidor. Este se comporta igual que el que revisamos anteriormente: recibe a un cliente, le manda un mensaje y cierra la conexión. Adicionalmente, este servidor presenta una singularidad respecto al otro código, ya que el servidor cerrará después de aceptar cinco clientes.\n",
    "\n",
    "```python\n",
    "## Código servidor\n",
    "\n",
    "import socket\n",
    "\n",
    "sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n",
    "\n",
    "# Poner como host 'localhost' indica que el host será nuestro computador\n",
    "host = 'localhost'\n",
    "port = 8998\n",
    "\n",
    "sock.bind((host, port))\n",
    "sock.listen()\n",
    "print(\"[Servidor] Aceptando conexiones en el puerto\", port)\n",
    "\n",
    "counter = 0\n",
    "while counter < 5:\n",
    "    try:\n",
    "        socket_cliente, address = sock.accept()\n",
    "        print(\"[Servidor] Conexión aceptada desde\", address)\n",
    "        socket_cliente.sendall(\"Gracias por conectarte\\n\".encode(\"utf-8\"))\n",
    "        socket_cliente.close()\n",
    "        counter += 1\n",
    "    except ConnectionError:\n",
    "        print(\"[Servidor] Ocurrió un error.\")\n",
    "\n",
    "print(\"[Servidor] Se han recibido 5 clientes, por lo que se cerrará la conexión\")\n",
    "sock.close()\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "En cuanto al cliente, este intentará conectarse al servidor y recibir el mensaje que envíe. Una vez recibido el mensaje, o si sucede un error, el cliente cerrará la conexión con el servidor y se terminará el programa.\n",
    "```python\n",
    "## Código cliente\n",
    "\n",
    "import socket\n",
    "\n",
    "sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n",
    "\n",
    "# Poner como host 'localhost' indica que el host será nuestro computador\n",
    "host = 'localhost'\n",
    "port = 8998\n",
    "\n",
    "try:\n",
    "    sock.connect((host, port))\n",
    "    data = sock.recv(4096)\n",
    "    print(data.decode(\"utf-8\"))\n",
    "except ConnectionError as e:\n",
    "    print(\"[Cliente] Ocurrió un error\")\n",
    "finally:\n",
    "    sock.close() \n",
    "```"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": ".venv",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.12.3"
  },
  "varInspector": {
   "cols": {
    "lenName": 16,
    "lenType": 16,
    "lenVar": 40
   },
   "kernels_config": {
    "python": {
     "delete_cmd_postfix": "",
     "delete_cmd_prefix": "del ",
     "library": "var_list.py",
     "varRefreshCmd": "print(var_dic_list())"
    },
    "r": {
     "delete_cmd_postfix": ") ",
     "delete_cmd_prefix": "rm(",
     "library": "var_list.r",
     "varRefreshCmd": "cat(var_dic_list()) "
    }
   },
   "types_to_exclude": [
    "module",
    "function",
    "builtin_function_or_method",
    "instance",
    "_Feature"
   ],
   "window_display": false
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
