socket编程
Socket是BSD UNIX的进程通信机制,通常也称作”套接字”,用于描述IP地址和端口,是一个通信链的句柄。Socket可以理解为TCP/IP网络的API,它定义了许多函数或例程,程序员可以用它们来开发TCP/IP网络上的应用程序。电脑上运行的应用程序通常通过”套接字”向网络发出请求或者应答网络请求。
Socket是应用层与TCP/IP协议族通信的中间软件抽象层。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket后面,对用户来说只需要调用Socket规定的相关函数,让Socket去组织符合指定的协议数据然后进行通信。
GO语言实现TCP通信
TCP协议
TCP/IP(Transmission Control Protocol/Internet Protocol) 即传输控制协议/网间协议,是一种面向连接(连接导向)的、可靠的、基于字节流的传输层(Transport layer)通信协议,因为是面向连接的协议,数据像水流一样传输,会存在黏包问题。
TCP服务端
一个TCP服务端可以同时连接很多个客户端,Go语言中创建多个goroutine实现并发非常方便和高效,所以可以每建立一次链接就创建一个goroutine去处理。
TCP服务端程序的处理流程:
-
监听端口
-
接收客户端请求建立链接
-
创建goroutine处理链接
TCP服务端://TCP server端
func process(conn net.Conn) { defer conn.Close() //关闭连接 for { reader := bufio.NewReader(conn) var buf [128]byte n,err := reader.Read(buf[:]) //读取数据 if err != nil{ fmt.Println("连接客户端失败,错误信息:",err) } recvStr := string(buf[:n]) fmt.Println("收到客户端信息:",recvStr) conn.Write([]byte(recvStr)) //发送数据 } } func main() { listen,err := net.Listen("tcp","127.0.0.1:8888") if err != nil{ fmt.Println("监听失败,错误:",err) return } for { conn,err := listen.Accept() //建立连接 if err!= nil{ fmt.Println("建立连接失败,错误:",err) continue } go process(conn) //启动一个goroutine处理连接 } }
TCP客户端
一个TCP客户端进行TCP通信的流程如下:
- 建立与服务端的链接
- 进行数据收发
- 关闭链接
TCP客户端:
1//客户端 2 3func main() { 4 conn ,err := net.Dial("tcp","127.0.0.1:8888") 5 if err != nil { 6 fmt.Println("连接失败,错误:",err) 7 return 8 } 9 defer conn.Close() 10 inputReader := bufio.NewReader(os.Stdout) 11 for { 12 input, _ := inputReader.ReadString('\n') //读取用户输入 13 inputInfo := strings.Trim(input,"\r\n") 14 if strings.ToUpper(inputInfo) == "q"{ 15 return //如果输入q就退出 16 } 17 _,err = conn.Write([]byte(inputInfo)) //发送数据 18 if err != nil{ 19 return 20 } 21 buf := [512]byte{} 22 n,err := conn.Read(buf[:]) 23 if err != nil{ 24 fmt.Println("接受失败,错误:",err) 25 return 26 } 27 fmt.Println(string(buf[:n])) 28 } 29}
先启动server,后启动client:
1$go run main.go 2我是客户端 3我是客户端 4$go run main.go 5收到客户端信息: 我是客户端
GO语言实现UDP通信
UDp协议
UDP协议(User Datagram Protocol)中文名称是用户数据报协议,是OSI(Open System Interconnection,开放式系统互联)参考模型中一种无连接的传输层协议,不需要建立连接就能直接进行数据发送和接收,属于不可靠的、没有时序的通信,但是UDP协议的实时性比较好,通常用于视频直播相关领域。
UDP服务端
1//服务端 2func main() { 3 listen,err := net.ListenUDP("udp",&net.UDPAddr{ 4 IP:net.IPv4(0,0,0,0), 5 Port:8888, 6 }) 7 if err != nil{ 8 fmt.Println("监听失败,错误:",err) 9 return 10 } 11 defer listen.Close() 12 for { 13 var data [1024]byte 14 n,addr,err := listen.ReadFromUDP(data[:]) 15 if err != nil{ 16 fmt.Println("接收udp数据失败,错误:",err) 17 continue 18 } 19 fmt.Printf("data:%v addr:%v count:%v\n", string(data[:n]), addr, n) 20 _ ,err = listen.WriteToUDP(data[:n],addr) //发送数据 21 if err != nil{ 22 fmt.Println("发送数据失败,错误:",err) 23 continue 24 } 25 } 26}
UDP客户端
1//客户端 2func main() { 3 socket,err := net.DialUDP("udp",nil,&net.UDPAddr{ 4 IP:net.IPv4(0,0,0,0), 5 Port:8888, 6 }) 7 if err != nil{ 8 fmt.Println("连接服务器失败,错误:",err) 9 return 10 } 11 defer socket.Close() 12 sendData := []byte("hello world!") 13 _,err = socket.Write(sendData) 14 if err != nil{ 15 fmt.Println("发送数据失败,错误:",err) 16 return 17 } 18 data := make([]byte,4096) 19 n,remoteAddr,err := socket.ReadFromUDP(data) 20 if err != nil{ 21 fmt.Println("接受数据失败,错误:",err) 22 return 23 } 24 fmt.Printf("recv:%v addr:%v count:%v\n", string(data[:n]), remoteAddr, n) 25}
先启动server,后启动client:
1$go run main.go 2recv:hello world! addr:127.0.0.1:8888 count:12 3$go run main.go 4data:hello world! addr:127.0.0.1:51222 count:12
HTTP客户端和服务端
HTTP协议
超文本传输协议(HTTP,HyperText Transfer Protocol)是互联网上应用最为广泛的一种网络传输协议,所有的WWW文件都必须遵守这个标准。设计HTTP最初的目的是为了提供一种发布和接收HTML页面的方法。
HTTP服务端
net/http包是对net包的进一步封装,专门用来处理HTTP协议的数据。
1// http server 2func sayHi(w http.ResponseWriter,r *http.Request) { 3 fmt.Fprintln(w,"你好,ares!") 4} 5func main() { 6 http.HandleFunc("/",sayHi) 7 err := http.ListenAndServe(":8888",nil) 8 if err != nil{ 9 fmt.Println("Http 服务建立失败,err:",err) 10 return 11 } 12}

HTTP客户端
1func main() { 2 resp, err := http.Get("https://www.baidu.com/") 3 if err != nil { 4 fmt.Println("get failed, err:", err) 5 return 6 } 7 defer resp.Body.Close() 8 body, err := ioutil.ReadAll(resp.Body) 9 fmt.Printf("%T\n",body) 10 fmt.Println(string(body)) 11}
执行之后就能在终端输出www.baidu.com网站首页的内容了。
TCP粘包
粘包服务端
1//粘包 2func process(conn net.Conn) { 3 defer conn.Close() 4 reader := bufio.NewReader(conn) 5 var buf [1024]byte 6 for { 7 n,err := reader.Read(buf[:]) 8 if err == io.EOF{ 9 break 10 } 11 if err != nil{ 12 fmt.Println("读取客户端失败,err",err) 13 break 14 } 15 recvStr := string(buf[:n]) 16 fmt.Println("收到client发来的数据:",recvStr) 17 } 18} 19func main() { 20 listen,err := net.Listen("tcp","127.0.0.1:8888") 21 if err != nil{ 22 fmt.Println("监听失败,err",err) 23 return 24 } 25 defer listen.Close() 26 for { 27 conn,err := listen.Accept() 28 if err != nil{ 29 fmt.Println("接受失败,err",err) 30 continue 31 } 32 go process(conn) 33 } 34}
粘包客户端
1func main() { 2 conn,err := net.Dial("tcp","127.0.0.1:8888") 3 if err != nil{ 4 fmt.Println("连接失败,err",err) 5 return 6 } 7 defer conn.Close() 8 for i:=0;i<20;i++{ 9 msg := "Ares is a bird!" 10 conn.Write([]byte(msg)) 11 } 12}
先启动服务端再启动客户端,可以看到服务端输出结果如下:
1$go run main.go 2收到client发来的数据: Ares is a bird!Ares is a bird!Ares is a bird!Ares is a bird!Ares is a bird!Ares is a bird!Ares is a bird!Ares is a bird!Ares is a bird!Ares is a bird!Ares is a bird!Ares is a bird!Ares is a bird!Ares is a bird! 3收到client发来的数据: Ares is a bird!Ares is a bird!Ares is a bird!Ares is a bird!Ares is a bird!Ares is a bird!
客户端分10次发送的数据,在服务端并没有成功的输出10次,而是多条数据“粘”到了一起。
TCP为什么会出现粘包
在socket网络程序中,TCP和UDP分别是面向连接和非面向连接的。因此TCP的socket编程,收发两端(客户端和服务器端)都要有成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小、数据量小的数据,合并成一个大的数据块,然后进行封包。这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。
对于UDP,不会使用块的合并优化算法,这样,实际上目前认为,是由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。所以UDP不会出现粘包问题。
粘包产生原因
1发送端需要等缓冲区满才发送出去,造成粘包
2接收方不及时接收缓冲区的包,造成多个包接收
具体点:
(1)发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一包数据。若连续几次发送的数据都很少,通常TCP会根据优化算法把这些数据合成一包后一次发送出去,这样接收方就收到了粘包数据。
(2)接收方引起的粘包是由于接收方用户进程不及时接收数据,从而导致粘包现象。这是因为接收方先把收到的数据放在系统接收缓冲区,用户进程从该缓冲区取数据,若下一包数据到达时前一包数据尚未被用户进程取走,则下一包数据放到系统接收缓冲区时就接到前一包数据之后,而用户进程根据预先设定的缓冲区大小从系统接收缓冲区取数据,这样就一次取到了多包数据。
参考:TCP通信粘包问题分析和解决
解决办法
出现”粘包”的关键在于接收方不确定将要传输的数据包的大小,因此我们可以对数据包进行封包和拆包的操作。
自定义一个协议,比如数据包的前4个字节为包头,里面存储的是发送的数据的长度。
1// Encode 将消息编码 2func Encode(message string)([]byte ,error) { 3 // 读取消息的长度,转换成int32类型(占4个字节) 4 var length = int32(len(message)) 5 var pkg = new(bytes.Buffer) 6 //写入消息头 7 err := binary.Write(pkg,binary.LittleEndian,length) 8 if err != nil{ 9 return nil,err 10 } 11 //写入消息实体 12 err = binary.Write(pkg,binary.LittleEndian,[]byte(message)) 13 if err != nil{ 14 return nil,err 15 } 16 return pkg.Bytes(),nil 17} 18 19// Decode 消息解码 20func Decode(reader *bufio.Reader)(string,error) { 21 //读取消息长度 22 lengthByte,_ := reader.Peek(4) //读取前4个字节数据 23 lengthBuff := bytes.NewBuffer(lengthByte) 24 var length int32 25 err := binary.Read(lengthBuff,binary.LittleEndian,&length) 26 if err != nil{ 27 return "",err 28 } 29 // Buffered返回缓冲中现有的可读取的字节数。 30 if int32(reader.Buffered()) < length+4{ 31 return "",err 32 } 33 //读取真正的消息数据 34 pack := make([]byte,int(4+length)) 35 _,err = reader.Read(pack) 36 if err != nil{ 37 return "",err 38 } 39 return string(pack[4:]),nil 40}
server端:
1func process(conn net.Conn) { 2 defer conn.Close() 3 reader := bufio.NewReader(conn) 4 for { 5 msg,err := proto.Decode(reader) 6 if err == io.EOF{ 7 return 8 } 9 if err != nil{ 10 fmt.Println("decode 失败,err",err) 11 return 12 } 13 fmt.Println("收到client数据:",msg) 14 } 15} 16func main() { 17 listen,err := net.Listen("tcp","127.0.0.1:8888") 18 if err != nil{ 19 fmt.Println("监听失败,err",err) 20 return 21 } 22 defer listen.Close() 23 for { 24 conn,err := listen.Accept() 25 if err != nil{ 26 fmt.Println("接受失败,err",err) 27 continue 28 } 29 go process(conn) 30 } 31}
client端:
1func main() { 2 conn,err := net.Dial("tcp","127.0.0.1:8888") 3 if err != nil{ 4 fmt.Println("dial失败,err",err) 5 return 6 } 7 defer conn.Close() 8 for i:=0;i<20;i++{ 9 msg := "Hello Ares!" 10 data,err := proto.Encode(msg) 11 if err != nil{ 12 fmt.Println("encode失败,err",err) 13 return 14 } 15 conn.Write(data) 16 } 17}
先启动服务端再启动客户端,可以看到服务端输出结果如下:
1go run main.go 2收到client数据: Hello Ares! 3收到client数据: Hello Ares! 4收到client数据: Hello Ares! 5收到client数据: Hello Ares! 6收到client数据: Hello Ares! 7收到client数据: Hello Ares! 8收到client数据: Hello Ares! 9收到client数据: Hello Ares! 10收到client数据: Hello Ares! 11收到client数据: Hello Ares! 12收到client数据: Hello Ares! 13收到client数据: Hello Ares! 14收到client数据: Hello Ares! 15收到client数据: Hello Ares! 16收到client数据: Hello Ares! 17收到client数据: Hello Ares! 18收到client数据: Hello Ares! 19收到client数据: Hello Ares! 20收到client数据: Hello Ares! 21收到client数据: Hello Ares!