Algunos desarrolladores de aplicaciones red dicen que las capas inferiores de progración son basadas en sockets. Esto puede no ser cierto para todos los casos, pero muchas aplicaciones modernas en realidad usan sockets para obtener ventaja. Te has preguntado ¿cuántos navegadores se conectan con servidores web cuando navegas por internet? O ¿cómo MSN te conecta a ti y a tus amigos cuando están en el mismo chat enviando un mensaje en tiempo real? Muchos servicios como estos utilizan sockets para transferir datos. Como puedes ver, los sockets ocupan una posición muy importante en la programación de red hoy, y vamos a aprender a usar sockets en Go en esta sección.
Los sockets se originaron de Unix, y dada la filosofía básica de "todo es un archivo", todo puede ser operado con "abrir -> escribir/leer -> cerrar". Los sockets implementan esta filosofía. Los sockets tienen una función para abrir un socket justo como lo haces con un archivo. Esto retorna un descriptor del socket que puede ser usado para operaciones como crear conexiones, transferir información, etc.
Hay dos tipos de sockets que son comúnmente usados que son los sockets de flujo (SOCK_STREAM) y sockets de datagramas (SOCK_DGRAM). Los sockets de flujo son orientadas a conexion, como TCP, mientras que los sockets de datagramas no establecen conexiones, como UDP.
Antes que entendamos como los sockets se comunican uno con otro, necesitamos asegurarnos que cada socket es único, de otra manera, establecer comunicaciones confiables está fuera del alcance. Podemos darle a cada proceso un único identificador (PID) que sirve a nuestro ambiente local, sin embargo, esto no funciona sobre la red. Afortunadamente TCP/IP nos ayuda con este problema. La dirección IP de la capa de red es única en una red de hosts. y el "protocolo + puerto" es único entre una red de aplicaciones. Entonecs podemos usar estor principios para hacer sockets que sean únicos.
Figura 8.1 capas del protocolo de red.
Las aplicaciones que están basadas en TCP/IP todas usan sockets en su código de una manera u otra. Dado que las aplicaciones web se están volviendo cada vez mas prevalentes en los días modernos, no es de extrañarse que los programadores digan que "todo es sobre sockets".
Sabemos que existen dos tipos de sockets, los TCP y los UDP. TCP y UDP son protocolos, y como mencioné, también necesitamos dirección IP y número de puerto para tener sockets únicos.
El internet globarl usa TCP/IP como su protocolo, donde IP es la capa de red y una parte principal de TCP/IP. IPv4 significa que es la versión 4; el dsarrollo de infraestructura lleva adelantado 30 años.
El número de bits en una dirección IPv4 es 32, lo que significa que solamente 2^32 dispositivos son capaces de conectarse a internet. A razón del rápido desarrollo del internet, las direcciones IP sobrepasaron este límite en los años recientes.
Formato de las direcciones IP: 127.0.0.1
, 172.122.121.111
.
IPv6 es la siguiente versión o la siguiente generación de internet. Ha sido desarrollada para solucionar muchos de los problemas inherentes a IPv4. Dispositivos usando IPv6 tienen una dirección de 128 bits de largo, entonces no tenemos que preocuparnos por la unicidad de la dirección. Para colocar esto en perspectiva, significa que puedes tener mas de 1000 direcciones IP por cada metro cuadrado en la tierra con IPv6. Otroso problema como conexión peer to peer, calida de servicio (QoS), seguridad, broadcast múltiple, etc también han sido mejorados.
Formato de direcciones: 2002:c0e8:82e7:0:0:0:c0e8:82e7
.
el paquete net
de Go provee muchos tipos, funciones y métodos para programación de red. La definición de IP es como sigue:
type IP []byte
LA función ParseIP(s string) IP
es para convertir la versión IPv4 o IPv6 a una IP.
package main
import (
"net"
"os"
"fmt"
)
func main() {
if len(os.Args) != 2 {
fmt.Fprintf(os.Stderr, "Usage: %s ip-addr\n", os.Args[0])
os.Exit(1)
}
name := os.Args[1]
addr := net.ParseIP(name)
if addr == nil {
fmt.Println("Invalid address")
} else {
fmt.Println("The address is ", addr.String())
}
os.Exit(0)
}
Este retorna la correspondiente formato IP para una correspondiente dirección IP.
¿Qué podemos hacer cuando sabemos como visitar un servicio web a través de un puerto de red? Como un cliente, podemos enviar una petición apuntada al puerto de red y obtener la respuesta; como un servidor necesitamos enlazar un servicio a un puerto de red, esperar por por las peticiones de los clientes y darles una respuesta.
En Go, el paquete net
tiene un tipo llamado TCPConn
que facilita este tipo de interacción de cliente/servidor. Este tipo tiene dos funciones principales:
func (c *TCPConn) Write(b []byte) (n int, err os.Error)
func (c *TCPConn) Read(b []byte) (n int, err os.Error)
TCPConn
puede ser usado como cliente o servidor para leer y escribir información:
También necesitamos una TCPAddr
para representar la información de la dirección TCP:
type TCPAddr struct {
IP IP
Port int
}
Usamos la función ResolveTCPAddr
para obtener una TCPAddr
en Go:
func ResolveTCPAddr(net, addr string) (*TCPAddr, os.Error)
- Los argumentos de
net
pueden ser "tcp4", "tcp6" o "tcp", donde cada uno significa IPV4 Solamente, IPv6 solamente o IPv4 o IPv6. addr
puede ser un nombre de dominio o una dirección ip, como "www.google.com:80" o "127.0.0.1:22"
Los clientes de Go usan la función DialTCP
en el paquete net
para crear una conexión TCP, donde el resultado es un objeto TCPConn
; después que una conexión es establecida, el servidor tiene el mismo tipo de objeto de conexión para la conexión actual, y cliente y servidor pueden comenzar a intercambiar información. En general, los clientes envían una petición a través de una TCPConn
y reciben información del servidor; los servidores leen y analizan las peticiones, y devuelven una respuesta. Esta conexión permanecerá válida hasta que el cliente o el servidor la cierre. La función para crear una conexión es como sigue:
func DialTCP(net string, laddr, raddr *TCPAddr) (c *TCPConn, err os.Error)
- Los argumentos de
net
pueden ser "tcp4", "tcp6" o "tcp", donde cada uno significa IPV4 Solamente, IPv6 solamente o IPv4 o IPv6. laddr
representa la conexión local, definida anil
la mayoría de las veces.raddr
representa la dirección remota.
Vamos a escribir un ejemplo para simular la petición de un cliente solicitando conexión a un servidor basado en una petición HTTP. Necesitamos un encabezado HTTP simple:
"HEAD / HTTP/1.0\r\n\r\n"
La respuesta del servidor puede verse de la siguiente manera:
HTTP/1.0 200 OK
ETag: "-9985996"
Last-Modified: Thu, 25 Mar 2010 17:51:10 GMT
Content-Length: 18074
Connection: close
Date: Sat, 28 Aug 2010 00:43:48 GMT
Server: lighttpd/1.4.23
Código del cliente:
package main
import (
"fmt"
"io/ioutil"
"net"
"os"
)
func main() {
if len(os.Args) != 2 {
fmt.Fprintf(os.Stderr, "Usage: %s host:port ", os.Args[0])
os.Exit(1)
}
service := os.Args[1]
tcpAddr, err := net.ResolveTCPAddr("tcp4", service)
checkError(err)
conn, err := net.DialTCP("tcp", nil, tcpAddr)
checkError(err)
_, err = conn.Write([]byte("HEAD / HTTP/1.0\r\n\r\n"))
checkError(err)
result, err := ioutil.ReadAll(conn)
checkError(err)
fmt.Println(string(result))
os.Exit(0)
}
func checkError(err error) {
if err != nil {
fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())
os.Exit(1)
}
}
En el ejemplo superior, usamos la entrada de un usuario como el argumento de service
para net.ResolveTCPAddr
para obtener un tcpAddr
. Pasando tcpAddr
a la función DialTCP
, creamos una conexión TCP conn
. Entonces podemos usar conn
para enviar la petición al servidor. Finalmente, usamos ioutil.ReadAll
para leer todo el contenido de conn
, que contiene la respuesta del servidor.
Ya tenemos un cliente TCP, También podemos usar el paquete net
para escribir servidores TCP. En el lado del servidor necesitamos enlazar nuestro servicio a un puerto específico y esperar por una conexión de cliente.
func ListenTCP(net string, laddr *TCPAddr) (l *TCPListener, err os.Error)
func (l *TCPListener) Accept() (c Conn, err os.Error)
Los argumentos requeridos aquí sin idénticos para por el la función DialTCP
que usamos antes.
Implementemos una sincronización usando el puerto 7777:
package main
import (
"fmt"
"net"
"os"
"time"
)
func main() {
service := ":7777"
tcpAddr, err := net.ResolveTCPAddr("tcp4", service)
checkError(err)
listener, err := net.ListenTCP("tcp", tcpAddr)
checkError(err)
for {
conn, err := listener.Accept()
if err != nil {
continue
}
daytime := time.Now().String()
conn.Write([]byte(daytime)) // No nos preocupamos por el valor de respuesta
conn.Close() // terminamos la conexión con el cliente
}
}
func checkError(err error) {
if err != nil {
fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())
os.Exit(1)
}
}
Después que el servicio es iniciado, esperamos por las peticiones de los clientes. Cuando se recibe una petición de un cliente, este la acepta (Accept
) y retorna una respuesta que contiene la información del tiempo actual. Vale la pena preocuparse por los errores que pueden ocurrir en el ciclo for
, el servicio continúa corriendo en vez de salir. En vez de terminar, el servidor almacenará el error en un log de errores del servidor.
El código de arriba todavía no es suficientemente bueno. Nosotros no usamos rutinas, lo que nos permitirá aceptar peticiones simultaneas. Hagamos esto ahora:
package main
import (
"fmt"
"net"
"os"
"time"
)
func main() {
service := ":1200"
tcpAddr, err := net.ResolveTCPAddr("tcp4", service)
checkError(err)
listener, err := net.ListenTCP("tcp", tcpAddr)
checkError(err)
for {
conn, err := listener.Accept()
if err != nil {
continue
}
go handleClient(conn)
}
}
func handleClient(conn net.Conn) {
defer conn.Close()
daytime := time.Now().String()
conn.Write([]byte(daytime)) // No nos preocupamos por el valor de retorno
// Terminamos con este cliente
}
func checkError(err error) {
if err != nil {
fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())
os.Exit(1)
}
}
Al separar nuestra proceso de negocio del la función handleClient
y usando la palabra reservada go
, ya hemos implementado concurrencia para nuestro servicio. Esto es una buena demostración del poder y la simplicidad de las rutinas.
Algunos deben de estar pensando lo siguiente: este servidor no hace nada útil. ¿Qué si necesitamos enviar múltiples peticiones para diferentes formatos de tiempo en una sola conexión? ¿Cómo hacemos eso?
package main
import (
"fmt"
"net"
"os"
"time"
"strconv"
)
func main() {
service := ":1200"
tcpAddr, err := net.ResolveTCPAddr("tcp4", service)
checkError(err)
listener, err := net.ListenTCP("tcp", tcpAddr)
checkError(err)
for {
conn, err := listener.Accept()
if err != nil {
continue
}
go handleClient(conn)
}
}
func handleClient(conn net.Conn) {
conn.SetReadDeadline(time.Now().Add(2 * time.Minute)) // Definimos un tiempo de cancelación de dos minutos
request := make([]byte, 128) // Definimos la longitud máxima de la petición para prevenir ataques de desbordamiento.
defer conn.Close() // Cerramos la conexión antes de salir
for {
read_len, err := conn.Read(request)
if err != nil {
fmt.Println(err)
break
}
if read_len == 0 {
break // Conexión cerrada por el cliente
} else if string(request[:read_len]) == "timestamp" {
daytime := strconv.FormatInt(time.Now().Unix(), 10)
conn.Write([]byte(daytime))
} else {
daytime := time.Now().String()
conn.Write([]byte(daytime))
}
}
}
func checkError(err error) {
if err != nil {
fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())
os.Exit(1)
}
}
En este ejemplo usamos conn.Read()
para constantemente leer las peticiones de un cliente. No podemos cerrar la conexión porque los clientes pueden enviar mas de una petición. Usando conn.SetReadDeadline()
la conexión se cerrará automáticamente después que pase este periodo de tiempo. Cuando el tiempo de caducidad ha pasado, nuestro programa termina desde el ciclo for
. Nota que las peticiones (request
) necesitan ser creadas con la limitante de tamaño en para prevenir ataques de desbordamiento.
Funciones de control para conexiones TCP:
func DialTimeout(net, addr string, timeout time.Duration) (Conn, error)
Para definir el tiempo límite de las conexiones, estos son los métodos para ser usados por clientes y servidores:
func (c *TCPConn) SetReadDeadline(t time.Time) error
func (c *TCPConn) SetWriteDeadline(t time.Time) error
Definir el tiempo límite de una conexión:
func (c *TCPConn) SetKeepAlive(keepalive bool) os.Error
Vale la pena tomar algún tiempo para pensar en cuanto quieres que sea el tiempo límite para la conexión. Conexiones largas pueden reducir el la carga extra producida por la creación de conexiones y es ua buena opción para aplicaciones que necesita intercambiar información frecuentemente.
Para información mas detallada, busca la documentación oficial del paquete net
de Go.
La única diferencia entre un socket UDP y uno TCP es el método de procesamiento para múltiples conexiones en el lado del servidor. Esto radica en que UDP no tinene una función como Accept
. Todas las otras funciones tienen una contraparte UDP
; slo reemplaza TCP
por UDP
en las funciones mencionadas arriba:
func ResolveUDPAddr(net, addr string) (*UDPAddr, os.Error)
func DialUDP(net string, laddr, raddr *UDPAddr) (c *UDPConn, err os.Error)
func ListenUDP(net string, laddr *UDPAddr) (c *UDPConn, err os.Error)
func (c *UDPConn) ReadFromUDP(b []byte) (n int, addr *UDPAddr, err os.Error
func (c *UDPConn) WriteToUDP(b []byte, addr *UDPAddr) (n int, err os.Error)
Ejemplo de un client UDP:
package main
import (
"fmt"
"net"
"os"
)
func main() {
if len(os.Args) != 2 {
fmt.Fprintf(os.Stderr, "Usage: %s host:port", os.Args[0])
os.Exit(1)
}
service := os.Args[1]
udpAddr, err := net.ResolveUDPAddr("udp4", service)
checkError(err)
conn, err := net.DialUDP("udp", nil, udpAddr)
checkError(err)
_, err = conn.Write([]byte("anything"))
checkError(err)
var buf [512]byte
n, err := conn.Read(buf[0:])
checkError(err)
fmt.Println(string(buf[0:n]))
os.Exit(0)
}
func checkError(err error) {
if err != nil {
fmt.Fprintf(os.Stderr, "Fatal error ", err.Error())
os.Exit(1)
}
}
Ejemplo de servidor UDP:
package main
import (
"fmt"
"net"
"os"
"time"
)
func main() {
service := ":1200"
udpAddr, err := net.ResolveUDPAddr("udp4", service)
checkError(err)
conn, err := net.ListenUDP("udp", udpAddr)
checkError(err)
for {
handleClient(conn)
}
}
func handleClient(conn *net.UDPConn) {
var buf [512]byte
_, addr, err := conn.ReadFromUDP(buf[0:])
if err != nil {
return
}
daytime := time.Now().String()
conn.WriteToUDP([]byte(daytime), addr)
}
func checkError(err error) {
if err != nil {
fmt.Fprintf(os.Stderr, "Fatal error ", err.Error())
os.Exit(1)
}
}
A través de la descripción y algunos códigos de ejemplos usando sockets TCP y UDP, podemos ver que Go provee un excelente soporte para la programación por sockets, y que son divertidos y fáciles de usar. Go también provee muchas funciones para construir aplicaciones de alto rendimiento con sockets.
- Índice
- Sección previa: Servicios web
- Siguiente secición: WebSocket