22.网络编程
22.1 TCP/IP协议栈
TCP/IP(Transmission Control Protocol/Internet Protocol),中文翻译为传输控制协议/因特网互联协议。它是一个包含很多工作在不同层的协议族,其中最著名的两个协议分别是TCP和IP协议。TCP/IP协议一共定义了四层:网络接入层、网络层、传输层、应用层

TCP/IP协议是事实标准,目前在广域网和局域网使用非常广泛。
22.2 TCP与UDP区别
TCP/IP协议在传输层又可以分为TCP和UDP两种,主要区分如下所示:
| TCP | UDP | |
|---|---|---|
| 连接类型 | 面向连接 | 无连接 |
| 可靠性 | 可靠 | 不可靠 |
| 有序 | 数据包有序号 | 没有包序 |
| 使用场景 | 适用于大多数场景,数据不能出现任何问题 | 视频和音频 |
| 服务对象 | 一对一 | 支持一对一、一对多和多对多 |
以下解释如下所示:
1.连接
- 使用TCP时,需要通信双方提前建立连接,建立连接前需要经过三次握手,结束连接时,需要四次挥手
- UPD不需要提前建立连接,即刻传输数据
2.可靠性
- TCP需要确定每一个包是否收到,如遇丢包,则需要重新发送数据包。可靠性高,但会牺牲效率
- UDP是尽最大努力保证数据传输,但不需要确认数据包,因此会出现丢包也无法知道,也不重新发送数据包,可靠性低,但效率高一些
3.有序
- TCP包有序号,可以进行顺序控制。第一个包序号随机生成,之后的序号都会与第一个序号相关
- UDP无序号,无法纠正,只能应用层进行验证
4.数据
- TCP协议是流协议,也就是一大段数据看作字节流,一段段持续发送这些字节,各字节流之间没有边界,通过序号保证顺序和可靠
- UDP协议是数据报协议,每一份数据封装在一个单独的数据包中,一份一份发送数据,各个数据包之间存在边界,但可能会导致丢包和乱序。
5.服务对象
- TCP是一对一的点对点服务,即一个连接只有两个端点
- UDP支持一对一、一对多、多对多的交互式通信
22.3 TCP/UDP编程
无论是基于TCP还是UDP编程,其底层原理都是基于Socket实现的。在Go语言中Socket都封装在内置net包。即通过net包就能进行TCP和UDP的网络编程。
22.3.1 TCP服务端编程
服务器端进行编程的步骤如下所示:
- 创建Socket对象
- 绑定IP地址和端口
- 开始在指定的IP端口上监听
- 获取用于传输数据的新Socket对象。
示意图如下所示:

以上示意图来自于马哥教育
示例代码如下所示:
package main
import (
"log"
"net"
)
func main() {
tcpAddr, err := net.ResolveTCPAddr("tcp", "0.0.0.0:9999")
if err != nil {
log.Panicf("解析IP和端口出错:%v\n", err)
}
// 底层创建Socket、bind、Listen,非阻塞IO
server, err := net.ListenTCP("tcp", tcpAddr)
if err != nil {
log.Panicf("绑定IP和端口出错%v\n", err)
}
// 关闭Server
defer server.Close()
// 循环接收客户端的连接
for {
// 开始接收数据,并分配新的Socket
conn, _ := server.Accept()
go func() {
// Client退出前,关闭连接
defer conn.Close()
// 创建缓冲区,进行收发操作
buffer := make([]byte, 4096)
// 成功接收到数据
n, err := conn.Read(buffer)
if err != nil {
log.Fatalf("读取数据出错:%v\n", err)
}
clientAddr := conn.RemoteAddr().String()
if n == 0 {
log.Printf("客户端%s主动断开连接", clientAddr)
return
}
data := buffer[:n]
log.Printf("从 %v 成功接收到数据长度为:%d,数据内容为:%s\n", clientAddr, n, data)
// Server 返回响应数据
conn.Write([]byte("Hello"))
}()
}
}
22.3.2 TCP客户端编程
客户端编程与服务端非常相似,其基本步骤如下所示:
- 创建Socket对象
- 随机选择端口即可以向服务端发起连接
- 连接成功后,就可以发起操作
- 最后关闭连接
package main
import (
"fmt"
"log"
"net"
"runtime"
"time"
)
func main() {
rAddr, err := net.ResolveTCPAddr("tcp", "0.0.0.0:9999")
if err != nil {
log.Panicf("解析IP和端口出错:%v\n", err)
}
// 底层创建Socket、并发起连接
client, err := net.DialTCP("tcp", nil, rAddr)
if err != nil {
log.Panicf("连接远程IP和端口出错%v\n", err)
}
log.Printf("本地连接地址和端口%v\n", client.LocalAddr())
// 关闭Server
defer client.Close()
quitFlag := make(chan struct{})
go func() {
// 收发数据
buffer := make([]byte, 4096)
for {
// 设置一个超时时间,超出设定的时间,则不再等待
client.SetReadDeadline(time.Now().Add(10 * time.Second))
// 会出现阻塞情况出现
n, err := client.Read(buffer)
if err != nil {
log.Printf("client接收数据出错%v\n", err)
if _, ok := err.(*net.OpError); !ok {
// 不是读超时
quitFlag <- struct{}{}
return
}
continue
}
log.Printf("client接收数据的为:%v\n", string(buffer[:n]))
}
}()
go func() {
var input string
for {
// 用户中止输入命令
if input == "q" {
quitFlag <- struct{}{}
return
}
n, err := fmt.Scan(&input)
if err != nil {
log.Printf("读取输入数据出错%v\n", err)
}
log.Printf("用户输入数据长度为%v\n", n)
_, err = client.Write([]byte(input))
if err != nil {
log.Printf("client发送的数据出错%v\n", err)
}
log.Printf("client发送的数据为:%v\n", input)
}
}()
t := time.NewTicker(1 * time.Second)
LOOP:
for {
select {
case <-quitFlag:
break LOOP
case <-t.C:
log.Printf("Goroutine number is: %d\n", runtime.NumGoroutine())
}
}
log.Println("客户端退出")
}
22.3.3 UDP服务端编程
UDP是无需建立连接就可以相互发送数据的网络传输协议,因此UDP存在不可靠,通信没有时序待缺点,但却非常适合于视频、音频等场景。其服务端通信示例如下所示:

以上示意图来自于马哥教育
UDP 基本创建步骤如下所示:
- 创建Socker对象
- 绑定IP和端口
- 收发传输数据
- 释放资源
package main
import (
"fmt"
"log"
"net"
"runtime"
"time"
)
func main() {
udpAddr, err := net.ResolveUDPAddr("udp", "0.0.0.0:9999")
if err != nil {
log.Fatalln("解析UDP IP和Port错误")
}
// 创建Socker对象,并绑定IP和端口
server, err := net.ListenUDP("udp", udpAddr)
if err != nil {
log.Fatalf("监听%v出错\n", udpAddr)
}
// 不需要Accept
defer server.Close()
quitFlag := make(chan struct{})
go func() {
// 进行收发
buffer := make([]byte, 4096)
for {
server.SetReadDeadline(time.Now().Add(5 * time.Second))
n, raddr, err := server.ReadFromUDP(buffer)
log.Printf("Server从%v读取到的数据为:%v\n", raddr, string(buffer[:n]))
if err != nil {
log.Printf("%v远程读取数据出错%v 数据类型%[2]T\n", raddr, err)
if _, ok := err.(*net.OpError); !ok {
log.Println("ok:", ok)
quitFlag <- struct{}{}
return
}
continue
}
s := fmt.Sprintf("client: %v ,data: %v \n", raddr, string(buffer[:n]))
server.WriteToUDP([]byte(s), raddr)
}
}()
t := time.NewTicker(5 * time.Second)
LOOP:
for {
select {
case <-quitFlag:
break LOOP
case <-t.C:
log.Printf("Goroutine Num:%d\n", runtime.NumGoroutine())
}
}
log.Println("主协程结束")
}
22.3.4 UDP客户端编程
UDP客户端编程与服务端也非常相似,基本操作步骤如下所示:
- 创建Socker对象
- 传输数据
- 释放资源
package main
import (
"fmt"
"log"
"net"
"runtime"
"time"
)
func main() {
raddr, err := net.ResolveUDPAddr("udp", "0.0.0.0:9999")
if err != nil {
log.Fatalln("解析UDP IP和Port错误")
}
// 创建Socker对象,无需绑定
client, err := net.DialUDP("udp", nil, raddr)
if err != nil {
log.Fatalf("连接%v出错\n", raddr)
}
// 不需要Accept
defer client.Close()
quitFlag := make(chan struct{})
go func() {
// 进行收发
buffer := make([]byte, 4096)
for {
client.SetReadDeadline(time.Now().Add(5 * time.Second))
n, raddr, err := client.ReadFromUDP(buffer)
log.Printf("Client从%v读取到的数据为:%v\n", raddr, string(buffer[:n]))
if err != nil {
log.Printf("%v远程读取数据出错%v 数据类型%[2]T\n", raddr, err)
if _, ok := err.(*net.OpError); !ok {
log.Println("ok:", ok)
quitFlag <- struct{}{}
return
}
continue
}
}
}()
go func() {
var input string
for {
fmt.Scan(&input)
// 客户端退出
if input == "q" {
quitFlag <- struct{}{}
return
}
n, err := client.Write([]byte(input))
if err != nil {
log.Printf("向%v发送数据失败", raddr)
}
log.Printf("向 %v 发送数据长度为:%d 数据为:%v\n", raddr, n, input)
}
}()
t := time.NewTicker(5 * time.Second)
LOOP:
for {
select {
case <-quitFlag:
break LOOP
case <-t.C:
log.Printf("Goroutine Num:%d\n", runtime.NumGoroutine())
}
}
log.Println("主协程结束")
}
22.4 WebSocket
22.4.1 WebSocket基本原理
HTTP协议,必须由客户端主动发起请求,服务器收到请求后被动响应请求,并返回信息给客户端,而且底层建立的TCP连接用完即断,并不会维持很久。但有些场景下,HTTP协议做不到。
- 例如服务端需要为某些客户端主动发送请求,解决方案只能定时发起请求轮询服务器,效率低下
- 例如客户端需要快速刷新数据,依然需要连续和服务器建立连接,HTTP协议会频繁建立、断开TCP连接、成本很高,而HTTP每一次发起请求的请求报文或响应报文,都需要携带请求头部信息,两端通信又存在冗余数据
为了解决这个问题,引入了WebSocket技术。WebSocket是一种网络传输协议,它实现在单个TCP连接上进行全双工通信,位于OSI模型中的应用层。其主要特点如下所示:
- WebSocket是一种在单个TCP连接上进行全双工通信的协议
- WebSocket使得客户端和服务端之间的数据交换变得更加简单,允许服务端主动向客户端推送数据
- WebSocket允许客户端和服务端在完成一次握手后,便可以直接创建持久性的连接,并进行数据双向传输
HTTP与WebSocket通信示意图如下所示:

WebSocket是一个基于应用层的协议,建立在TCP协议之上,和HTTP协议是兄弟关系,但却又依赖HTTP。因为在需要使用WebSocket时,在建立HTTP连接后,在应用层完成一次握手和确认后,需要进行协议升级至WebSocket协议。
HTTP的请求-响应模式每次新连接时均需要建立新的三次握手,不仅增加了延迟、也对资源造成浪费,不适宜实时响应速度要求高的应用。WebSocket的双向通信以及基于事件的传输模式,为应用提供了低延迟和高效率的实时通信能力。优化了性能和资源利用。主要应用场景如下所示:
- 聊天室
- 在线协同编辑
- 实时数据更新
- 弹幕
- 股票等数据实时报价
- WebShell
22.4.2 WebSocket服务端编程
在Go中进行WebSocket需要使用到第三方库https://github.com/gorilla/websocket/,安装命令如下所示:
go get github.com/gorilla/websocket
服务端示例代码如下所示:
- 后端代码:
package main
import (
"flag"
"log"
"net/http"
"github.com/gorilla/websocket"
)
var addr = flag.String("addr", "0.0.0.0:9999", "HTTP Server Address")
var upgrader = websocket.Upgrader{}
func home(w http.ResponseWriter, r *http.Request) {
log.Printf("%s %s", r.Method, r.URL)
w.Header().Add("X-Protocol", "HTTP")
// 返回一个网页,所有文件都可以看作为一个二进制字节序列
http.ServeFile(w, r, "./index.html")
// w.Write([]byte("Hello http"))
}
func ws(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Printf("Upgrade Protocol error :%v", err)
}
defer conn.Close()
for {
mt, message, err := conn.ReadMessage()
if err != nil {
log.Println("read:", err)
break
}
log.Printf("recv: %s", message)
err = conn.WriteMessage(mt, message)
log.Printf("write: %s", message)
if err != nil {
log.Println("write:", err)
break
}
}
}
func main() {
flag.Parse()
http.HandleFunc("/ws", ws)
http.HandleFunc("/http", home)
http.ListenAndServe(*addr, nil)
}
- 前端代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Web Sockert 测试</title>
<script>
// Create WebSocket connection.
// 1.握手http 2.协议升级 3.建立长连接
const ws = new WebSocket("ws://127.0.0.1:9999/ws");
// Connection opened
// 建立某个事件绑定到函数addEventListener,js回调函数
ws.addEventListener("open", function (event) {
ws.send("Hello Server!");
});
// Listen for messages
// 服务器端消息到了事件
ws.addEventListener("message", function (event) {
console.log("Message from server ", event.data);
});
ws.addEventListener("onclose", function (event) {
console.log("WebSocket is closed now.");
});
ws.addEventListener("error", function (event) {
console.log("WebSocket error: ", event);
});
</script>
</head>
<h1>Go WebSocket 测试示例</h1>
<br>
<a href="ws://127.0.0.1:9999/ws">切换至WebSocket测试</a>
</body>
</html>
WebSocket 前端页面参考资料:https://developer.mozilla.org/zh-CN/docs/Web/API/WebSocket
22.4.3 WebSocket客户端编程
大部分情况下,WebSocket的客户端可以使用Postman、Apifox这类工具来进行测试,也可以参考代码 https://github.com/gorilla/websocket/blob/main/examples/echo/client.go
本文同步在微信订阅号上发布,如各位小伙伴们喜欢我的文章,也可以关注我的微信订阅号:woaitest,或扫描下面的二维码添加关注:

作者: Surpassme
来源: http://www.jianshu.com/u/28161b7c9995/
http://www.cnblogs.com/surpassme/
声明:本文版权归作者所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出 原文链接 ,否则保留追究法律责任的权利。如有问题,可发送邮件 联系。让我们尊重原创者版权,共同营造良好的IT朋友圈。

浙公网安备 33010602011771号