TCP通信过程
下图是一次TCP通讯的时序图。TCP连接建立断开。包含大家熟知的三次握手和四次握手。
在这个例子中,首先客户端主动发起连接、发送请求,然后服务器端响应请求,然后客户端主动关闭连接。两条竖线表示通讯的两端,从上到下表示时间的先后顺序。注意,数据从一端传到网络的另一端也需要时间,所以图中的箭头都是斜的。
三次握手 建立连接
建立连接(三次握手)的过程:
- 客户端发送一个带SYN标志的TCP报文到服务器。这是上图中三次握手过程中的段1。客户端发出SYN位表示连接请求。序号是1000,这个序号在网络通讯中用作临时的地址,每发一个数据字节,这个序号要加1,这样在接收端可以根据序号排出数据包的正确顺序,也可以发现丢包的情况。
另外,规定SYN位和FIN位也要占一个序号,这次虽然没发数据,但是由于发了SYN位,因此下次再发送应该用序号1001。
mss表示最大段尺寸,如果一个段太大,封装成帧后超过了链路层的最大长度,就必须在IP层分片,为了避免这种情况,客户端声明自己的最大段尺寸,建议服务器端发来的段不要超过这个长度。
- 服务器端回应客户端,是三次握手中的第2个报文段,同时带ACK标志和SYN标志。表示对刚才客户端SYN的回应;同时又发送SYN给客户端,询问客户端是否准备好进行数据通讯。
服务器发出段2,也带有SYN位,同时置ACK位表示确认,确认序号是1001,表示“我接收到序号1000及其以前所有的段,请你下次发送序号为1001的段”,也就是应答了客户端的连接请求,同时也给客户端发出一个连接请求,同时声明最大尺寸为1024。
- 客户必须再次回应服务器端一个ACK报文,这是报文段3。
客户端发出段3,对服务器的连接请求进行应答,确认序号是8001。在这个过程中,客户端和服务器分别给对方发了连接请求,也应答了对方的连接请求,其中服务器的请求和应答在一个段中发出。
因此一共有三个段用于建立连接,称为“三方握手”。在建立连接的同时,双方协商了一些信息,例如,双方发送序号的初始值、最大段尺寸等。
数据传输的过程:
- 客户端发出段4,包含从序号1001开始的20个字节数据。
- 服务器发出段5,确认序号为1021,对序号为1001-1020的数据表示确认收到,同时请求发送序号1021开始的数据,服务器在应答的同时也向客户端发送从序号8001开始的10个字节数据。
- 客户端发出段6,对服务器发来的序号为8001-8010的数据表示确认收到,请求发送序号8011开始的数据。
在数据传输过程中,ACK和确认序号是非常重要的,应用程序交给TCP协议发送的数据会暂存在TCP层的发送缓冲区中,发出数据包给对方之后,只有收到对方应答的ACK段才知道该数据包确实发到了对方,可以从发送缓冲区中释放掉了,如果因为网络故障丢失了数据包或者丢失了对方发回的ACK段,经过等待超时后TCP协议自动将发送缓冲区中的数据包重发。
总结:
3次握手:
1、主动: 发送 SYN 标志位。
2、被动:接收 SYN、同时回复 ACK 并且发送SYN
3、主动: 发送 ACK 标志位。 ―――――― Accpet() / Dial()
四次挥手
关闭连接(四次握手)的过程:
由于TCP连接是全双工的,因此每个方向都必须单独进行关闭。这原则是当一方完成它的数据发送任务后就能发送一个FIN来终止这个方向的连接。收到一个 FIN只意味着这一方向上没有数据流动,一个TCP连接在收到一个FIN后仍能发送数据。首先进行关闭的一方将执行主动关闭,而另一方执行被动关闭。
- 客户端发出段7,FIN位表示关闭连接的请求。
- 服务器发出段8,应答客户端的关闭连接请求。
- 服务器发出段9,其中也包含FIN位,向客户端发送关闭连接请求。
- 客户端发出段10,应答服务器的关闭连接请求。
建立连接的过程是三次握手,而关闭连接通常需要4个段,服务器的应答和关闭连接请求通常不合并在一个段中,因为有连接半关闭的情况,这种情况下客户端关闭连接之后就不能再发送数据给服务器了,但是服务器还可以发送数据给客户端,直到服务器也关闭连接为止。
总结:
4次挥手:
1、主动关闭连接:发送 FIN 标志位。
2、被动关闭连接:接收 FIN、同时回复 ACK ―― 半关闭完成。
3、被动关闭连接:发送 FIN 标志位。
4、主动关闭连接:接收 FIN、同时回复 ACK ―― Close()/Close() ―― 4次挥手完成。
TCP状态转换
TCP状态图很多人都知道,它对排除和定位网络或系统故障时大有帮助。如果能熟练掌握这张图,了解图中的每一个状态,能大大提高我们对于TCP的理解和认识。下面对这张图的11种状态详细解析一下,以便加强记忆!不过在这之前,一定要熟练掌握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 )
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
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, 1024) 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)
代码如下:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
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, 1024) 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 无效。
服务器:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
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, 1024) 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)
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
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, 1024) 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, 1024) 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程序结构较简单 |
使用流式 |
使用数据包式 |
保证数据准确性 |
不保证数据准确性 |
保证数据顺序 |
不保证数据顺序 |
通讯速度较慢 |
通讯速度较快 |
文件传输
网络文件传输:思路
发送端:(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{}
}
获取文件属性示例:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
package main import ( "os" "fmt" ) func main() { list := os.Args // 获取命令行参数,存入list中 if len(list) != 2 { // 确保用户输入了一个命令行参数 fmt.Println("格式为:xxx.go 文件名") return } fileName := list[1] // 从命令行保存文件名(含路径) 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()) // 得到文件大小。单位字节 }
客户端实现:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
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, 4096) 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, 1024) 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 } }
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
package main import ( "net" "fmt" "os" "io" ) func filesend(filepath string,conn net.Conn){ buf:=make([]byte,4096) f1,err:=os.OpenFile(filepath,os.O_RDONLY,0666) 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[1] fileinfo,err:=os.Stat(filepath) if err!=nil{ fmt.Println("stat err",err) return } str:=fileinfo.Name() //fmt.Println(str) buf:=make([]byte,4096) 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) } }
服务端实现:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
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, 4096) 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, 1024) 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 }
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
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, 4096) 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,4096) 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() }
提取文件 不带路径的“文件名”