13-Go语言网络编程
socket编程
Socket是BSD UNIX的进程通信机制,读作套接字,用于描述IP地址和端口。
Socket可以理解为是TCP/IP网络的API,定义了很多函数,可以用来开发网络应用。
计算机之间,通过套接字和网络进行请求与响应。

例如linux中的网络socket、以及本地socket。

Go与TCP编程
TCP/IP(Transmission Control Protocol/Internet Protocol) 即传输控制协议/网间协议,是一种面向连接的、可靠的、基于字节流的传输层(Transport layer)通信协议。
因为是面向连接的协议,数据像水流一样传输,会存在黏包问题。
TCP服务端处理流程
- 监听ip:port
- 接收客户端新connection
- 创建goroutine处理链接
TCP客户端处理流程
- 拨号、链接TCP服务端
- 数据收发
- 关闭链接
TCP服务端
TCP服务端可以同时和很多个客户端连接,例如有N个用户登录了虎牙TV看直播,这就是TCP服务端、与客户端的连接关系。
Go语言创建TCP服务端优势在于天然的goroutine优势,一个链接就交给一个goroutine去处理。
Go使用内置的net包进行socket编程。
package main
import (
"bufio"
"fmt"
"net"
)
// tcp
func process(conn net.Conn) {
defer conn.Close() //关闭链接
//服务端死循环处理数据
for {
reader := bufio.NewReader(conn)
var buf [128]byte
n, err := reader.Read(buf[:]) //每次读取128字节数据,返回数据个数
if err != nil {
fmt.Println("Read from client failed:", err)
break
}
//接收到数据
recvStr := string(buf[:n])
fmt.Println("收到client发来的数据:", recvStr)
conn.Write([]byte(recvStr)) //直接返回 收到的数据
}
}
func main() {
listen, err := net.Listen("tcp", "127.0.0.1:18888")
if err != nil {
fmt.Printf("Start http server failed: %v\n", err)
return
}
fmt.Println("Server is running http://127.0.0.1:18888 ")
//死循环夯住,不断接受新客户端
for {
conn, err := listen.Accept()
if err != nil {
fmt.Printf("Accept client server failed: %v\n", err)
continue
}
go process(conn)
}
}
编译运行,如图你运行的nginx命令
➜ goStudy go build server.go
➜ goStudy ./server
Server is running http://127.0.0.1:18888
TCP客户端

package main
import (
"bufio"
"fmt"
"net"
"os"
"strings"
)
func sendMsg() {
conn, err := net.Dial("tcp", "127.0.0.1:18888")
if err != nil {
fmt.Println("Error.. ", err)
return
}
defer conn.Close()
//从标准输入接收数据
fmt.Println("请输入你要发送的数据:")
inputReader := bufio.NewReader(os.Stdin)
for {
input, _ := inputReader.ReadString('\n')
inputInfo := strings.Trim(input, "\r\n") //去掉尾部换行符
//允许用户输入Q结束
if strings.ToUpper(inputInfo) == "Q" {
return
}
_, err = conn.Write([]byte(inputInfo)) //发数据
if err != nil {
fmt.Println("Error.. ", err)
return
}
//客户端接收服务端返回数据
buf := [512]byte{}
n, err := conn.Read(buf[:])
if err != nil {
fmt.Println("Recv error.. ", err)
return
}
fmt.Printf("客户端收到响应:%s\n", string(buf[:n]))
}
}
func main() {
sendMsg()
}
TCP粘包问题
server
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: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 failed, err:", err)
continue
}
go process(conn)
}
}
client
package main
import (
"fmt"
"net"
)
func sendMsg() {
conn, err := net.Dial("tcp", "127.0.0.1:30000")
if err != nil {
fmt.Println("dial failed, err", err)
return
}
defer conn.Close()
for i := 0; i < 20; i++ {
msg := `Hello, Hello. How are you?`
conn.Write([]byte(msg))
}
}
func main() {
sendMsg()
}
粘包问题
因为TCP的数据传输模式是流模式,保持长链接的时候可以多次的收发。
- client问题
- 网络传输底层算法是提交一段TCP数据时,不会立即发送该数据,而是等待短暂的时间看是否还有需要发送的数据,有则一起发出去。
- server问题
- TCP会把接收到的数据放在缓冲区,再通知应用层去读取,但可能由于某些原因无法立即取走数据,就导致TCP缓冲区存放了好几段数据
解决粘包
出现”粘包”的关键在于接收方不确定将要传输的数据包的大小,因此我们可以对数据包进行封包和拆包的操作。
封包:封包就是给一段数据加上包头,这样一来数据包就分为包头和包体两部分内容了(过滤非法包时封包会加入”包尾”内容)。
包头部分的长度是固定的,并且它存储了包体的长度,根据包头长度固定以及包头中含有包体长度的变量就能正确的拆分出一个完整的数据包。
我们可以自己定义一个协议,比如数据包的前4个字节为包头,里面存储的是发送的数据的长度。
// socket_stick/proto/proto.go
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
}
接下来在服务端和客户端分别使用上面定义的proto包的Decode和Encode函数处理数据。
服务端代码如下:
// socket_stick/server2/main.go
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: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 failed, err:", err)
continue
}
go process(conn)
}
}
客户端代码如下:
// socket_stick/client2/main.go
func main() {
conn, err := net.Dial("tcp", "127.0.0.1:30000")
if err != nil {
fmt.Println("dial failed, err", err)
return
}
defer conn.Close()
for i := 0; i < 20; 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)
}
}
更多实际开发场景下,使用web框架以解决此类问题。
Go与UDP编程
UDP协议(User Datagram Protocol)中文名称是用户数据报协议,是OSI(Open System Interconnection,开放式系统互联)参考模型中一种无连接的传输层协议。
不需要建立连接就能直接进行数据发送和接收,属于不可靠的、没有时序的通信,但是UDP协议的实时性比较好,通常用于视频直播相关领域。
UDP服务端
使用Go语言的net包实现的UDP服务端代码如下:
package main
import (
"fmt"
"net"
)
// UDP/server/main.go
// UDP server端
func main() {
listen, err := net.ListenUDP("udp", &net.UDPAddr{
IP: net.IPv4(0, 0, 0, 0),
Port: 30000,
})
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
}
}
}
UDP客户端
使用Go语言的net包实现的UDP客户端代码如下:
package main
import (
"fmt"
"net"
)
// UDP 客户端
func main() {
socket, err := net.DialUDP("udp", nil, &net.UDPAddr{
IP: net.IPv4(0, 0, 0, 0),
Port: 30000,
})
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)
}