本文中涉及很多的位运算,如果对位运算不太了解的请看这篇博文《Java中的位运算》。
TCP建立连接时会经历3次授权确认的过程,具体如下:
发送 | 接收 | 标志位 | 序列码 | 确认码 |
---|---|---|---|---|
client | service | SYN = 1 | x | – |
service | client | SYN = 1 ACK = 1 | y | x +1 = x1 |
client | service | ACK = 1 | – | y + 1 = y1 |
其中x为客户端报文段的起始序号 随机生成,y为服务端报文段的起始序号 随机生成。序号取值范围0-65535。为什么需要随机生成起始序号呢?是因为如果不是随机产生初始序列号,黑客将会以很容易的方式获取到你与其他主机之间通信的初始化序列号,并且伪造序列号进行攻击,这已经成为一种很常见的网络攻击手段。
TCP建立连接后传输数据过程如下
重复以上步骤直到数据传递结束,某一段发起断开连接的请求。
发送 | 接收 | 标志位 | 序列码 | 确认码 |
---|---|---|---|---|
client | service | PSH = 1 ACK = 1 | x+3 = x3 | x+3 = x3 |
service | client | ACK = 1 | x3 + 1 = x4 | x4 + data length = x5 |
client | service | ACK = 1 | x5+1 = x6 | x5 |
service | client | ACK = 1 | x6+1 = x7 | x7+ data length = x8 |
– | – | – | – | – |
– | – | – | – | – |
client | service | ACK = 1 | xn+1 | xn |
service | service | ACK = 1 | xn+2 | xn+1 |
其中x为客户端随机的数据初始序号(x+3是因为前面3个序号依次进行了 SYN-SENT(同步已发送)状态、服务器ESTABLISHED(已建立连接)状态,客户端ESTABLISHED(已建立连接)状态。为什么+3后没有+1是因为该报文段只是起到一个通知作用,没有进入到数据序列中).
TCP断开连接时会经历4次申请确认过程,具体如下:
TCP与UDP计算校验和时都需要添加一个12字节的伪首部,其格式为:
TCP/UDP校验和计算的步骤如下:
具体实现代请看文末的完整代码。
TCP/UDP的数据校验与校验和计算大体一样,只是在数据校验时无需将校验和位置为0,最终得到的结果取反后取16位的值(~sum) & 16), 如果该值为零 则校验通过 如果不为零 则校验失败 (( ~sum ) & 16 == 0 ? true : false)
TCP为了实现可靠传输,校验和是不可缺少的,校验不通过会要求服务端数据重传。UDP校验和则可以省略。
public class TCPPacket extends Packet{
private static final String TAG = TCPPacket.class.getSimpleName();
static final int LOCAL_PORT_BIT = 0;
static final int REMOTE_PORT_BIT = 2;
static final int SEQUENCE_BIT = 4;
static final int ACKNOWLEDGEMENT_SEQUENCE_BIT = 8;
static final int HEADER_LENGTH_BIT = 12;
static final int TAG_BIT = 13;
static final int WINDOWS_SIZE_BIT = 14;
static final int CHECK_SUM_BIT = 16;
static final int URG_BIT = 18;
private byte[] pseudoHeader;
public TCPPacket(byte[] bytes , int... parameters){
super(bytes,parameters);
}
/**
* 获取源端口
* 源端口
* 在TCP数据报中第0、1字节 占2字节 共16位
* @return
*/
public int getLocalPort(){
return byteToInt(bytes[LOCAL_PORT_BIT + offset],bytes[LOCAL_PORT_BIT + offset + 1]);
}
/**
* 获取目的端口
* 目的端口
* 在TCP数据报中第2、3字节 占2字节 共16位
* @return
*/
public int getRemotePort(){
return byteToInt(bytes[REMOTE_PORT_BIT + offset],bytes[REMOTE_PORT_BIT + offset + 1]);
}
/**
* 获取序列码
*
* 序列码
* 在TCP数据报中第4、5、6、7字节 占4字节 共32位
* 由初始请求方随机生成,用来标识数据起始位置的编码。
* TCP将应用层发来的数据对每一字节顺序编号
* TCP首部中的序列号是指在本段报文段所携带数据的第一个字节编号
* 应答方收到请求后值+1后 作为确认序列码返回给请求方标识该请求被成功送达
* 序列码和标志位配合使用,以实现不同的状态逻辑
*/
public int getSequence(){
return byteToInt(bytes[SEQUENCE_BIT + offset],bytes[SEQUENCE_BIT + offset + 1],bytes[SEQUENCE_BIT + offset + 2],bytes[SEQUENCE_BIT + offset + 3]);
}
public void setSequence(int seq){
for(int i = 0;i < 4;i ++){
bytes[SEQUENCE_BIT + offset + i] = (byte) (seq >> ((3 - i) * 8));
}
}
/**
* 获取确认码
*
* 确认序列码
* 在TCP数据报中第8、9、10、11字节 占4字节 共32位
* 如果是请求方 则确认码就是序列码
* 如果是应答方 则确认码是收到的确认码+1
* @return
*/
public int getAcknowledgementSequence(){
return byteToInt(bytes[ACKNOWLEDGEMENT_SEQUENCE_BIT + offset],bytes[ACKNOWLEDGEMENT_SEQUENCE_BIT + offset + 1]
,bytes[ACKNOWLEDGEMENT_SEQUENCE_BIT + offset + 2],bytes[ACKNOWLEDGEMENT_SEQUENCE_BIT + offset + 3]);
}
public void setAcknowledgementSequence(int ack){
for(int i = 0;i < 4;i ++){
bytes[ACKNOWLEDGEMENT_SEQUENCE_BIT + offset + i] = (byte) (ack >> ((3 - i) * 8));
}
}
/**
* 获取首部长
* 头部长
* 在TCP数据报中第12字节前4位 占1/2字节 共4位 单位 4 byte
* TCP默认报头长20字节
* @return
*/
public int getHeaderLength(){
return ((bytes[HEADER_LENGTH_BIT + offset] & 0xFF) >> 4 ) * 4;
}
/**
* 获取标志位
*
* 标志位
* 在TCP数据报中第13字节后6位 占2/3字节 共6位
* 从第13字节第3位起 左至右依次表示为 URG(urgent) ACK(acknowledge) PSH(push) RST(reset) SYN(synchronous) FIN(fin)
* URG 紧急标志 说明这个报文中包含需紧急处理的数据 该位值为 1 时紧急指针生效 指向需紧急处理的数据
* ACK 确认标志 说明发送的请求被送达并返回响应
* PSH 推标志 该标志置位时,接收端不将该数据进行队列处理,而是尽可能快地将数据转由应用处理。在处理Telnet或r login等交互模式的连接时,该标志总是置位的。
* RST 复位标志 要求复位相应的TCP连接
* SYN 同步标志 该标志位只在3次握手建立TCP连接时有效 提示TCP连接的服务端检查序列编号
* FIN 结束标志 该标志位只在4次挥手断开连接时有效
*/
public int getTag(){
return bytes[TAG_BIT + offset] & 0x2F;
}
public boolean isURG(){
return (getTag() >> 5) == 1;
}
public boolean isACK(){
return ((getTag() >> 4) & 1) == 1;
}
public boolean isPSH(){
return ((getTag() >> 3) & 1) == 1;
}
public boolean isRST(){
return ((getTag() >> 2) & 1) == 1;
}
public boolean isSYN(){
return ((getTag() >> 1) & 1) == 1;
}
public boolean isFIN(){
return (getTag() & 1) == 1;
}
/**
* 获取窗口大小
* 窗口大小
* 在TCP数据报中第14、15字节 占2字节 共16位
* 用来表示想收到的每个TCP数据段的大小。TCP的流量控制由连接的每一端通过声明的窗口大小来提供
* 窗口大小最大为65535字节
* @return
*/
public int getWindowsSize(){
return byteToInt(bytes[WINDOWS_SIZE_BIT + offset],bytes[WINDOWS_SIZE_BIT + offset + 1]);
}
/**
* 获取紧急指针
* 紧急指针
* 在TCP数据报中第18、19字节 占2字节 共16位
* 用于标识数据报中需紧急处理的数据位置
* @return
*/
public int getUrg(){
return byteToInt(bytes[URG_BIT + offset],bytes[URG_BIT + offset + 1]);
}
/**
* 获取校验和
* 校验和
* 在TCP数据报中第16、17字节 占2字节 共16位
* 用于对TCP数据的校验 由发送端对数据报中所有数据计算得出
* @return
*/
public int getCheckSum(){
return byteToInt(bytes[CHECK_SUM_BIT + offset],bytes[CHECK_SUM_BIT + offset + 1]);
}
/**
* 检查校验和
* @return
*/
public boolean checkSum(){
return (~getSum() & 0xFFFF) == 0;
}
// 获取和
private int getSum(){
int sum = 0;
if(pseudoHeader != null){
for(int i = 0;i < pseudoHeader.length; i += 2){
sum += byteToInt(pseudoHeader[i],pseudoHeader[i + 1]);
}
}
for(int i = offset;i < validLength;i += 2){
sum += byteToInt(bytes[i],bytes[i + 1]);
}
if((validLength - offset) % 2 > 0){
sum += (bytes[validLength - 1] & 0xFF) << 8;
}
while ((sum >> 16) > 0){
sum = (sum >> 16) + (sum & 0xFFFF);
}
return sum;
}
// 初始化伪首部 在数据校验 与 计算校验和之前必须进行初始化
public void initPseudoHeader(int localIP,int remoteIP,int length){
pseudoHeader = new byte[12];
for(int i = 0;i < 4;i ++){
pseudoHeader[i] = (byte) (localIP >> ((3 - i) * 8));
pseudoHeader[i + 4] = (byte) (remoteIP >> ((3 - i) * 8));
}
pseudoHeader[8] = 0 ;
pseudoHeader[9] = 6 ;
pseudoHeader[10] = (byte) (length >> 8);
pseudoHeader[11] = (byte) length;
}
public void refreshCheckSum(){
bytes[CHECK_SUM_BIT + offset] = 0;
bytes[CHECK_SUM_BIT + offset + 1] = 0;
int m = ~getSum();
bytes[CHECK_SUM_BIT + offset] = (byte) (m >> 8);
bytes[CHECK_SUM_BIT + offset + 1] = (byte)m;
}
/**
* 设置本地锻端口
* @return
*/
public void setLocalPort(int port){
bytes[LOCAL_PORT_BIT + offset] = (byte) (port >> 8);
bytes[LOCAL_PORT_BIT + offset + 1] = (byte) port;
}
/**
* 设置远程端口
* @param port
*/
public void setRemotePort(int port){
bytes[REMOTE_PORT_BIT + offset] = (byte) (port >> 8);
bytes[REMOTE_PORT_BIT + offset + 1] = (byte) port;
}
}
注释很简洁,不过配合上面几部分的内容,应该能看懂是什么意思。关于代码中Packet类的代码在《IP/TCP/UDP报文解析(1)IP报文》中有完整代码。
到此TCP的报文格式、连接过程、数据传递过程、断开连接过程、报文的校验、校验和的计算我知道的知识点都在这里了,希望你能有所收获。
信息始发地主机 ↩︎
两个主机建立信息交互时随机分配的未被占用的标识符,用来在本地保存一些信息 如:发出信息的应用或接收后交给那个应用处理,这样当消息送达或是收到回信时,才能知道应该交给那个应用来处理。因为TCP报文中端口位用16位保存,所以端口的取值范围是0 - 65535 ↩︎