[TOC]
GO的网络编程分享
回顾一下我们上次分享的网络协议5层模型
- 物理层
- 数据链路层
- 网络层
- 传输层
- 应用层
每一层有每一层的独立功能,大多数网络都采用分层的体系结构,每一层都建立在它的下层之上,向它的上一层提供一定的服务,而把如何实现这一服务的细节对上一层加以屏蔽。
每一层背后的协议有哪些,具体有啥为什么出现的,感兴趣的可以看看互联网协议知多少
了解了网络协议的分层,数据包是如何封包,如何拆包,如何得到源数据的,往下看心里就有点谱了
GO网络编程指的是什么?
GO网络编程,这里是指的是SOCKET编程
相信写过c/c++
网络编程的朋友看到这里并不陌生吧,我们再来回顾一下
网络编程这一块,分为客户端部分的开发,和服务端部分的开发,会涉及到相应的关键流程
服务端涉及的流程
- socket建立套接字
- bind绑定地址和端口
- listen设置最大监听数
- accept开始阻塞等待客户端的连接
- read读取数据
- write回写数据
- close 关闭
客户端涉及的流程
- socket建立套接字
- connect 连接服务端
- write写数据
- read读取数据
我们来看看SOCKET编程是啥?
SOCKET
就是套接字,是BSD UNIX的进程通信机制,他是一个句柄,用于描述IP地址
和端口
的。
当然SOCKET
也是可以理解为TCP/IP网络
的API(应用程序接口)
,SOCKET
定义了许多函数,我们可以用它们来开发TCP/IP网络
上的应用程序。
电脑上运行的应用程序通常通过SOCKET
向网络发出请求或者应答网络请求。
哈,突然想到面向接口编程
顾名思义,就是在一个面向对象的系统中,系统的各种功能是由许许多多的不同对象协作完成的。
在这种情况下,各个对象内部是如何实现自己的,对系统设计人员来讲就不那么重要了;
各个对象之间的协作关系则成为系统设计的关键,面向接口编程的知道思想就是,无论模块大小,对应模块之间的交互都必须要在系统设计的时候着重考虑。
哈,一味的依赖别人提供的接口,关于接口内部会不会有坑,为什么会失败,我们就不得而知了
开始socket编程
先上一张图,我们一起瞅瞅
Socket
是应用层与TCP/IP协议族
通信的中间软件抽象层
在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族
隐藏在Socket后面
对用户来说只需要调用Socket规定的相关函数就可以了,让Socket
去做剩下的事情
Socket
,应用程序通常通过Socket
向网络发出请求
/ 应答
网络请求
常用的Socket类型有2种:
- 流式Socket(stream)
流式是一种面向连接的Socket
,针对于面向连接的TCP服务应用
- 数据报式Socket
数据报式Socket
是一种无连接的Socket,针对于无连接的UDP服务应用
简单对比一下:
- TCP:比较靠谱,面向连接,安全,可靠的传输方式 , 但是 比较慢
- UDP: 不是太靠谱,不可靠的,丢包不会重传,但是 比较快
举一个现在生活中最常见的例子:
案例一
别人买一个小礼物给你,还要货到付款,这个时候快递员将货送到你家的时候,必须看到你人,而且你要付钱,这才是完成了一个流程 , 这是TCP
案例二
还是快递的例子,比如你在网上随便抢了一些不太重要的小东西,小玩意,快递员送货的时候,直接就把你的包括扔到某个快递点,头都不回一下的那种, 这是UDP
网络编程无非简单来看就是TCP编程
和UDP编程
我们一起来看看GOLANG如何实现基于TCP通信 和 基于UDP通信的
GO基于TCP编程
那我们先来看看TCP协议是个啥?
TCP/IP(Transmission Control Protocol/Internet Protocol)
传输控制协议/网间协议,是一种面向连接(连接导向)的、可靠的、基于字节流的传输层(Transport layer)通信协议
因为是面向连接的协议,数据像水流一样传输,这样会产生黏包问题。
上述提了一般socket编程的服务端流程和客户端流程,实际上go的底层实现也离不开这几步,但是我们从应用的角度来看看go的TCP编程,服务端有哪些流程
TCP服务端
TCP服务端可以同时连接很多个客户端,这个毋庸置疑,要是一个服务端只能接受一个客户端的连接,那么你完了,你可以收拾东西回家了
举个栗子
最近也要开始的各种疯狂购物活动,他们的服务端,全球各地的客户端都会去连接,那么TCP服务端又是如何处理的嘞,在C/C++
中我们会基于epoll模型来进行处理,来一个客户端的连接/请求事件,我们就专门开一个线程去进行处理
那么golang中是如何处理的呢?
golang中,每建立一个连接,就会开辟一个协程goroutine
来处理这个请求
服务端处理流程大致分为如下几步
- 监听端口
- 接收客户端请求建立链接
- 创建
goroutine
处理链接 - 关闭
能做大这么简洁和友好的处理方式,得益于Go中的net包
TCP服务端的具体实现:
func process(conn net.Conn) {// 关闭连接defer conn.Close() | |
for { | |
reader := bufio.NewReader(conn)var buf [256]byte// 读取数据 | |
n, err := reader.Read(buf[:]) | |
if err != nil { | |
fmt.Println("reader.Read error : ", err)break} | |
recvData := string(buf[:n]) | |
fmt.Println("receive data :", recvData)// 将数据再发给客户端 | |
conn.Write([]byte(recvData)) | |
} | |
} | |
func main() {// 监听tcp | |
listen, err := net.Listen("tcp", "127.0.0.1:8888")if err != nil { | |
fmt.Println("net.Listen error : ", err)return}for {// 建立连接 , 看到这里的朋友,有没有觉得这里和C/C++的做法一毛一样 | |
conn, err := listen.Accept() | |
if err != nil { | |
fmt.Println("listen.Accept error : ", err)continue}// 专门开一个goroutine去处理连接go process(conn) | |
} | |
} |
TCP的服务端写起来是不是很简单呢
我们 看看TCP的客户端
TCP客户端
客户端流程如下:
- 与服务端建立连接
- 读写数据
- 关闭
func main() { | |
conn, err := net.Dial("tcp", "127.0.0.1:8888")if err != nil { | |
fmt.Println("net.Dial error : ", err)return}// 关闭连接defer conn.Close() | |
// 键入数据 | |
inputReader := bufio.NewReader(os.Stdin)for {// 读取用户输入 | |
input, _ := inputReader.ReadString('\n') | |
// 截断 | |
inputInfo := strings.Trim(input, "\r\n")// 读取到用户输入q 或者 Q 就退出if strings.ToUpper(inputInfo) == "Q" { | |
return}// 将输入的数据发送给服务端_, err = conn.Write([]byte(inputInfo)) | |
if err != nil {return} | |
buf := [512]byte{} | |
n, err := conn.Read(buf[:])if err != nil { | |
fmt.Println("conn.Read error : ", err)return} | |
fmt.Println(string(buf[:n]))} | |
} |
注意事项:
- 服务端与客户端联调,需要先启动服务端,等待客户端的连接,
- 若顺序弄反,客户端会因为找不到服务端而报错
上面有说到TCP是流式协议,会存在黏包的问题,我们就来模拟一下,看看实际效果
TCP黏包如何解决?
来模拟写一个服务端
server.go
package main | |
import ("bufio""fmt""io""net" | |
) | |
// 专门处理客户端连接 | |
func process(conn net.Conn) {defer conn.Close() | |
reader := bufio.NewReader(conn)var buf [2048]bytefor { | |
n, err := reader.Read(buf[:])// 如果客户端关闭,则退出本协程if err == io.EOF {break}if err != nil { | |
fmt.Println("reader.Read error :", err)break} | |
recvStr := string(buf[:n])// 打印收到的数据,稍后我们主要是看这里输出的数据是否是我们期望的 | |
fmt.Printf("received data:%s\n\n", recvStr)} | |
} | |
func main() { | |
listen, err := net.Listen("tcp", "127.0.0.1:8888")if err != nil { | |
fmt.Println("net.Listen error : ", err)return}defer listen.Close() | |
fmt.Println("server start ... ") | |
for { | |
conn, err := listen.Accept()if err != nil { | |
fmt.Println("listen.Accept error :", err)continue}go process(conn)} | |
} |
写一个客户端进行配合
client.go
package main | |
import ("fmt""net" | |
) | |
func main() { | |
conn, err := net.Dial("tcp", "127.0.0.1:8888")if err != nil { | |
fmt.Println("net.Dial error : ", err)return}defer conn.Close() | |
fmt.Println("client start ... ") | |
for i := 0; i < 30; i++ { | |
msg := `Hello world, hello xiaomotong!` | |
conn.Write([]byte(msg))} | |
fmt.Println("send data over... ") | |
} |
实际效果
server start ... | |
received data:Hello world, hello xiaomotong!Hello world, hello xiaomotong! | |
received data:Hello world, hello xiaomotong!Hello world, hello xiaomotong!Hello world, hello xiaomotong!Hello world, hello xiaomotong!Hello world, hello xiaomotong!Hello worl | |
d, hello xiaomotong!Hello world, hello xiaomotong!Hello world, hello xiaomotong!Hello world, hello xiaomotong!Hello world, hello xiaomotong!Hello world, hello xiaomotong!Helloworld, hello xiaomotong! | |
received data:Hello world, hello xiaomotong!Hello world, hello xiaomotong!Hello world, hello xiaomotong!Hello world, hello xiaomotong!Hello world, hello xiaomotong!Hello world, hello xiaomotong!Hello world, hello xiaomotong!Hello world, hello xiaomotong!Hello world, hello xiaomotong! | |
received data:Hello world, hello xiaomotong!Hello world, hello xiaomotong!Hello world, hello xiaomotong!Hello world, hello xiaomotong!Hello world, hello xiaomotong!Hello worl | |
d, hello xiaomotong!Hello world, hello xiaomotong! |
由上述效果我们可以看出来,客户端发送了30次数据给到服务端,可是服务端只输出了4次,而是多条数据黏在了一起输出了,这个现象就是黏包,那么我们如何处理呢?
如何处理TCP黏包问题
黏包原因:
tcp
数据传递模式是流式的,在保持长连接的时候可以进行多次的收和发
实际情况有如下2种
- 由Nagle算法造成的发送端的粘包
Nagle算法
是一种改善网络传输效率的算法
当我们提交一段数据给TCP发送时,TCP并不会立刻发送此段数据
而是等待一小段时间看看,在这段等待时间里,是否还有要发送的数据,若有则会一次把这两段数据发送出去
- 接收端接收不及时造成的接收端粘包
TCP会把接收到的数据存在自己的缓冲区中,通知应用层取数据
当应用层由于某些原因不能及时的把TCP的数据取出来,就会造成TCP缓冲区中存放了几段数据。
知道原因之后,我们来看看如何解决吧
开始解决TCP黏包问题
知道了黏包的原因,我们就针对原因下手就好了,分析一下,为什么tcp会等一段时间,是不是因为tcp他不知道我们要发送给他的数据包到底是多大,所以他就想尽可能的多吃点?
那么,我们的解决方式就是 对数据包进行封包和拆包的操作。
- 封包:
封包就是给一段数据加上包头,这样一来数据包就分为包头和包体两部分内容了,有时候为了过滤非法包,我们还会加上包尾。
包头部分的长度是固定的,他会明确的指出包体的大小是多少,这样子我们就可以正确的拆除一个完整的包了
- 根据包头长度固定
- 根据包头中含有包体长度的变量
我们可以自己定义一个协议,比如数据包的前2个字节为包头,里面存储的是发送的数据的长度。
这一个自定义协议,客户端和服务端都要知道,否则就没得玩了
开始解决问题
server2.go
package main | |
import ("bufio""bytes""encoding/binary""fmt""io""net" | |
) | |
// Decode 解码消息 | |
func Decode(reader *bufio.Reader) (string, error) {// 读取消息的长度 | |
lengthByte, _ := reader.Peek(2) // 读取前2个字节,看看包头 | |
lengthBuff := bytes.NewBuffer(lengthByte)var length int16// 读取实际的包体长度 | |
err := binary.Read(lengthBuff, binary.LittleEndian, &length)if err != nil {return "", err | |
}// Buffered返回缓冲中现有的可读取的字节数。if int16(reader.Buffered()) < length+2 {return "", err | |
} | |
// 读取真正的消息数据 | |
realData := make([]byte, int(2+length))_, err = reader.Read(realData)if err != nil {return "", err | |
}return string(realData[2:]), nil | |
} | |
func process(conn net.Conn) {defer conn.Close() | |
reader := bufio.NewReader(conn) | |
for { | |
msg, err := Decode(reader)if err == io.EOF {return}if err != nil { | |
fmt.Println("Decode error : ", err)return} | |
fmt.Println("received data :", msg)} | |
} | |
func main() { | |
listen, err := net.Listen("tcp", "127.0.0.1:8888")if err != nil { | |
fmt.Println("net.Listen error :", err)return}defer listen.Close()for { | |
conn, err := listen.Accept()if err != nil { | |
fmt.Println("listen.Accept error :", err)continue}go process(conn)} | |
} |
client2.go
package main | |
import ("bytes""encoding/binary""fmt""net" | |
) | |
// Encode 编码消息 | |
func Encode(message string) ([]byte, error) {// 读取消息的长度,并且要 转换成int16类型(占2个字节) ,我们约定好的 包头2字节var length = int16(len(message))var nb = new(bytes.Buffer) | |
// 写入消息头 | |
err := binary.Write(nb, binary.LittleEndian, length)if err != nil {return nil, err | |
} | |
// 写入消息体 | |
err = binary.Write(nb, binary.LittleEndian, []byte(message))if err != nil {return nil, err | |
}return nb.Bytes(), nil | |
} | |
func main() { | |
conn, err := net.Dial("tcp", "127.0.0.1:8888")if err != nil { | |
fmt.Println("net.Dial error : ", err)return}defer conn.Close()for i := 0; i < 30; i++ { | |
msg := `Hello world,hello xiaomotong!` | |
data, err := Encode(msg)if err != nil { | |
fmt.Println("Encode msg error : ", err)return} | |
conn.Write(data)} | |
} |
此处为了演示方便简单,我们将封包放到了 客户端代码中,拆包,放到了服务端代码中
效果演示
这下子,就不会存在黏包的问题了,因为tcp
他知道自己每一次要读多少长度的包,要是缓冲区数据不够期望的长,那么就等到数据够了再一起读出来,然后打印出来
看到这里的朋友,对于golang的TCP编程还有点兴趣了吧,那么我们可以看看UDP编程了,相对TCP来说就简单多了,不会有黏包的问题
GO基于UDP编程
同样的,我们先来说说UDP协议
UDP协议(User Datagram Protocol)
是用户数据报协议,一种无连接的传输层协议
不需要建立连接就能直接进行数据发送和接收
属于不可靠的、没有时序的通信,正是因为这样的特点,所以UDP协议
的实时性比较好,通常用于视频直播相关领域,因为对于视频传输,传输过程中丢点一些帧,对整体影响很小
UDP服务端
我们来撸一个UDP客户端和服务端
server3.go
func main() { | |
listen, err := net.ListenUDP("udp", &net.UDPAddr{ | |
IP: net.IPv4(0, 0, 0, 0), | |
Port: 8888,})if err != nil { | |
fmt.Println("net.ListenUDP error : ", err)return}defer listen.Close()for {var data [1024]byte// 接收数据报文 | |
n, addr, err := listen.ReadFromUDP(data[:]) | |
if err != nil { | |
fmt.Println("listen.ReadFromUDP error : ", err)continue} | |
fmt.Printf("data == %v , addr == %v , count == %v\n", string(data[:n]), addr, n)// 将数据又发给客户端_, err = listen.WriteToUDP(data[:n], addr) | |
if err != nil { | |
fmt.Println("listen.WriteToUDP error:", err)continue}} | |
} |
UDP客户端
client3.go
func main() { | |
socket, err := net.DialUDP("udp", nil, &net.UDPAddr{ | |
IP: net.IPv4(0, 0, 0, 0), | |
Port: 8888,})if err != nil { | |
fmt.Println("net.DialUDP error : ", err)return}defer socket.Close() | |
sendData := []byte("hello xiaomotong!!")// 发送数据_, err = socket.Write(sendData)if err != nil { | |
fmt.Println("socket.Write error : ", err)return} | |
data := make([]byte, 2048)// 接收数据 | |
n, remoteAddr, err := socket.ReadFromUDP(data)if err != nil { | |
fmt.Println("socket.ReadFromUDP error : ", err)return} | |
fmt.Printf("data == %v , addr == %v , count == %v\n", string(data[:n]), remoteAddr, n) | |
} |
效果展示
服务端打印: | |
data == hello xiaomotong!! , addr == 127.0.0.1:50487 , count == 18 | |
客户端打印: | |
data == hello xiaomotong!! , addr == 127.0.0.1:8888 , count == 18 |
总结
- 回顾网络的5层模型,SOCKET编程的服务端和客户端的流程
- GO基于TCP如何编程,如何解决TCP黏包问题
- GO基于UDP如何编程
欢迎点赞,关注,收藏
朋友们,你的支持和鼓励,是我坚持分享,提高质量的动力
好了,本次就到这里,下一次 分享GO中如何设置HTTPS,
技术是开放的,我们的心态,更应是开放的。拥抱变化,向阳而生,努力向前行。
我是小魔童哪吒,欢迎点赞关注收藏,下次见~