Golang网络编程

协议

协议可以理解为规则,是数据传输和解释的规则,是通信双方都要遵守的规则。

协议存在的意义是为了让双方更好的沟通。

在双方之间被遵守的协议成为原始协议

当此协议被更多的人采用后,不断的完善,最终形成一个稳定的、完整的文件传输协议,被广泛应用于各种文件传输过程中。该协议就成为了一个标准协议

网络应用模型

C/S模型 : 需要在服务器端和客户端都安装部署,才能完成数据通信。性能好,因为可以提前把数据缓存到客户端本地,提高用户体验,限制也少,可采用的协议相对灵活。

B/S模型:只需要在服务器端安装部署,客户端只需要一个浏览器即可。优势是移植性非常好,不收平台限制,只需一个浏览器就可以打开。缺点是无法想C/S那样提前缓存大量数据在本地,网络受限时,应用的体验感非常差,而且浏览器采用的协议是标准http协议通信,没有C/S灵活 .

标准库net包基本使用

TCP编程

net库是Golang原生自带标准库,无需第三方包来支持网络功能。

服务器端基本代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package main

import (
	"fmt"
	"net"
)

func main() {
	listener, err := net.Listen("tcp", "10.205.6.142:8088")
	if err != nil {
		fmt.Printf("net.Listen error: %v", err)
		return
	}
	fmt.Println("服务器已开启...")

	defer listener.Close() //设置延迟关闭,防止服务器关闭

	for { //设置阻塞持续监听
		conn, err := listener.Accept()
		if err != nil {
			fmt.Printf("listener.Accept error: %v", err)
			return
		} else {
			fmt.Println("服务器: ", conn.LocalAddr().String())
		}

	}

}

链接验证:

在Windows功能里面开启telnet 功能,然后CMD窗口输入 telnet 本机IP 监听的端口号(注意:IP和端口号中间为空格),即可链接.

golangtcp

golangTcp

客户端基本代码

客户端是用net.Dial()来进行链接.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
	"fmt"
	"net"
)

func main() {
	fmt.Println("客户端开始...")
	conn, err := net.Dial("tcp", "10.205.6.142:8088") //链接服务器端口
	if err != nil {
		fmt.Printf("net.Dial error: %v", err)
		return
	}
	defer conn.Close()
	fmt.Println("客户链接成功")
	fmt.Println("服务器: ", conn.RemoteAddr().String())  //输出服务器和IP和端口号结束
	fmt.Println("客户端: ", conn.LocalAddr().String())

}

输出结果:

服务器端:

server

客户端:

client

数据交互

客户端

数据交互需要调用 os.Stdin 来实现检测

bufio来实现IO流,进而实现数据传递。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package main

import (
   "bufio"
   "fmt"
   "net"
   "os"
)

func main() {
   fmt.Println("客户端开始...")

   conn, err := net.Dial("tcp", "10.205.6.142:8088") //链接服务器端口
   if err != nil {
      fmt.Printf("net.Dial error: %v", err)
      return
   }

   defer conn.Close()
   fmt.Println("客户端链接成功")

   Wirte(conn)

   fmt.Println("服务器: ", conn.RemoteAddr().String()) //输出服务器和IP和端口号结束
   fmt.Println("客户端: ", conn.LocalAddr().String())

}

func Wirte(conn net.Conn) {
   fmt.Print("请输入: ")
   reader := bufio.NewReader(os.Stdin) //标准输入流
   line, err := reader.ReadString('\n')
   if err != nil {
      fmt.Printf("reader.ReadStrin error: %v", err)
      return
   }

   len, err := conn.Write([]byte(line))  
   if err != nil {
      fmt.Printf("conn.Write error: %v", err)
      return
   }
   fmt.Printf("写了 %v 字节\n", len)

}

输出结果:

IOclient

服务器端

向对应写一个输入流来持续接收客户端发送的数据

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package main

import (
   "fmt"
   "net"
)

func main() {
   listener, err := net.Listen("tcp", "10.205.6.142:8088")
   if err != nil {
      fmt.Printf("net.Listen error: %v", err)
      return
   }
   fmt.Println("服务器已开启...")

   defer listener.Close() //设置延迟关闭,防止服务器关闭
   for {                  //设置阻塞持续监听
      conn, err := listener.Accept()
      if err != nil {
         fmt.Printf("listener.Accept error: %v", err)
         return
      } else {
         fmt.Println("服务器: ", conn.LocalAddr().String())
         get(conn) //获取客户端数据
      }

   }

}

// 获取客户端输入
func get(conn net.Conn) {
   defer conn.Close()

   for {
      buf := make([]byte, 1024)
      n, err := conn.Read(buf)
      if err != nil {
         fmt.Printf("conn.Read error: %v", err)
         return
      }
      fmt.Println(n)

   }

}

但此时会发现有个EOF的错误

EOF

这个错误是说明Cilent端(客户端)此时已经退出了(关闭了)。

可以专门优化一下:

1
2
3
4
if err == io.EOF { //处理EOF 客户端断开error
	fmt.Printf("客户端(%v)已断开", conn.RemoteAddr())
	return
}

此时观察接收到的数据,返回的是4

对比客户端,

https://xenolies-blog-images.oss-cn-hangzhou.aliyuncs.com/Pics/image-20221104153629384.png

发现是返回的写入字节大小。所以就需要转为string

1
fmt.Println(string(buf[:n]))

整体完整代码

服务器完整代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
/**
* TCP服务器端
*/

package main

import (
	"fmt"
	"io"
	"net"
)

func main() {
	listener, err := net.Listen("tcp", "localhost:8808")
	if err != nil {
		fmt.Printf("net.Listen error: %v", err)
		return
	}
	fmt.Println("服务器已开启...")

	defer listener.Close() //设置延迟关闭,防止服务器关闭
	for {                  //设置阻塞持续监听
		conn, err := listener.Accept()

		go get(conn)

		if err != nil {
			fmt.Printf("listener.Accept error: %v", err)
			return
		} else {

			fmt.Println("服务器: ", conn.LocalAddr().String())

		}

	}

}

// 获取客户端输入
func get(conn net.Conn) {
	defer conn.Close()

	for {
		buf := make([]byte, 1024)
		n, err := conn.Read(buf)
		if err != nil {
			if err == io.EOF { //处理EOF 客户端断开error
				fmt.Printf("客户端(%v)已断开", conn.RemoteAddr())
				return
			}
			fmt.Printf("conn.Read error: %v", err)
			return
		}
		fmt.Printf("接收到客户端输入: %v", string(buf[:n]))
	}

}

客户端完整代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
/**
* TCP客户端
*/

package main

import (
	"bufio"
	"fmt"
	"net"
	"os"
)

func main() {
	fmt.Println("客户端开始...")

	conn, err := net.Dial("tcp", "localhost:8808") //链接服务器端口
	if err != nil {
		fmt.Printf("net.Dial error: %v", err)
		return
	}

	defer conn.Close()
	fmt.Println("客户端链接成功")

	Wirte(conn)

	fmt.Println("服务器: ", conn.RemoteAddr().String()) //输出服务器和IP和端口号结束
	fmt.Println("客户端: ", conn.LocalAddr().String())

}

func Wirte(conn net.Conn) {
	fmt.Print("请输入: ")
	reader := bufio.NewReader(os.Stdin) //标准输入流
	line, err := reader.ReadString('\n')
	if err != nil {
		fmt.Printf("reader.ReadStrin error: %v", err)
		return
	}

	len, err := conn.Write([]byte(line))
	if err != nil {
		fmt.Printf("conn.Write error: %v\n", err)
		return
	}
	fmt.Printf("写了 %v 字节\n", len)

}

TCP粘包

以这样的TCP代码为例

服务端

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package main

import (
	"fmt"
	"net"
)

func main() {
	fmt.Println("客户端开始...")

	conn, err := net.Dial("tcp", "localhost:8808") //链接服务器端口
	if err != nil {
		fmt.Printf("net.Dial error: %v", err)
		return
	}

	defer conn.Close()
	fmt.Println("客户端链接成功")

	Wirte(conn)

	fmt.Println("服务器: ", conn.RemoteAddr().String()) //输出服务器和IP和端口号结束
	fmt.Println("客户端: ", conn.LocalAddr().String())

}

func Wirte(conn net.Conn) {

	line := "啊啊啊啊啊啊啊" 
	for i := 0; i < 5; i++ {  //发送五次一样的数据
		_, err := conn.Write([]byte(line))
		if err != nil {
			fmt.Printf("conn.Write error: %v\n", err)
			return
		}

	}

}

客户端

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package main

import (
	"fmt"
	"io"
	"net"
)

func main() {
	listener, err := net.Listen("tcp", "localhost:8808")
	if err != nil {
		fmt.Printf("net.Listen error: %v", err)
		return
	}
	fmt.Println("服务器已开启...")

	defer listener.Close() //设置延迟关闭,防止服务器关闭
	for {                  //设置阻塞持续监听
		conn, err := listener.Accept()

		go get(conn)

		if err != nil {
			fmt.Printf("listener.Accept error: %v", err)
			return
		} else {

			fmt.Println("服务器: ", conn.LocalAddr().String())

		}

	}

}

// 获取客户端输入
func get(conn net.Conn) {
	defer conn.Close()
	
	buf := make([]byte, 1024)
	n, err := conn.Read(buf)
	if err != nil {
		if err == io.EOF { //处理EOF 客户端断开error
			fmt.Printf("客户端(%v)已断开", conn.RemoteAddr())
			return
		}
		fmt.Printf("conn.Read error: %v", err)
		return
	}
	fmt.Printf("接收到客户端输入: %v", string(buf[:n]))

}

此时的服务端输出的结果是:

1
2
3
服务器已开启...
服务器:  127.0.0.1:8808
接收到客户端输入: 啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊

客户端发送五次数据,结果数据反而“粘”在了一起。

粘包出现的原因

主要原因就是TCP数据传递模式是流模式,在保持长连接的时候可以进行多次的收和发。

1.由Nagle算法造成的发送端的粘包:Nagle算法是一种改善网络传输效率的算法。简单来说就是当我们提交一段数据给TCP发送时,TCP并不立刻发送此段数据,而是等待一小段时间看看在等待期间是否还有要发送的数据,若有则会一次把这两段数据发送出去。 2.接收端接收不及时造成的接收端粘包:TCP会把接收到的数据存在自己的缓冲区中,然后通知应用层取数据。当应用层由于某些原因不能及时的把TCP的数据取出来,就会造成TCP缓冲区中存放了几段数据。

粘包的解决方法

解决TCP传输粘包有两种方法:

  • 使用边界符

  • 使用成熟的应用层协议

感谢大佬写的文章 :

Golang处理TCP“粘包”问题 | JWang的博客 (wangbjun.site)

方法1

再来说说方法2

出现”粘包”的一个原因在于接收方不确定将要传输的数据包的大小,所以可以用边界符来解决粘包问题

客户端

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package main

import (
	"fmt"
	"net"
	"time"
)

func main() {
	fmt.Println("客户端开始...")

	conn, err := net.Dial("tcp", "localhost:8808") //链接服务器端口
	if err != nil {
		fmt.Printf("net.Dial error: %v", err)
		return
	}

	defer conn.Close()
	fmt.Println("客户端链接成功")

	Wirte(conn)

	fmt.Println("服务器: ", conn.RemoteAddr().String()) //输出服务器和IP和端口号结束
	fmt.Println("客户端: ", conn.LocalAddr().String())

}

func Wirte(conn net.Conn) {

	line := "啊啊啊啊啊啊啊\n"
	_, err := conn.Write([]byte(line))
	time.Sleep(time.Second * 1)
	_, err = conn.Write([]byte(line))
	time.Sleep(time.Second * 1)
	if err != nil {
		fmt.Printf("conn.Write error: %v\n", err)
		return
	}

}

服务端

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package main

import (
	"bufio"
	"fmt"
	"io"
	"net"
)

func main() {
	listener, err := net.Listen("tcp", "localhost:8808")
	if err != nil {
		fmt.Printf("net.Listen error: %v", err)
		return
	}
	fmt.Println("服务器已开启...")

	defer listener.Close() //设置延迟关闭,防止服务器关闭
	for {                  //设置阻塞持续监听
		conn, err := listener.Accept()

		go get(conn)

		if err != nil {
			fmt.Printf("listener.Accept error: %v", err)
			return
		} else {
			fmt.Println("服务器: ", conn.LocalAddr().String())
		}
	}
}

// 获取客户端输入
func get(conn net.Conn) {
	defer conn.Close()

	for {
		reader := bufio.NewReader(conn)
		slice, err := reader.ReadSlice('\n')
		fmt.Printf("检测到客户端输入: %s", slice)
		if err != nil {
			if err == io.EOF { //处理EOF 客户端断开error
				fmt.Printf("客户端(%v)已断开\n", conn.RemoteAddr())
				return
			}
			fmt.Printf("conn.Read error: %v", err)
			return
		}
	}
}

输出结果:

![image-20221105191742090](D:\Hexo Blog\Blog\source_posts\Golang网络编程\image-20221105191742090.png)

方法2

使用HTTP、SSH这样成熟的应用层协议

UDP编程

UDP协议(User Datagram Protocol)中文名称是用户数据报协议,是OSI(Open System Interconnection,开放式系统互联)参考模型中一种无连接的传输层协议,不需要建立连接就能直接进行数据发送和接收,属于不可靠的、没有时序的通信,但是UDP协议的实时性比较好,通常用于视频直播相关领域。

服务器端
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package main

import (
	"fmt"
	"net"
)

func main() {
	addr := &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 8808}
	listener, err := net.ListenUDP("udp", addr) //创建UDP4链接,监听127.0.0.1:8808
	if err != nil {
		fmt.Printf("net.Listen err: %v", err)
		return
	}
	//延迟关闭监听
	defer listener.Close()

	for {
		var data [1024]byte
		//接收数据
		n, addr, err := listener.ReadFromUDP(data[:]) //写入流接收数据
		if err != nil {
			fmt.Println("listener.ReadFromUDP err:", err)
			continue
		}
		fmt.Printf("接收数据:%v\n 客户端:%v\n 接收字节大小:%v\n", string(data[:n]), addr, n) //
		//发送数据
		_, err = listener.WriteToUDP(data[:n], addr) //输出流输出数据
		if err != nil {
			fmt.Println(" listener.WriteToUDP err:", err)
			continue
		}

		_, err = listener.WriteToUDP([]byte("Hello,UDP Client!"), addr) //向客户端发送数据
		if err != nil {
			fmt.Printf("listener.WriteToUDP err: %v", err)
			return
		}

	}

}
客户端
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package main

import (
	"fmt"
	"net"
)

func main() {
	addr := &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 8808}
	socket, err := net.DialUDP("udp", nil, addr)
	if err != nil {
		fmt.Printf("net.DialUDP err: %v", err)
		return
	}
	defer socket.Close()

	line := "Hello,UDP Server"
	data, err := socket.Write([]byte(line))
	if err != nil {
		fmt.Printf(" socket.Write err: %v", err)
		return
	}
	fmt.Printf("向UDP客户端%v,发送了 %v 字节的数据", socket.RemoteAddr(), data)

	var get [1024]byte
	len, addr, err := socket.ReadFromUDP(get[:]) //获取服务端返回的数据
	if err != nil {
		fmt.Printf("socket.ReadFromUDP err: %v", err)
		return
	}

	fmt.Printf("UDP服务器:%v\n 发送的内容为 %v\n 接收字节大小: %v\n", addr, string(get[:len]), len)

}

HTTP编程

通过http.HandleFunc()http.ListenAndServe()两个函数就可以轻松创建一个简单的Go web服务器

Web服务器工作流程

Web服务器的工作原理可以简单地归纳为:

  • 客户机通过TCP/IP协议建立到服务器的TCP连接-
  • 客户端向服务器发送HTTP协议请求包,请求服务器里的资源文档
  • 服务器向客户机发送HTTP协议应答包,如果请求的资源包含有动态语言的内容,那么服务器会调用动态语言的解释引擎负责处理“动态内容”,并将处理得到的数据返回给客户端
  • 客户机与服务器断开。由客户端解释HTML文档,在客户端屏幕上渲染图形结果

HTTP协议

超文本传输协议(Hyper Text Transfer Protocol,HTTP)是一个简单的请求-响应协议,它通常运行在TCP之上。它指定了客户端可能发送给服务器什么样的消息以及得到什么样的响应。

HTTP服务端

服务端基本代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package main

import (
	"fmt"
	"net/http"
)

func main() {
	http.HandleFunc("/", hello)  
	http.ListenAndServe(":8080", nil)  //启动一个8088端口的服务器

}

func hello(writer http.ResponseWriter, request *http.Request) {
	str := "Hello,This is Golang HTTP Server!"
	fmt.Fprintf(writer, str) //Fprintf实现格式化的I/O
}

然后访问localhost:8088就可以访问到建立的HTTP服务器了

HTTP

Hander操作
获取请求的Body内容
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package main

import (
	"fmt"
	"io"
	"net/http"
)

func main() {
	http.HandleFunc("/", hander)
	http.ListenAndServe(":8088", nil) //设置监听端口,使用默认hander
}

// 这是Hander
func hander(resp http.ResponseWriter, req *http.Request) {

	resp.Write([]byte("Hello!\n")) //response返回Hellp
	switch req.Method {            //获取请求的方法

	case "GET":
		data, err := io.ReadAll(req.Body)
		if err != nil {
			fmt.Printf("io.ReadAll err: %v", err)
		}
		resp.Write(data) //返回请求的Body

		resp.Write([]byte("This is GET"))
		break

	case "POST":
		data, err := io.ReadAll(req.Body)
		if err != nil {
			fmt.Printf("io.ReadAll err: %v", err)
		}
		resp.Write(data) //返回请求的Body

		resp.Write([]byte("This is POST\n"))
		break

	}

}

返回结果:

image-20221109205533303

HTTP客户端

获取服务端返回的响应

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main  
  
import (  
   "fmt"  
   "io"   "net/http")  
  
func main() {  
   client := new(http.Client)  
   req, err := http.NewRequest("GET", "http://localhost:8088", nil) //设置请求方法,请求URL,输入流   
   if err != nil {  
      fmt.Printf("http.NewRequest err: %v", err)  
   }  
   res, err := client.Do(req) //Client请求,获取响应  
   if err != nil {  
      fmt.Printf("client.Do err: %v", err)  
   }  
  
   body := res.Body  //去除响应的Body  
   r, err := io.ReadAll(body) //输入流读取返回的Body  
   if err != nil {  
      fmt.Printf("io.ReadAll err: %v", err)  
   }  
   fmt.Println(string(r)) //输出返回的响应  
}

其他

net.Dial

Dial() 函数支持如下几种网络协议:tcptcp4(仅限 IPv4)、tcp6(仅限 IPv6)、udpudp4(仅限IPv4)udp6(仅限IPv6)、ipip4(仅限IPv4)ip6(仅限IPv6)unixunixgramunixpacket

在成功建立连接后,我们就可以进行数据的发送和接收,发送数据时,使用连接对象 conn 的 Write() 方法,接收数据时使用Read() 方法。

defer关键字

最后使用defer的会保存在栈顶,会被最先调用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
    "fmt"
)

func main() {

    fmt.Println("defer begin")

    // 将defer放入延迟调用栈
    defer fmt.Println(1)

    defer fmt.Println(2)

    // 最后一个放入, 位于栈顶, 最先调用
    defer fmt.Println(3)

    fmt.Println("defer end")
}

其他笔记: [[Golang 操作数据库]] [[Golang Gin 框架入门]]

0%