• go语言基础操作---七


    socket简单介绍—套接字编程

    什么是Socket
    Socket,英文含义是【插座、插孔】,一般称之为套接字,用于描述IP地址和端口。可以实现不同程序间的数据通信。
    Socket起源于Unix,而Unix基本哲学之一就是“一切皆文件”,都可以用“打开open –> 读写write/read –> 关闭close”模式来操作。Socket就是该模式的一个实现,网络的Socket数据传输是一种特殊的I/O,Socket也是一种文件描述符。Socket也具有一个类似于打开文件的函数调用:Socket(),该函数返回一个整型的Socket描述符,随后的连接建立、数据传输等操作都是通过该Socket实现的。
    在这里插入图片描述
    在TCP/IP协议中,“IP地址+TCP或UDP端口号”唯一标识网络通讯中的一个进程。“IP地址+端口号”就对应一个socket。欲建立连接的两个进程各自有一个socket来标识,那么这两个socket组成的socket pair(套接字对)就唯一标识一个连接因此可以用Socket来描述网络连接的一对一关系。
    常用的Socket类型有两种:流式Socket(SOCK_STREAM)和数据报式Socket(SOCK_DGRAM)流式是一种面向连接的Socket,针对于面向连接的TCP服务应用数据报式Socket是一种无连接的Socket,对应于无连接的UDP服务应用

    网络应用程序设计模式

    C/S模式
    传统的网络应用设计模式,客户机(client)/服务器(server)模式。需要在通讯两端各自部署客户机和服务器来完成数据通信。
    B/S模式
    浏览器(Browser)/服务器(Server)模式。只需在一端部署服务器,而另外一端使用每台PC都默认配置的浏览器即可完成数据的传输。
    优缺点
    对于C/S模式来说,其优点明显。客户端位于目标主机上可以保证性能,将数据缓存至客户端本地,从而提高数据传输效率。且,一般来说客户端和服务器程序由一个开发团队创作,所以他们之间所采用的协议相对灵活。可以在标准协议的基础上根据需求裁剪及定制。例如,腾讯所采用的通信协议,即为ftp协议的修改剪裁版。
    因此,传统的网络应用程序及较大型的网络应用程序都首选C/S模式进行开发。如,知名的网络游戏魔兽世界。3D画面,数据量庞大,使用C/S模式可以提前在本地进行大量数据的缓存处理,从而提高观感。
    C/S模式的缺点也较突出。由于客户端和服务器都需要有一个开发团队来完成开发。工作量将成倍提升,开发周期较长。另外,从用户角度出发,需要将客户端安插至用户主机上,对用户主机的安全性构成威胁。这也是很多用户不愿使用C/S模式应用程序的重要原因。
    B/S模式相比C/S模式而言,由于它没有独立的客户端,使用标准浏览器作为客户端,其工作开发量较小。只需开发服务器端即可。另外由于其采用浏览器显示数据,因此移植性非常好,不受平台限制。如早期的偷菜游戏,在各个平台上都可以完美运行。
    B/S模式的缺点也较明显。由于使用第三方浏览器,因此网络应用支持受限。另外,没有客户端放到对方主机上,缓存数据不尽如人意,从而传输数据量受到限制。应用的观感大打折扣。第三,必须与浏览器一样,采用标准http协议进行通信,协议选择不灵活。
    因此在开发过程中,模式的选择由上述各自的特点决定。根据实际需求选择应用程序设计模式。

    TCP的C/S架构

    在这里插入图片描述
    TCP服务器代码编写

    package main
    
    import (
    	"fmt"
    	"net"
    )
    
    func main() {
    	//监听nerwork为tcp和udp,address为ip:端口,本地ip地址可以不写
    	listener, err1 := net.Listen("tcp", "127.0.0.1:8080")
    	if err1 != nil {
    		fmt.Println("err =", err1)
    		return
    	}
    	defer listener.Close() //监听关闭
    	//阻塞等待用户连接
    	conn, err := listener.Accept()
    	if err != nil {
    		fmt.Println("阻塞:err = ", err)
    		return
    	}
    
    	//接收用户请求
    	buf := make([]byte, 1024) //1024大小的缓冲区
    	n, err2 := conn.Read(buf)
    	if err2 != nil {
    		fmt.Println("接收: err2 = ", err2)
    		return
    	}
    
    	fmt.Println("buf = ", string(buf[:n])) //指定处理的读多少
    
    	defer conn.Close() //关闭当前用户连接
    }
    
    • 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

    在这里插入图片描述

    TCP客户端代码编写

    package main
    
    import (
    	"fmt"
    	"net"
    )
    
    func main() {
    
    	//主动连接服务器
    	conn, err := net.Dial("tcp", "127.0.0.1:8080")
    	if err != nil {
    		fmt.Println("err = ", err)
    		return
    	}
    
    	defer conn.Close() //关闭
    
    	//发送数据
    	conn.Write([]byte("are you ok?"))
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    并发的C/S模型通信

    并发Server

    现在已经完成了客户端与服务端的通信,但是服务端只能接收一个用户发送过来的数据,怎样接收多个客户端发送过来的数据,实现一个高效的并发服务器呢?
    Accept()函数的作用是等待客户端的链接,如果客户端没有链接,该方法会阻塞。如果有客户端链接,那么该方法返回一个Socket负责与客户端进行通信。所以,每来一个客户端,该方法就应该返回一个Socket与其通信,因此,可以使用一个死循环,将Accept()调用过程包裹起来。
    需要注意的是,实现并发处理多个客户端数据的服务器,就需要针对每一个客户端连接,单独产生一个Socket,并创建一个单独的goroutine与之完成通信。

    简单版并发服务器

    package main
    
    import (
    	"fmt"
    	"net"
    	"strings"
    )
    
    // 处理用户请求
    func HandleConn(conn net.Conn) {
    	//函数调用完毕,自动关闭conn
    	defer conn.Close()
    
    	//获取客户端的网络地址信息
    	addr := conn.RemoteAddr().String()
    	fmt.Println(addr, "addr conncet sucessful") //连接成功
    
    	//读取用户数据
    	buf := make([]byte, 2048)
    	for true {
    		//读取用户数据
    		read, err := conn.Read(buf)
    		if err != nil {
    			//read tcp 127.0.0.1:8080->127.0.0.1:56933:
    			//wsarecv: An existing connection was forcibly closed by the remote host.
    			fmt.Println("err = ", err)
    			return
    		}
    		fmt.Printf("[%s]: = %s\n", addr, string(buf[:read])) //read为读取数据的个数
    		fmt.Println("len = ", len(string(buf[:read])))
    		if "exit" == string(buf[:read-1]) {
    			fmt.Println(addr, "exit")
    			return
    		}
    		//把数据转化为大写,再给用户发送
    		conn.Write([]byte(strings.ToUpper(string(buf[:read]))))
    	}
    }
    
    func main() {
    	//监听nerwork为tcp和udp,address为ip:端口,本地ip地址可以不写
    	listener, err1 := net.Listen("tcp", "127.0.0.1:8080")
    	if err1 != nil {
    		fmt.Println("err =", err1)
    		return
    	}
    	defer listener.Close() //监听关闭
    
    	//接收多个用户
    	for true {
    		conn, err := listener.Accept() //等待客户端的链接,如果客户端没有链接,该方法会阻塞
    		if err != nil {
    			fmt.Println("err = ", err)
    			return
    		}
    
    		//处理用户请求,新建一个协程,每来一个就单独为它获取
    		go HandleConn(conn)
    	}
    }
    
    • 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
    • 59
    • 60

    在这里插入图片描述

    并发Client

    客户端不仅需要持续的向服务端发送数据,同时也要接收从服务端返回的数据。因此可将发送和接收放到不同的协程中。
    主协程循环接收服务器回发的数据(该数据应已转换为大写),并打印至屏幕;子协程循环从键盘读取用户输入数据,写给服务器。读取键盘输入可使用 os.Stdin.Read(str)。定义切片str,将读到的数据保存至str中。
    这样,客户端也实现了多任务。

    客户端即可输入也可接收服务器回复

    package main
    
    import (
    	"fmt"
    	"net"
    	"os"
    )
    
    func main() {
    
    	//主动连接服务器
    	conn, err := net.Dial("tcp", "127.0.0.1:8080")
    	if err != nil {
    		fmt.Println("net Dial err = ", err)
    		return
    	}
    
    	//main调用完毕,关闭连接
    	defer conn.Close() //关闭
    
    	//接收服务器回复的数据,新任务
    	go func() {
    		//从键盘输入内容,给服务器发送内容
    		str := make([]byte, 1024)
    		for true {
    			n, err2 := os.Stdin.Read(str) //从键盘读取内容,放在str
    			if err2 != nil {
    				fmt.Println("os.Stdin.err = ", err)
    				return
    			}
    			//把输入的内容给服务器发送
    			conn.Write(str[:n])
    		}
    	}()
    	//切片缓存
    	buf := make([]byte, 1024)
    	//不停地接收
    	for true {
    		n, err := conn.Read(buf) //接收服务器的请求
    		if err != nil {
    			fmt.Println("conn.Read err = ", err)
    			return
    		}
    		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

    在这里插入图片描述

    TCP通信

    下图是一次TCP通讯的时序图。TCP连接建立断开。包含大家熟知的三次握手和四次握手。
    在这里插入图片描述
    三次握手:
    所谓三次握手(Three-Way Handshake)即建立TCP连接,就是指建立一个TCP连接时,需要客户端和服务端总共发送3个包以确认连接的建立。好比两个人在打电话:
    Client:“喂,你听得到吗?”
    Server:“我听得到,你听得到我吗?”
    Client:“我能听到你,今天balabala…”
    建立连接(三次握手)的过程:
    1.客户端发送一个带SYN标志的TCP报文到服务器。这是上图中三次握手过程中的段1。客户端发出SYN位表示连接请求。序号是1000,这个序号在网络通讯中用作临时的地址,每发一个数据字节,这个序号要加1,这样在接收端可以根据序号排出数据包的正确顺序,也可以发现丢包的情况。
    另外,规定SYN位和FIN位也要占一个序号,这次虽然没发数据,但是由于发了SYN位,因此下次再发送应该用序号1001。
    mss表示最大段尺寸,如果一个段太大,封装成帧后超过了链路层的最大长度,就必须在IP层分片,为了避免这种情况,客户端声明自己的最大段尺寸,建议服务器端发来的段不要超过这个长度。
    2.服务器端回应客户端,是三次握手中的第2个报文段,同时带ACK标志和SYN标志。表示对刚才客户端SYN的回应;同时又发送SYN给客户端,询问客户端是否准备好进行数据通讯。
    服务器发出段2,也带有SYN位,同时置ACK位表示确认,确认序号是1001,表示“我接收到序号1000及其以前所有的段,请你下次发送序号为1001的段”,也就是应答了客户端的连接请求,同时也给客户端发出一个连接请求,同时声明最大尺寸为1024。
    3.客户必须再次回应服务器端一个ACK报文,这是报文段3。
    客户端发出段3,对服务器的连接请求进行应答,确认序号是8001。在这个过程中,客户端和服务器分别给对方发了连接请求,也应答了对方的连接请求,其中服务器的请求和应答在一个段中发出。
    因此一共有三个段用于建立连接,称为“三方握手”。在建立连接的同时,双方协商了一些信息,例如,双方发送序号的初始值、最大段尺寸等。
    数据传输的过程:
    1.客户端发出段4,包含从序号1001开始的20个字节数据。
    2.服务器发出段5,确认序号为1021,对序号为1001-1020的数据表示确认收到,同时请求发送序号1021开始的数据,服务器在应答的同时也向客户端发送从序号8001开始的10个字节数据。
    3.客户端发出段6,对服务器发来的序号为8001-8010的数据表示确认收到,请求发送序号8011开始的数据。
    在数据传输过程中,ACK和确认序号是非常重要的,应用程序交给TCP协议发送的数据会暂存在TCP层的发送缓冲区中,发出数据包给对方之后,只有收到对方应答的ACK段才知道该数据包确实发到了对方,可以从发送缓冲区中释放掉了,如果因为网络故障丢失了数据包或者丢失了对方发回的ACK段,经过等待超时后TCP协议自动将发送缓冲区中的数据包重发。
    四次挥手:
    所谓四次挥手(Four-Way-Wavehand)即终止TCP连接,就是指断开一个TCP连接时,需要客户端和服务端总共发送4个包以确认连接的断开。在socket编程中,这一过程由客户端或服务器任一方执行close来触发。好比两个人打完电话要挂断:
    Client:“我要说的事情都说完了,我没事了。挂啦?”
    Server:“等下,我还有一个事儿。Balabala…”
    Server:“好了,我没事儿了。挂了啊。”
    Client:“ok!拜拜”
    关闭连接(四次握手)的过程:
    由于TCP连接是全双工的,因此每个方向都必须单独进行关闭。这原则是当一方完成它的数据发送任务后就能发送一个FIN来终止这个方向的连接。收到一个 FIN只意味着这一方向上没有数据流动,一个TCP连接在收到一个FIN后仍能发送数据。首先进行关闭的一方将执行主动关闭,而另一方执行被动关闭。
    1.客户端发出段7,FIN位表示关闭连接的请求。
    2.服务器发出段8,应答客户端的关闭连接请求。
    3.服务器发出段9,其中也包含FIN位,向客户端发送关闭连接请求。
    4.客户端发出段10,应答服务器的关闭连接请求。
    建立连接的过程是三次握手,而关闭连接通常需要4个段,服务器的应答和关闭连接请求通常不合并在一个段中,因为有连接半关闭的情况,这种情况下客户端关闭连接之后就不能再发送数据给服务器了,但是服务器还可以发送数据给客户端,直到服务器也关闭连接为止。

    UDP通信

    在之前的案例中,我们一直使用的是TCP协议来编写Socket的客户端与服务端。其实也可以使用UDP协议来编写Socket的客户端与服务端。
    UDP服务器
    由于UDP是“无连接”的,所以,服务器端不需要额外创建监听套接字,只需要指定好IP和port,然后监听该地址,等待客户端与之建立连接,即可通信。
    创建监听地址:
    func ResolveUDPAddr(network, address string) (*UDPAddr, error)
    创建监听连接:
    func ListenUDP(network string, laddr *UDPAddr) (*UDPConn, error)
    接收udp数据:
    func (c *UDPConn) ReadFromUDP(b []byte) (int, *UDPAddr, error)
    写出数据到udp:
    func (c *UDPConn) WriteToUDP(b []byte, addr *UDPAddr) (int, error)

    文件传输原理

    在这里插入图片描述

    UDP与TCP的差异

    在这里插入图片描述

    os.Stat的使用

    package main
    
    import (
    	"fmt"
    	"os"
    )
    
    func main() {
    	list := os.Args
    	fmt.Println(len(list))
    	if len(list) != 2 {
    		fmt.Println("useage :xxx file")
    		return
    	}
    
    	for i, s := range list {
    		fmt.Println(i, s)
    	}
    	fileName := list[1]
    	fmt.Printf("fileName = %s\n", fileName)
    
    	//Stat返回一个描述name指定的文件对象的FileInfo。如果指定的文件对象是一个符号链接,
    	//返回的FileInfo描述该符号链接指向的文件的信息,本函数会尝试跳转该链接。如果出错,返回的错误值为*PathError类型。
    	//过滤路径
    	info, err := os.Stat(fileName)
    
    	if err != nil {
    		fmt.Println("err = ", err)
    	}
    	fmt.Println("name = ", info.Name()) //name =  01_昨日回顾.mp4
    	fmt.Println("size = ", info.Size()) //size =  67010611
    }
    
    • 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

    在这里插入图片描述

    传输文件:发送方

    package main
    
    import (
    	"fmt"
    	"io"
    	"net"
    	"os"
    )
    
    func sendFile(path string, conn net.Conn) {
    	//以只读方式打开文件
    	file, err := os.Open(path)
    	if err != nil {
    		fmt.Println("send Open err = ", err)
    		return
    	}
    
    	defer file.Close()
    
    	//读文件内容,读多少发多少,一点不差
    	buf := make([]byte, 1024*4)
    	for true {
    		read, err := file.Read(buf) //从文件读取内容
    		if err != nil {
    			if err == io.EOF {
    				fmt.Println("文件发送完毕")
    			} else {
    				fmt.Println("send Rend err = ", err)
    			}
    			return
    		}
    		//发送内容
    		conn.Write(buf[:read]) //给服务器发送内容
    	}
    }
    
    func main() {
    
    	//提示输入文件
    	fmt.Println("请输入需要传输的文件: ")
    	var path string
    	_, err := fmt.Scan(&path)
    	if err != nil {
    		fmt.Println("Scan open err = ", err)
    		return
    	}
    
    	//获取文件名
    	fileInfo, err := os.Stat(path)
    	if err != nil {
    		fmt.Println("os.Stat err = ", err)
    		return
    	}
    
    	//主动连接服务器
    	conn, err := net.Dial("tcp", "127.0.0.1:8000")
    	if err != nil {
    		fmt.Println("net.Dial err = ", err)
    		return
    	}
    
    	defer conn.Close()
    
    	//给接收方,先发送文件名
    	_, err = conn.Write([]byte(fileInfo.Name()))
    	if err != nil {
    		fmt.Println("conn.write err = ", err)
    		return
    	}
    
    	//接收对方的回复,如果回复ok,说明对方准备好,可以发文件
    	buf := make([]byte, 1024)
    	readSize, err := conn.Read(buf)
    	if err != nil {
    		fmt.Println("conn.Read err = ", err)
    		return
    	}
    
    	if "ok" == string(buf[:readSize]) {
    		//发送文件内容
    		sendFile(path, conn)
    	}
    }
    
    • 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
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83

    传输文件:接收方

    package main
    
    import (
    	"fmt"
    	"io"
    	"net"
    	"os"
    )
    
    // RecvFile 接收文件内容
    func RecvFile(fileName string, conn net.Conn) {
    	//新建文件
    	file, err := os.Create(fileName)
    	if err != nil {
    		fmt.Println("os create err =", err)
    		return
    	}
    
    	buf := make([]byte, 1024*4)
    
    	//接收多少,写多少,一点不差
    	for true {
    		readSize, err := conn.Read(buf) //接收对方发过来的文件内容
    		if err != nil {
    			if err == io.EOF {
    				fmt.Println("文件接收完毕")
    			} else {
    				fmt.Println("conn read err =", err)
    			}
    			return
    		}
    		if readSize == 0 {
    			fmt.Println("n == 0 文件接收完毕")
    			return
    		}
    
    		file.Write(buf[:readSize])
    	}
    }
    
    func main() {
    	//监听
    	listener, err := net.Listen("tcp", "127.0.0.1:8000")
    	if err != nil {
    		fmt.Println("net listener err = ", err)
    		return
    	}
    
    	defer listener.Close()
    
    	//阻塞等待用户连接
    	conn, err := listener.Accept()
    	if err != nil {
    		fmt.Println("listener accept err =", err)
    		return
    	}
    
    	defer conn.Close()
    	buf := make([]byte, 1024)
    
    	//读取对方发送的文件名
    	readSize, err := conn.Read(buf)
    	if err != nil {
    		fmt.Println("conn read err =", err)
    		return
    	}
    
    	fileName := string(buf[:readSize])
    
    	//回复"ok"
    	_, err1 := conn.Write([]byte("ok"))
    	if err1 != nil {
    		return
    	}
    
    	//接收文件内容
    	RecvFile(fileName, conn)
    }
    
    • 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
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
  • 相关阅读:
    ceph 14.2.10 aarch64 非集群内 客户端 挂载块设备
    思科防火墙NAT——实验
    flex布局
    AWVS+BP+XRAY三层联动扫描漏洞
    SQLSugar查询返回DataTable
    工业视觉——打光
    【KCP】UDP可靠性传输
    QT 介绍
    appinventor多个拓展导入导致编译出错:
    Python 可迭代对象、迭代器、生成器
  • 原文地址:https://blog.csdn.net/qq_40432598/article/details/132763579