TCP通信过程

下图是一次TCP通讯的时序图。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协议自动将发送缓冲区中的数据包重发。

总结:

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个段,服务器的应答和关闭连接请求通常不合并在一个段中,因为有连接半关闭的情况,这种情况下客户端关闭连接之后就不能再发送数据给服务器了,但是服务器还可以发送数据给客户端,直到服务器也关闭连接为止。

总结:

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 )

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程序结构较简单

使用流式

使用数据包式

保证数据准确性

不保证数据准确性

保证数据顺序

不保证数据顺序

通讯速度较慢

通讯速度较快

文件传输

网络文件传输:思路

发送端:(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() }

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

最新文章

  1. 爬虫初探(1)之urllib.request
  2. mongodb(分片)
  3. BZOJ 4325: NOIP2015 斗地主
  4. UVA 11865 Stream My Contest 组网 (朱刘算法,有向生成树,树形图)
  5. Android ViewPager的简单实现
  6. Ubuntu下的网络配置(USTC)
  7. [置顶] 《Windows编程零基础》__2 一个完整的程序
  8. DropdownListFor无法正确绑定值-同名问题
  9. Nginx启动报错:10013: An attempt was made to access a socket in a way forbidden
  10. 更具体的描述JNI
  11. Linux文件系统简介及常用命令
  12. 大数据学习(2)HDFS文件管理
  13. Uva - 816 - Abbott's Revenge
  14. technologies
  15. Python内置函数(59)——sorted
  16. Nmap 命令操作详解
  17. Baby Coins
  18. (常用)configparser,hashlib,hamc模块
  19. Linux桌面环境安装matlab并创建快捷方式
  20. Linux的简单介绍.

热门文章

  1. FileFilter过滤器
  2. vivo Xplay 5的Usb调试模式在哪里,打开vivo Xplay 5Usb调试模式的经验
  3. 3、SpringBoot 集成Storm wordcount
  4. 常用VIM插件配置
  5. CentOS中使用tcpdump抓包
  6. 我的第一篇博客。(JavaScript的声明和数据类型的一些笔记)
  7. Unable to start Ocelot because either a ReRoute or GlobalConfiguration
  8. docker容器的时间同步
  9. 【GO】【环境配置】
  10. Linux 搭建批量网络装机