分类 TCP/IP 下的文章

TCP滑动窗口和流控

TCP的滑动窗口是一个很重要的概念,也是很晦涩的一个知识点。下面就大概介绍下TCP滑动窗口为什么出现?它是怎么工作的的?

什么是TCP窗口

首先,要理解,client和server各自协议栈都有自己的buffer,应用层读写数据的源都是协议栈buffer里。以接收端为例,应用程序调用read()时,会从buffer里移走数据到用户空间,应用程序读的速度越快(read(1024)必然比read(1)要快),那么buffer里的内容消费的越快,buffer也会越空。那么TCP就可以告诉client,我现在很闲,你可以发送更多的数据来。"更多"是多少?这就说窗口,窗口就是量化接收端和服务端当前能处理数据的能力。

TCP窗口是如何工作的

client和server端建立连接后,client会告诉server,自己的"接收窗口"大小(自己能接收多少的数据,受上面所说的buffer影响),server端接收到client的"接收窗口"大小,就会变成server端自己的"发送窗口"大小。同样的,server端告诉client自己的"接收窗口"大小,就会变成客户端的"发送窗口"大小。

为了理解TCP的窗口大小是怎么样变化的,我们先需要理解它的含义。最简单的方式就是认为窗口大小"意味着接收方能接收数据的大小",这也是说接收端设备再应用程序读取buffer中数据之前,能从对端连接处理多少数据。比如说server端窗口大小是360,那么就意味着server端一次只能从客户端接收不超过360bytes的数据。当server端收到数据,它会将数据放到buffer里,然后server端必须对这份数据做两件事

1. server端必须发送一个 ACK 到client端来确认数据已经收到
2. server端必须处理这份数据,把它交给对应的应用程序

要区分上面两件事对理解窗口很重要,接收方收到数据后会确认,但是数据并不一定是里面就是从buffer里取出的,这是受应用层逻辑控制的。所以很有可能如果接收数据过快,而取出数据更慢,就会导致buffer满。一旦这种情况发生,窗口大小就开始调整来防止接收方负载过高。

正是因为窗口大小的调整可以用来调节数据传输的速率,所以就可以实现TCP的流控,在传输层的流控就是典型的例子,流控对于TCP的通信是很重要的,通过增大或者减小窗口的大小,client和server各自确保彼此设备发数据和收数据平衡。

- 阅读剩余部分 -

理解TIME_WAIT(2)

在前面一篇文章理解TIME_WAIT解释了TIME_WAIT出现的原因以及场景,TIME_WAIT出现在socket主动close()端。好了,现在问题来了

1. 为什么那些web server,比如Nginx和Apache上还是能看到很多TIME_WAIT?
2. Web服务器上提供服务一般都是一个端口,为什么这个端口上出现TIME_WAIT还是能正常服务?

下面动手做个小测试来学习

- 阅读剩余部分 -

理解TIME_WAIT

前言

TIME_WAIT 是在TCP协议中很模糊的概念,它可能使socke能陷入的一种时间相对比较长的状态,过多的TIME_WAIT会影响新socket的建立。TIME_WAIT为什么会存在?它的作用又是什么?下面我们就来理解下TIME_WAIT。

这张图详细的列出了TCP建立连接和断开连接的各个TCP状态之间的转换。红色的代表server,蓝色的代表client。下面列出各自的TCP状态转换条件

TCP建立连接

  1. Client: 向server发送 SYN 包,表示请求建立连接,进入 SYN_SENT 状态;
  2. Server: 接收来自client的 SYN 包,发送 SYN/ACK 包,代表client->server单向tcp连接已经建立, 进入 SYN_RCVD 状态;
  3. Client: 接收到来自server的 SYN/ACK 包,发送给server ACK 包,进入 Established 状态;
  4. Server: 收到client的 ACK 包,代表 server->client 的单向tcp连接也建立,此时进入 Established 状态;

- 阅读剩余部分 -

CRC 校验的基本过程

采用 CRC 校验时,发送方和接收方用同一个生成多项式 g(x) , g(x) 是一个 GF(2) 多项式,并且 g(x) 的首位和最后一位的系数必须为 1 。

CRC 的处理方法是:发送方用发送数据的二进制多项式 t(x) 除以 g(x) ,得到余数 y(x) 作为 CRC 校验码。校验时,以计算的校正结果是否为 0 为据,判断数据帧是否出错。设生成多项式是 r 阶的(最高位是 x^r )具体步骤如下面的描述。

发送方:

1 )在发送的 m 位数据的二进制多项式 t(x) 后添加 r 个 0 ,扩张到 m+ r 位,以容纳 r 位的校验码,追加 0 后的二进制多项式为  T(x) ;

2 )用 T(x) 除以生成多项式 g(x) ,得到 r 位的余数 y(x) ,它就是 CRC 校验码;

3 )把 y(x) 追加到 t(x) 后面,此时的数据 s(x) 就是包含了 CRC 校验码的待发送字符串;由于 s(x) = t(x) y(x) ,因此 s(x) 肯定能被 g(x) 除尽。

接收方:

1 )接收数据 n(x) ,这个 n(x) 就是包含了 CRC 校验码的 m+r 位数据;

2 )计算 n(x) 除以 g(x) ,如果余数为 0 则表示传输过程没有错误,否则表示有错误。从 n(x) 去掉尾部的 r 位数据,得到的就是原始数据。

accept()函数

今天在优化Web服务器的时候发现了一个问题,抓包的时候看了下,这是chrome处理的过程,发起的连接

一看已经有很多次三次握手了,那不是有很多个连接了。但是我的web服务器是单线程的啊?并且是阻塞

的,按理说每次都应该阻塞在accept那里,为什么会建立这么多次连接了呢?

仔细想想,协议栈里面是存在一个请求队列的,listen()函数就是把请求的socket放到队列中,而accept呢就

是从队列中取出一个socket,然后返回套接字的标识,该sock就从队列中删除了。所以上面wireshark抓出来

的包有这么多次连接是正常。

自己动手学TCP/IP–http协议(http报文格式)

HTTP(HyperText Transport Protocol,超文本传送协议)

HTTP请求报文


http请求数据包的格式:头部(request line + header)+  数据(data)

头部和数据包体通过一个空行来隔开,头部的格式主要包括请求行+请求头部。如下图

HTTP请求头

请求行


请求行由请求方法字段URL字段HTTP协议版本字段3个字段组成,它们用空格分隔如:

GET /index.html HTTP/1.1。

HTTP协议的请求方法有GET、POST、HEAD、PUT、DELETE、OPTIONS、TRACE、CONNECT。这里介绍最常用的GET方法和POST方法。

GET方式:在URL里面就说明要请求的资源,URL里面包含参数,“?”后面就是参数,而“?”前面就是URL的结束。“?ip=192.168.156.11&active=on”这种就是GET方式的包,而服务器把客户端请求的内容在数据段里面发回给客户端。

POST方式:传输的数据不在URL里面出现,而是在数据段里面出现。但是请求头部多了Content-Type和Content-Length两个字段。

请求头部


请求头部由(关键字:<空格>值)对组成,每行一对,关键字和值用英文冒号“:<空格>”分隔。请求头部通知服务器有关于客户端请求的信息,典型的请求头有:

User-Agent:产生请求的浏览器类型。

Accept:客户端可识别的内容类型列表。

Host:请求的主机名,允许多个域名同处一个IP地址,即虚拟主机。

下面是GET包的一个例子:传输的数据在URL里

再看看POST包的例子:传输的数据在数据段里面


HTTP响应报文


HTTP响应也由两个个部分组成,分别是:响应头(状态行+消息报头)+响应正文

状态行格式如下:

HTTP-Version Status-Code Reason-Phrase CRLF

HTTP-Version表示服务器HTTP协议的版本;Status-Code表示服务器发回的响应状态代码;Reason-Phrase表示状态代码的文本描述。状态代码由三位数字组成,第一个数字定义了响应的类别,且有五种可能取值。

1xx:指示信息--表示请求已接收,继续处理。

2xx:成功--表示请求已被成功接收、理解、接受。

3xx:重定向--要完成请求必须进行更进一步的操作。

4xx:客户端错误--请求有语法错误或请求无法实现。

5xx:服务器端错误--服务器未能实现合法的请求。

下面是http响应包的例子

自己动手学TCP/IP--tftp协议

TFTP(Trivial File Transfer Protocol,简单文件传输协议

1.tftp的服务端口号是69

2.tftp是基于udp协议的

3.tftp是明文传输的,是一种比较轻量型的协议,一般用于bootloader加载内核

TFTP工作流程

服务端开启tftp服务,tftp是一种stand_alone服务,不是常驻内存的,是在有需要的时候才去调用的。首先,客户端发送一个读(RRQ:2个字节)或者写(WRQ:2个字节)的请求,数据包的目标端口是69。对于读或者写的报文格式如下:

RRQ/WRQ(2个字节)+文件名(N字节)+0(1字节)+模式(N字节)+0(1字节)

目前模式字段主要有2种:netascii,这是8位的ASCII码形式;另一种是octet,这是8位源数据类型。对于netascii是把回车和换行(CR/LF)解释成两个字节的。可以查看http://www.firefoxbug.com/?p=1041

tftp-server接收到数据包:如果是发现是读(RRQ),就重新随机分配一个端口,直接发送数据(DATA:2个字节)+块编号(2个字节),然后是0~512字节数据包。客户端接收到数据包,发给服务端(ACK:2个字节)+块编号(2个字节)。如果是普通的数据包,那么数据段的大小一定是512字节,如果是最后一个数据包,肯定是小于512字节的。tftp就是通过发现了一个数据段小于512字节的数据包来声明结束文件的传输了。那么一个要传输的文件刚还是512字节的整数倍怎么办呢?tftp会在最后传输一个数据段大小是0包。

tftp-server接收到数据包:如果发现是写(WRQ),服务端就发回(ACK:2个字节)+(块编号0:2个字节)的包,接着客户端就发送(DATA:2个字节)+(块编号1:2个字节)+数据段给服务端,服务端发回(ACK:2个字节)+(块编号1:2个字节)。。。依次发送。

错误信息是系统自定义的,格式主要是error(2个字节)+错误码(2个字节)+错误信息(N个字节)

下面是tftp数据包的格式图

下面是C语言解析tftp包的一小段代码:

struct tftphdr {
short th_opcode; /* packet type */
union {
unsigned short tu_block; /* block # */
short tu_code; /* error code */
char tu_stuff[1]; /* request packet stuff */
} __attribute__ ((__packed__)) th_u;
char th_data[1]; /* data or error string */
} __attribute__ ((__packed__));

// 解析udp包,packet_buffer是用rawsocket抓出来的以太网包,
void ParseUDPPacket(unsigned char *packet_buffer)
{
struct ethhdr *eth_header;//以太网头
struct iphdr *ip_header; //ip头
struct udphdr *udp_header; //tcp头
eth_header = (struct ethhdr*)packet_buffer;
ip_header = (struct iphdr*)(packet_buffer + sizeof(struct ethhdr));
udp_header = (struct udphdr*)(packet_buffer + sizeof(struct ethhdr) + ip_header->ihl*4);
unsigned char *data = NULL;
data = (packet_buffer + sizeof(struct ethhdr) + ip_header->ihl*4 + 8);//8代表UDP包头
struct tftphdr *tp = (struct tftphdr *)data; // /usr/include/arpa/tftp.h
tftp_print(data,ntohs(udp_header->len)-8); //ntohs(udp_header->len)-8表示udp数据包长度
}

/*
* Print trivial file transfer program requests
*/
void tftp_print(register const u_char *bp, u_int length)
{
register const struct tftphdr *tp;
register const u_char *p;
register int opcode,i;
static char tstr[] = " [|tftp]";
char buffer[520] = {'\0'};
tp = (const struct tftphdr *)bp;

// printf(" %d", length);
// printf("length of tftp_data = %d\n",length);

/* Print tftp request type */

opcode = EXTRACT_16BITS(&tp->th_opcode);
printf(" %s",tok2str(op2str, "tftp-#%d", opcode));
/* Bail if bogus opcode */

switch (opcode) {

case RRQ:
break;

case WRQ:
break;

case ACK:
break;

case DATA:
break;

case ERROR:
break;

default:
/* We shouldn't get here */
printf("(unknown #%d)", opcode);
break;
}
return;
}

详细的可以查看tcpdump的源码。

小议回车和换行

最近写一个网络分析的玩意,用RAW_SOCKET从网卡上面直接取出数据帧,然后解析,为了就是找出tftp包。解析到数据内容的时候,把数据写出到文件里面,是用fprintf()直接写进去的,结果发现每次自己生成的文件都比原来的文件大。比如原文件是100bytes,获取到的文件可能是108字节。

先用diff 查看,发现竟然每行都显示不同,可是我进去vim编辑的时候,看上去都是一样的。

 hexdump  -C 查看,总算知道不一样的地方了。

linux上都是以‘\n’来表示换行的,ascii码是0A,表示LR,netascii表示数据都是以成行的ASCII码字符组成的,以两个字节---回车字符后换行(CR/LF,0D/0A)表示行结束。这两个行结束的字符在这种格式和本地主机使用的行定界之间进行转化。而octet则将数据看作8bit一组的字节流而不做解释。

linux下RAW SOCKET

raw socket,即原始套接字,可以接收本机网卡上的数据帧或者数据包,对与监听网络的流量和分析是很有作用的.一共可以有3种方式创建这种socket

1.socket(AF_INET, SOCK_RAW, IPPROTO_TCP|IPPROTO_UDP|IPPROTO_ICMP)发送接收ip数据包

2.socket(PF_PACKET, SOCK_RAW, htons(ETH_P_IP|ETH_P_ARP|ETH_P_ALL))发送接收以太网数据帧

3.socket(AF_INET, SOCK_PACKET, htons(ETH_P_IP|ETH_P_ARP|ETH_P_ALL))过时了,不要用啊

理解一下SOCK_RAW的原理, 比如网卡收到了一个 14+20+8+100+4 的udp的以太网数据帧.

- 阅读剩余部分 -