ZhangZhihui's Blog  

Problem: You want to create a TCP server to receive data from a TCP client.


Solution: Use the Listen function in the net package to listen for connections, then accept the connection using Accept.

 

TCP is a connection-oriented protocol at the transport layer. It ensures a reliable and ordered delivery of bidirectional data by managing message acknowledgments and sequences of data packets. As a result, it’s more reliable. When a TCP connection is established, it is maintained until the applications on both ends finish exchanging messages and close it. 

Sockets are application-level connections between two computers and represent endpoints for sending and receiving data to other programs across the network. Sockets abstract the complexities behind networking, allowing programmers to develop programs that communicate through the network. As a result, writing network programs are often about socket programming

There are three parts to a simple TCP server program:
1 Listen for incoming connections.
2 Accept the connection.
3 Read and optionally write data to the connection.

 

func main() {
    listener, err := net.Listen("tcp", "localhost:9000")
    if err != nil {
        log.Fatal(err)
    }
    defer listener.Close()
    for {
        conn, err := listener.Accept()
        if err != nil {
            log.Fatal(err)
        }

        go func(c net.Conn) {
            buf := make([]byte, 1024)
            _, err := c.Read(buf)
            if err != nil {
                log.Fatal()
            }
            log.Print(string(buf))
            conn.Write([]byte("Hello from TCP server"))
            c.Close()
        }(conn)
    }
}

First, you set up the server to listen to a socket using the net.Listen function. Sockets are identified by a combination of the transport protocol, IP address, and port number. Then you loop indefinitely to accept connections. When a connection comes, it is accepted and handled in a separate goroutine. The goroutine reads the data from the connection and prints it out. The connection is then closed. 

The net.Listen function returns a net.Listener interface, a generic network listener for stream-oriented protocols. The net.Listen function takes two arguments. The first is the network protocol, which is tcp in this case. The second is the address to listen on, which is in the form <host>:<port> . If the host is provided, the listener will listen only to that IP address. If it’s left empty, as in this case, for example, :9000 , it will listen on all available unicast and anycast IP addresses of the local system. Interestingly if it’s a single hostname it will also listen only for IPv4 traffic. If you leave it empty it will listen to both IPv4 and IPv6. If the port is 0, a random port is chosen and the Addr method of net.Listener can be used to retrieve the port number

You read from the connection using the Read method. The Read method takes a byte slice as an argument and returns the number of bytes read and an error. The byte slice is used to store the data read from the connection.
You also write to the connection using the Write method. The Write method takes a byte slice as an argument and returns the number of bytes written and an error.

 

Here’s how this works. Start the server first:

$  go  run  main.go

Then you can use the nc (netcat) command to connect and send data to the server. In another terminal, run the following command as the client:

$  echo  "Hello from TCP client" | nc localhost 9000
Hello from TCP server
zzh@ZZHPC:/zdata/Github/ztest$ go run main.go
2024/03/17 08:06:28 Hello from TCP client

Echo the string “Hello from TCP client” to the nc command, which sends it to the server. The server then prints out the string and sends back “Hello from TCP server” to the client. “Hello from TCP server” prints out on the client side.

You might wonder what happens if you have more than 1,024 bytes from the client. You can look until io.EOF is reached. Here is the snippet:

        go func(c net.Conn) {
            bytes := []byte{}
            for {
                buf := make([]byte, 32)
                _, err := c.Read(buf)
                if err != nil {
                    if err == io.EOF {
                        break
                    } else {
                        log.Fatal(err)
                    }
                }
                bytes = append(bytes, buf...)
            }
            log.Print(string(bytes))
            _, err = conn.Write([]byte("Hello from TCP server"))
            if err != nil {
                log.Fatal(err)
            }
            c.Close()
        }(conn)

You will loop until all the data from the client is read (and therefore io.EOF is encountered). 

You’re sending data from the nc client using IPv4. If you want to use IPv6, you can use the -6 flag at the client but you also need to change the listener:

listener,  err := net.Listen("tcp", ":9000")

If you do this, you can use the nc command to connect to the server using IPv6:

zzh@ZZHPC:~$ echo  "Hello from TCP client" | nc -6 localhost 9000
nc: getaddrinfo for host "localhost" port 9000: No address associated with hostname

 

zzh@ZZHPC:~$ cat /etc/hosts
127.0.0.1    localhost
127.0.1.1    ZZHPC

# The following lines are desirable for IPv6 capable hosts
::1     ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters

 

zzh@ZZHPC:~$ echo "Hello from TCP client" | nc -6 ip6-localhost 9000
Hello from TCP server

 

The net.Conn interface returned by the Accept method of net.Listener represents a generic stream-oriented network connection. It is an abstraction of a network connection and has Read and Write methods, meaning it is both a Reader and Writer . 

Both net.Listener and net.Conn are interfaces, and in this case, they are implemented by the net.TCPListener and the net.TCPConn structs, respectively. Instead of using the net.Listen and Accept functions, you could have created the ne⁠t.T⁠CPLi⁠st⁠en⁠er and net.TCPConn structs directly using the net.ListenTCP and AcceptTCP functions. However, the net.Listen and Accept functions are more convenient and portable:

func main() {
    addr, err := net.ResolveTCPAddr("tcp", ":9000")
    if err != nil {
        log.Fatal(err)
    }
    listener, err := net.ListenTCP("tcp", addr)
    if err != nil {
        log.Fatal(err)
    }
    defer listener.Close()
    for {
        conn, err := listener.AcceptTCP()
        if err != nil {
            log.Fatal(err)
        }
        go func(c net.Conn) {
            buf := make([]byte, 1024)
            _, err := c.Read(buf)
            if err != nil {
                log.Fatal()
            }
            log.Print(string(buf))
            conn.Write([]byte("Hello from TCP server"))
            c.Close()
        }(conn)
    }
}

It might seem redundant that you have more than one way of creating a TCP server. The net.Listen and Accept functions are more convenient and simpler to use. net.ListenTCP and AcceptTCP are more verbose but give you more control over the connection; for example, you could set the KeepAlive property of the connection to true to keep the TCP connection alive longer.

 

posted on 2023-10-16 11:06  ZhangZhihuiAAA  阅读(5)  评论(0编辑  收藏  举报