如何在Golang中使用套接字

Go套接字教程展示了如何在Golang中使用套接字。套接字编程是低级的。本教程的目的是介绍网络编程,包括这些底层细节。有更高级别的API在现实场景中可能更实用。

$ go version
go version go1.18.1 linux/amd64

我们使用Go版本1.18。

网络协议

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

去网包

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

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

Dial函数连接到指定网络上的地址——它打开一个套接字。我们使用Write函数写入套接字,使用Read函数从套接字读取数据。

已知的网络是:

  • tcp
  • tcp4
  • tcp6
  • udp
  • udp4
  • udp6
  • ip
  • ip4
  • ip6
  • unix
  • unixgram
  • unixpacket

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

GoUDP套接字示例

UDP是一种在网络上传输独立数据包的通信协议,不保证到达且不保证传递顺序。使用UDP的一种服务是echo。

Echo协议是RFC862中定义的Internet协议套件中的一项服务。Echo协议可以在端口号7上使用TCP或UDP。服务器发回它收到的数据的相同副本。

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

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

端口7保留用于回显服务。

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

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

# apt install xinetd

我们安装xinetd包。该软件包包含xinetd守护进程,它是一个TCP封装的超级服务,用于访问流行网络服务的子集,包括echo、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

我们启动服务。

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网络的端口7上创建到Debian系统的套接字。

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

我们使用Write将消息写入套接字。

reply := make([]byte, 1024)

_, err = con.Read(reply)

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

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

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

$ go run echo_client.go cau
reply: cau

GoTCP套接字示例

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

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

端口17保留用于当日服务的报价。

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

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套接字是用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)

去socketHEAD请求

HEAD请求是没有消息正文的HTTPGET请求。请求/响应的标头包含元数据,例如HTTP协议版本或内容类型。

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字符是通信过程的必需部分。RFC7231文档中描述了详细信息。

$ 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

去HTTPGET请求

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

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"

我们向套接字写入一个简单的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>

去套接字发送邮件

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

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中的套接字。

列出所有Go教程。

赞(0) 打赏

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏