The socket function is a system call that creates a new network socket in the operating system kernel. A socket is an abstract object for communication over a network. Through sockets, applications can communicate using different network protocols, such as TCP, UDP, etc., and we can even implement custom protocols.

syscall.Socket

There are many articles introducing the epoll method of Go standard library, but there are not too many articles introducing how to create TCP and UDP connections at the bottom of Go. The bottom of Go also creates sockets by means of system calls to sockets, such as:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// descriptor as nonblocking and close-on-exec.
func sysSocket(family, sotype, proto int) (int, error) {
    // See ../syscall/exec_unix.go for description of ForkLock.
    syscall.ForkLock.RLock()
    s, err := socketFunc(family, sotype, proto)
    if err == nil {
        syscall.CloseOnExec(s)
    }
    syscall.ForkLock.RUnlock()
    if err != nil {
        return -1, os.NewSyscallError("socket", err)
    }
    if err = syscall.SetNonblock(s, true); err != nil {
        poll.CloseFunc(s)
        return -1, os.NewSyscallError("setnonblock", err)
    }
    return s, nil
    }
var (
    testHookDialChannel  = func() {} // for golang.org/issue/5349
    testHookCanceledDial = func() {} // for golang.org/issue/16523
    // Placeholders for socket system calls.
    socketFunc        func(int, int, int) (int, error)  = syscall.Socket
    connectFunc       func(int, syscall.Sockaddr) error = syscall.Connect
    listenFunc        func(int, int) error              = syscall.Listen
    getsockoptIntFunc func(int, int, int) (int, error)  = syscall.GetsockoptInt
)

You can see that the final one still relies on syscall.Socket to create the socket and finally return the file descriptor of this socket.

In the Linux operating system, the prototype of the socket function is defined as follows:

1
2
#include <sys/socket.h>
int socket(int domain, int type, int protocol);

Its main function is to create an endpoint for network communication and return a file descriptor through which data can be read and written, even listen and accept.

The meaning of the parameters of this function is as follows:

  • domain: Specify the address family of the socket, for example, AF_INET for IPv4 address family, AF_INET6 for IPv6 address family, AF_UNIX for local Unix domain socket.
  • type: Specifies the type of the socket, e.g. SOCK_STREAM for connection-oriented TCP sockets, SOCK_DGRAM for connectionless UDP sockets.
  • protocol: Specify the protocol used by the socket, usually you can set it to 0 to use the default protocol.

The return value of the socket function is the file descriptor of the newly created socket, or -1 if there is an error, and sets the errno error code. The newly created socket can be configured using other system calls, such as bind, connect, listen, accept, sendto, recvfrom, etc. These system calls are defined in Go. These system calls are defined in Go, which contains two systems, the standard library syscall, such as syscall.Socket, and unix.Socket. If you are doing Linux network development, it’s all the same, so in this article, we take the syscall.Socket library as an example.

domain defines the address family of the socket, such as our common AF_INET , AF_PACKET represents the raw socket of the data link layer. type types commonly used are SOCK_STREAM, SOCK_DGRAM, SOCK_RAW, etc.

protocol refers to the commonly used protocols, and there are different settings for this parameter depending on the previous parameters. For example, when the second parameter is SOCK_STREAM or SOCK_DGRAM, we often set the protocol to 0. When the second parameter is SOCK_RAW, we set it to syscall.IPPROTO_XXX or syscall.ETH_P_ALL depending on the protocol.

Why do we need to learn syscall.Socket when the Go standard library already provides programming libraries for TCP, UDP and even IP? syscall.Socket provides the underlying network communication, richer network programming capabilities than the standard library, new protocols, new patterns and the ability to provide more underlying network control.

In this article, different parameters of syscall.Socket and different usage scenarios will be demonstrated, using a lot of examples.

Implementing a rudimentary HTTP Server using syscall.Socket

The first example is to create an http server using syscall.Socket, this http server is purely for demonstration purpose, we don’t consider tls, performance and other aspects, we don’t consider HTTP protocol full feature support, just to demonstrate that we can use syscall.Socket to create a TCP Server.

At first, we define the type netSocket, which contains the file descriptor created by the socket, and we wrap the system calls Read, Write, Accept and Close to make them easier to use later.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
type netSocket struct {
    fd int
}
func (ns netSocket) Read(p []byte) (int, error) {
    if len(p) == 0 {
        return 0, nil
    }
    n, err := syscall.Read(ns.fd, p)
    if err != nil {
        n = 0
    }
    return n, err
}
func (ns netSocket) Write(p []byte) (int, error) {
    n, err := syscall.Write(ns.fd, p)
    if err != nil {
        n = 0
    }
    return n, err
}
func (ns *netSocket) Accept() (*netSocket, error) {
    nfd, _, err := syscall.Accept(ns.fd)
    if err == nil {
        syscall.CloseOnExec(nfd)
    }
    if err != nil {
        return nil, err
    }
    return &netSocket{nfd}, nil
}
func (ns *netSocket) Close() error {
    return syscall.Close(ns.fd)
}

The above code is also easy to understand. By manipulating the socket file descriptors through system calls, we can implement a generic server-side network handling capability.

The next problem to solve is how to generate a socket that listens to the specified IP and port.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
func newNetSocket(ip net.IP, port int) (*netSocket, error) {
    // The ForkLock documentation indicates that a lock is required
    syscall.ForkLock.Lock()
    // Here we use syscall.AF_INET, the IPv4 address family for the first parameter.
    // The second parameter specifies the data flow method, that is, the TCP method.
    // The third parameter uses the SOCK_STREAM default protocol.
    fd, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_STREAM, 0)
    if err != nil {
        return nil, os.NewSyscallError("socket", err)
    }
    syscall.ForkLock.Unlock()
    // Having created the socket and gotten the file descriptor, we can set some options, such as reusable addresses
    if err = syscall.SetsockoptInt(fd, syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 1); err != nil {
        syscall.Close(fd)
        return nil, os.NewSyscallError("setsockopt", err)
    }
    // Bind the specified address and port
    sa := &syscall.SockaddrInet4{Port: port}
    copy(sa.Addr[:], ip)
    if err = syscall.Bind(fd, sa); err != nil {
        return nil, os.NewSyscallError("bind", err)
    }
    // Start listening for client connections
    if err = syscall.Listen(fd, syscall.SOMAXCONN); err != nil {
        return nil, os.NewSyscallError("listen", err)
    }
    return &netSocket{fd: fd}, nil
}

Below we implement the main function to string together the entire creation, listening, connection acceptance, and request processing.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
func main() {
    ipFlag := flag.String("ip_addr", "127.0.0.1", "监听的地址")
    portFlag := flag.Int("port", 8080, "监听的端口")
    flag.Parse()
    ip := net.ParseIP(*ipFlag)
    socket, err := newNetSocket(ip, *portFlag)
    if err != nil {
        panic(err)
    }
    defer socket.Close()
    log.Printf("http addr: http://%s:%d", ip, port)
    for {
        // 开始等待客户端的连接
        rw, e := socket.Accept()
        log.Printf("incoming connection")
        if e != nil {
            panic(e)
        }
        // Read request
        log.Print("reading request")
        req, err := parseRequest(rw)
        log.Print("request: ", req)
        if err != nil {
            panic(err)
        }
        // Write response
        log.Print("writing response")
        io.WriteString(rw, "HTTP/1.1 200 OK\r\n"+
            "Content-Type: text/html; charset=utf-8\r\n"+
            "Content-Length: 20\r\n"+
            "\r\n"+
            "<h1>hello world</h1>")
        if err != nil {
            log.Print(err.Error())
            continue
        }
    }
}

Here is a parseRequest function I have not yet provided, it is responsible for parsing the client’s request, parsing out the http request, the actual example does not use this parsed out request, but simply return a hello world page. This function is implemented as follows.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
type request struct {
    method string // GET, POST, etc.
    header textproto.MIMEHeader
    body   []byte
    uri    string // The raw URI from the request
    proto  string // "HTTP/1.1"
}
func parseRequest(c *netSocket) (*request, error) {
    b := bufio.NewReader(*c)
    tp := textproto.NewReader(b)
    req := new(request)
    // First line: parse "GET /index.html HTTP/1.0"
    var s string
    s, _ = tp.ReadLine()
    sp := strings.Split(s, " ")
    req.method, req.uri, req.proto = sp[0], sp[1], sp[2]
    // Parse headers
    mimeHeader, _ := tp.ReadMIMEHeader()
    req.header = mimeHeader
    // Parse body
    if req.method == "GET" || req.method == "HEAD" {
        return req, nil
    }
    if len(req.header["Content-Length"]) == 0 {
        return nil, errors.New("no content length")
    }
    length, err := strconv.Atoi(req.header["Content-Length"][0])
    if err != nil {
        return nil, err
    }
    body := make([]byte, length)
    if _, err = io.ReadFull(b, body); err != nil {
        return nil, err
    }
    req.body = body
    return req, nil
}

You can run this program and then visit http://127.0.0.1:8080 in your browser, you should see the hello world page.

Through this program, you can see that it is not complicated to use syscall.Socket, just call the corresponding system call.

Using syscall.Socket to request a web page

The above example is an example of using syscall.Socket to provide an http server, which is actually a tcp server, so if you want to implement a client to access a web page, how can you use syscall.Socket to achieve it?

TCP socket needs to call the system call Connect to connect to the server, and after connection, you can read/write via Read/Write:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package main
import (
    "fmt"
    "net"
    "os"
    "syscall"
)
func main() {
    // Create a TCP socket
    sockfd, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_STREAM, 0)
    if err != nil {
        fmt.Println("socket creation failed:", err)
        os.Exit(1)
    }
    defer syscall.Close(sockfd)
    // Get the address and port of the web page to be accessed, i.e. a TCPAddr
    serverAddr, err := net.ResolveTCPAddr("tcp", "bing.com:80")
    if err != nil {
        fmt.Println("address resolution failed:", err)
        syscall.Close(sockfd)
        os.Exit(1)
    }
    // Connect to this address using syscall.Connect and the created socket
    err = syscall.Connect(sockfd, &syscall.SockaddrInet4{
        Port: serverAddr.Port,
        Addr: [4]byte{serverAddr.IP[0], serverAddr.IP[1], serverAddr.IP[2], serverAddr.IP[3]},
    })
    if err != nil {
        fmt.Println("connection failed:", err)
        syscall.Close(sockfd)
        os.Exit(1)
    }
    // Send a request
    request := "GET / HTTP/1.1\r\nHost: bing.com\r\n\r\n"
    _, err = syscall.Write(sockfd, []byte(request))
    if err != nil {
        fmt.Println("write failed:", err)
        syscall.Close(sockfd)
        os.Exit(1)
    }
    // Processing of the returned results, where there is no parsing of the http response
    response := make([]byte, 1024)
    n, err := syscall.Read(sockfd, response)
    if err != nil {
        fmt.Println("read failed:", err)
        syscall.Close(sockfd)
        os.Exit(1)
    }
    fmt.Println(string(response[:n]))
}

In this example we first created a socket, then connected to the bing.com website, sent a simple http request and got the return result.

Implementing a UDP server using syscall.Socket

In the above example we implemented TCP socket method, next we will see how to implement UDP server and client using syscall.Socket.

The UDP implementation is much simpler, there are no other system calls like Bind, Listen, Accept, etc.

Note the setting of the three parameters of Socket: syscall.AF_INET, syscall.SOCK_DGRAM and syscall.IPPROTO_UDP. UDP we read and write via Sendto and Recvfrom:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package main
import (
    "fmt"
    "net"
    "syscall"
)
func main() {
    // Creating a UDP Socket
    fd, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_DGRAM, syscall.IPPROTO_UDP)
    if err != nil {
        fmt.Println("socket failed:", err)
        return
    }
    defer syscall.Close(fd)
    // Bind local address and port
    addr := syscall.SockaddrInet4{Port: 8080}
    copy(addr.Addr[:], net.ParseIP("0.0.0.0").To4())
    if err := syscall.Bind(fd, &addr); err != nil {
        fmt.Println("bind failed:", err)
        return
    }
    fmt.Println("UDP server is listening on port 8080...")
    buf := make([]byte, 1024)
    for {
        // Read the data sent by the client
        n, from, err := syscall.Recvfrom(fd, buf, 0)
        if err != nil {
            fmt.Println("recvfrom failed:", err)
            continue
        }
        // Returning data to the client
        if err := syscall.Sendto(fd, buf[:n], 0, from); err != nil {
            fmt.Println("sendto failed:", err)
        }
    }
}

In this example, we have implemented the echo function, where the server receives something and returns the result to the client intact.

Implementing a UDP client using syscall.Socket

Since we have implemented a UDP server using syscall.Socket, we can also implement a UDP client.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package main
import (
    "fmt"
    "net"
    "syscall"
)
func main() {
    // Creating a UDP Socket
    fd, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_DGRAM, syscall.IPPROTO_UDP)
    if err != nil {
        fmt.Println("socket failed:", err)
        return
    }
    defer syscall.Close(fd)
    // Set the destination address and port
    addr := syscall.SockaddrInet4{Port: 8080}
    copy(addr.Addr[:], net.ParseIP("127.0.0.1").To4())
    // Sending data to the UDP server
    message := "Hello, UDP server!"
    if err := syscall.Sendto(fd, []byte(message), 0, &addr); err != nil {
        fmt.Println("sendto failed:", err)
        return
    }
    // Read the UDP server response
    buf := make([]byte, 1024)
    n, _, err := syscall.Recvfrom(fd, buf, 0)
    if err != nil {
        fmt.Println("recvfrom failed:", err)
        return
    }
    fmt.Println("received response:", string(buf[:n]))
}

At first, we also create a UDP socket, and then call Sendto and Recvfrom to send and receive data. You can see that we don’t need to deal with IP packet headers and UDP packet headers.

Implementing a custom protocol

In the previous TCP and UDP example, when we call syscall.Socket, the first parameter is syscall.AF_INET, and then set the parameters of data stream or datagram according to TCP or UDP respectively, which is the network programming method of IP layer + UDP/TCP layer. In fact, we can use syscall.Socket to implement data link layer communication, such as the next example, we implement a CHAO protocol in the data link layer. The CHAO protocol is very simple, its first four bytes are the letters C, H, A, O, it has no IP data, it is purely built on top of the data link layer, and the data frames are sent and received through MAC Addr.

First look at the server-side implementation:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
package main
import (
    "fmt"
    "net"
    "syscall"
)
func main() {
    // Creating a raw socket
    protocol := htons(syscall.ETH_P_ALL)
    fd, err := syscall.Socket(syscall.AF_PACKET, syscall.SOCK_RAW, int(protocol))
    if err != nil {
        fmt.Println("socket failed:", err)
        return
    }
    defer syscall.Close(fd)
    // Bind to the specified network interface
    ifi, err := net.InterfaceByName("eth0")
    if err != nil {
        fmt.Println("interfaceByName failed:", err)
        return
    }
    addr := syscall.SockaddrLinklayer{
        Protocol: protocol,
        Ifindex:  ifi.Index,
    }
    if err := syscall.Bind(fd, &addr); err != nil {
        fmt.Println("bind failed:", err)
        return
    }
    // Receive custom protocol packets
    buf := make([]byte, 1024)
    for {
        n, raddr, err := syscall.Recvfrom(fd, buf, 0)
        if err != nil {
            fmt.Println("recvfrom failed:", err)
            return
        }
        if n < 4 || !(buf[0] == 'C' && buf[1] == 'H' && buf[2] == 'A' && buf[3] == 'O') {
            continue
        }
        fmt.Printf("received request: %s\n", string(buf[4:n]))
        // Replying to custom protocol packets
        response := []byte("Hello, custom chao protocol!")
        header := []byte{'C', 'H', 'A', 'O'} // Custom Protocol Header, CHAO Protocol
        packet := append(header, response...)
        if err := syscall.Sendto(fd, packet, 0, raddr); err != nil {
            fmt.Println("sendto failed:", err)
            return
        }
    }
}
func htons(i uint16) uint16 {
    return (i<<8)&0xff00 | i>>8
}

First we create a raw socket using syscall.AF_PACKET, syscall.SOCK_RAW, syscall.ETH_P_ALL, note that it is different from our previous TCP/UDP example, the first parameter of TCP/UDP socket creation is the IPv4 address. We use syscall.AF_PACKET, and the second parameter is syscall.SOCK_RAW, and the third parameter is syscall.ETH_P_ALL, which means receiving data from all protocols.

The next step is to bind the socket to some NIC. When I was testing, I found that syscall.SockaddrLinklayer needs to convert the protocol from host byte order to network byte order by htons, but the third parameter of the call to syscall.Socket seems to be fine without conversion.

Next is to call Recvfrom to read the client’s request, here check if it is not CHAO protocol data to ignore, if it is CHAO protocol data, then write back the request as is.

The above is the server-side code, and the next is the client-side code.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
package main
import (
    "fmt"
    "net"
    "syscall"
)
func main() {
    // Creating a raw socket
    protocol := htons(syscall.ETH_P_ALL)
    fd, err := syscall.Socket(syscall.AF_PACKET, syscall.SOCK_RAW, int(protocol))
    if err != nil {
        fmt.Println("socket failed:", err)
        return
    }
    defer syscall.Close(fd)
    // Bind to the specified network interface
    ifi, err := net.InterfaceByName("eth0")
    if err != nil {
        fmt.Println("interfaceByName failed:", err)
        return
    }
    addr := syscall.SockaddrLinklayer{
        Protocol: protocol,
        Halen:    6,
        Ifindex:  ifi.Index,
    }
    copy(addr.Addr[:6], ifi.HardwareAddr)
    if err := syscall.Bind(fd, &addr); err != nil {
        fmt.Println("bind failed:", err)
        return
    }
    // Sending custom protocol packets
    message := []byte("Hello, custom chao protocol!")
    header := []byte{'C', 'H', 'A', 'O'} // Custom Protocol Header, CHAO Protocol
    packet := append(header, message...)
    if err := syscall.Sendto(fd, packet, 0, &addr); err != nil {
        fmt.Println("sendto failed:", err)
        return
    }
    buf := make([]byte, 1024)
    for {
        n, _, err := syscall.Recvfrom(fd, buf, 0)
        if err != nil {
            fmt.Println("recvfrom failed:", err)
            return
        }
        if n < 4 || !(buf[0] == 'C' && buf[1] == 'H' && buf[2] == 'A' && buf[3] == 'O') {
            continue
        }
        fmt.Println("received response:", string(buf[4:n]))
        break
    }
    }
func htons(i uint16) uint16 {
    return (i<<8)&0xff00 | i>>8
}

Similar to the server side code, it also creates a socket and binds it. Here we are testing on the same machine, so the bindings are to the address of the local eth0 NIC. The client sends a data first, and then waits for the server to return.

Other

Because syscall.Socket is a very low-level way of network programming, you can use it to do a lot of functions that the standard library doesn’t provide. For example, you can implement protocols like arp, dhcp, icmp, etc. I won’t list them all due to the length of the article.

Ref

  • https://colobu.com/2023/04/09/use-syscall-Socket-in-go/