# Golang 网络编程
# 一、计算机网络
- 参考:计算机网络丨网络层
# 二、Socket 编程
# 1. Socket 简介
对于底层网络应用开发者而言,几乎所有网络编程都是 Socket,因为大部分底层网络 的编程都离不开 Socket 编程。 HTTP 编程、 Web 开发、 IM 通信 、视频流传输的底层都是 Socket 编程 。
日常生活中我们每天打开浏览器浏览网页、使用 QQ 聊天、 邮件收发、 直播等,客户端和服务器端的通信在底层看来都是依靠 Socket 通信的。
Socket 起源于 UNIX,而 UNIX 的基本哲学之一就是“一切皆文件”,都可以用 “打开(open)→读写(write/read)→关闭(close)” 模式来操作, Socket 就是该模式的一个 实现,网络的 Socket 数据传输是 一种特殊的 I/0,Socket 也是一种文件描述符。 Socket 也具有一个类似于打开文件的函数调用:Socket(), 该函数返回一个整型的 Socket 描述符, 随后的连接建立、数据传输等操作都是通过该 Socket 实现的 。
# 2. Socket 如何通信
网络中的进程之间如何通过 Socket 通信呢?首要解决的问题是如何唯一标识一个进程,否则通信无从谈起 ! 在本地可以通过进程 PID 来唯一标识一个进程,但是在网络中这是行不通的。其实 TCP/IP 协议族己经帮我们解决了这个问题,网络层的“IP 地址”可以唯一标识网络中的主机,而传输层的“协议+端口” 可以唯一标识主机中的应用程序(进程)。这样利用三大要素(IP 地址、协议、端口)就可以标识网络的进程了,网络中需要互相通信的进程,就可以利用这个标志在它们之间进行交互。
使用 TCP/IP 协议的应用程序通常采用应用编程接口:UNIX BSD 的套接字和 UNIX System V 的 TLI(己经被淘汰),来实现网络进程之间的通信。就目前而言, 几乎所有的应用程序都是采用 Socket,而现在又是网络时代,网络中进程通信是无处不在的,这就是为什么说“一切皆 Socket”。
Socket有两种:
- TCPSocket
- UDP Socket
TCP 和 UDP 是协议,而要确定一个进程得需要三要素, 所以还需要 IP 地址和端口。
# 3. Golang 语言中的 IP 类型
// An IP is a single IP address, a slice of bytes.
// Functions in this package accept either 4-byte (IPv4)
// or 16-byte (IPv6) slices as input.
//
// Note that in this documentation, referring to an
// IP address as an IPv4 address or an IPv6 address
// is a semantic property of the address, not just the
// length of the byte slice: a 16-byte slice can still
// be an IPv4 address.
type IP []byte
在 net 包中有很多函数用于操作 IP,但是其中比较常用的也就几个,其中 ParseIP(s string) IP
函数会把一个 IPv4 或者 IPv6 的地址转化成 IP 类型,请看下面的例子:
package main
import (
"fmt"
"net"
)
func main() {
addr := net.ParseIP("127.0.0.1")
fmt.Println("addr:", addr)
addr = net.ParseIP("111111")
fmt.Println("addr:", addr)
}
输出:
addr: 127.0.0.1
addr: <nil>
# 4. Dial() 函数
在 Go 语言中编写网络程序时,将看不到传统的编码形式。 Go 语言中 Socket 编程的 API 都在 net 包中 。 Go语言提供了 Dial() 函数来连接服务器,使用 Listen 监听, Accept 接收连接,所以 Go 语言的网络编程和其他同类语言(C 语言)一样有着相似的 API,只需了解即可。
Go 语言标准库对传统的 Socket 编程过程进行了抽象和封装 。 无论期望使用什么协议建立什么形式的连接,都只需要调用 net.Dial()
即可。
// Dial connects to the address on the named network.
//
// Known networks are "tcp", "tcp4" (IPv4-only), "tcp6" (IPv6-only),
// "udp", "udp4" (IPv4-only), "udp6" (IPv6-only), "ip", "ip4" (IPv4-only),
// "ip6" (IPv6-only), "unix", "unixgram" and "unixpacket".
func Dial(network, address string) (Conn, error) {
var d Dialer
return d.Dial(network, address)
}
- network:网络协议的名字
- address:IP 地址或域名
// TCP 连接
conn, err := net.Dial("tcp", "127.0.0.1:8080")
// UPD 连接
conn, err := net.Dial("udp", "127.0.0.1:8080")
// ICMP 连接(使用协议名称)
conn, err := net.Dial("ip4:icmp", "www.baidu.com")
// ICMP 连接(使用协议编号)
conn, err := net.Dial("ip4:1", "127.0.0.1")
在成功建立连接后,就可以进行数据的发送和接收。在发送数据时,使用 conn 的 Write() 成员方法,在接收数据时使用 Read()方法。
# 5. Golang 实现 TCP 通信
TCP/IP(Transmission Control Protocol/Internet Protocol)即传输控制协议/网间协议,是一种面向连接(连接导向)的、可靠的、基于字节流的传输层(Transport layer)通信协议,因为是面向连接的协议,数据像水流一样传输,会存在黏包问题。
5.1 TCP 服务端
一个 TCP 服务端可以同时连接很多个客户端,例如世界各地的用户使用自己电脑上的浏览器访问淘宝网。因为 Go 语言中创建多个 Goroutine 实现并发非常方便和高效,所以我们可以每建立一次链接就创建一个 Goroutine 去处理。
TCP 服务端程序的处理流程:
- 监听端口
- 接收客户端请求建立链接
- 创建 Goroutine 处理链接
示例代码:
package main
import (
"bufio"
"fmt"
"net"
)
func main() {
// 开放端口 20000,监听客户端
listen, err := net.Listen("tcp", "127.0.0.1:20000")
if err != nil {
fmt.Println("listen failed, err:", err)
return
}
// 循环监听
for {
// 建立连接
conn, err := listen.Accept()
if err != nil {
fmt.Println("accept failed, err:", err)
continue
}
go process(conn)
}
}
/**
处理函数
*/
func process(conn net.Conn) {
defer conn.Close()
for {
reader := bufio.NewReader(conn)
var buf [128]byte
// 读取数据
n, err := reader.Read(buf[:])
if err != nil {
fmt.Println("read from client failed, err:", err)
}
receivedStr := string(buf[:n])
fmt.Println("收到来自 client 的数据:", receivedStr)
conn.Write([]byte("server 已收到:" + receivedStr))
}
}
5.2 TCP 客户端
一个 TCP 客户端进行 TCP 通信的流程如下:
- 建立与服务端的链接
- 进行数据收发
- 关闭链接
示例代码:
package main
import (
"bufio"
"fmt"
"net"
"os"
"strings"
)
func main() {
// 建立连接,绑定到服务端开放的端口上
conn, err := net.Dial("tcp", "127.0.0.1:20000")
if err != nil {
fmt.Println("client dial failed, err:", err)
return
}
// 关闭连接
defer conn.Close()
// 建立控制台信息读取器
consoleReader := bufio.NewReader(os.Stdin)
for {
// 读取控制台输入
console, err := consoleReader.ReadString('\n')
if err != nil {
fmt.Println("read input string failed, err:", err)
continue
}
// 处理控制台输入
consoleInfo := strings.Trim(console, "\r\n")
if strings.ToUpper(consoleInfo) == "Q" {
// 如果控制台输入 q 就退出
fmt.Println("程序正常退出...")
return
}
// 发送数据
_, err = conn.Write([]byte(consoleInfo))
if err != nil {
fmt.Println("send data to server failed, err:", err)
return
}
// 建立缓冲区
buf := make([]byte, 1024)
// 读取 server 数据
n, err := conn.Read(buf)
if err != nil {
fmt.Println("receive from server failed, err:", err)
return
}
fmt.Printf("receive from server: %s\n", buf[:n])
}
}
先运行 Server 再运行 Client,然后在 Client 控制台输入:Hedon
Hedon
receive from server: server 已收到:Hedon
来看 Server 控制台:
收到来自 client 的数据: Hedon
至此,我们就使用 Socket 完成了 TCP 通信。
# 6. Golang TCP 粘包
TCP 粘包
半包
指接受方没有接受到一个完整的包,只接受了部分,这种情况主要是由于 TCP 为提高传输效率,将一个包分配的足够大,导致接受方并不能一次接受完。(在长连接和短连接中都会出现)。
粘包
指发送方发送的若干包数据到接收方接收时粘成一包,从接收缓冲区看,后一包数据的头紧接着前一包数据的尾。出现粘包现象的原因是多方面的,它既可能由发送方造成,也可能由接收方造成。
- 发送方引起的粘包是由 TCP 协议本身造成的,TCP 为提高传输效率,发送方往往要收集到足够多的数据后才发送一包数据。若连续几次发送的数据都很少,通常 TCP 会根据优化算法把这些数据合成一包后一次发送出去,这样接收方就收到了粘包数据。
- 接收方引起的粘包是由于接收方用户进程不及时接收数据,从而导致粘包现象。这是因为接收方先把收到的数据放在系统接收缓冲区,用户进程从该缓冲区取数据,若下一包数据到达时前一包数据尚未被用户进程取走,则下一包数据放到系统接收缓冲区时就接到前一包数据之后,而用户进程根据预先设定的缓冲区大小从系统接收缓冲区取数据,这样就一次取到了多包数据。
半包影响
无法接受到完整的数据包。
粘包影响
无法区分不同的数据包。
半包解决
- 封包,加入数据长度这个变量。
粘包解决
- 对于发送方引起的粘包现象,用户可通过编程设置来避免,TCP 提供了强制数据立即传送的操作指令 push,TCP 软件收到该操作指令后,就立即将本段数据发送出去,而不必等待发送缓冲区满;
- 对于接收方引起的粘包,则可通过优化程序设计、精简接收进程工作量、提高接收进程优先级等措施,使其及时接收数据,从而尽量避免出现粘包现象;
- 由接收方控制,将一包数据按结构字段,人为控制分多次接收,然后合并,通过这种手段来避免粘包。
- 封包:封包就是给一段数据加上包头,这样一来数据包就分为包头和包体两部分内容(过滤非法包时封包会加入”包尾”内容)。包头部分的长度是固定的,并且它存储了包体的长度,根据包头长度固定以及包头中含有包体长度的变量就能正确的拆分出一个完整的数据包。
UDP 有粘包问题吗?
不会。UDP 是基于报文的,每一份数据都是一个报文,都有明显的边界可以区分。
下面通过一个例子来演示一下 TCP 中的粘包现象。
6.1 TCP 服务端
package main
import (
"bufio"
"fmt"
"net"
"os"
)
func main() {
// 打开端口监听
listen, err := net.Listen("tcp", "127.0.0.1:20000")
if err != nil {
fmt.Println("listen failed, err:", err)
os.Exit(1)
}
for {
// 监听客户端
conn, err := listen.Accept()
if err != nil {
fmt.Println("accept failed, err:", err)
continue
}
go process(conn)
}
}
/**
处理函数
*/
func process(conn net.Conn) {
defer conn.Close()
for {
reader := bufio.NewReader(conn)
// 建立缓冲区
buf := make([]byte, 128)
// 读取数据
n, err := reader.Read(buf)
if err != nil {
fmt.Println("read from client failed, err:", err)
break
}
receivedStr := string(buf[:n])
fmt.Println("receive from client:", receivedStr)
// 回应 client
conn.Write([]byte("server has received from client, data:" + receivedStr))
}
}
6.2 TCP 客户端
package main
import (
"fmt"
"net"
)
func main() {
// 建立连接
conn, err := net.Dial("tcp", "127.0.0.1:20000")
if err != nil {
fmt.Println("client dial failed, err:", err)
return
}
// 关闭连接
defer conn.Close()
// 发送数据
for i := 0; i < 20; i++ {
msg := `Hello, Hello. How are you?`
conn.Write([]byte(msg))
}
}
先运行 Server 再运行 Client,会发现 Client 直接停止,然后看 Server 控制台:
receive from client: Hello, Hello. How are you?Hello, Hello. How are you?
receive from client: Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are yo
receive from client: Hello, Hello. How are you?
read from client failed, err: read tcp 127.0.0.1:20000->127.0.0.1:49978: read: connection reset by peer
receive from client: Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are yo
receive from client: Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are yo
receive from client: Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?
read from client failed, err: EOF
上述现象便是由 粘包 引起的。
6.3 解决 TCP 粘包 —— 封包
我们可以自己定义一个协议,比如数据包的前 4 个字节为包头,里面存储的是发送的数据的长度。
package proto
import (
"bufio"
"bytes"
"encoding/binary"
)
// Encode 消息编码
func Encode(message string) ([]byte, error) {
// 读取消息长度,转为 int32 类型(占 4 个字节)
var length = int32(len(message))
// 封包
var pkg = new (bytes.Buffer)
// 写入消息头
err := binary.Write(pkg, binary.LittleEndian, length)
if err != nil {
return nil, err
}
// 写入消息实体
err = binary.Write(pkg, binary.LittleEndian, []byte(message))
if err != nil {
return nil, err
}
return pkg.Bytes(), nil
}
// Decode 消息解码
func Decode(reader *bufio.Reader) (string, error) {
// 读取消息长度
firstFourByte, _ := reader.Peek(4) //读取前 4 个字节数据
firstFourByteBuff := bytes.NewReader(firstFourByte)
var length int32
err := binary.Read(firstFourByteBuff, binary.LittleEndian, &length)
if err != nil {
return "", err
}
// Buffered 返回缓冲中现有的可读取的字节数
if int32(reader.Buffered()) < length + 4 {
// 长度不够,不读
return "", err
}
// 读取真正的消息实体
pack := make([]byte, int(4 + length))
_, err = reader.Read(pack)
if err != nil {
return "", err
}
return string(pack[4:]), nil
}
服务端:
package main
import (
"baiscstudy/learning/network/tcp/proto"
"bufio"
"fmt"
"io"
"net"
)
func main() {
// 打开端口监听
listen, err := net.Listen("tcp", "127.0.0.1:30000")
if err != nil {
fmt.Println("listen failed, err:", err)
return
}
// 关闭监听
defer listen.Close()
for {
// 监听客户端
conn, err := listen.Accept()
if err != nil {
fmt.Println("accept client failed, err:", err)
continue
}
go process(conn)
}
}
/**
处理函数
*/
func process(conn net.Conn) {
// 关闭连接
defer conn.Close()
// 读取器
reader := bufio.NewReader(conn)
for {
// 解码
msg, err := proto.Decode(reader)
if err == io.EOF {
return
}
if err != nil {
fmt.Println("decode message failed, err:", err)
return
}
fmt.Println("receive from client, data:", msg)
}
}
客户端:
package main
import (
"baiscstudy/learning/network/tcp/proto"
"fmt"
"net"
)
func main() {
// 建立连接
conn, err := net.Dial("tcp", "127.0.0.1:30000")
if err != nil {
fmt.Println("client dial failed, err", err)
return
}
// 关闭连接
defer conn.Close()
// 发送数据
for i := 0; i < 9; i++ {
msg := `Hello, Hello. How are you?`
// 编码
data, err := proto.Encode(msg)
if err != nil {
fmt.Println("encode msg failed, err:", err)
return
}
// 发送
conn.Write(data)
}
}
先启动 Server,再启动 Client,看 Server 控制台:
receive from client, data: Hello, Hello. How are you?
receive from client, data: Hello, Hello. How are you?
receive from client, data: Hello, Hello. How are you?
receive from client, data: Hello, Hello. How are you?
receive from client, data: Hello, Hello. How are you?
receive from client, data: Hello, Hello. How are you?
receive from client, data: Hello, Hello. How are you?
receive from client, data: Hello, Hello. How are you?
receive from client, data: Hello, Hello. How are you?
这个时候就会发现粘包现象已经解决了。
# 7. Golang 实现 UDP 通信
UDP 协议(User Datagram Protocol)中文名称是用户数据报协议,是 OSI(Open System Interconnection,开放式系统互联)参考模型中一种 无连接 的传输层协议,不需要建立连接就能直接进行数据发送和接收,属于不可靠的、没有时序的通信,但是 UDP 协议的实时性比较好,通常用于视频直播相关领域。
7.1 UDP 服务端
package main
import (
"fmt"
"net"
)
func main() {
// 开放 30000 端口监听
listen, err := net.ListenUDP("udp", &net.UDPAddr{
IP: net.IPv4(0, 0, 0, 0),
Port: 30000,
})
if err != nil {
fmt.Println("server open listen failed, err:", err)
return
}
// 关闭监听
defer listen.Close()
// 循环监听
for {
data := make([]byte, 1024)
// 接收数据
n, addr, err := listen.ReadFromUDP(data)
if err != nil {
fmt.Println("read udp failed, err:", err)
continue
}
fmt.Printf("server received from client data: %v addr: %v count: %v\n", string(data[:n]), addr, n)
// 发送数据
_, err = listen.WriteToUDP(append([]byte("server send: "), data...), addr)
if err != nil {
fmt.Println("write to udp failed, err:", err)
continue
}
}
}
7.2 UDP 客户端
package main
import (
"fmt"
"net"
)
func main() {
// 建立连接
conn, err := net.DialUDP("udp", nil, &net.UDPAddr{
IP: net.IPv4(0, 0, 0, 0),
Port: 30000,
})
if err != nil {
fmt.Println("client created connection failed, err:", err)
return
}
// 关闭连接
defer conn.Close()
// 发送数据
sendData := []byte("client data")
n, err := conn.Write(sendData)
if err != nil {
fmt.Println("client send data failed, err:", err)
return
}
// 接收数据
receivedData := make([]byte, 1024)
n, addr, err := conn.ReadFromUDP(receivedData)
if err != nil {
fmt.Println("client received data from server, err:", err)
return
}
fmt.Printf("client received data from server: %v addr: %v count: %v\n", string(receivedData), addr, n)
}
先开 Server 再开 Client。
可以看到 Client 控制台:
client received data from server: server send: client data addr: 127.0.0.1:30000 count: 1024
再看 Server 控制台:
server received from client data: client data addr: 127.0.0.1:63009 count: 11
# 三、HTTP 编程
参考:
- 《Go语言编程入门与实战技巧.黄靖钧》
- https://www.liwenzhou.com/posts/Go/15_socket/