package main
import (
"fmt"
"net"
)
func main() {
listener, err := net.Listen("tcp", "127.0.0.1:8080")
if err != nil {
fmt.Printf("listen failed err: %v", err)
return
}
var a int
// 阻塞在这里
fmt.Scanf("%d", &a)
fmt.Println("start accept")
for {
conn, err := listener.Accept()
if err != nil {
fmt.Printf("accept failed err: %v", err)
break
}
go HandleConn(conn)
}
}
func HandleConn(conn net.Conn) {
defer conn.Close()
buff := make([]byte, 1024)
for {
n, err := conn.Read(buff)
if err != nil {
fmt.Printf("read failed conn:%v err:%v\n", conn, err)
break
}
msg := string(buff[0:n])
fmt.Println(msg)
}
}
内核缓冲区的变化
上面的代码listen了一个端口之后,并没有去accept,因为被scanf阻塞住了。我们在linux下面跑上面代码之后,用netstat -antp 查看下网络状态:
tcp 0 0 127.0.0.1:8080 0.0.0.0:* LISTEN 36535/server
然后我们用tcpdump抓个包,使用nc连接下服务(也可使用telnet),命令分别如下:
tcpdump -nn -i lo port 8080
nc 127.0.0.1 8080
此时看到tcpdump抓的包为:
00:22:53.528171 IP 127.0.0.1.47150 > 127.0.0.1.8080: Flags [S], seq 4152235494, win 43690, options [mss 65495,sackOK,TS val 44901801 ecr 0,nop,wscale 7], length 0
09:56:09.032928 IP 127.0.0.1.8080 > 127.0.0.1.47150: Flags [S.], seq 3160960910, ack 4152235495, win 43690, options [mss 65495,sackOK,TS val 44901801 ecr 44901801,nop,wscale 7], length 0
00:22:53.528194 IP 127.0.0.1.47150 > 127.0.0.1.8080: Flags [.], ack 1, win 342, options [nop,nop,TS val 44901801 ecr 44901801], length 0
表示三次握手成功,使用netstat -antp查看网络状态结果为:
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 127.0.0.1:8080 127.0.0.1:47150 ESTABLISHED -
我们可以看到虽然我们的程序并没有accept,但是连接已经在内核里面建立好,但是连接的资源还没交给进程(PID为空)。
我们随便传输几个字符(例如:123456),发现网络状态变为:
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program namel
tcp 7 0 127.0.0.1:8080 127.0.0.1:47151 ESTABLISHED -
发现Recv-Q为7,表示内核缓冲区有7个字节的数据没有被读出去(网上看一些资料说,如果内核缓冲被打满,会有数据包丢失,我本地测下来发现,缓冲区满了之后,接收端会告诉发送端 win为0,此时发送端不在发送携带数据的包,但是这个点还是值得注意下的,业务处理的能力很慢,导致数据包无法及时处理,会有包丢失的情况)。
使用lsof 命令可以看到进程打开的文件描述符,命令为 lsof -p ${PID},结果为:
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
server 37671 kang cwd DIR 253,2 22 105990016 /home/kang/test-tcp/server
server 37671 kang rtd DIR 253,0 4096 128 /
server 37671 kang txt REG 253,0 2252800 135546735 /tmp/go-build605900202/b001/exe/server
server 37671 kang mem REG 253,0 2107816 201328826 /usr/lib64/libc-2.17.so
server 37671 kang mem REG 253,0 142296 201404660 /usr/lib64/libpthread-2.17.so
server 37671 kang mem REG 253,0 164432 201328819 /usr/lib64/ld-2.17.so
server 37671 kang 0u CHR 136,0 0t0 3 /dev/pts/0
server 37671 kang 1u CHR 136,0 0t0 3 /dev/pts/0
server 37671 kang 2u CHR 136,0 0t0 3 /dev/pts/0
server 37671 kang 3u IPv4 113803 0t0 TCP localhost:webcache (LISTEN)
server 37671 kang 5u a_inode 0,9 0 6835 [eventpoll]
server 37671 kang 6r FIFO 0,8 0t0 113804 pipe
server 37671 kang 7w FIFO 0,8 0t0 113804 pipe
看到37671 这个进程并没有接收socket连接,这时我们让程序开始accpet,输出结果为:
start accept
123456
然后netstat看下网络状态为:
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 127.0.0.1:8080 127.0.0.1:47151 ESTABLISHED 37671/server
发现连接已经交给了 37671这个进程,并且内核的Recv-Q数据也被程序读出
执行lsof -p 37671结果为:
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
server 37671 kang cwd DIR 253,2 22 105990016 /home/kang/test-tcp/server
server 37671 kang rtd DIR 253,0 4096 128 /
server 37671 kang txt REG 253,0 2252800 816394 /tmp/go-build283231499/b001/exe/server
server 37671 kang mem REG 253,0 2107816 201328826 /usr/lib64/libc-2.17.so
server 37671 kang mem REG 253,0 142296 201404660 /usr/lib64/libpthread-2.17.so
server 37671 kang mem REG 253,0 164432 201328819 /usr/lib64/ld-2.17.so
server 37671 kang 0u CHR 136,0 0t0 3 /dev/pts/0
server 37671 kang 1u CHR 136,0 0t0 3 /dev/pts/0
server 37671 kang 2u CHR 136,0 0t0 3 /dev/pts/0
server 37671 kang 3u IPv4 115369 0t0 TCP localhost:webcache (LISTEN)
server 37671 kang 4u IPv4 115372 0t0 TCP localhost:webcache->localhost:47151 (ESTABLISHED)
server 37671 kang 5u a_inode 0,9 0 6835 [eventpoll]
server 37671 kang 6r FIFO 0,8 0t0 115370 pipe
server 37671 kang 7w FIFO 0,8 0t0 115370 pipe
close wait状态的出现
我们把上述代码 defer conn.Close() 注释掉,使用nc发起一个连接,主动关闭,使用tcpdump抓包情况如下:
01:26:31.145725 IP 127.0.0.1.47153 > 127.0.0.1.8080: Flags [F.], seq 1, ack 1, win 342, options [nop,nop,TS val 48719418 ecr 48713789], length 0
01:26:31.146179 IP 127.0.0.1.8080 > 127.0.0.1.47153: Flags [.], ack 2, win 342, options [nop,nop,TS val 48719419 ecr 48719418], length 0
这时候我们发现 127.0.0.1.8080(被动关闭的一端)收到FIN的包之后,回了ack但是并没有发送FIN包告诉主动关闭的一端可以关闭了,然后通过netstat看到状态后发现,被动关闭的一端处于 close wait的状态:
tcp 0 0 127.0.0.1:8080 127.0.0.1:47153 CLOSE_WAIT 39634/server
主动关闭的一端处于FIN_WAIT2的状态
tcp 0 0 127.0.0.1:47153 127.0.0.1:8080 FIN_WAIT2 -
close wait的状态代表,收到了对方的关闭请求,自己这边没有主动发FIN包的导致的。
time wait的状态
我们把上述代码 defer conn.Close() 注释掉,按上述流程抓到tcpdump的包为:
02:25:50.976745 IP 127.0.0.1.47161 > 127.0.0.1.8080: Flags [.], ack 1, win 342, options [nop,nop,TS val 52279249 ecr 52279249], length 0
02:25:54.424235 IP 127.0.0.1.47161 > 127.0.0.1.8080: Flags [F.], seq 1, ack 1, win 342, options [nop,nop,TS val 52282697 ecr 52279249], length 0
02:25:54.424466 IP 127.0.0.1.8080 > 127.0.0.1.47161: Flags [F.], seq 1, ack 2, win 342, options [nop,nop,TS val 52282697 ecr 52282697], length 0
02:25:54.424476 IP 127.0.0.1.47161 > 127.0.0.1.8080: Flags [.], ack 2, win 342, options [nop,nop,TS val 52282697 ecr 52282697], length 0
发现数据又完整的四次挥手的状态,此时使用netstat 看下状态发现发起关闭端处于time wait的状态
tcp 0 0 127.0.0.1:47161 127.0.0.1:8080 TIME_WAIT -
这个time wait的状态出现在发起关闭的一端,是为了防止对方可能因为未收到最后发出的ack包而重发fin,主动发起的一端好进行重发ack,而time wait的持续时长为2MSL。
为什么需要四次挥手
这是由tcp的半关闭造成的,既然一个tcp连接时全双工的(即数据在两个方向上能同时传递),因此每个方向都必须单独地进行关闭挥手过程中(被动关闭的一方的 fin跟ack是可以放到一起同时发送的,也就是四次挥手流程中的第二次跟第三次的数据包可以合并为一个数据包)。需要三次握手同理。
TCP三次握手与四次挥手流程图:
两端同时打开
两个应用程序同时彼此执行主动打开的情况是有可能的,尽管发生的可能性极小。例如,主机A中的一个应用程序使用本地端口7777,并于主机B的端口8888执行主动打开。主机B中的应用程序则使用本地端口8888,并与主机A的端口7777执行主动打开。TCP特意设计为了可以处理同时打开,对于同时打开它仅建立一条连接而不是两条连接。连接流程图如下:
两端同时关闭
两端同时关闭的流程图如下: