sockets

No artigo anterior, exploramos os file handles ee como eles são empregados na manipulação de arquivos. Neste artigo, continuaremos a discussão sobre handles, focando agora nos socket handles. Contudo, o conceito fundamental permanece o mesmo: um Socket Handle é um identificador utilizado pelo sistema operacional para distinguir uma conexão específica. Um canal de comunicação gerenciado pelo sistema operacional é denominado Socket; já o identificador que um processo emprega para referenciar esse canal é conhecido como socket handle. Usar um socket handle é similar a usar um file handle. Podemos solicitar ao SO a abertura de um socket, e ele nos retornará um socket handle que podemos usar para enviar e receber dados, e devemos fechá-lo uma vez que tenhamos terminado de usá-lo. Agora, ao invés de interagir com um arquivo, estaremos interagindo com processos que podem estar localizados tanto localmente quanto remotamente, de maneira transparente.

Criando Socket

O socket possui um communication domain que limita o tipo de comunicação que pode ser realizada e também determina o formato do endereço do socket.

namedescrição
PF_UNIXcomunicação local (Unix)
PF_INETcomunicação via internet (IPv4)
PF_INET6comunicação via internet (IPv6)

PF_UNIX utiliza como endereço o nome do file system da máquina, limitando a comunicação a processos executados na mesma máquina. Por outro lado PF_INET e PF_INET6 são usados para comunicação via internet, onde o endereço é um par de host e port. Isso possibilita a comunicação entre processos executados em quaisquer duas máquinas conectadas à internet.

O Communication type do socket, por sua vez, indica se a comunicação é confiável (sem perda de pacotes ou duplicação de dados) e também determina como os dados são enviados e recebidos (stream de bytes ou sequência de pacotes). O Communication type restringe o protocolo utilizado para a transmissão de dados.

TypeConfiavelRepresentacao dos dados
SOCK_STREAMsimStream de bytes
SOCK_DGRAMnãoPacotes
SOCK_RAWnãoPacotes
SOCK_SEQPACKETsimSequencia de pacotes

A função socket, localizada no módulo Unix, possui a seguinte assinatura:

type socket_domain =
    PF_UNIX                     (** Unix domain *)
  | PF_INET                     (** Internet domain (IPv4) *)
  | PF_INET6                    (** Internet domain (IPv6) *)
(** The type of socket domains.  Not all platforms support
    IPv6 sockets (type [PF_INET6]).
 
    On Windows: [PF_UNIX] not implemented.  *)
 
type socket_type =
    SOCK_STREAM                 (** Stream socket *)
  | SOCK_DGRAM                  (** Datagram socket *)
  | SOCK_RAW                    (** Raw socket *)
  | SOCK_SEQPACKET              (** Sequenced packets socket *)
(** The type of socket kinds, specifying the semantics of
   communications.  [SOCK_SEQPACKET] is included for completeness,
   but is rarely supported by the OS, and needs system calls that
   are not available in this library. *)
 
val socket : socket_domain -> socket_type -> int -> file_descr

Então, com base no que vimos até agora, precisamos apenas entender o terceiro parâmetro da função socket. Esse parâmetro é o protocolo que será utilizado para a comunicação, representado por um inteiro. O valor 0 indica a seleção do protocolo padrão para um dado communication domain e type (ex: UDP para SOCK_DGRAM). Os números desses protocolos podem ser encontrados no arquivo /etc/protocols ou na tabela protocols do NIS (Network Information Service).

/etc/protocols
# Internet (IP) protocols
#
# Updated from http://www.iana.org/assignments/protocol-numbers and other
# sources.
# New protocols will be added on request if they have been officially
# assigned by IANA and are not historical.
# If you need a huge list of used numbers please install the nmap package.

ip      0       IP              # internet protocol, pseudo protocol number
hopopt  0       HOPOPT          # IPv6 Hop-by-Hop Option [RFC1883]
icmp    1       ICMP            # internet control message protocol
igmp    2       IGMP            # Internet Group Management
ggp     3       GGP             # gateway-gateway protocol
ipencap 4       IP-ENCAP        # IP encapsulated in IP (officially ``IP'')
st      5       ST              # ST datagram mode
tcp     6       TCP             # transmission control protocol
egp     8       EGP             # exterior gateway protocol
igp     9       IGP             # any private interior gateway (Cisco)
pup     12      PUP             # PARC universal packet protocol
udp     17      UDP             # user datagram protocol
hmp     20      HMP             # host monitoring protocol
xns-idp 22      XNS-IDP         # Xerox NS IDP
rdp     27      RDP             # "reliable datagram" protocol
iso-tp4 29      ISO-TP4         # ISO Transport Protocol class 4 [RFC905]
dccp    33      DCCP            # Datagram Congestion Control Prot. [RFC4340]
xtp     36      XTP             # Xpress Transfer Protocol
ddp     37      DDP             # Datagram Delivery Protocol
idpr-cmtp 38    IDPR-CMTP       # IDPR Control Message Transport
ipv6    41      IPv6            # Internet Protocol, version 6
ipv6-route 43   IPv6-Route      # Routing Header for IPv6
ipv6-frag 44    IPv6-Frag       # Fragment Header for IPv6
idrp    45      IDRP            # Inter-Domain Routing Protocol
rsvp    46      RSVP            # Reservation Protocol
gre     47      GRE             # General Routing Encapsulation
esp     50      IPSEC-ESP       # Encap Security Payload [RFC2406]
ah      51      IPSEC-AH        # Authentication Header [RFC2402]
skip    57      SKIP            # SKIP
ipv6-icmp 58    IPv6-ICMP       # ICMP for IPv6
ipv6-nonxt 59   IPv6-NoNxt      # No Next Header for IPv6
ipv6-opts 60    IPv6-Opts       # Destination Options for IPv6
rspf    73      RSPF CPHB       # Radio Shortest Path First (officially CPHB)
vmtp    81      VMTP            # Versatile Message Transport
eigrp   88      EIGRP           # Enhanced Interior Routing Protocol (Cisco)
ospf    89      OSPFIGP         # Open Shortest Path First IGP
ax.25   93      AX.25           # AX.25 frames
ipip    94      IPIP            # IP-within-IP Encapsulation Protocol
etherip 97      ETHERIP         # Ethernet-within-IP Encapsulation [RFC3378]
encap   98      ENCAP           # Yet Another IP encapsulation [RFC1241]
#       99                      # any private encryption scheme
pim     103     PIM             # Protocol Independent Multicast
ipcomp  108     IPCOMP          # IP Payload Compression Protocol
vrrp    112     VRRP            # Virtual Router Redundancy Protocol [RFC5798]
l2tp    115     L2TP            # Layer Two Tunneling Protocol [RFC2661]
isis    124     ISIS            # IS-IS over IPv4
sctp    132     SCTP            # Stream Control Transmission Protocol
fc      133     FC              # Fibre Channel
mobility-header 135 Mobility-Header # Mobility Support for IPv6 [RFC3775]
udplite 136     UDPLite         # UDP-Lite [RFC3828]
mpls-in-ip 137  MPLS-in-IP      # MPLS-in-IP [RFC4023]
manet   138                     # MANET Protocols [RFC5498]
hip     139     HIP             # Host Identity Protocol
shim6   140     Shim6           # Shim6 Protocol [RFC5533]
wesp    141     WESP            # Wrapped Encapsulating Security Payload
rohc    142     ROHC            # Robust Header Compression

Com isso, podemos criar um socket para comunicação via internet (IPv4) utilizando o protocolo TCP da seguinte maneira:

let socket = Unix.socket Unix.PF_INET Unix.SOCK_STREAM 0

O próximo passo é conectar o socket a um endereço. Para isso, utilizamos a função connect:

val connect : file_descr -> sockaddr -> unit
(** Connect a socket to an address. *)

Conectando a um endereço

Agora, fomos apresentados a um novo tipo, sockaddr. Esse tipo é utilizado para representar um endereço de socket.

type sockaddr =
    | ADDR_UNIX of string
    | ADDR_INET of inet_addr * int

ADDR_UNIX f é um endereço de socket local, onde f é o nome do arquivo no sistema de arquivos. ADDR_INET (a,p) é um endereço de socket de internet, onde a é o endereço IP e p é a porta.

A função host realiza um DNS lookup no nome passado como parâmetro e retorna o endereço IP associado ao domínio.

$ host ocaml.org
ocaml.org has address 51.159.83.169
ocaml.org mail is handled by 50 fb.mail.gandi.net.
ocaml.org mail is handled by 10 spool.mail.gandi.net.

Recebemos 1 endereço IP associado ao domínio ocaml.org. É sabido que por convenção a porta 80 é utilizada para o HTTP. Uma forma equivalente de realizar a mesma ação em OCaml seria:

# string_of_inet_addr (Unix.gethostbyname "ocaml.org").h_addr_list.(0);;
- : string = "51.159.83.169"

Logo, podemos criar nosso sockaddr da seguinte maneira:

let ocaml_org_address =
  let ocaml_org_host_entry = Unix.gethostbyname "ocaml.org" in
  Unix.ADDR_INET (ocaml_org_host_entry.h_addr_list.(0), 80)
;;

Agora, usando tudo o que vimos e adicionando algumas funções novas, que vou explicar logo em seguida, temos:

let print_server_response fdin =
  let buffer_size = 4096 in
  let buffer = BytesLabels.create buffer_size in
  let rec copy () = match read fdin buffer 0 buffer_size with
    | 0 -> ()
    | n ->
        let response = Bytes.sub_string buffer 0 n in
        print_string response;
        copy ()
  in
  copy ();;
 
let make_friend address =
  let s = socket Unix.PF_INET Unix.SOCK_STREAM 0 in
  try
    connect s address;
    let message = "Olá Mundo! \n" in
    send_substring s message 0 (String.length message) [] |> ignore;
    print_server_response s stdout;
    shutdown s Unix.SHUTDOWN_ALL;
  with exn ->
    close s;
    raise exn;;
;;
 
let ocaml_org_address =
  let ocaml_org_host_entry = Unix.gethostbyname "ocaml.org" in
  Unix.ADDR_INET (ocaml_org_host_entry.h_addr_list.(0), 80)
;;
 

A função print_server_response recebe um file_descriptor e imprime o conteúdo recebido do servidor. Para isso, ela cria um buffer de 4096 bytes e lê o conteúdo do file_descriptor para o buffer, imprimindo o conteúdo do buffer até que não haja mais nada para ler.

A função make_friend cria um socket e tenta conectar a um endereço passado como parâmetro. Caso a conexão seja bem-sucedida, ela envia a mensagem "Olá Mundo! \n" para o servidor e imprime a resposta recebida. Por fim, ela fecha o socket.

Gostaria de pontuar o uso da função read:

val read : file_descr -> bytes -> int -> int -> int
(** [read fd buf pos len] reads [len] bytes from descriptor [fd],
    storing them in byte sequence [buf], starting at position [pos] in
    [buf]. Return the number of bytes actually read. *)

A função read lê até len bytes do file_descr fd e armazena no buffer buf a partir da posição pos. Ela retorna o número de bytes lidos. Caso a função retorne 0, significa que não há mais dados para serem lidos. Essa é exatamente a lógica que aplicamos na função print_server_response.

Para realizar uma chamada à função make_friend, basta passar o endereço do servidor como parâmetro:

make_friend ocaml_org_address
- HTTP/1.1 400 Bad Request
- Content-Type: text/plain; charset=utf-8
- Connection: close
 
- 400 Bad Request- : unit = ()

Bem, ao menos fizemos a conexão com o servidor. Encontramos alguém lá fora e enviamos uma mensagem; eles nos responderam. O que a resposta significa é que eles não entenderam nossa mensagem. A resposta começa com "HTTP/1.1", e esse é o tópico do nosso próximo artigo.

Conclusão

Neste artigo, exploramos a criação de sockets e a conexão a um endereço. Vimos como criar um socket para comunicação via internet (IPv4) utilizando o protocolo TCP e como conectar a um endereço. Além disso, vimos como criar um sockaddr e como realizar um DNS lookup para obter o endereço IP associado a um domínio.