ARP部分


IP&ICMP部分


UDP部分


TCP部分


 


1. TCP介绍


TCP与UDP都属于传输层,但是与UDP不同的是,TCP是面向连接的,可靠的传输协议。


ps:需要找几篇文章来看看两者的不同和各自的用武之地了,虽然对下面的代码分析之后对何为“面向连接”,何为“可靠”有一个具象的了解,但是不够全面和系统,比如何时采用TCP,何时采用UDP,效果如何,当然还得解释清楚其中的原因所在。


 


2.  TCP首部


TCP数据被封装在一个IP数据报中,如图17 - 1所示。


分析TCP/IP协议栈代码之TCP(STM32平台)_数据


图17 - 2显示TCP首部的数据格式。如果不计任选字段,它通常是20个字节。


分析TCP/IP协议栈代码之TCP(STM32平台)_#define_02



  •  每个TCP段都包含源端和目的端的端口号,用于寻找发端和收端应用进程。这两个值加上IP首部中的源端IP地址和目的端IP地址唯一确定一个TCP连接。有时,一个IP地址和一个端口号也称为一个插口或套接字(socket) 。这个术语出现在最早的TCP规范(RFC793)中,后来它也作为表示伯克利版的编程接口 。插口对或套接字对(socket pair)(包含客户IP地址、客户端口号、服务器 IP地址和服务器端口号的四元组 )可唯一确定互联网络中每个TCP连接的双方。
  • 序号用来标识从TCP发端向TCP收端发送的数据字节流,它表示在这个报文段中的的第一个数据字节。如果将字节流看作在两个应用程序间的单向流动,则 TCP用序号对每个字节进行计数。序号是32 bit的无符号数,序号到达2^32-1后又从0开始。

  • 在TCP首部中有6个标志比特(此处结合下面的状态变迁图是实现的关键之所在)。它们中的多个可同时被设置为 1。我们在这儿简单介绍它们的用法,在随后的章节中有更详细的介绍。
  • URG 紧急指针(urgent pointer)有效(见2 0 . 8节) 。
  • ACK 确认序号有效。
  • PSH 接收方应该尽快将这个报文段交给应用层。
  • RST 重建连接。
  • SYN 同步序号用来发起一个连接。这个标志和下一个标志将在第 18章介绍。
  • FIN 发端完成发送任务。
  • TCP的流量控制由连接的每一端通过声明的窗口大小来提供。窗口大小为字节数,起始于确认序号字段指明的值,这个值是接收端正期望接收的字节。窗口大小是一个 16bit字段,因而窗口大小最大为65535字节。在24 . 4节我们将看到新的窗口刻度选项,它允许这个值按比例变化以提供更大的窗口。
  • 检验和覆盖了整个的TCP报文段:TCP首部和TCP数据。这是一个强制性的字段,一定是由发端计算和存储,并由收端进行验证。 TCP检验和的计算和UDP检验和的计算相似,使用如11 . 3节所述的一个伪首部。
  • 只有当URG标志置1时紧急指针才有效。紧急指针是一个正的偏移量,和序号字段中的值相加表示紧急数据最后一个字节的序号。 TCP的紧急方式是发送端向另一端发送紧急数据的一种方式。我们将在20 . 8节介绍它。
  • 最常见的可选字段是最长报文大小,又称为 MSS (Maximum Segment Size)。每个连接方通常都在通信的第一个报文段(为建立连接而设置 SYN标志的那个段)中指明这个选项。它指明本端所能接收的最大长度的报文段。我们将在 18 . 4节更详细地介绍MSS选项,TCP的其他选项中的一些将在第24章中介绍。从图17 - 2中我们注意到TCP报文段中的数据部分是可选的。我们将在 18章中看到在一个连接建立和一个连接终止时,双方交换的报文段仅有 TCP首部。如果一方没有数据要发送,也使用没有任何数据的首部来确认收到的数据。在处理超时的许多情况中,也会发送不带任何数据的报文段。


        小结:


        TCP提供了一种可靠的面向连接的字节流运输层服务。我们简单地介绍了 TCP首部中的各个字段,并在随后的几章里详细讨论它们。


        TCP将用户数据打包构成报文段;它发送数据后启动一个定时器;另一端对收到的数据进行确认,对失序的数据重新排序,丢弃重复数据; TCP提供端到端的流量控制,并计算和验证一个强制性的端到端检验和。


        许多流行的应用程序如Telnet、Rlogin、FTP和SMTP都使用TCP。


3. TCP连接的建立与终止


3.1 建立连接协议



  1. 请求端(通常称为客户)发送一个 SYN段指明客户打算连接的服务器的端口,以及初  
    始序号(ISN,在这个例子中为1415531521) 。这个SYN段为报文段1。
  2. 服务器发回包含服务器的初始序号的SYN报文段(报文段2)作为应答。同时,将确认  
    序号设置为客户的ISN加1以对客户的SYN报文段进行确认。一个SYN将占用一个序号。
  3. 客户必须将确认序号设置为服务器的 ISN加1以对服务器的SYN报文段进行确认(报文  
    段3) 。


    这三个报文段完成连接的建立。这个过程也称为三次握手( three-way handshake) 。


以下为WireShark的建立连接数据的抓包,其中HTTP为服务器端:


注意:这里PC端的默认MSS是1460(因为是以太网),而STM32端的MSS是1408,这个可以在程序里面修改。


分析TCP/IP协议栈代码之TCP(STM32平台)_字段_03


3.2 连接终止协议


建立一个连接需要三次握手,而终止一个连接要经过 4次握手。如下图所示,过程与建立连接的三次握手过程类似。


分析TCP/IP协议栈代码之TCP(STM32平台)_#define_04


4.  TCP的状态变迁图


ps:跟着箭头走就ok了,当然不会所有的状态变迁都实现,看具体协议栈的实现,下面的代码就只实现了其中的一部分。


分析TCP/IP协议栈代码之TCP(STM32平台)_字段_05


ESTABLISHED状态是连接双方能够进行双向数据传递的状态。


注意:并不是所有的状态变迁都需要实现的,这取决于协议栈的具体实现,但是必须要有至少一条状态回路来保证数据的传输。



----------------------------------以上内容整理于《TCP/IP协议详解:卷1》------------------------------


------------------------------------------以下内容产生于代码及分析--------------------------------------


 



5.  TCP宏定义实现



 与上文中的首部对着看,位置是一一对应的。


 




// ******* TCP *******
//TCP首部长度
#define TCP_HEADER_LEN_PLAIN 20
//源端口位置
#define TCP_SRC_PORT_H_P 0x22
#define TCP_SRC_PORT_L_P 0x23
//目标端口位置
#define TCP_DST_PORT_H_P 0x24
#define TCP_DST_PORT_L_P 0x25
// the tcp seq number is 4 bytes 0x26-0x29
//32位序列号
#define TCP_SEQ_H_P 0x26
//32位确认序列号 ox2a-0x2d
#define TCP_SEQACK_H_P 0x2a

//flags位置,最高两位保留
#define TCP_FLAGS_P 0x2f
// flags: SYN=2 6个标志位
#define TCP_FLAGS_FIN_V 0x01
#define TCP_FLAGS_SYN_V 0x02
#define TCP_FLAGS_PUSH_V 0x08
#define TCP_FLAGS_SYNACK_V 0x12
#define TCP_FLAGS_ACK_V 0x10
#define TCP_FLAGS_PSHACK_V 0x18
// plain len without the options:
//4位首部长度
#define TCP_HEADER_LEN_P 0x2e
//校验和位置
#define TCP_CHECKSUM_H_P 0x32
#define TCP_CHECKSUM_L_P 0x33
//选项起始位置
#define TCP_OPTIONS_P 0x36
//


 


6.  TCP函数实现




 make_tcphead : TCP首部的填充,与IP和UDP等类似,但是TCP加入了握手MSS可选项的设置


 




// make a return tcp header from a received tcp packet
// rel_ack_num is how much we must step the seq number received from the
// other side. We do not send more than 255 bytes of text (=data) in the tcp packet.
// If mss=1 then mss is included in the options list
//
// After calling this function you can fill in the first data byte at TCP_OPTIONS_P+4
// If cp_seq=0 then an initial sequence number is used (should be use in synack)
// otherwise it is copied from the packet we received
void make_tcphead(unsigned char * buf,unsigned int rel_ack_num,unsigned char mss,unsigned char cp_seq)
{
unsigned char i=0;
unsigned char tseq;
while(i<2)
{
buf[TCP_DST_PORT_H_P+i]=buf[TCP_SRC_PORT_H_P+i];
buf[TCP_SRC_PORT_H_P+i]=0; // clear source port
i++;
}
// set source port (http):
buf[TCP_SRC_PORT_L_P]=wwwport;
//序列号和确认序列号的长度为32位
i=4;
// sequence numbers: add the rel_ack_num to SEQACK
//将序列号的值+rel_ack_num之后返回,来完成握手过程
while(i>0)
{
rel_ack_num=buf[TCP_SEQ_H_P+i-1]+rel_ack_num;
tseq=buf[TCP_SEQACK_H_P+i-1];
buf[TCP_SEQACK_H_P+i-1]=0xff&rel_ack_num;
if (cp_seq)
{
// copy the acknum sent to us into the sequence number
buf[TCP_SEQ_H_P+i-1]=tseq;
}
else
{
buf[TCP_SEQ_H_P+i-1]= 0; // some preset vallue
}
rel_ack_num=rel_ack_num>>8;
i--;
}
if (cp_seq==0)
{
// put inital seq number
buf[TCP_SEQ_H_P+0]= 0;
buf[TCP_SEQ_H_P+1]= 0;
// we step only the second byte, this allows us to send packts
// with 255 bytes or 512 (if we step the initial seqnum by 2)
buf[TCP_SEQ_H_P+2]= seqnum;
buf[TCP_SEQ_H_P+3]= 0;
// step the inititial seq num by something we will not use
// during this tcp session:
seqnum+=2;
}
// zero the checksum
buf[TCP_CHECKSUM_H_P]=0;
buf[TCP_CHECKSUM_L_P]=0;

// The tcp header length is only a 4 bit field (the upper 4 bits).
// It is calculated in units of 4 bytes.
// E.g 24 bytes: 24/4=6 => 0x60=header len field
//buf[TCP_HEADER_LEN_P]=(((TCP_HEADER_LEN_PLAIN+4)/4)) <<4; // 0x60
//TCP可选项里面的MSS (Maximum Segment Size)
if (mss)
{
// the only option we set is MSS to 1460:
// 1460 in hex is 0x5B4
buf[TCP_OPTIONS_P]=2;
buf[TCP_OPTIONS_P+1]=4;
buf[TCP_OPTIONS_P+2]=0x05;
buf[TCP_OPTIONS_P+3]=0xb4;
// 24 bytes:
buf[TCP_HEADER_LEN_P]=0x60;
}
else
{
// no options:
// 20 bytes:
buf[TCP_HEADER_LEN_P]=0x50;
}
}


make_tcp_synack_from_syn : 与make_udp_reply_from_request过程类似



void make_tcp_synack_from_syn(unsigned char *buf)
{
unsigned int ck, i = 0;
make_eth(buf);
// total length field in the IP header must be set:
// 20 bytes IP + 24 bytes (20tcp+4tcp options)
buf[IP_TOTLEN_H_P] = 0;
buf[IP_TOTLEN_L_P] = IP_HEADER_LEN + TCP_HEADER_LEN_PLAIN + 4;
make_ip(buf);
buf[TCP_FLAGS_P] = TCP_FLAGS_SYNACK_V;
make_tcphead(buf, 1, 1, 0);
#ifdef TCP_DEBUG
i = 0;
printf("TCP Server Test. \r\n");
printf("tcp客户端的IP地址及端口号 : \r\n");

while(i < sizeof(ipv4_addr))
{
//注意这里我们建立的是UDP Server,输出UDP Client的IP地址
printf("%d", buf[IP_DST_P + i]);

if(i != sizeof(ipv4_addr) - 1)
{
printf(".");
}

i++;
}

i = 0;
//输出pc端的tcp port
printf(":%d \r\n", wwwport);
#endif
// calculate the checksum, len=8 (start from ip.src) + TCP_HEADER_LEN_PLAIN + 4 (one option: mss)
ck = checksum(&buf[IP_SRC_P], 8 + TCP_HEADER_LEN_PLAIN + 4, 2);
buf[TCP_CHECKSUM_H_P] = ck >> 8;
buf[TCP_CHECKSUM_L_P] = ck & 0xff;
// add 4 for option mss:
enc28j60PacketSend(IP_HEADER_LEN + TCP_HEADER_LEN_PLAIN + 4 + ETH_HEADER_LEN, buf);
}


 

 Web_Server函数中的while(1)死循环中的TCP部分,这是个主过程,里面有很多子函数将在下面说明。



/*-----------------tcp port www start, compare only the lower byte-----------------------------------*/
if(buf[IP_PROTO_P] == IP_PROTO_TCP_V && buf[TCP_DST_PORT_H_P] == 0 && buf[TCP_DST_PORT_L_P] == mywwwport)
{
/*
以下为 TCP的状态变迁图 的部分实现。
*/
//若为客户端的SYN请求,则返回SYN+ACK
if(buf[TCP_FLAGS_P] & TCP_FLAGS_SYN_V) //第一次握手
{
make_tcp_synack_from_syn(buf); //第二次握手
// make_tcp_synack_from_syn does already send the syn,ack
continue;
}

//若为客户端的ACK请求,即完成三次握手,可以传输数据了
if(buf[TCP_FLAGS_P] & TCP_FLAGS_ACK_V) //第三次握手
{
init_len_info(buf); // init some data structures
// we can possibly have no data, just ack:
dat_p = get_tcp_data_pointer();

//无数据,返回ack
if(dat_p == 0)
{
if(buf[TCP_FLAGS_P] & TCP_FLAGS_FIN_V)
{
// finack, answer with ack
make_tcp_ack_from_any(buf);
}

// just an ack with no data, wait for next packet
continue;
}

//有数据,好了,下面就是HTTP协议规定的数据了
if(strncmp("GET ", (char *) & (buf[dat_p]), 4) != 0)
{
// head, post and other methods:
//
// for possible status codes see:
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
plen = fill_tcp_data_p(buf, 0, PSTR("HTTP/1.0 200 OK\r\nContent-Type: text/html\r\n\r\n<h1>200 OK</h1>"));
goto SENDTCP;
}

//密码部分
if(strncmp("/ ", (char *) & (buf[dat_p + 4]), 2) == 0)
{
plen = fill_tcp_data_p(buf, 0, PSTR("HTTP/1.0 200 OK\r\nContent-Type: text/html\r\n\r\n"));
plen = fill_tcp_data_p(buf, plen, PSTR("<p>Usage: "));
plen = fill_tcp_data(buf, plen, baseurl);
plen = fill_tcp_data_p(buf, plen, PSTR("password</p>"));
goto SENDTCP;
}

cmd = analyse_get_url((char *) & (buf[dat_p + 5]));

// for possible status codes see:
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
if(cmd == -1)
{
plen = fill_tcp_data_p(buf, 0, PSTR("HTTP/1.0 401 Unauthorized\r\nContent-Type: text/html\r\n\r\n<h1>401 Unauthorized</h1>"));
goto SENDTCP;
}

if(cmd == 0) // 用户程序
{
GPIO_SetBits(GPIOA, GPIO_Pin_8);
i = 0; // 命令 = 1
}

if(cmd == 1) // 用户程序
{
GPIO_ResetBits(GPIOA, GPIO_Pin_8);
i = 1; // 命令 = 0
}

// if (cmd==-2) or any other value
// just display the status:
plen = print_webpage(buf, (i));
SENDTCP:
make_tcp_ack_from_any(buf); // send ack for http get
make_tcp_ack_with_data(buf, plen); // send data
continue;
}
}

/*-------------------------------------- tcp port www end ---------------------------------------*


主过程中的各TCP相关的子函数



// do some basic length calculations and store the result in static varibales
void init_len_info(unsigned char *buf)
{
//IP Packet长度
info_data_len = (buf[IP_TOTLEN_H_P] << 8) | (buf[IP_TOTLEN_L_P] & 0xff);
//减去IP首部长度
info_data_len -= IP_HEADER_LEN;
//TCP首部长度,因为TCP协议规定了只有四位来表明长度,所需要如下处理,4*6=24
info_hdr_len = (buf[TCP_HEADER_LEN_P] >> 4) * 4; // generate len in bytes;
//减去TCP首部长度
info_data_len -= info_hdr_len;

if(info_data_len <= 0)
{
info_data_len = 0;
}
}

// get a pointer to the start of tcp data in buf
// Returns 0 if there is no data
// You must call init_len_info once before calling this function
unsigned int get_tcp_data_pointer(void)
{
if(info_data_len)
{
//在buf中数据开始的位置
return((unsigned int)TCP_SRC_PORT_H_P + info_hdr_len);
}

else
{
return(0);
}
}

// fill in tcp data at position pos. pos=0 means start of
// tcp data. Returns the position at which the string after
// this string could be filled.
unsigned int fill_tcp_data_p(unsigned char *buf, unsigned int pos, const unsigned char *progmem_s)
{
char c;

// fill in tcp data at position pos
//
// with no options the data starts after the checksum + 2 more bytes (urgent ptr)
while((c = pgm_read_byte(progmem_s++)))
{
buf[TCP_CHECKSUM_L_P + 3 + pos] = c;
pos++;
}

return(pos);
}

// fill in tcp data at position pos. pos=0 means start of
// tcp data. Returns the position at which the string after
// this string could be filled.
unsigned int fill_tcp_data(unsigned char *buf, unsigned int pos, const char *s)
{
// fill in tcp data at position pos
//
// with no options the data starts after the checksum + 2 more bytes (urgent ptr)
while(*s)
{
buf[TCP_CHECKSUM_L_P + 3 + pos] = *s;
pos++;
s++;
}

return(pos);
}

// Make just an ack packet with no tcp data inside
// This will modify the eth/ip/tcp header
void make_tcp_ack_from_any(unsigned char *buf)
{
unsigned int j;
make_eth(buf);
// fill the header:
buf[TCP_FLAGS_P] = TCP_FLAGS_ACK_V;

if(info_data_len == 0)
{
// if there is no data then we must still acknoledge one packet
make_tcphead(buf, 1, 0, 1); // no options
}

else
{
make_tcphead(buf, info_data_len, 0, 1); // no options
}

// total length field in the IP header must be set:
// 20 bytes IP + 20 bytes tcp (when no options)
j = IP_HEADER_LEN + TCP_HEADER_LEN_PLAIN;
buf[IP_TOTLEN_H_P] = j >> 8;
buf[IP_TOTLEN_L_P] = j & 0xff;
make_ip(buf);
// calculate the checksum, len=8 (start from ip.src) + TCP_HEADER_LEN_PLAIN + data len
j = checksum(&buf[IP_SRC_P], 8 + TCP_HEADER_LEN_PLAIN, 2);
buf[TCP_CHECKSUM_H_P] = j >> 8;
buf[TCP_CHECKSUM_L_P] = j & 0xff;
enc28j60PacketSend(IP_HEADER_LEN + TCP_HEADER_LEN_PLAIN + ETH_HEADER_LEN, buf);
}

// you must have called init_len_info at some time before calling this function
// dlen is the amount of tcp data (http data) we send in this packet
// You can use this function only immediately after make_tcp_ack_from_any
// This is because this function will NOT modify the eth/ip/tcp header except for
// length and checksum
void make_tcp_ack_with_data(unsigned char *buf, unsigned int dlen)
{
unsigned int j;
// fill the header:
// This code requires that we send only one data packet
// because we keep no state information. We must therefore set
// the fin here:
buf[TCP_FLAGS_P] = TCP_FLAGS_ACK_V | TCP_FLAGS_PUSH_V | TCP_FLAGS_FIN_V;
// total length field in the IP header must be set:
// 20 bytes IP + 20 bytes tcp (when no options) + len of data
j = IP_HEADER_LEN + TCP_HEADER_LEN_PLAIN + dlen;
buf[IP_TOTLEN_H_P] = j >> 8;
buf[IP_TOTLEN_L_P] = j & 0xff;
fill_ip_hdr_checksum(buf);
// zero the checksum
buf[TCP_CHECKSUM_H_P] = 0;
buf[TCP_CHECKSUM_L_P] = 0;
// calculate the checksum, len=8 (start from ip.src) + TCP_HEADER_LEN_PLAIN + data len
j = checksum(&buf[IP_SRC_P], 8 + TCP_HEADER_LEN_PLAIN + dlen, 2);
buf[TCP_CHECKSUM_H_P] = j >> 8;
buf[TCP_CHECKSUM_L_P] = j & 0xff;
enc28j60PacketSend(IP_HEADER_LEN + TCP_HEADER_LEN_PLAIN + dlen + ETH_HEADER_LEN, buf);
}


TCP比UDP要复杂的很多,这是由两者的特性所决定的,需要再找点文章来消化下两者的不同;还有一些HTTP相关的子函数,属于应用层的东东,加油,加油看了~~~

7.  TCP实验


串口现象:


分析TCP/IP协议栈代码之TCP(STM32平台)_数据_06


浏览器现象:


分析TCP/IP协议栈代码之TCP(STM32平台)_#define_07


分析TCP/IP协议栈代码之TCP(STM32平台)_数据_08