Skip to content
Permalink
Branch: master
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
executable file 343 lines (266 sloc) 9.5 KB

04.1 - Basic TCP and UDP clients

TCP client

The building blocks for the basic TCP client is explained in the net package overview.

net.Dial - TCP

net.Dial is the general-purpose connect command.

  • First parameter is a string specifying the network. In this case we are using tcp.
  • Second parameter is a string with the address of the endpoint in format of host:port.
// 04.1-01-basic-tcp1.go
package main

import (
    "bufio"
    "flag"
    "fmt"
    "net"
)

var (
    host, port string
)

func init() {
    flag.StringVar(&port, "port", "80", "target port")
    flag.StringVar(&host, "host", "example.com", "target host")
}

func main() {

    flag.Parse()

    // Converting host and port to host:port
    t := net.JoinHostPort(host, port)

    // Create a connection to server
    conn, err := net.Dial("tcp", t)
    if err != nil {
        panic(err)
    }

    // Write the GET request to connection
    // Note we are closing the HTTP connection with the Connection: close header
    // Fprintf writes to an io.writer
    req := "GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n"
    fmt.Fprintf(conn, req)

    // Another way to do it to directly write bytes to conn with conn.Write
    // However we must first convert the string to bytes with []byte("string")
    // reqBytes := []byte(req)
    // conn.Write(reqBytes)

    // Reading the response

    // Create a new reader from connection
    connReader := bufio.NewReader(conn)

    // Create a scanner
    scanner := bufio.NewScanner(connReader)

    // Combined into one line
    // scanner := bufio.NewScanner(bufio.NewReader(conn))

    // Read from the scanner and print
    // Scanner reads until an I/O error
    for scanner.Scan() {
        fmt.Printf("%s\n", scanner.Text())
    }

    // Check if scanner has quit with an error
    if err := scanner.Err(); err != nil {
        fmt.Println("Scanner error", err)
    }
}

The only drawback with scanner is having to close the HTTP connection with the Connection: close header. Otherwise we have to manually kill the application.

$ go run 04.1-01-basic-tcp1.go -host example.com -port 80
HTTP/1.1 200 OK
Cache-Control: max-age=604800
Content-Type: text/html
Date: Sat, 16 Dec 2017 05:21:33 GMT
Etag: "359670651+gzip+ident"
Expires: Sat, 23 Dec 2017 05:21:33 GMT
Last-Modified: Fri, 09 Aug 2013 23:54:35 GMT
Server: ECS (dca/53DB)
Vary: Accept-Encoding
X-Cache: HIT
Content-Length: 1270
Connection: close

<!doctype html>
<html>
<head>
    <title>Example Domain</title>

    <meta charset="utf-8" />
...

Instead of using a scanner we can use ReadString(0x00) and stop when we reach an error (in this case EOF):

// 04.1-02-basic-tcp2.go

...
// Read until a null byte (not safe in general)
// Response will not be completely read if it has a null byte
if status, err := connReader.ReadString(byte(0x00)); err != nil {
    fmt.Println(err)
    fmt.Println(status)
}
...

Using 0x00 as delimiter is not ideal. If the response payload contains NULL bytes, we are not reading everything. But it works in this case.

net.DialTCP

net.DialTCP is the TCP specific version of Dial. It's a bit more complicated to call:

  • func DialTCP(network string, laddr, raddr *TCPAddr) (*TCPConn, error)
  • network is the same as net.Dial but can only be tcp, tcp4 and tcp6.
  • laddr is local address and can be chosen. If nil, a local address is automatically chosen.
  • raddr is remote address and is the endpoint.

The type for both local and remote address is *TCPAddr:

type TCPAddr struct {
        IP   IP
        Port int
        Zone string // IPv6 scoped addressing zone
}

We can pass the network (e.g. "tcp") along with host:port or ip:port string to net.ResolveTCPAddr to get a *TCPAddr.

DialTCP returns a *TCPConn. It's a normal connection but with extra methods like SetLinger, SetKeepAlive or SetKeepAlivePeriod.

Let's re-write the TCP client with TCP-specific methods:

// 04.1-03-basic-tcp-dialtcp.go
// Basic TCP client using TCPDial and TCP specific methods
package main

import (
    "bufio"
    "flag"
    "fmt"
    "net"
)

var (
    host, port string
)

func init() {
    flag.StringVar(&port, "port", "80", "target port")
    flag.StringVar(&host, "host", "example.com", "target host")
}

// CreateTCPAddr converts host and port to *TCPAddr
func CreateTCPAddr(target, port string) (*net.TCPAddr, error) {
    return net.ResolveTCPAddr("tcp", net.JoinHostPort(host, port))
}

func main() {

    // Converting host and port
    a, err := CreateTCPAddr(host, port)
    if err != nil {
        panic(err)
    }

    // Passing nil for local address
    tcpConn, err := net.DialTCP("tcp", nil, a)
    if err != nil {
        panic(err)
    }

    // Write the GET request to connection
    // Note we are closing the HTTP connection with the Connection: close header
    // Fprintf writes to an io.writer
    req := "GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n"
    fmt.Fprintf(tcpConn, req)

    // Reading the response

    // Create a scanner
    scanner := bufio.NewScanner(bufio.NewReader(tcpConn))

    // Read from the scanner and print
    // Scanner reads until an I/O error
    for scanner.Scan() {
        fmt.Printf("%s\n", scanner.Text())
    }

    // Check if scanner has quit with an error
    if err := scanner.Err(); err != nil {
        fmt.Println("Scanner error", err)
    }
}

This is a bit better.

UDP client

Similar to TCP, we can make a UDP client with both net.Dial and net.DialUDP.

net.Dial - UDP

Creating a UDP client is very similar. We will just call net.Dial("udp", t). Being UDP, we will use net.DialTimeout to pass a timeout value.

// 04.1-04-basic-udp.go

// Create a connection to server with 5 second timeout
conn, err := net.DialTimeout("udp", t, 5*time.Second)
if err != nil {
    panic(err)
}

Each second is one time.Second (remember to import the time package).

net.DialUDP

net.DialUDP is similar to the TCP equivalent:

  • func DialUDP(network string, laddr, raddr *UDPAddr) (*UDPConn, error)
  • *UDPAddr is acquired through net.ResolveUDPAddr.
  • network should be udp.
// 04.1-05-udp-dialudp.go
package main

import (
    "bufio"
    "flag"
    "fmt"
    "net"
)

var (
    host, port string
)

func init() {
    flag.StringVar(&port, "port", "80", "target port")
    flag.StringVar(&host, "host", "example.com", "target host")
}

// CreateUDPAddr converts host and port to *UDPAddr
func CreateUDPAddr(target, port string) (*net.UDPAddr, error) {
    return net.ResolveUDPAddr("udp", net.JoinHostPort(host, port))
}

func main() {

    // Converting host and port to host:port
    a, err := CreateUDPAddr(host, port)
    if err != nil {
        panic(err)
    }

    // Create a connection with DialUDP
    connUDP, err := net.DialUDP("udp", nil, a)
    if err != nil {
        panic(err)
    }

    // Write the GET request to connection
    // Note we are closing the HTTP connection with the Connection: close header
    // Fprintf writes to an io.writer
    req := "UDP PAYLOAD"
    fmt.Fprintf(connUDP, req)

    // Reading the response

    // Create a scanner
    scanner := bufio.NewScanner(bufio.NewReader(connUDP))

    // Read from the scanner and print
    // Scanner reads until an I/O error
    for scanner.Scan() {
        fmt.Printf("%s\n", scanner.Text())
    }

    // Check if scanner has quit with an error
    if err := scanner.Err(); err != nil {
        fmt.Println("Scanner error", err)
    }
}

Lessons learned

  1. Convert int to string using strconv.Itoa. strconv.Atoi does the opposite (note Atoi also returns an err so check for errors after using it.
  2. String(int) converts the integer to corresponding Unicode character.
  3. Create TCP connections with net.Dial.
  4. We can read/write bytes directly to connections returned by net.Dial or create a Scanner.
  5. Convert a string to bytes with []byte("12345").
  6. Get seconds of type Duration with time.Second.
  7. net package has TCP and UDP specific methods.

Continue reading ⇒ 04.2 - TCP servers

You can’t perform that action at this time.