socket通信比较好理解的,首先我们都知道socket=ip+port,由此两个不同的主机通过上的两个应用通信通过端口号进行标识。而在传输层通信的传输协议主要包含基于可靠连接的TCP协议和基于UDP无连接的不可靠协议,本文主要用GO实现TCP和UDP的socket通信。TCP的三次握手和四次挥手可参见:两张动图-彻底明白TCP的三次握手与四次挥手
一、TCP 1.1 服务端Server.go一个TCP服务端可以同时连接很多个客户端,Go语言中创建多个goroutine实现并发非常方便和高效,因此我们可以每建立一次连接就创建一个goroutine去处理。
TCP服务端程序的处理流程:
启动监听端口接收客户端请求建立链接创建goroutine处理连接。我们使用Go语言的net包实现的TCP服务端代码如下:
package main
import (
"bufio"
"fmt"
"io"
"net"
)
func process(conn net.Conn) {
defer conn.Close()
reader := bufio.NewReader(conn)
var buf [1024]byte
for {
n, err := reader.Read(buf[:])
if err == io.EOF {
break
}
if err != nil {
fmt.Println("read from client failed, err:", err)
break
}
recvStr := string(buf[:n])
fmt.Println("收到client发来的数据:", recvStr)
}
}
func main() {
listen, err := net.Listen("tcp", "127.0.0.1:3000")
if err != nil {
fmt.Println("listen failed, err:", err)
return
}
defer listen.Close()
for {
conn, err := listen.Accept()
if err != nil {
fmt.Println("accept failed, err:", err)
continue
}
go process(conn)
}
}
1.2 客户端Client.go
相比而言,客户端的逻辑就很简单了,TCP客户端进行TCP通信的流程可以总结为如下几点:
建立与服务端的链接进行数据收发关闭链接使用Go语言的net包实现的TCP客户端代码如下:
package main
import (
"fmt"
"net"
)
/**
出现粘包问题,“粘包”可发生在发送端也可发生在接收端:
*/
func main() {
conn, err := net.Dial("tcp", "127.0.0.1:3000")
if err != nil {
fmt.Println("dial failed, err", err)
return
}
defer conn.Close()
for i := 0; i < 20; i++ {
msg := `Hello, How are you!`
conn.Write([]byte(msg))
}
}
go mod模式(go mod init去初始化当前项目)build两个文件后生成server.exe
和client.exe
文件:
先双击运行服务端server.exe
再双击运行客户端client.exe
文件后,服务端出现如下结果:
代码里面逻辑很清晰,客户端发送了20条消息给服务端,但是服务端显示却只收到了三条消息,仔细分析发现server端显示的是多条消息粘在一起了。这是什么原因呢?我们都知道TCP是可靠传输的,信息丢失后会进行重传。TCP是一种面向连接(连接导向)的、可靠的、基于字节流的传输层(Transport layer)通信协议,因为是面向连接的协议,数据像水流一样传输,会存在黏包问题。
为什么会出现粘包问题?
主要原因就是tcp数据传递模式是流模式,在保持长连接的时候可以进行多次的收和发。“粘包”可发生在发送端也可发生在接收端:
解决办法?
出现”粘包”的关键在于接收方不确定将要传输的数据包的大小,因此我们可以对数据包进行组包和拆包的 *** 作。组包就是给一段数据加上包头,这样一来数据包就分为包头和包体两部分内容了(过滤非法包时封包会加入”包尾”内容)。包头部分的长度是固定的,并且它存储了包体的长度,根据包头长度固定以及包头中含有包体长度的变量就能正确的拆分出一个完整的数据包。
因此,我们可以自己定义一个协议,比如数据包的前4个字节为包头,里面存储的是发送的数据的长度。
因为需要自定义包,因此贴出自己的文件目录结构,tcp文件路径为GOPATH/src/tcp:
采用go mod模式,因此可以使用go env
命令查看go配置,确保开启go module模式,如果未开启则直接执行set GO111MODULE=on
命令即可修改。
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) {
// 读取消息的长度
lengthByte, _ := reader.Peek(4) // 读取前4个字节的数据
lengthBuff := bytes.NewBuffer(lengthByte)
var length int32
err := binary.Read(lengthBuff, 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
}
然后在tcp目录下执行go mod命令初始化项目,go mod init xxx
,xxx表示项目名,可自己指定,不写则默认未文件名,执行完之后会在该目录出现一个go.mod文件,这个供我们后面导入自定义的proto包时候使用。
更改新的客户端和服务端代码如下:
Server.go
package main
import (
"bufio"
"fmt"
"io"
"net"
"tcp/proto" //此时导入的包格式
)
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 msg failed, err:", err)
return
}
fmt.Println("收到client发来的数据:", msg)
}
}
func main() {
listen, err := net.Listen("tcp", "127.0.0.1:3000")
if err != nil {
fmt.Println("listen failed, err:", err)
return
}
defer listen.Close()
for {
conn, err := listen.Accept()
if err != nil {
fmt.Println("accept failed, err:", err)
continue
}
go process(conn)
}
}
Client.go
package main
import (
"fmt"
"net"
"tcp/proto"
)
func main() {
conn, err := net.Dial("tcp", "127.0.0.1:3000")
if err != nil {
fmt.Println("dial failed, err:", err)
return
}
defer conn.Close()
for i := 0; i < 20; i++ {
msg := `Hello, How are you?`
data, err := proto.Encode(msg)
if err != nil {
fmt.Println("encode msg failed, err:", err)
return
}
conn.Write(data)
}
}
对两个文件执行go build
生成可执行exe文件之后再运行则发现不会出现粘包问题了。
udp相比tcp而言更加实时,主要应用与语音、视频等实时的场景,但不能保证信息的完整,实现如下:
2.1 服务端server.gofunc main() {
listen, err := net.ListenUDP("udp", &net.UDPAddr{
IP: net.IPv4(0, 0, 0, 0),
Port: 3000,
})
if err != nil {
fmt.Println("listen failed, err:", err)
return
}
defer listen.Close()
for {
var data [1024]byte
n, addr, err := listen.ReadFromUDP(data[:]) // 接收数据
if err != nil {
fmt.Println("read udp failed, err:", 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("write to udp failed, err:", err)
continue
}
}
}
2.2 客户端client.go
func main() {
socket, err := net.DialUDP("udp", nil, &net.UDPAddr{
IP: net.IPv4(0, 0, 0, 0),
Port: 3000,
})
if err != nil {
fmt.Println("连接服务端失败,err:", err)
return
}
defer socket.Close()
sendData := []byte("Hello server")
_, err = socket.Write(sendData) // 发送数据
if err != nil {
fmt.Println("发送数据失败,err:", err)
return
}
data := make([]byte, 4096)
n, remoteAddr, err := socket.ReadFromUDP(data) // 接收数据
if err != nil {
fmt.Println("接收数据失败,err:", err)
return
}
fmt.Printf("recv:%v addr:%v count:%v\n", string(data[:n]), remoteAddr, n)
}
参考:Go语言基础之网络编程
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)