![](https://i-blog.csdnimg.cn/blog_migrate/893d6bba8c2ac0060b0237bd6dd3a36a.jpeg)
public struct TCPHeader
{
public ushort 源端口; //16位源端口
public ushort 目的端口; //16位目的端口
public uint 序列号; //32位序列号
public uint 确认号; //32位确认号
public byte 首部长度; //4位首部长度/6位保留字
public byte 标志位; //6位标志位
public ushort 窗口大小; //16位窗口大小
public ushort 校验和; //16位校验和
public ushort 紧急指针; //16位紧急数据偏移量
}
TCP选项
每个选项的开始是1字节的kind字段,说明选项的类型。
Kind=0:选项表结束(1字节)
Kind=1:无操作(1字节)
Kind=2:最大报文段长度(4字节)
Kind=3:窗口扩大因子(4字节)
Kind=8:时间戳(10字节)
3、窗口扩大选项:
窗口扩大选项使TCP的窗口定义从16位增加到32位,这并不是通过修改TCP首部来实现的,TCP首部仍然使用16位,而是通过定义一个选项实现对16位的扩大操作来完成的。
4、时间戳选项:
时间戳选项使发送方在每个报文段中放置一个时间戳值。接收方在确认中返回这个数值,从而允许发送方为每一个收到的ACK计算RTT。
其实如果要自己实现一个TCP通信过程是相当麻烦的一件事情:
(1)发送一个要建立连接的TCP报文;(2)接收到对方同意建立连接的报文;(3)发送一个确认连接的报文;(4)收发数据;(5)发送释放连接的报文;(6)接收对方同意释放连接的报文;(7)接收对方释放连接的报文;(8)发送一个同意释放连接的报文。
还有流量控制、差错控制、计时器的问题……
下面给出的函数仅仅是根据给出的所有参数构造TCP报文的字节流。
public byte[] 构造TCP报文段(UInt32 源IP, UInt32 目的IP, //这是为了计算校验和
UInt16 源端口号, UInt16 目的端口号, UInt32 序列号, UInt32 确认号,
byte URG, byte ACK, byte PSH, byte RST, byte SYN, byte FIN,
UInt16 窗口大小, UInt16 紧急指针,
byte[] 选项与填充部分,
Byte[] 数据部分)
UInt16 选项与填充部分的长度 = (UInt16)选项与填充部分.Length;
UInt16 数据部分的长度 = (UInt16)数据部分.Length;
UInt16 TCP总长度 = (UInt16)(20 + 选项与填充部分的长度 + 数据部分的长度);
Byte[] tcpbytes = new Byte[TCP总长度];
UInt16 校验和 = 0;
//网络字节顺序
源IP = (uint)IPAddress.HostToNetworkOrder((int)源IP);
目的IP = (uint)IPAddress.HostToNetworkOrder((int)目的IP);
源端口号 = (ushort)IPAddress.HostToNetworkOrder((Int16)源端口号);
目的端口号 = (ushort)IPAddress.HostToNetworkOrder((Int16)目的端口号);
序列号 = (uint)IPAddress.HostToNetworkOrder((int)序列号);
确认号 = (uint)IPAddress.HostToNetworkOrder((int)确认号);
窗口大小 = (ushort)IPAddress.HostToNetworkOrder((Int16)窗口大小);
紧急指针 = (ushort)IPAddress.HostToNetworkOrder((Int16)紧急指针);
//填充字节组
BitConverter.GetBytes(源端口号).CopyTo(tcpbytes, 0);//填入源端口号
BitConverter.GetBytes(目的端口号).CopyTo(tcpbytes, 2);//填入目的端口号
BitConverter.GetBytes(序列号).CopyTo(tcpbytes, 4);//填入序列号
BitConverter.GetBytes(确认号).CopyTo(tcpbytes, 8);//
//头部长度和标志位的处理
byte 头部长度 = (byte)((20 + 选项与填充部分的长度) / 4);
头部长度 <<= 4;//左移4位,余下部分为0
tcpbytes[12] = 头部长度;
URG <<= 5; ACK <<= 4; PSH <<= 3; RST <<= 2; SYN <<= 1; FIN = (byte)(URG + ACK + PSH + RST + SYN + FIN);
tcpbytes[13] = FIN;
BitConverter.GetBytes(窗口大小).CopyTo(tcpbytes, 14);//
BitConverter.GetBytes(校验和).CopyTo(tcpbytes, 16);//校验和
BitConverter.GetBytes(紧急指针).CopyTo(tcpbytes, 18);//
if (选项与填充部分的长度 > 0) 选项与填充部分.CopyTo(tcpbytes, 20);
if (数据部分的长度 > 0) 数据部分.CopyTo(tcpbytes, 20 + 选项与填充部分的长度);
//下面是计算校验和
byte[] 伪报文 = new byte[12 + (TCP总长度 + 1) / 2 * 2];//如果不够偶数个字节要补上0
BitConverter.GetBytes(源IP).CopyTo(伪报文, 0);//填入源端口号
BitConverter.GetBytes(目的IP).CopyTo(伪报文, 4);//填入目的端口号
伪报文[8] = 0;//填充0.网络字节顺序
伪报文[9] = 6;//和TCP协议号
TCP总长度 = (ushort)IPAddress.HostToNetworkOrder((Int16)TCP总长度);//网络字节顺序
BitConverter.GetBytes(TCP总长度).CopyTo(伪报文, 10);//填入总长度
tcpbytes.CopyTo(伪报文, 12);
//string ss = 网络字节串(伪报文 );//用于调试检测
unsafe
{
fixed (byte* pt = 伪报文)
{
ushort* pp = (ushort*)pt;
校验和 = 计算校验和(pp, 伪报文.Length / 2);
}
}
//校验和 = (ushort)IPAddress.HostToNetworkOrder((Int16)校验和);
BitConverter.GetBytes(校验和).CopyTo(tcpbytes, 16);//校验和
return tcpbytes;
}
unsafe static UInt16 计算校验和(UInt16* buffer, int size)
{
Int32 cksum = 0;
int counter;
counter = 0;
while (size > 0)
{
UInt16 val = buffer[counter];
cksum += Convert.ToInt32(buffer[counter]);
counter += 1;
size -= 1;
}
cksum = (cksum >> 16) + (cksum & 0xffff);
cksum += (cksum >> 16);
return (UInt16)(~cksum);
}
校验和不需要调整为网络字节顺序,不知道为什么,可能是调不调整校验的结果都一样吧