TCP协议,UDP,以及TCP通信服务器的文件传输

时间:2023-03-10 00:34:13
TCP协议,UDP,以及TCP通信服务器的文件传输

TCP通信过程

下图是一次TCP通讯的时序图。TCP连接建立断开。包含大家熟知的三次握手和四次握手。

TCP协议,UDP,以及TCP通信服务器的文件传输

在这个例子中,首先客户端主动发起连接、发送请求,然后服务器端响应请求,然后客户端主动关闭连接。两条竖线表示通讯的两端,从上到下表示时间的先后顺序。注意,数据从一端传到网络的另一端也需要时间,所以图中的箭头都是斜的。

三次握手 建立连接

建立连接(三次握手)的过程:

  1. 客户端发送一个带SYN标志的TCP报文到服务器。这是上图中三次握手过程中的段1。客户端发出SYN位表示连接请求。序号是1000,这个序号在网络通讯中用作临时的地址,每发一个数据字节,这个序号要加1,这样在接收端可以根据序号排出数据包的正确顺序,也可以发现丢包的情况。

另外,规定SYN位和FIN位也要占一个序号,这次虽然没发数据,但是由于发了SYN位,因此下次再发送应该用序号1001。

mss表示最大段尺寸,如果一个段太大,封装成帧后超过了链路层的最大长度,就必须在IP层分片,为了避免这种情况,客户端声明自己的最大段尺寸,建议服务器端发来的段不要超过这个长度。

  1. 服务器端回应客户端,是三次握手中的第2个报文段,同时带ACK标志和SYN标志。表示对刚才客户端SYN的回应;同时又发送SYN给客户端,询问客户端是否准备好进行数据通讯。

服务器发出段2,也带有SYN位,同时置ACK位表示确认,确认序号是1001,表示“我接收到序号1000及其以前所有的段,请你下次发送序号为1001的段”,也就是应答了客户端的连接请求,同时也给客户端发出一个连接请求,同时声明最大尺寸为1024。

  1. 客户必须再次回应服务器端一个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协议自动将发送缓冲区中的数据包重发。

TCP协议,UDP,以及TCP通信服务器的文件传输

总结:

3次握手:
1、主动: 发送 SYN 标志位。

2、被动:接收 SYN、同时回复 ACK 并且发送SYN

3、主动: 发送 ACK 标志位。 ―――――― Accpet() / Dial()

四次挥手

关闭连接(四次握手)的过程:

由于TCP连接是全双工的,因此每个方向都必须单独进行关闭。这原则是当一方完成它的数据发送任务后就能发送一个FIN来终止这个方向的连接。收到一个 FIN只意味着这一方向上没有数据流动,一个TCP连接在收到一个FIN后仍能发送数据。首先进行关闭的一方将执行主动关闭,而另一方执行被动关闭。

  1. 客户端发出段7,FIN位表示关闭连接的请求。
  2. 服务器发出段8,应答客户端的关闭连接请求。
  3. 服务器发出段9,其中也包含FIN位,向客户端发送关闭连接请求。
  4. 客户端发出段10,应答服务器的关闭连接请求。

建立连接的过程是三次握手,而关闭连接通常需要4个段,服务器的应答和关闭连接请求通常不合并在一个段中,因为有连接半关闭的情况,这种情况下客户端关闭连接之后就不能再发送数据给服务器了,但是服务器还可以发送数据给客户端,直到服务器也关闭连接为止。

TCP协议,UDP,以及TCP通信服务器的文件传输

总结:

4次挥手:
1、主动关闭连接:发送 FIN 标志位。

2、被动关闭连接:接收 FIN、同时回复 ACK ―― 半关闭完成。

3、被动关闭连接:发送 FIN 标志位。

4、主动关闭连接:接收 FIN、同时回复 ACK ―― Close()/Close() ―― 4次挥手完成。

TCP状态转换

TCP状态图很多人都知道,它对排除和定位网络或系统故障时大有帮助。如果能熟练掌握这张图,了解图中的每一个状态,能大大提高我们对于TCP的理解和认识。下面对这张图的11种状态详细解析一下,以便加强记忆!不过在这之前,一定要熟练掌握TCP建立连接的三次握手过程,以及关闭连接的四次挥手过程。

TCP协议,UDP,以及TCP通信服务器的文件传输

CLOSED:表示初始状态。

LISTEN:该状态表示服务器端的某个SOCKET处于监听状态,可以接受连接。

SYN_SENT:这个状态与SYN_RCVD遥相呼应,当客户端SOCKET执行CONNECT连接时,它首先发送SYN报文,随即进入到了SYN_SENT状态,并等待服务端的发送三次握手中的第2个报文。SYN_SENT状态表示客户端已发送SYN报文。

SYN_RCVD: 该状态表示接收到SYN报文,在正常情况下,这个状态是服务器端的SOCKET在建立TCP连接时的三次握手会话过程中的一个中间状态,很短暂。此种状态时,当收到客户端的ACK报文后,会进入到ESTABLISHED状态。

ESTABLISHED:表示连接已经建立。

FIN_WAIT_1:  FIN_WAIT_1和FIN_WAIT_2状态的真正含义都是表示等待对方的FIN报文。区别是:

FIN_WAIT_1状态是当socket在ESTABLISHED状态时,想主动关闭连接,向对方发送了FIN报文,此时该socket进入到FIN_WAIT_1状态。

FIN_WAIT_2状态是当对方回应ACK后,该socket进入到FIN_WAIT_2状态,正常情况下,对方应马上回应ACK报文,所以FIN_WAIT_1状态一般较难见到,而FIN_WAIT_2状态可用netstat看到。

FIN_WAIT_2:主动关闭链接的一方,发出FIN收到ACK以后进入该状态。称之为半连接或半关闭状态。该状态下的socket只能接收数据,不能发。

TIME_WAIT: 表示收到了对方的FIN报文,并发送出了ACK报文,等2MSL后即可回到CLOSED可用状态。如果FIN_WAIT_1状态下,收到对方同时带 FIN标志和ACK标志的报文时,可以直接进入到TIME_WAIT状态,而无须经过FIN_WAIT_2状态。

CLOSING: 这种状态较特殊,属于一种较罕见的状态。正常情况下,当你发送FIN报文后,按理来说是应该先收到(或同时收到)对方的 ACK报文,再收到对方的FIN报文。但是CLOSING状态表示你发送FIN报文后,并没有收到对方的ACK报文,反而却也收到了对方的FIN报文。什么情况下会出现此种情况呢?如果双方几乎在同时close一个SOCKET的话,那么就出现了双方同时发送FIN报文的情况,也即会出现CLOSING状态,表示双方都正在关闭SOCKET连接。

CLOSE_WAIT: 此种状态表示在等待关闭。当对方关闭一个SOCKET后发送FIN报文给自己,系统会回应一个ACK报文给对方,此时则进入到CLOSE_WAIT状态。接下来呢,察看是否还有数据发送给对方,如果没有可以 close这个SOCKET,发送FIN报文给对方,即关闭连接。所以在CLOSE_WAIT状态下,需要关闭连接。

LAST_ACK: 该状态是被动关闭一方在发送FIN报文后,最后等待对方的ACK报文。当收到ACK报文后,即可以进入到CLOSED可用状态。

2MSL (Maximum Segment Lifetime) 和与之对应的TIME_WAIT状态,可以让4次握手关闭流程更加可靠。4次握手的最后一个ACK是是由主动关闭方发送出去的,若这个ACK丢失,被动关闭方会再次发一个FIN过来。若主动关闭方能够保持一个2MSL的TIME_WAIT状态,则有更大的机会让丢失的ACK被再次发送出去。注意,TIME_WAIT状态一定出现在主动关闭这一方

总结:

TCP状态转换:

1. 主动端:

CLOSE --> SYN --> SYN_SEND状态 --> ESTABLISHED状态(数据通信期间处于的状态) ---> FIN --> FIN_WAIT_1状态。

---> 接收 ACK ---> FIN_WAIT_2状态 (半关闭―― 只出现在主动端) ---> 接收FIN、回ACK ――> TIME_WAIT (等2MSL)

---> 确保最后一个ACK能被对端收到。(只出现在主动端)
2. 被动端:

CLOSE --> LISTEN ---> ESTABLISHED状态(数据通信期间处于的状态) ---> 接收 FIN、回复ACK -->

CLOSE_WAIT(对应 对端处于 半关闭) --> 发送FIN --> LAST_ACK ---> 接收ACK ---> CLOSE

查看状态命令:

windows:netstat -an | findstr 8001(端口号)

Linux: netstat -an | grep 8001

UDP通信

UDP服务器

由于UDP是“无连接”的,所以,服务器端不需要额外创建监听套接字,只需要指定好IP和port,然后监听该地址,等待客户端与之建立连接,即可通信。

创建监听地址:
func ResolveUDPAddr(network, address string) (*UDPAddr, error)
创建用户通信的socket:
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简单服务器:

1. 获取 服务器的 UDP地址结构体 srvAddr := ResolveUDPAddr(“udp”,“IP+port”)

2. 创建 用于数据通信套接字。 conn := ListenUDP("udp", srvAddr )

3. 读取客户端发送数据。 n, cltAddr, err := conn.ReadFromUDP(buf)

4. 回写数据给客户端。 conn.WriteToUDP("数据内容", cltAddr )

package main

import (
"fmt"
"net"
) func main() {
//创建监听的地址,并且指定udp协议
udp_addr, err := net.ResolveUDPAddr("udp", "127.0.0.1:8002")
if err != nil {
fmt.Println("ResolveUDPAddr err:", err)
return
}
conn, err := net.ListenUDP("udp", udp_addr) //创建数据通信socket
if err != nil {
fmt.Println("ListenUDP err:", err)
return
}
defer conn.Close() buf := make([]byte, )
n, raddr, err := conn.ReadFromUDP(buf) //接收客户端发送过来的数据,填充到切片buf中。
if err != nil {
return
}
fmt.Println("客户端发送:", string(buf[:n])) _, err = conn.WriteToUDP([]byte("nice to see u in udp"), raddr) // 向客户端发送数据
if err != nil {
fmt.Println("WriteToUDP err:", err)
return
}
}

UDP客户端

udp客户端的编写与TCP客户端的编写,基本上是一样的,只是将协议换成udp。注意只能使用小写。

UDP客户端:

与TCP通信客户端实现手法一致。

net.Dial("udp", server 的IP+port)

代码如下:

package main

import (
"net"
"fmt"
) func main() {
conn, err := net.Dial("udp", "127.0.0.1:8002")
if err != nil {
fmt.Println("net.Dial err:", err)
return
}
defer conn.Close() conn.Write([]byte("Hello! I'm client in UDP!")) buf := make([]byte, )
n, err1 := conn.Read(buf)
if err1 != nil {
return
}
fmt.Println("服务器发来:", string(buf[:n]))
}

并发

其实对于UDP而言,服务器不需要并发,只要循环处理客户端数据即可。客户端也等同于TCP通信并发的客户端。

UDP并发服务器: ―――― UDP 默认支持并发。

1. 获取 服务器的 UDP地址结构体 srvAddr := ResolveUDPAddr(“udp”,“IP+port”)

2. 创建 用于数据通信套接字。 conn := ListenUDP("udp", srvAddr )

3. for 循环 读取客户端发送的数据 for {
n, cltAddr, err := conn.ReadFromUDP(buf)
}

4. 创建 go 程 完成 写操作,提高程序的并行效率。

go func() {
conn.WriteToUDP("数据内容", cltAddr )
}()

5.由于UDP没有建立连接过程。所以 TCP 通信状态 对于 UDP 无效。

服务器:

package main

import (
"net"
"fmt"
) func main() {
// 创建 服务器 UDP 地址结构。指定 IP + port
laddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:8003")
if err != nil {
fmt.Println("ResolveUDPAddr err:", err)
return
}
// 监听 客户端连接
conn, err := net.ListenUDP("udp", laddr)
if err != nil {
fmt.Println("net.ListenUDP err:", err)
return
}
defer conn.Close() for {
buf := make([]byte, )
n, raddr, err := conn.ReadFromUDP(buf)
if err != nil {
fmt.Println("conn.ReadFromUDP err:", err)
return
}
fmt.Printf("接收到客户端[%s]:%s", raddr, string(buf[:n])) conn.WriteToUDP([]byte("I-AM-SERVER"), raddr) // 简单回写数据给客户端
}
}

客户端:

UDP并发客户端:

并发读取 键盘 和 conn。 编码实现参考 TCP 并发客户端实现。

修改内容: net.Dial("udp", server 的IP+port)

package main

import (
"net"
"os"
"fmt"
) func main() {
conn, err := net.Dial("udp", "127.0.0.1:8003")
if err != nil {
fmt.Println("net.Dial err:", err)
return
}
defer conn.Close()
go func() {
str := make([]byte, )
for {
n, err := os.Stdin.Read(str) //从键盘读取内容, 放在str
if err != nil {
fmt.Println("os.Stdin. err1 = ", err)
return
}
conn.Write(str[:n]) // 给服务器发送
}
}()
buf := make([]byte, )
for {
n, err := conn.Read(buf)
if err != nil {
fmt.Println("conn.Read err:", err)
return
}
fmt.Println("服务器写来:", string(buf[:n]))
}
}

                                                        UDP与TCP的差异

TCP

UDP

面向连接

面向无连接

要求系统资源较多

要求系统资源较少

TCP程序结构较复杂

UDP程序结构较简单

使用流式

使用数据包式

保证数据准确性

不保证数据准确性

保证数据顺序

不保证数据顺序

通讯速度较慢

通讯速度较快

文件传输

TCP协议,UDP,以及TCP通信服务器的文件传输

网络文件传输:思路

发送端:(client)

1. 建立连接请求 net.Dial() ――> conn defer conn.Close()

2. 通过命令行参数,提取 文件名(带路径) os.Args

3. 获取文件属性 ,提取 文件名(不带路径)os.Stat()

4. 发送文件名 给 接收端 conn.Write

5. 接收对端回发的数据,确认是否是“ok”

6. 发送文件内容 给 接收端。封装 sendFile(文件名, conn) 函数

1) 只读方式打开 待发送文件

2) 创建 buf 读文件,存入buf中

3) 借助 conn 写 buf中的 数据到 接收端 ―― 读多少、写多少。

4) 判断文件读取、发送完毕。结束 conn 。断开连接。

接收端:(sever)

1. 创建监听套接字 listener := net.Listen()

2. 阻塞等待客户端连接请求。 conn = listener.Accept()

3. 读取发送端发送的文件名(不含路径)-- 保存

4. 回复“ok”给发送端。

5. 接收文件内容,保存成一个新文件。封装 RecvFile (文件名, conn) 函数

1) os.Create() 按文件名创建文件。 -- f

2) 从 conn 中读取文件内容。

3) 使用 f 写到本地新建文件中。 ―― 读多少、写多少

4) 判断文件读取完毕。结束 conn 。断开连接。

首先获取文件名。借助os包中的stat()函数来获取文件属性信息。在函数返回的文件属性中包含文件名和文件大小。Stat参数name传入的是文件访问的绝对路径。FileInfo中的Name()函数可以将文件名单独提取出来。

func Stat(name string) (FileInfo, error)

type FileInfo interface {
   Name() string

   Size() int64

   Mode() FileMode     
   ModTime() time.Time
   IsDir() bool        
   Sys() interface{}   
}

获取文件属性示例:

package main

import (
"os"
"fmt"
) func main() {
list := os.Args // 获取命令行参数,存入list中
if len(list) != { // 确保用户输入了一个命令行参数
fmt.Println("格式为:xxx.go 文件名")
return
}
fileName := list[] // 从命令行保存文件名(含路径) fileInfo, err := os.Stat(fileName) //根据文件名获取文件属性信息 fileInfo
if err != nil {
fmt.Println("os.Stat err:", err)
return
}
fmt.Println("文件name为:", fileInfo.Name()) // 得到文件名(不含路径)
fmt.Println("文件size为:", fileInfo.Size()) // 得到文件大小。单位字节
}

客户端实现:

package main

import (
"fmt"
"os"
"net"
"io"
) func SendFile(path string, conn net.Conn) {
// 以只读方式打开文件
f, err := os.Open(path)
if err != nil {
fmt.Println("os.Open err:", err)
return
}
defer f.Close() // 发送结束关闭文件。 // 循环读取文件,原封不动的写给服务器
buf := make([]byte, )
for {
n, err := f.Read(buf) // 读取文件内容到切片缓冲中
if err != nil {
if err == io.EOF {
fmt.Println("文件发送完毕")
} else {
fmt.Println("f.Read err:", err)
}
return
}
conn.Write(buf[:n]) // 原封不动写给服务器
}
} func main() {
// 提示输入文件名
fmt.Println("请输入需要传输的文件:")
var path string
fmt.Scan(&path) // 获取文件名 fileInfo.Name()
fileInfo, err := os.Stat(path)
if err != nil {
fmt.Println("os.Stat err:", err)
return
} // 主动连接服务器
conn, err := net.Dial("tcp", "127.0.0.1:8005")
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, )
n, err := conn.Read(buf)
if err != nil {
fmt.Println("conn.Read err:", err)
return
} // 判断如果是ok,则发送文件内容
if "ok" == string(buf[:n]) {
SendFile(path, conn) // 封装函数读文件,发送给服务器,需要path、conn
}
}

客户端

package main
import (
"net"
"fmt"
"os"
"io"
)
func filesend(filepath string,conn net.Conn){
buf:=make([]byte,)
f1,err:=os.OpenFile(filepath,os.O_RDONLY,)
if err!=nil{
fmt.Println("打开文件错误",err)
return
}
defer f1.Close()
for {
n, err := f1.Read(buf)
if err != nil {
if err ==io.EOF{
fmt.Println("读取完毕")
break
}else{
fmt.Println("read err", err)
return
}
}
_, err = conn.Write(buf[:n])
if err != nil {
if err==io.EOF{
fmt.Println("文件发送完毕")
break
}
fmt.Println("发送err", err)
return
}
}
}
func main() {
list:=os.Args
filepath:=list[]
fileinfo,err:=os.Stat(filepath)
if err!=nil{
fmt.Println("stat err",err)
return
}
str:=fileinfo.Name()
//fmt.Println(str)
buf:=make([]byte,)
conn,err:=net.Dial("tcp","127.0.0.1:8000")
if err!=nil{
fmt.Println("conn err",err)
return
}
defer conn.Close()
n,err:=conn.Write([]byte(str))
if err!=nil{
fmt.Println("write err",err)
return
}
fmt.Printf("发送的文件名%q",string(buf[:n]))
//buf2:=make([]byte,4096)
n,err=conn.Read(buf)
if err!=nil{
fmt.Println("服务器发来错误",err)
return
}
if string(buf[:n])=="ok"{
fmt.Println("服务器接收成功")
filesend(filepath,conn)
}
}

自己的思路

服务端实现:

package main

import (
"net"
"fmt"
"os"
"io"
) func RecvFile(fileName string, conn net.Conn) {
// 创建新文件
f, err := os.Create(fileName)
if err != nil {
fmt.Println("Create err:", err)
return
}
defer f.Close() // 接收客户端发送文件内容,原封不动写入文件
buf := make([]byte, )
for {
n, err := conn.Read(buf)
if err != nil {
if err == io.EOF {
fmt.Println("文件接收完毕")
} else {
fmt.Println("Read err:", err)
}
return
}
f.Write(buf[:n]) // 写入文件,读多少写多少
}
} func main() {
// 创建监听
listener, err := net.Listen("tcp", "127.0.0.1:8005")
if err != nil {
fmt.Println("Listen err:", err)
return
}
defer listener.Close() // 阻塞等待客户端连接
conn, err := listener.Accept()
if err != nil {
fmt.Println("Accept err:", err)
return
}
defer conn.Close() // 读取客户端发送的文件名
buf := make([]byte, )
n, err := conn.Read(buf)
if err != nil {
fmt.Println("Read err:", err)
return
}
fileName := string(buf[:n]) // 保存文件名 // 回复 0k 给发送端
conn.Write([]byte("ok")) // 接收文件内容
RecvFile(fileName, conn) // 封装函数接收文件内容, 传fileName 和 conn
}

服务端

package main
import (
"net"
"fmt"
"os"
"io"
)
func main() {
listener, err := net.Listen("tcp", "127.0.0.1:8000")
if err != nil {
fmt.Println("listener err", err)
return
}
defer listener.Close()
conn, err := listener.Accept()
if err != nil {
fmt.Println("conn err", err)
return
}
defer conn.Close()
buf := make([]byte, )
n, err := conn.Read(buf)
if err != nil {
fmt.Println("read err", )
return
}
pathname := string(buf[:n])
fmt.Println(pathname)
_, err = conn.Write([]byte("ok"))
if err != nil {
fmt.Println("write err", err)
return
}
recvfile(pathname,conn) }
func recvfile(pathname string,conn net.Conn){
str:="D:/1/"+pathname
fmt.Println(str)
f1,err:=os.Create(str)
if err!=nil{
fmt.Println("create err",err)
return
}
defer f1.Close()
buf:=make([]byte,)
for {
n,err:=conn.Read(buf)
if err!=nil{
if err==io.EOF{
fmt.Println("文件接收完毕")
break
}
fmt.Println("conn read err",err)
break
}
f1.Write(buf[:n])
} }

自己的思路

小知识

获取命令行参数:

os.Args 提取命令行参数,保存成 []string

使用格式: go run xxx.go arg1 arg2 arg3 arg4 ...

获取命令行参数:

arg[0]: xxx.go ――> xxx.exe 的绝对路径

arg[1]: arg1
arg[2]: arg2
arg[3]: arg3
....
获取文件属性:

os.Stat(文件访问绝对路径) ――> fileInfo interface { Name() Size() }

提取文件 不带路径的“文件名”