我们知道,基于TCP/IP协议的网络数据传输大致过程:
- 发送端将数据加上tcp报头(包含发送方端口和目的方端口信息)交给自己的IP模块;
- 发送端IP模块再加上IP报头(包含发送端IP地址和目的端IP地址),并根据路由表选择将封好的IP包交给哪个IP路由;
- 发送端数据链路层在当前局域网根据路由IP查询或从arp缓存找到路由IP对应的硬件MAC地址,加上MAC头,发给路由节点,路由节点收到数据帧去掉MAC头得到IP包,以同样的方式给下一个路由节点,直到IP数据包到达目标主机;
- 目标主机拿到tcp报文根据目的端口将数据交给绑定该端口的应用程序处理;
附上:
tcp报头结构:
IPv4报头结构:
基于tcp协议传输数据前要先建立到目的IP:Port的连接,服务端需要先绑定监听某个端口给数据传输使用,就是告诉机器:发给这个端口的数据交给我处理。
数据传输完成连接不再使用要断开连接。
就是常说的tcp建立连接“三次握手”和断开连接“四次挥手”。
一般过程就是下面这个经典的图:
建立连接:
tcp4 0 0 127.0.0.1.12345 *.* LISTEN
- 客户端向服务端发出一个报头SYN标志位为1的tcp报文,序号seq为n,并将这个连接状态标记为SYN_SENT;
- 服务端收到客户端发来的这个报头SYN标志位为1的tcp报文,会回给客户端一个报头SYN标志位和ACK标志位都为1的tcp包,序号假设是m,确认号为n+1, 并在服务端把这个连接状态标记为SYN_RCVD;
- 客户端收到服务端这个SYN标志位和ACK标志位都为1的tcp包响应,知道连接没问题,将连接状态标记为ESTABLISHED,并给服务端回一个ACK标志位为1的包,确认号给m+1;
- 服务端收到这个确认包后也知道了连接没问题,将此连接标记为ESTABLISHED,建立连接过程结束。
tcp4 0 0 127.0.0.1.12345 127.0.0.1.51594 ESTABLISHED
tcp4 0 0 127.0.0.1.51594 127.0.0.1.12345 ESTABLISHED
tcp4 0 0 127.0.0.1.12345 *.* LISTEN
断开连接:
- 主动关闭方发送 报头FIN标志位为1的报文给对方,序号seq为n,并将此连接标记为FIN_WAIT_1;(告诉对方,我这边不会再write了)
- 被动关闭方收到这个报头FIN标志位为1的报文,会回一个报头ACK标志位为1的报文,确认号为n+1,并将该连接的状态标记为CLOSE_WAIT;
- 主动关闭方收到被动方的确认后将连接标记为FIN_WAIT_2;
tcp4 0 0 127.0.0.1.12345 127.0.0.1.61331 CLOSE_WAIT
tcp4 0 0 127.0.0.1.61331 127.0.0.1.12345 FIN_WAIT_2
tcp4 0 0 127.0.0.1.12345 *.* LISTEN
- 此时,主动关闭方不会再写,但是被动关闭方还可以正常write,主动关闭方还可以正常read!!(为了全双工共,所以连接断开要4次,被动方FIN可能要晚点发)
- 当被动关闭方判断往主动方这个方向的通道可以关闭时,发送报头FIN标志位为1的报文给主动方(序号seq为m),并将状态标记为LAST_ACK;
- 主动关闭方收到被动方的FIN包,回ACK包给被动方(确认号为m+1),将连接状态从FIN_WAIT_2标记为TIME_WAIT,等待2MSL(Maximum Segment Lifetime)后释放连接资源;
- 被动关闭方收到主动方对FIN包的确认直接释放连接资源;
tcp4 0 0 127.0.0.1.12345 *.* LISTEN
tcp4 0 0 127.0.0.1.61331 127.0.0.1.12345 TIME_WAIT
为什么主动关闭方要在TIME_WAIT状态等待?
我的理解:
- 为对方(被动关闭方)负责:如果被动方没有收到我对它FIN包的ACK,被动方将重发FIN,2MSL时间内我会收到被动方重发的FIN,如果没收到被动方重发的FIN,我认为我的ACK它收到了。
- 防止重用这客户端端口建立新连接,收到给旧连接的报文。
有没有注意到:
我这里说的一直是“主动关闭方”和“被动关闭方”。
我发现书上和网上关于tcp连接关闭过程的时序图画的都是客户端主动关闭。
实际上,主动关闭也可能是服务端主动发起,最后TIME_WAIT等待在服务端。
这是我今天想说的重点。
我的实验环境:
服务端:golang,做完数据传输5s关闭。
package main
import (
"fmt"
"net"
"strconv"
"strings"
"time"
)
//主结构--主要操作都定义为它的方法
type TcpServer struct {
work bool
proxyTransListener *net.TCPListener //用于关闭,绑定清理
}
//主方法
func main() {
fmt.Println("main program started")
tcpServer := new(TcpServer)
//模拟打开服务开关
tcpServer.startTcpService()
time.Sleep(3600 * 1e9)
fmt.Println("main program end")
defer tcpServer.stopProxyService() //主界面终止时,关闭服务
}
//统一记日志方法
func (tcpServer *TcpServer) Loglog(msg string) {
timeObj := time.Now()
fmt.Println("\n[" + timeObj.Format("2006-01-02 15:04:05") + "] " + msg)
}
//开启服务
func (tcpServer *TcpServer) startTcpService() {
tcpServer.work = true
tcpServer.Loglog("startTcpService run")
go tcpServer.proxyTransService()
}
//关闭服务,并清理端口绑定
func (tcpServer *TcpServer) stopProxyService() {
tcpServer.work = false
tcpServer.Loglog("stopProxyService run")
tcpServer.proxyTransListener.Close()
}
//tcp报头填充
func (tcpServer *TcpServer) headPadding(wantLength int, content string) string {
contentLenth := len(content)
contentLenthStr := strconv.Itoa(contentLenth)
repeatCount := wantLength - len(contentLenthStr)
return contentLenthStr + strings.Repeat(" ", repeatCount)
}
//请求报文转发服务
func (tcpServer *TcpServer) proxyTransService() {
tcpServer.Loglog("proxyTransService run")
var proxy_host, proxy_trans_port string
proxy_host = "127.0.0.1"
proxy_trans_port = "12345"
serverHostPort := proxy_host + ":" + proxy_trans_port
serverAddr, rsvAdrsErr := net.ResolveTCPAddr("tcp", serverHostPort)
if rsvAdrsErr != nil {
tcpServer.Loglog(fmt.Sprintf("Resolving address:port failed: %v", rsvAdrsErr))
return
}
proxyTransListener, lisnErr := net.ListenTCP("tcp", serverAddr)
tcpServer.proxyTransListener = proxyTransListener
if lisnErr != nil {
tcpServer.Loglog(fmt.Sprintf("ListenTCP err: %v", lisnErr))
return
}
for {
if tcpServer.work == true {
tcpServer.Loglog("trans_wait")
clientConnection, acptErr := proxyTransListener.Accept()
tcpServer.Loglog(fmt.Sprintf("accept err: %v", acptErr))
if acptErr == nil {
go tcpServer.bussDeal(clientConnection)
}
} else {
tcpServer.Loglog("trans_stop_wait")
return
}
}
}
//接到客户端连接业务处理
func (tcpServer *TcpServer) bussDeal(clientConnection net.Conn) {
clientIpPort := clientConnection.RemoteAddr().String()
tcpServer.Loglog(fmt.Sprintf("clientIpPort: %v", clientIpPort))
var clientRequestProxyLengthStrByte []byte = make([]byte, 10)
clientConnection.Read(clientRequestProxyLengthStrByte)
clientRequestProxyLength, _ := strconv.Atoi(strings.TrimSpace(string(clientRequestProxyLengthStrByte)))
tcpServer.Loglog(fmt.Sprintf("clientRequestProxyLength: %v", clientRequestProxyLength))
var clientRequestProxy string
if clientRequestProxyLength > 0 {
var clientRequestProxyByte []byte = make([]byte, clientRequestProxyLength)
clientConnection.Read(clientRequestProxyByte)
clientRequestProxy = string(clientRequestProxyByte)
tcpServer.Loglog(fmt.Sprintf("clientRequestProxy: %v", clientRequestProxy))
}
proxyResp := "backToClientMsg"
tcpServer.clientConnectionEnd(clientConnection, proxyResp)
}
//返回财务并关闭连接
func (tcpServer *TcpServer) clientConnectionEnd(clientConnection net.Conn, proBk string) {
proBkLengthStr := tcpServer.headPadding(10, proBk)
bkToClientData := proBkLengthStr + proBk
clientConnection.Write([]byte(bkToClientData))
clientIpPort := clientConnection.RemoteAddr().String()
fmt.Println("server close before")
time.Sleep(1 * 1e9)
tcpServer.Loglog("5")
time.Sleep(1 * 1e9)
tcpServer.Loglog("4")
time.Sleep(1 * 1e9)
tcpServer.Loglog("3")
time.Sleep(1 * 1e9)
tcpServer.Loglog("2")
time.Sleep(1 * 1e9)
tcpServer.Loglog("1")
time.Sleep(1 * 1e9)
clientConnection.Close()
tcpServer.Loglog("server close after")
fmt.Println(proBkLengthStr+proBk+"Closed connection: ", clientIpPort)
}
View Code
客户端:php, 做完数据传输直接结束程序(关闭),或者等100s模拟让服务端先关。
<?php
class tcpClient
{
private static $serverHost = '127.0.0.1';
private static $serverPort = '12345';
const TCP_HEADER_LEN = 10;//约定报头长度
/**
* * 统一日志方法
* @param string $msg content
* @author:songjm
* @return void
*/
private static function logLog($msg)
{
echo "\r\n".'['.date('Y-m-d H:i:s').']'.$msg;
}
/**
* * tcp客户端测试
* @param string $requestMsg 发送消息
* @author:songjm
* @return array
*/
public static function hello($requestMsg)
{
try {
//创建socketConnectToServer
$socketConnectToServer = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
if ($socketConnectToServer === false) {
$socketCreateErr = "socket_create() failed:reason:" . socket_strerror(socket_last_error());
self::logLog($socketCreateErr);
throw new \Exception($socketCreateErr);
}
self::logLog("socketConnectToServer created.");
//尝试连接服务端socket
if (!socket_connect($socketConnectToServer, self::$serverHost, self::$serverPort)) {
$connectToSignServerErr = "connect to server failed :reason:" . socket_strerror(socket_last_error($socketConnectToServer));
self::logLog($connectToSignServerErr);
throw new \Exception($connectToSignServerErr);
}
self::logLog("connect to server ".self::$serverHost.":".self::$serverPort.".");
//发送请求报文
$clientRequestData = str_pad(strlen($requestMsg), self::TCP_HEADER_LEN, ' ', STR_PAD_RIGHT).$requestMsg;
if (socket_write($socketConnectToServer, $clientRequestData, strlen($clientRequestData)) === false) {
$reqErr = "socket_write() failed reason:" . socket_strerror(socket_last_error($socketConnectToServer));
self::logLog($reqErr);
throw new \Exception($reqErr);
}
self::logLog("send requestMsg:\r\n".$clientRequestData);
//读服务端响应报文
$responLengthStr = socket_read($socketConnectToServer, self::TCP_HEADER_LEN, PHP_BINARY_READ);
self::logLog("responLengthStr:\r\n" .$responLengthStr);
$responLength = (int)$responLengthStr;
$responStr = '';
$responLeftLength = $responLength;
$responReadStartTime = time();
do {
$responThisTime = socket_read($socketConnectToServer, $responLeftLength, PHP_BINARY_READ);
if ($responThisTime !== false && $responThisTime != '') {
$responStr .= $responThisTime;
}
$responLeftLength = $responLength - strlen($responStr);
} while (($responLeftLength > 0) && (time() - $responReadStartTime < 5));//读5秒超时
self::logLog("respon:".$responStr);
$bkArr = array(
'code' => 1,
'msg' => 'ok',
'respData' => $responStr,
);
//echo "\r\nwait 100s\r\n";
//sleep(1000);//让服务端先关闭
return $bkArr;
} catch (\Exception $exception) {
return array(
'code' => 0,
'msg' => $exception->getMessage(),
'respData' => '',
);
}
}
}
$respArr = tcpClient::hello('client跟server说');
echo "<pre>";
print_r($respArr);
die('program end');
View Code
查看tcp连接状态:
netstat -p tcp -an | grep '12345' >> /a.txt && echo '\r\n' >> /a.txt
客户端主动关闭:
客户端方法结束主动关闭后:
tcp4 0 0 127.0.0.1.12345 127.0.0.1.61331 CLOSE_WAIT
tcp4 0 0 127.0.0.1.61331 127.0.0.1.12345 FIN_WAIT_2
tcp4 0 0 127.0.0.1.12345 *.* LISTEN
服务端关闭之后:(被动)
tcp4 0 0 127.0.0.1.12345 *.* LISTEN
tcp4 0 0 127.0.0.1.61331 127.0.0.1.12345 TIME_WAIT
服务端主动关闭:
服务端主动关闭:
tcp4 0 0 127.0.0.1.12345 127.0.0.1.61493 FIN_WAIT_2
tcp4 0 0 127.0.0.1.61493 127.0.0.1.12345 CLOSE_WAIT
tcp4 0 0 127.0.0.1.12345 *.* LISTEN
客户端方法结束关闭之后:(被动)
tcp4 0 0 127.0.0.1.12345 *.* LISTEN
tcp4 0 0 127.0.0.1.12345 127.0.0.1.61493 TIME_WAIT