原文:https://learnku.com/articles/57947
开始 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]byte
for {
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 如何编程