8-Go:Socket通信(2)

一、TCP状态转换

  • 主动发起连接请求端(实线):CLOSED —— 完成三次握手 —— ESTABLISEHED(数据通信状态)—— Dial()函数返回
  • 被动发起连接请求端(虚线):CLOSED —— 调用Accept()函数 —— LISTEN —— 完成三次握手 —— ESTABLISEHED (数据通信状态)—— Accept()函数返回
  • 数据传递期间:ESTABLISEHED (数据通信状态)
  • 主动关闭连接请求端:ESTABLISEHED —— FIN_WAIT_2 (半关闭)—— TIME_WAIT —— 2MSL —— 确认最后一个ACK被对端成功接收。—— CLOSE
  • 被动关闭连接请求端:ESTABLISEHED —— CLOSE
  • 查看状态命令
    • windows:netstat -an | findstr 8001(端口号)
    • Linux:netstat -apn | grep 8001
  • TCP状态转换图
    在这里插入图片描述

二、UDP通信

  • 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)
  • TCP与UDP区别
    • TCP通信:面向连接的,可靠的数据包传输
    • UDP通信:无连接的,不可靠的报文传递

1 - UDP简单服务器

  • UDP简单服务器实现步骤
    • ①.创建 server端地址结构(IP + port):net.ResolveUDPAddr()
    • ②.创建用于通信的socket, 绑定地址结构:udpConn = net.ListenUDP(“udp”, server端地址结构)
    • ③.defer udpConn.Close()
    • ④.读取客户端发送数据:ReadFromUDP(buf);返回: n, cltAddr(客户端的IP+port) , err
    • ⑤.写数据给客户端:WriteToUDP("待写数据",cltAddr)
package main

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

func main() {
	// 组织一个udp地址结构, 指定服务器的IP+port;srvAddr是一个地址结构
	srvAddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:8006")
	if err != nil {
		fmt.Println("ResolveUDPAddr err:", err)
		return
	}
	fmt.Println("udp 服务器地址结构,创建完程!!!")
	// 创建用于通信的socket
	udpConn, err := net.ListenUDP("udp", srvAddr)
	if err != nil {
		fmt.Println("ListenUDP err:", err)
		return
	}
	defer udpConn.Close()
	fmt.Println("udp 服务器通信socket创建完成!!!")

	// 读取客户端发送的数据
	buf := make([]byte, 4096)

	// 返回3个值,分别是读取到的字节数、客户端的地址、error
	n, cltAddr, err := udpConn.ReadFromUDP(buf)
	if err != nil {
		fmt.Println("ReadFromUDP err:", err)
		return
	}
	// 模拟处理数据
	fmt.Printf("服务器读到 %v 的数据:%s\n", cltAddr, string(buf[:n]))

	// 提取系统当前时间
	daytime := time.Now().String()

	// 回写数据给客户端
	_, err = udpConn.WriteToUDP([]byte(daytime), cltAddr)
	if err != nil {
		fmt.Println("WriteToUDP err:", err)
		return
	}
}

2 - UDP简单客户端

  • UDP简单客户端实现步骤:参考 TCP 客户端net.Dial("udp", server 的IP+port)
package main

import (
	"fmt"
	"net"
)

func main() {
	// 指定 服务器 IP + port 创建 通信套接字。
	conn, err := net.Dial("udp", "127.0.0.1:8006")
	if err != nil {
		fmt.Println("net.Dial err:", err)
		return
	}
	defer conn.Close()

	// 主动写数据给服务器
	conn.Write([]byte("Are you Ready?"))

	buf := make([]byte, 4096)
	// 接收服务器回发的数据
	n, err := conn.Read(buf)
	if err != nil {
		fmt.Println("conn.Read err:", err)
		return
	}
	fmt.Println("服务器回发:", string(buf[:n]))
}

3 - UDP并发服务器

  • UDP并发服务器实现
    • ①.UDP默认支持客户端并发访问
    • ②.使用go程将服务器处理 ReadFromUDP和 WriteToUDP操作分开,提高并发效率
package main

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

func main() {

	// 组织一个udp地址结构, 指定服务器的IP+port
	srvAddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:8006")
	if err != nil {
		fmt.Println("ResolveUDPAddr err:", err)
		return
	}
	fmt.Println("udp 服务器地址结构,创建完程!!!")
	// 创建用于通信的socket
	udpConn, err := net.ListenUDP("udp", srvAddr)
	if err != nil {
		fmt.Println("ListenUDP err:", err)
		return
	}
	defer udpConn.Close()
	fmt.Println("udp 服务器通信socket创建完成!!!")

	// 读取客户端发送的数据
	buf := make([]byte, 4096)

	for {
		// 返回3个值,分别是 读取到的字节数, 客户端的地址, error
		n, cltAddr, err := udpConn.ReadFromUDP(buf) // --- 主go程读取客户端发送数据
		if err != nil {
			fmt.Println("ReadFromUDP err:", err)
			return
		}
		// 模拟处理数据
		fmt.Printf("服务器读到 %v 的数据:%s\n", cltAddr, string(buf[:n]))

		go func() { // 每有一个客户端连接上来,启动一个go程写数据
			// 提取系统当前时间
			daytime := time.Now().String() + "\n"

			// 回写数据给客户端
			_, err = udpConn.WriteToUDP([]byte(daytime), cltAddr)
			if err != nil {
				fmt.Println("WriteToUDP err:", err)
				return
			}
		}()
	}
}
  • 模拟UDP客户端并发
package main

import (
	"fmt"
	"net"
)

func main() {
	// 指定 服务器 IP + port 创建 通信套接字。
	conn, err := net.Dial("udp", "127.0.0.1:8006")
	if err != nil {
		fmt.Println("net.Dial err:", err)
		return
	}
	defer conn.Close()

	for i := 0; i < 1000000; i++ {
		// 主动写数据给服务器
		conn.Write([]byte("Are you Ready?"))

		buf := make([]byte, 4096)
		// 接收服务器回发的数据
		n, err := conn.Read(buf)
		if err != nil {
			fmt.Println("conn.Read err:", err)
			return
		}
		fmt.Println("服务器回发:", string(buf[:n]))
	}
}

4 - TCP和UDP的区别

TCPUDP
面向连接面向无连接
要求系统资源较多要求系统资源较少
TCP程序结构较复杂UDP程序结构较简单
使用流式使用数据包式
保证数据准确性不保证数据准确性
保证数据顺序不保证数据顺序
通讯速度较慢通讯速度较快
  • TCP使用场景:对数据传输安全性、稳定性要求较高的场合,如网络文件传输、下载、上传
  • UDP使用场景:对数据实时传输要求较高的场合,如视频直播、在线电话会议、游戏

三、案例:文件传输

1 - 命令行参数获取

  • 命令行参数:在main函数启动时,向整个程序传参
  • 语法go run xxx.go argv1 argv2 argv3 argv4
    • xxx.go: 第0个参数(可执行文件的全路径)
    • argv1 :第1个参数
    • argv2 :第2个参数
    • argv3 :第3个参数
    • argv4 :第3个参数
  • 命令行参数获取list := os.Args 参数3 = list[3]
package main

import (
	"fmt"
	"os"
)

func main() {
	list := os.Args // 获取命令行参数
	fmt.Println(list)
}

在这里插入图片描述

2 - 文件属性获取

  • 获取文件属性:在函数返回的文件属性中包含文件名和文件大小。Stat参数name传入的是文件访问的绝对路径func Stat(name string) (FileInfo, error)
  • FileInfo原型
    • Name():获取文件名
    • Size():获取文件大小
type FileInfo interface {
   Name() string
   Size() int64
   Mode() FileMode
   ModTime() time.Time
   IsDir() bool
   Sys() interface{}
}

3 - 文件传输:发送端

  • 文件传输——发送端(客户端)流程
    • ①.提示用户使用命令行参数输入文件名。接收文件名 filepath(含访问路径)
    • ②.使用 os.Stat()获取文件属性,得到纯文件名 fileName(去除访问路径)
    • ③.主动发起连接服务器请求,结束时关闭连接
    • ④.发送文件名到接收端 conn.Write()
    • ⑤.读取接收端回发的确认数据 conn.Read()
    • ⑥.判断是否为“ok”。如果是,封装函数 SendFile() 发送文件内容。传参 filePath 和 conn
    • ⑦.只读 Open 文件, 结束时Close文件
    • ⑧.循环读本地文件,读到 EOF,读取完毕
    • ⑨.将读到的内容原封不动 conn.Write 给接收端(服务器)
package main

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

func sendFile(conn net.Conn, filePath string) {
	// 只读打开文件
	f, err := os.Open(filePath)
	if err != nil {
		fmt.Println("os.Open err:", err)
		return
	}
	defer f.Close()

	// 从本文件中,读数据,写给网络接收端。 读多少,写多少。原封不动。
	buf := make([]byte, 4096)
	for {
		n, err := f.Read(buf)
		if err != nil {
			if err == io.EOF {
				fmt.Println("发送文件完成。")
			} else {
				fmt.Println("os.Open err:", err)
			}
			return
		}
		// 写到网络socket中
		_, err = conn.Write(buf[:n])
		if err != nil {
			fmt.Println("conn.Write err:", err)
			return
		}
	}
}

func main() {
	list := os.Args // 获取命令行参数

	if len(list) != 2 {
		fmt.Println("格式为:go run xxx.go 文件绝对路径")
		return
	}
	// 提取文件的绝对路径
	filePath := list[1]

	//提取文件名
	fileInfo, err := os.Stat(filePath)
	if err != nil {
		fmt.Println("os.Stat err:", err)
		return
	}
	fileName := fileInfo.Name()

	// 主动发起连接请求
	conn, err := net.Dial("tcp", "127.0.0.1:8008")
	if err != nil {
		fmt.Println("net.Dial err:", err)
		return
	}
	defer conn.Close()

	// 发送文件名给 接收端
	_, err = conn.Write([]byte(fileName))
	if err != nil {
		fmt.Println("conn.Write err:", err)
		return
	}
	// 读取服务器回发的 OK
	buf := make([]byte, 16)
	n, err := conn.Read(buf)
	if err != nil {
		fmt.Println("conn.Read err:", err)
		return
	}

	if "ok" == string(buf[:n]) {
		// 写文件内容给服务器——借助conn
		sendFile(conn, filePath)
	}
}

4 - 文件传输:接收端

  • 文件传输——接收端(服务器)流程
    • ①.创建监听 listener,程序结束时关闭
    • ②.阻塞等待客户端连接 conn,程序结束时关闭conn
    • ③.读取客户端发送文件名。保存 fileName
    • ④.回发“ok”
    • ⑤.封装函数 RecvFile 接收客户端发送的文件内容。传参 fileName 和 conn
    • ⑥.按文件名 Create 文件,结束时 Close
    • ⑦.循环 Read 发送端网络文件内容,当读到 0 说明文件读取完毕
    • ⑧.将读到的内容原封不动Write到创建的文件中
package main

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

func recvFile(conn net.Conn, fileName string) {
	// 按照文件名创建新文件
	f, err := os.Create(fileName)
	if err != nil {
		fmt.Println("os.Create err:", err)
		return
	}
	defer f.Close()

	// 从网络中读数据,写入本地文件
	buf := make([]byte, 4096)
	for {
		n, _ := conn.Read(buf)
		if n == 0 {
			fmt.Println("接收文件完成。")
			return
		}
		// 写入本地文件,读多少,写多少。
		f.Write(buf[:n])
	}
}

func main() {
	// 创建用于监听的socket
	listener, err := net.Listen("tcp", "127.0.0.1:8008")
	if err != nil {
		fmt.Println(" net.Listen 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, 4096)
	n, err := conn.Read(buf)
	if err != nil {
		fmt.Println(" conn.Read err:", err)
		return
	}
	fileName := string(buf[:n])

	// 回写ok给发送端
	conn.Write([]byte("ok"))

	// 获取文件内容
	recvFile(conn, fileName)
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

无休止符

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值