ZetCode

Go Socket

最后修改时间 2024 年 4 月 11 日

在本文中,我们将介绍如何在 Golang 中使用 Socket。Socket 编程是低级别的。本教程的目的是介绍网络编程,包括这些低级别细节。还有一些更高级别的 API,在实际场景中可能更实用。

注意: 在网络中,套接字一词有不同的含义。它用于 IP 地址和端口号的组合。

网络协议

TCP/IP 是设备通过 Internet 和大多数本地网络进行通信的协议套件。TCP 更可靠,具有广泛的错误检查,并需要更多资源。它被 HTTP、SMTP 或 FTP 等服务使用。UDP 的可靠性差得多,错误检查有限,并且需要的资源更少。它被 VoIP 等服务使用。

Go net 包

Go 的 net 包为网络 I/O 提供了一个可移植的接口,包括 TCP/IP、UDP、域名解析和 Unix 域 Socket。

func Dial(network, address string) (Conn, error)

Dial 函数连接到命名网络上的地址——它会打开一个 Socket。我们使用 Write 函数向 Socket 写入数据,使用 Read 函数从 Socket 读取数据。

已知的网络有

对于 TCP 和 UDP 网络,地址的格式为 host:port。主机必须是字面 IP 地址,或者是一个可以解析为 IP 地址的主机名。在使用 TCP 时,如果主机解析为多个 IP 地址,Dial 函数会按顺序尝试每个 IP 地址,直到其中一个成功。

Go UDP Socket 示例

UDP 是一种通信协议,通过网络传输独立的数据包,不保证到达,也不保证传递顺序。回显是一项使用 UDP 的服务。

回显协议是 RFC 862 中定义的 Internet 协议套件中的一项服务。回显协议可以使用 TCP 或 UDP,端口号为 7。服务器会发送回它接收到的数据的相同副本。

我们在本地 Debian 系统上设置了一个回显服务。

$ cat /etc/services | grep echo | head -4
echo            7/tcp
echo            7/udp
echo            4/ddp                   # AppleTalk Echo Protocol

端口 7 保留给回显服务。

出于安全原因,回显服务在大多数情况下是禁用的。因此,我们在本地网络中创建了自己的服务。

我们在本地网络中的另一台计算机上启动回显服务。

# apt install xinetd

我们安装了 xinetd 包。该包包含 xinetd 守护进程,它是一个 TCP 包装的超级服务,用于访问回显、FTP、IMAP 和 telnet 等部分常用网络服务。

...
# This is the udp version.
service echo
{
        disable         = no
        type            = INTERNAL
        id              = echo-dgram
        socket_type     = dgram
        protocol        = udp
        user            = root
        wait            = yes
}

/etc/xinetd.d/echo 文件中,我们将 disable 选项设置为 no。

# systemctl start xinetd

我们启动了该服务。

echo_client.go
package main

import (
    "fmt"
    "log"
    "net"
    "os"
)

func main() {

    if len(os.Args) != 2 {

        fmt.Println("Usage: echo_client message")
        os.Exit(1)
    }

    msg := os.Args[1]

    con, err := net.Dial("udp", "debian:7")

    checkErr(err)

    defer con.Close()

    _, err = con.Write([]byte(msg))

    checkErr(err)

    reply := make([]byte, 1024)

    _, err = con.Read(reply)

    checkErr(err)

    fmt.Println("reply:", string(reply))
}

func checkErr(err error) {

    if err != nil {

        log.Fatal(err)
    }
}

该示例向本地网络机器上的回显服务发送一条小消息。消息会被回显回来。

con, err := net.Dial("udp", "debian:7")

使用 Dial 函数,我们可以在 UDP 网络上连接到 Debian 系统的 7 端口,创建一个 Socket。

_, err = con.Write([]byte(msg))

我们使用 Write 将消息写入 Socket。

reply := make([]byte, 1024)

_, err = con.Read(reply)

我们使用 make 函数创建一个字节切片。然后我们创建对该切片的响应。

fmt.Println("reply:", string(reply))

最后,我们在终端上显示响应。

$ go run echo_client.go cau
reply: cau

Go TCP Socket 示例

TCP 为在通过 IP 网络通信的主机上运行的应用程序提供可靠、有序且经过错误检查的字节流传输。

$ cat /etc/services | grep qotd
qotd            17/tcp          quote

端口 17 保留给每日名言服务。

每日引言服务是一个有用的调试和测量工具。每日引言服务只是发送一条短消息,而不考虑输入。

qotd.go
package main

import (
    "fmt"
    "log"
    "net"
)

func main() {

    con, err := net.Dial("tcp", "djxmmx.net:17")

    checkErr(err)

    defer con.Close()

    msg := ""

    _, err = con.Write([]byte(msg))

    checkErr(err)

    reply := make([]byte, 1024)

    _, err = con.Read(reply)

    checkErr(err)

    fmt.Println(string(reply))
}

func checkErr(err error) {

    if err != nil {

        log.Fatal(err)
    }
}

该示例创建了一个连接到 QOTD 服务的客户端程序。

con, err := net.Dial("tcp", "djxmmx.net:17")

TCP Socket 使用 net.Dial 创建。我们提供主机名和端口号。请注意,此类服务是短暂的;它们可能会随时被删除。

msg := ""

_, err = con.Write([]byte(msg))

我们向套接字发送了一条空消息。

_, err = con.Read(reply)

我们使用 Read 读取响应。

fmt.Println(string(reply))

我们打印响应。

$ go run qotd.go 
"The secret of being miserable is to have leisure to bother about whether
 you are happy or not.  The cure for it is occupation."
 George Bernard Shaw (1856-1950)

Go Socket HEAD 请求

HEAD 请求是一种 HTTP GET 请求,但不包含消息正文。请求/响应的头部包含元数据,例如 HTTP 协议版本或内容类型。

head_req.go
package main

import (
    "fmt"
    "io/ioutil"
    "log"
    "net"
)

func main() {

    con, err := net.Dial("tcp", "webcode.me:80")
    checkError(err)

    req := "HEAD / HTTP/1.0\r\n\r\n"

    _, err = con.Write([]byte(req))
    checkError(err)

    res, err := ioutil.ReadAll(con)
    checkError(err)

    fmt.Println(string(res))
}

func checkError(err error) {

    if err != nil {
        log.Fatal(err)
    }
}

在代码示例中,我们向 webcode.me 发送了一个 HEAD 请求。

req := "HEAD / HTTP/1.0\r\n\r\n"

HEAD 请求是使用 HEAD 命令后跟资源 URL 和 HTTP 协议版本发出的。请注意,\r\n 字符是通信过程的强制组成部分。详细信息在 RFC 7231 文档中有描述。

$ go run head_req.go 
HTTP/1.1 200 OK
Server: nginx/1.6.2
Date: Tue, 29 Jun 2021 13:09:11 GMT
Content-Type: text/html
Content-Length: 348
Last-Modified: Sat, 20 Jul 2019 11:49:25 GMT
Connection: close
ETag: "5d32ffc5-15c"
Accept-Ranges: bytes

Go HTTP GET 请求

HTTP GET 方法请求指定资源的表示。使用 GET 的请求应该只检索数据。

get_req.go
package main

import (
    "fmt"
    "io/ioutil"
    "log"
    "net"
)

func main() {

    con, err := net.Dial("tcp", "webcode.me:80")
    checkError(err)

    req := "GET / HTTP/1.0\r\n" +
        "Host: webcode.me\r\n" +
        "User-Agent: Go client\r\n\r\n"

    _, err = con.Write([]byte(req))
    checkError(err)

    res, err := ioutil.ReadAll(con)
    checkError(err)

    fmt.Println(string(res))
}

func checkError(err error) {

    if err != nil {
        log.Fatal(err)
    }
}

该示例使用 GET 请求读取 webcode.me 的主页。

req := "GET / HTTP/1.0\r\n" +
    "Host: webcode.me\r\n" +
    "User-Agent: Go client\r\n\r\n"

我们向 Socket 写入了一个简单的 GET 请求。

$ go run get_req.go 
HTTP/1.1 200 OK
Server: nginx/1.6.2
Date: Tue, 29 Jun 2021 13:12:48 GMT
Content-Type: text/html
Content-Length: 348
Last-Modified: Sat, 20 Jul 2019 11:49:25 GMT
Connection: close
ETag: "5d32ffc5-15c"
Access-Control-Allow-Origin: *
Accept-Ranges: bytes

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>My html page</title>
</head>
<body>

    <p>
        Today is a beautiful day. We go swimming and fishing.
    </p>
    
    <p>
         Hello there. How are you?
    </p>
    
</body>
</html>

Go Socket 发送邮件

要通过套接字发送电子邮件,我们使用 SMTP 命令,例如 HELO、MAIL FROM 和 DATA。

send_mail.go
package main

import (
    "fmt"
    "io/ioutil"
    "log"
    "net"
)

func main() {

    from := "john.doe@example.com"
    to := "root@core9"
    name := "John Doe"
    subject := "Hello"
    body := "Hello there"

    host := "core9:25"

    con, err := net.Dial("tcp", host)
    checkError(err)

    req := "HELO core9\r\n" +
        "MAIL FROM: " + from + "\r\n" +
        "RCPT TO: " + to + "\r\n" +
        "DATA\r\n" +
        "From: " + name + "\r\n" +
        "Subject: " + subject + "\r\n" +
        body + "\r\n.\r\n" + "QUIT\r\n"

    _, err = con.Write([]byte(req))
    checkError(err)

    res, err := ioutil.ReadAll(con)
    checkError(err)

    fmt.Println(string(res))
}

func checkError(err error) {

    if err != nil {
        log.Fatal(err)
    }
}

该示例将电子邮件发送到本地网络上托管邮件服务器的一台计算机。

$ go run send_mail.go 
220 core9 ESMTP Sendmail 8.15.2/8.15.2; Wed, 30 Jun 2021 14:21:21 +0200 (CEST)
250 core9 Hello spartan.local [192.168.0.20], pleased to meet you
250 2.1.0 john.doe@example.com... Sender ok
250 2.1.5 root@core9... Recipient ok
354 Enter mail, end with "." on a line by itself
250 2.0.0 15UCLLd3001374 Message accepted for delivery
221 2.0.0 core9 closing connection

我们发送了电子邮件。

From john.doe@example.com Wed Jun 30 14:21:21 2021
Return-Path: <john.doe@example.com>
Received: from core9 (spartan.local [192.168.0.20])
	by core9 (8.15.2/8.15.2) with SMTP id 15UCLLd3001374
	for root@core9; Wed, 30 Jun 2021 14:21:21 +0200 (CEST)
	(envelope-from john.doe@example.com)
Date: Wed, 30 Jun 2021 14:21:21 +0200 (CEST)
Message-Id: <202106301221.15UCLLd3001374@core9>
From: John.Doe
Subject: Hello
To: undisclosed-recipients:;
Status: RO

Hello there

我们在接收端检查了电子邮件。

来源

Go net 包 - 参考

在本文中,我们使用了 Go 中的 Socket。

作者

我叫 Jan Bodnar,是一名热情的程序员,拥有丰富的编程经验。我自 2007 年以来一直在撰写编程文章。迄今为止,我已撰写了 1,400 多篇文章和 8 本电子书。我在编程教学方面拥有十多年的经验。

列出所有 Go 教程