热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

spring建立一个tcpsocket长连接_动手学习TCP:TCP连接建立与终止,它是一个面向连接的协议...

TCP是一个面向连接的协议,任何一方在发送数据之前,都必须先在双方之间建立一条连接。所以,本文就主要看看TCP连接的建立和终止。在开始介绍

TCP是一个面向连接的协议,任何一方在发送数据之前,都必须先在双方之间建立一条连接。所以,本文就主要看看TCP连接的建立和终止。

在开始介绍TCP连接之前,先来看看TCP数据包的首部,首部里面有很多重要的字段,在我们实现程序的时候需要进行设置。

TCP的首部

在OSI七层模型中,上层的数据包都会作为下层数据包的数据部分(payload)。

也就是说,当构造TCP数据包的时候,会把应用层的数据包作为TCP包的数据部分,然后加上TCP头构成TCP数据包;同样,当构造IP数据包的时候,整个TCP包就会被当作数据部分,然后加上IP头构成IP数据包。

013dcc1f00d3455a79240d1096da6143.png

TCP头的数据格式如下,在不包括可选字段的情况下,一般TCP头会占用20个字节。

bcaf500107bc80de63b7cd117931c24a.png

在TCP首部中,有几个字段是需要关注一下:

  • 在TCP首部中没有源和目标的IP、MAC地址(IP和MAC地址分别是网络层和链路层首部的信息),只有源和目标的端口
  • Sequence Number是包的序号,网络层(IP层)的传输是不可靠的,可能产生包乱序,所以这个需要可以解决网络包乱序的问题
  • Acknowledgement Number用来确认收到数据包的确认序号,为TCP的传输提供了可靠性保证
  • TCP Flags包括了8个bit,通过对这些bit的设置,可以代表不同类型的TCP数据包

下面就看看TCP连接的建立和终止。

TCP连接建立

TCP连接建立的过程被称为三次握手过程:

  1. 连接建立发起端发送[SYN]包,该端将主动打开(active open)
  2. 接收端将发送[SYN, ACK]包,该端将被动打开(passive open),ACK标志表示对收到的[SYN]包的确认
  3. 连接建立发起端发送[ACK]包确认[SYN, ACK]包
a6afc3e6ec531ce094b5ac305e298943.png
Initial Sequence Number

连接建立过程中,一个重要的工作就是初始化Sequence Number,通信的双方在建立连接的过程中互相通知对方自己的初始Sequence Number(ISN:Initial Sequence Number)。ISN不是固定的,ISN跟时钟绑定,根据特定的间隔自增,直到超过2^32,又从0开始。

SYN全称就是Synchronize Sequence Number,通过seq序号,TCP就可以保证数据包的顺序;通过ack序号,TCP就有了可靠性。

连接建立注意点

在建立TCP连接的过程中,有以下两点需要注意一下:

  1. [SYN]标志的数据包会使用消耗一个序号,所以对端的确认号(ack)是当前序号(seq)加一
  2. 当被动打开端发送[ACK]确认包的时候,同时设置了[SYN]标志,所以TCP连接建立的过程只需要三次握手,而不是四次
TCP连接终止

TCP连接终止的过程被称为四次挥手过程,以下图为例:

  1. 连接终止端(client)发送[FIN, ACK] 包,关闭client到server方向的数据发送通路
  2. server端发送[ACK]包来确认来自client的[FIN, ACK] 包
  3. server端发送[FIN, ACK] 包,关闭server到client方向的数据发送通路
  4. client端发送[ACK]包来确认来自server的[FIN, ACK] 包,到此TCP连接关闭
9fba88e267d2d3260cb850178d5434b6.png
连接终止注意点

在建立TCP连接的过程中,有以下两点需要注意一下:

  • [FIN]标志的数据包会使用消耗一个序号,所以对端的确认号(ack)是当前序号(seq)加一
  • 与建立连接时的三次 握手不同,终止连接需要四次挥手因为TCP连接是全双工的,每个方向都必须单独进行关闭。当一方完成它的数据发送任务后就能发送一个FIN来终止这个方向的连接。收到一个 FIN只意味着这一方向上没有数据流动,但是TCP连接在收到一个FIN后仍能发送数据
TCP连接实验

好了,了解了TCP连接建立和终止的基本知识后,就可以通过Pcap.Net来进行TCP连接建立和终止的实验了。

建立连接代码的基本流程如下:

  1. client程序使用一个初始的seq序号(100),然后生成并发送一个带[SYN]标志的TCP包
  2. client将期待来自服务端的[SYN, ACK]包
  3. 当收到[SYN, ACK]包之后,client需要生成并发送一个[ACK]包进行确认,这个[ACK]包的ack号是[SYN, ACK]包seq号加一

终止连接代码的基本流程如下:

  1. client程序delay 10秒钟,然后发送[FIN, ACK]包关闭client到server的通路,继续使用全局的seq号
  2. client将期待来自服务端的[ACK]包,以及[FIN, ACK]包
  3. 最后client发送[ACK]包,seq号需要加一,因为[FIN]标志的包将消耗一个序号,TCP连接终止完成

主程序如下,发送TCP连接建立和终止请求,每个请求发送后都用PacketHandler处理收到的包:

communicator.SendPacket(Utils.BuildTcpPacket(endPointInfo, TcpControlBits.Synchronize, null));

PacketHandler(communicator, endPointInfo);

// delay 10 secs, then client to send Fin

Thread.Sleep(10000);

communicator.SendPacket(Utils.BuildTcpPacket(endPointInfo, TcpControlBits.Fin | TcpControlBits.Acknowledgment));

PacketHandler(communicator, endPointInfo);

程序的主要逻辑在PacketHandler中,这个函数根据收到的不同TCP包的类型完成不同的逻辑,产生并发送不同类型的包。

例如,当PacketHandler接收到来自服务端的[SYN, ACK]包后,处理函数就会生成并发送一个[ACK]确认包。也就是说,PacketHandler的逻辑就是实现了TCP连接建立和终止的逻辑。

private static void PacketHandler(PacketCommunicator communicator, EndPointInfo endPointInfo)

{

Packet packet = null;

bool running = true;

do

{

PacketCommunicatorReceiveResult result = communicator.ReceivePacket(out packet);

switch (result)

{

case PacketCommunicatorReceiveResult.Timeout:

// Timeout elapsed

continue;

case PacketCommunicatorReceiveResult.Ok:

bool isRecvedPacket = (packet.Ethernet.IpV4.Destination.ToString() == endPointInfo.SourceIp) ? true : false;

if (isRecvedPacket)

{

switch (packet.Ethernet.IpV4.Tcp.ControlBits)

{

case (TcpControlBits.Synchronize | TcpControlBits.Acknowledgment):

Utils.PacketInfoPrinter(packet);

Packet ack4SynAck = Utils.BuildTcpResponsePacket(packet, TcpControlBits.Acknowledgment);

communicator.SendPacket(ack4SynAck);

break;

case (TcpControlBits.Fin | TcpControlBits.Acknowledgment):

Utils.PacketInfoPrinter(packet);

Packet ack4FinAck = Utils.BuildTcpResponsePacket(packet, TcpControlBits.Acknowledgment);

communicator.SendPacket(ack4FinAck);

break;

case TcpControlBits.Acknowledgment:

Utils.PacketInfoPrinter(packet);

break;

default:

Utils.PacketInfoPrinter(packet);

break;

}

}

else

{

switch (packet.Ethernet.IpV4.Tcp.ControlBits)

{

case (TcpControlBits.Fin | TcpControlBits.Acknowledgment):

Utils.PacketInfoPrinter(packet);

break;

case TcpControlBits.Synchronize:

Utils.PacketInfoPrinter(packet);

break;

case TcpControlBits.Acknowledgment:

Utils.PacketInfoPrinter(packet);

running = false;

break;

default:

Utils.PacketInfoPrinter(packet);

break;

}

}

break;

default:

throw new InvalidOperationException("The result " + result + " should never be reached here");

}

} while (running);

}

在PacketHandler函数中用到了BuildTcpResponsePacket这个函数,这个函数根据收到的TCP包,来构建response包。

这个函数有下面几个注意点:

  • 该函数会根据收到的包,设置response包的源和目的地址
  • 该函数会接受PacketHandler传递来的TCP flags,并设置到TCP首部中
  • 该函数的另一个重要部分就是会计算并设置TCP首部中的seq号ack号,这一点很重要

public static Packet BuildTcpResponsePacket(Packet packet, TcpControlBits tcpControlBits)

{

EthernetLayer ethernetHeader = new EthernetLayer

{

Source = new MacAddress(packet.Ethernet.Destination.ToString()),

Destination = new MacAddress(packet.Ethernet.Source.ToString()),

EtherType = EthernetType.None, // Will be filled automatically.

};

IpV4Layer ipHeader = new IpV4Layer

{

Source = new IpV4Address(packet.Ethernet.IpV4.Destination.ToString()),

CurrentDestination = new IpV4Address(packet.Ethernet.IpV4.Source.ToString()),

Fragmentation = IpV4Fragmentation.None,

HeaderChecksum = null, // Will be filled automatically.

Identification = 123,

Options = IpV4Options.None,

Protocol = null, // Will be filled automatically.

Ttl = 100,

TypeOfService = 0,

};

TcpLayer tcpHeader = new TcpLayer

{

SourcePort = packet.Ethernet.IpV4.Tcp.DestinationPort,

DestinationPort = packet.Ethernet.IpV4.Tcp.SourcePort,

Checksum = null, // Will be filled automatically.

SequenceNumber = seqNum = packet.Ethernet.IpV4.Tcp.AcknowledgmentNumber,

AcknowledgmentNumber = ackNum = packet.Ethernet.IpV4.Tcp.SequenceNumber + (uint)((packet.Ethernet.IpV4.Tcp.Payload.Length > 0) ? packet.Ethernet.IpV4.Tcp.Payload.Length : 1),

ControlBits = tcpControlBits,

Window = windowSize,

UrgentPointer = 0,

Options = TcpOptions.None,

};

PacketBuilder builder = new PacketBuilder(ethernetHeader, ipHeader, tcpHeader);

return builder.Build(DateTime.Now);

}

运行效果

打开Wireshark监听"VirtualBox Host-Only Network"网卡,并设置filter为"port 8081"。

然后运行程序,通过console可以看到客户端发送的包,以及服务端返回的包,通过这些包完成了TCP连接的建立和终止。

32f7c5d305383ed5c8f1c963a77ddcd8.png

下面是Wireshark中显示的结果,Wireshark比较友好,会显示相对seq号,所以看到的都是从0开始编号。

注意seq号和ack号的变化,[SYN]和[FIN]标志的TCP包都会消耗一个序号。

c02e1b3708a1553bab7eb576ba027fce.png
总结

本文介绍了TCP首部,通过设置TCP首部中的[SYN]标志,可以构造TCP连接建立请求包;通过设置[FIN]标志,可以构造TCP连接终止请求包。

文中使用Pcap.Net构建了一个简单的客户端,完成了向服务器建立(三次握手)和终止(四次挥手)连接的过程。

通过这个实验,一定会对TCP连接的建立和终止有一个比较直观的认识。



推荐阅读
  • Java SE从入门到放弃(三)的逻辑运算符详解
    本文详细介绍了Java SE中的逻辑运算符,包括逻辑运算符的操作和运算结果,以及与运算符的不同之处。通过代码演示,展示了逻辑运算符的使用方法和注意事项。文章以Java SE从入门到放弃(三)为背景,对逻辑运算符进行了深入的解析。 ... [详细]
  • 本文主要解析了Open judge C16H问题中涉及到的Magical Balls的快速幂和逆元算法,并给出了问题的解析和解决方法。详细介绍了问题的背景和规则,并给出了相应的算法解析和实现步骤。通过本文的解析,读者可以更好地理解和解决Open judge C16H问题中的Magical Balls部分。 ... [详细]
  • 开发笔记:实验7的文件读写操作
    本文介绍了使用C++的ofstream和ifstream类进行文件读写操作的方法,包括创建文件、写入文件和读取文件的过程。同时还介绍了如何判断文件是否成功打开和关闭文件的方法。通过本文的学习,读者可以了解如何在C++中进行文件读写操作。 ... [详细]
  • SpringBoot uri统一权限管理的实现方法及步骤详解
    本文详细介绍了SpringBoot中实现uri统一权限管理的方法,包括表结构定义、自动统计URI并自动删除脏数据、程序启动加载等步骤。通过该方法可以提高系统的安全性,实现对系统任意接口的权限拦截验证。 ... [详细]
  • 本文介绍了一个Java猜拳小游戏的代码,通过使用Scanner类获取用户输入的拳的数字,并随机生成计算机的拳,然后判断胜负。该游戏可以选择剪刀、石头、布三种拳,通过比较两者的拳来决定胜负。 ... [详细]
  • 本文讨论了如何优化解决hdu 1003 java题目的动态规划方法,通过分析加法规则和最大和的性质,提出了一种优化的思路。具体方法是,当从1加到n为负时,即sum(1,n)sum(n,s),可以继续加法计算。同时,还考虑了两种特殊情况:都是负数的情况和有0的情况。最后,通过使用Scanner类来获取输入数据。 ... [详细]
  • 本文详细介绍了Spring的JdbcTemplate的使用方法,包括执行存储过程、存储函数的call()方法,执行任何SQL语句的execute()方法,单个更新和批量更新的update()和batchUpdate()方法,以及单查和列表查询的query()和queryForXXX()方法。提供了经过测试的API供使用。 ... [详细]
  • Go Cobra命令行工具入门教程
    本文介绍了Go语言实现的命令行工具Cobra的基本概念、安装方法和入门实践。Cobra被广泛应用于各种项目中,如Kubernetes、Hugo和Github CLI等。通过使用Cobra,我们可以快速创建命令行工具,适用于写测试脚本和各种服务的Admin CLI。文章还通过一个简单的demo演示了Cobra的使用方法。 ... [详细]
  • This article discusses the efficiency of using char str[] and char *str and whether there is any reason to prefer one over the other. It explains the difference between the two and provides an example to illustrate their usage. ... [详细]
  • 本文讨论了编写可保护的代码的重要性,包括提高代码的可读性、可调试性和直观性。同时介绍了优化代码的方法,如代码格式化、解释函数和提炼函数等。还提到了一些常见的坏代码味道,如不规范的命名、重复代码、过长的函数和参数列表等。最后,介绍了如何处理数据泥团和进行函数重构,以提高代码质量和可维护性。 ... [详细]
  • Android自定义控件绘图篇之Paint函数大汇总
    本文介绍了Android自定义控件绘图篇中的Paint函数大汇总,包括重置画笔、设置颜色、设置透明度、设置样式、设置宽度、设置抗锯齿等功能。通过学习这些函数,可以更好地掌握Paint的用法。 ... [详细]
  • HDU 2372 El Dorado(DP)的最长上升子序列长度求解方法
    本文介绍了解决HDU 2372 El Dorado问题的一种动态规划方法,通过循环k的方式求解最长上升子序列的长度。具体实现过程包括初始化dp数组、读取数列、计算最长上升子序列长度等步骤。 ... [详细]
  • 本文讨论了使用差分约束系统求解House Man跳跃问题的思路与方法。给定一组不同高度,要求从最低点跳跃到最高点,每次跳跃的距离不超过D,并且不能改变给定的顺序。通过建立差分约束系统,将问题转化为图的建立和查询距离的问题。文章详细介绍了建立约束条件的方法,并使用SPFA算法判环并输出结果。同时还讨论了建边方向和跳跃顺序的关系。 ... [详细]
  • C语言注释工具及快捷键,删除C语言注释工具的实现思路
    本文介绍了C语言中注释的两种方式以及注释的作用,提供了删除C语言注释的工具实现思路,并分享了C语言中注释的快捷键操作方法。 ... [详细]
  • 本文介绍了Oracle存储过程的基本语法和写法示例,同时还介绍了已命名的系统异常的产生原因。 ... [详细]
author-avatar
嫣嫣细语dingdingjq
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有