一、说明
1.1、什么是socket
Socket起源于Unix,而Unix基本哲学之一就是“一切皆文件”,都可以用“打开open –> 读写write/read –> 关闭close”模式来操作。Socket就是该模式的一个实现,网络的Socket数据传输是一种特殊的I/O,Socket也是一种文件描述符。Socket也具有一个类似于打开文件的函数调用:Socket(),该函数返回一个整型的Socket描述符,随后的连接建立、数据传输等操作都是通过该Socket实现的。
常用的Socket类型有两种:流式Socket(SOCK_STREAM)和数据报式Socket(SOCK_DGRAM)。流式是一种面向连接的Socket,针对于面向连接的TCP服务应用;数据报式Socket是一种无连接的Socket,对应于无连接的UDP服务应用。
1.2、socket种类
Socket有两种:TCP Socket和UDP Socket,TCP和UDP是协议,而要确定一个进程的需要三元组,需要IP地址、协议和端口,具体看下面示例部分。
二、实战
2.1、tcp socket实战
(1) tcp socket 服务端实现
package main
import (
"bufio"
"fmt"
"net"
)
func process(conn net.Conn) {
// 5. 定义defer关闭连接
defer conn.Close()
// 6.无限循环
for {
// 7.创建一个带缓存的读读取对象,读取tcp连接里的内容
reader := bufio.NewReader(conn)
// 8.定义一个128字节的数组
var buf [128]byte
// 9.调用从当前tcp连接里读取内容的对象的Read方法将从tcp里读取的数据存入buf[:]切片里,有两个返回值,一个是读取数据总数,另一个是报错
n, err := reader.Read(buf[:])
if err != nil { // 处理报错
fmt.Println("read data error:", err)
break
}
// 10.打印切片里的内容(下标从0到读取的总数)
fmt.Printf(string(buf[0:n]))
// 11.回复客户端消息(发送的消息一定要是byte类型的切片)
conn.Write([]byte("Welcome to Golang"))
}
}
func main() {
// 1.调用net包下的Listen()方法启动tcp的端口监听
listen, err := net.Listen("tcp", "0.0.0.0:8088")
if err != nil { // 判断是否有错误
fmt.Println("listen failed error", err)
return
}
// 2.无限循环,处理请求
for {
// 3.等待链接(没有客户端连接时在这里阻塞,有客户端连接时处理连接)
conn, err := listen.Accept()
if err != nil { // 判断错误(打印错误,退出当前循环进入下次循环)
fmt.Println("accept failed err", err)
continue
}
// 4.启动一个携程处理连接
go process(conn) // 5.启用一个goroutine处理当前的连接
}
}
(2) tcp socket 客户端实现
package main
import (
"fmt"
"net"
"bufio"
"os"
"strings"
)
func main() {
// 1.调用net.Dial()方法以tcp协议的方式连接localhost的8088端口
conn, err := net.Dial("tcp","localhost:8088")
if err != nil { // 判断错误
fmt.Println("Error dialing",err.Error())
return
}
// 2.定义defer,在结束时关闭连接
defer conn.Close()
// 3.从标准输入里创建一个读的对象
inputReader := bufio.NewReader(os.Stdin)
// 4.无限循环
for {
// 5.调用读的对象的ReadString方法判断,如果有'\n'(也就是换行符)就认为输入结束了,返回两个值,一个是字符串,另一个是报错
input, _ := inputReader.ReadString('\n')
// 6.利用strings.Trim方法给要发送的数据去掉空格
trimmedInput := strings.Trim(input,"\r\n")
// 7.如果输入内容是q就退出了
if trimmedInput == "Q" {
return
}
// 8.发送数据,发送的数据必须要是byte类型的切片,返回值是发送的总数据和错误
_, err = conn.Write([]byte(trimmedInput))
if err != nil { // 错误处理
return
}
// 9.读取服务端发来的消息
var buf [1024]byte
n, err := conn.Read(buf[:]) // 获取到一个读取的总数量和错误
if err != nil {
fmt.Println("read server data err", err)
return
}
// 10.打印服务端返回的消息
fmt.Println(string(buf[:n]))
}
}
2.2、udp socket实战
(1)udp socket 服务端实现
package main
import (
"net"
"fmt"
)
func main() {
// 1.使用net.ListenUDP()启动一个UDP的监听
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
}
// 2.使用defer关闭连接
defer listen.Close()
// 3.无限循环
for {
// 4.定义存储从连接里读取消息的变量
var data [1024]byte
// 5.从udp连接里读取消息到data变量里,返回值有:接收的数据总数量,谁发过来的数据,错误
n, addr, err := listen.ReadFromUDP(data[:])
if err != nil { // 错误处理
fmt.Println("read udp failed, err:", err)
continue
}
// 6.打印接收到的消息、发消息的地址和消息长度
fmt.Printf("data:%v addr:%v count:%v\n", string(data[:n]), addr, n)
// 7.给客户端返回消息,谁发过来的消息就返回给谁,接收到的消息时什么就返回什么消息
_, err = listen.WriteToUDP(data[:n], addr)
if err != nil { // 错误处理
fmt.Println("write to udp failed, err:", err)
continue
}
}
}
(2)udp socket 客户端实现
package main
import (
"net"
"fmt"
"os"
"bufio"
)
func main() {
// 1.调用net.DialUDP()方法以udp协议连接到0.0.0.0:30000地址
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
}
// 2.定义defer关闭连接
defer socket.Close()
// 3.无限循环
for {
// 4.实例化从终端输入对象
input := bufio.NewReader(os.Stdin)
// 5.没有输入内容时会等待,输入的内容遇到'\n'就认为输入结束,进入下一次循环。
s, _ := input.ReadString('\n')
// 6.发送数据,发送的数据必须转为byte类型的切片
_, err = socket.Write([]byte(s))
if err != nil { // 判断错误
fmt.Println("发送数据失败,err:", err)
return
}
// 7.定义存储服务端返回消息的变量
data := make([]byte, 4096)
// 8.接收服务端返回的消息存储到变量data里,返回值有:接收数据的总大小,谁发来的,错误
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)
}
}
2.3、tcp客户端请求百度实现
package main
import (
"fmt"
"net"
"io"
)
func main() {
conn, err := net.Dial("tcp","www.baidu.com:80")
if err != nil {
fmt.Println("Error dialing",err.Error())
return
}
defer conn.Close()
msg := "GET / HTTP/1.1\r\n"
msg += "Host: www.baidu.com\r\n"
msg += "Connection: close\r\n"
msg += "\r\n\r\n"
_, err = io.WriteString(conn, msg)
if err != nil {
fmt.Println("write string failed",err)
return
}
buf := make([]byte,4096)
for {
count, err := conn.Read(buf)
if err != nil {
break
}
fmt.Println(string(buf[0:count]))
}
}
三、tcp secket粘包问题解决
说明:
一般所谓的TCP粘包是在一次接收数据不能完全地体现一个完整的消息数据。TCP通讯为何存在粘包呢?主要原因是TCP是以流的方式来处理数据,再加上网络上MTU的往往小于在应用处理的消息数据,所以就会引发一次接收的数据无法满足消息的需要,导致粘包的存在。处理粘包的唯一方法就是制定应用层的数据通讯协议,通过协议来规范现有接收的数据是否满足消息数据的需要。在应用中处理粘包的基础方法主要有两种分别是以4节字描述消息大小或以结束符,实际上也有两者相结合的如HTTP,redis的通讯协议等。
示例:
1、服务端实现
package main
import (
"bufio"
"fmt"
"encoding/binary"
"io"
"net"
"bytes"
)
func Decode(reader *bufio.Reader) (string, error) {
// 9.读取接收到消息的前四个字节获取本次接收的消息长度,Peek读取消息时不移动读取位置
lengthByte, _ := reader.Peek(4)
// 10.bytes.NewBuffer:从一个切片构造一个buffer(缓冲区)
lengthBuff := bytes.NewBuffer(lengthByte)
// 11.定义一个int32类型的变量存储要读取消息的长度
var length int32
// 12.读取lengthBuff变量里的数据存到length变量里,此时length的值是要从连接里读取数据的长度
err := binary.Read(lengthBuff, binary.LittleEndian, &length)
if err != nil { // 错误处理
return "", err
}
// 13.如果缓冲里的数据没有要读取的数据长就返回错误信息
if int32(reader.Buffered()) < length+4 {
return "", err
}
// 14.定义一个要读取数据长度+4的byte类型切片变量
pack := make([]byte, int(4+length))
// 15.将从连接里读取的消息存入pack变量里(即每次从连接里读取 “int(4+length)”长度的数据)
_, err = reader.Read(pack)
if err != nil { // 错误处理
return "", err
}
// 16.返回从连接里读取的数据(前四个是读取消息的长度(不返回),只返回下标4之后的内容)
return string(pack[4:]), nil
}
func process(conn net.Conn) {
// 5.定义defer关闭连接
defer conn.Close()
// 6.实例化一个从当前连接里读取消息的对象
reader := bufio.NewReader(conn)
// 7.无限循环
for {
// 8.将读取消息的对象给Decode函数解码,返回值是从连接里读取到的消息和错误提示
msg, err := Decode(reader)
// 17.判断错误,读取到结尾错误和其他错误
if err == io.EOF {
return
}
if err != nil {
fmt.Println("decode msg failed, err:", err)
return
}
// 18.最终将客户端发来的消息打印出来
fmt.Println("收到client发来的数据:", msg)
}
}
func main() {
// 1.启动 tcp协议的监听
listen, err := net.Listen("tcp", "127.0.0.1:30000")
if err != nil { // 错误判断
fmt.Println("listen failed, err:", err)
return
}
defer listen.Close() // 定义defer关闭连接
// 2.无限循环
for {
// 3.等待客户端连接(无连接时会阻塞,有连接时自动处理连接)
conn, err := listen.Accept()
if err != nil { // 错误处理
fmt.Println("accept failed, err:", err)
continue
}
// 4.启动一个携程,将连接传给携程处理
go process(conn)
}
}
2、客户端实现
package main
import (
"fmt"
"net"
)
func Encode(message string) ([]byte, error) {
// 6.读取消息的长度,转换成int32类型(占4个字节)
var length = int32(len(message))
// 7.定义缓冲区变量
var pkg = new(bytes.Buffer)
// 8.往缓冲区变量里写入消息的长度(binary.LittleEndian:按照小端写入的方式写入(百度搜大小端读写了解相关内容))
err := binary.Write(pkg, binary.LittleEndian, length)
if err != nil { // 错误处理
return nil, err
}
// 9.往缓冲区变量里写入消息实体
err = binary.Write(pkg, binary.LittleEndian, []byte(message))
if err != nil { // 错误处理
return nil, err
}
// 10.返回缓冲区里的所有字节和错误信息
return pkg.Bytes(), nil
}
func main() {
// 1.建立拨号连接连接服务端
conn, err := net.Dial("tcp", "127.0.0.1:30000")
if err != nil { // 错误处理
fmt.Println("dial failed, err", err)
return
}
// 2.定义defer关闭连接
defer conn.Close()
// 3.循环20次
for i := 0; i < 20; i++ {
// 4.定义要发送消息内容
msg := `Hello, Hello. How are you?`
// 5.将要发送的消息传给Encode函数进行编码
data, err := Encode(msg)
if err != nil { // 错误处理
fmt.Println("encode msg failed, err:", err)
return
}
// 11.往连接里写入编码后的数据
conn.Write(data)
}
}