• TCP包头html
ACK为1时,确认序号有效,表示指望收到的下一个序号,是上次成功收到的字节序加1。算法
SYN, FIN都占用一个序号。shell
• TCP链接的创建promise
client经过connect()来创建TCP链接,connect()会发送SYN报文;缓存
server经过bind()、listen()、accept()来接受一个TCP链接,listen()会处理三次握手。网络
SYN报文中会指明滑动窗口的初始大小,滑动窗口是TCP接收方的流控,代表了当前时刻接受方能够接收的数据大小。数据结构
SYN报文一般附带TCP选项,其中有 MSS大小,发送方会用接受方的MSS来分割数据。并发
• TCP链接的停止dom
发送方经过close()发送FIN报文,接收方收到FIN以后会传给应用程序,应用程序将其看做为EOF,使得read()/recv()返回0。以后,一般接收方也会调用close(),因而也发送一个FIN报文。异步
被动收到FIN的一方在发送本身的FIN以前还能够发送数据给先主动发送FIN的一方,这种成为half-close,更多信息参考shutdown()。
应用程序显明的调用close()会发送FIN报文(前提是文件描述符的引用参考已经递减为0)。另外,应用程序意外退出的时候(好比被kill掉),内核会关闭文件描述符,也会发送FIN报文。
• TCP链接的数据交互
TCP数据报文发送以后,启动一个定时器,等待接收ACK报文,若是超时,则从新发送。TCP协议会动态计算RTT(往返时间),该值用于对超时的判断。
收到对方发来的数据以后,接收方不会立刻恢复ACK报文,而是延后必定时间(通常200ms),若此时接收方也有数据要回复给对方,ACK报文就会和数据报文一块儿发送,这种叫作捎带延迟的ACK报文发送。
TCP创建在IP之上,因此到达的数据可能会失序。TCP会对收到的数据从新排序,再交给应用程序。
IPv4的主机和路由都有可能对数据包进行分片,IPv6中只有可能主机对数据包进行分片。分片发生在当须要发送的数据报文的长度超过了链路上的MTU(Maximum transmission unit)时。IPv4头中的DF(don’t fragment)标志位能够用于阻止主机或者路由对数据包进行分片。MSS的值一般是MTU减去TCP头再减去IP头再减去以太网头,一般对于IPv4来讲就是1460,对于IPv6来讲就是1440。MSS的目的就是防止TCP的分片,可是IPv4的中间路由有可能会形成分片的。
• TCP状态机
解释一下TIME_WAIT状态,主动调用close()的一方在发送FIN报文以后进入FIN_WAIT_1状态,若是收到了对方回复的ACK报文而且也收到了对方发来的FIN报文以后就会进入TIME_WAIT状态。
停留在TIME_WAIT状态的时间为2MSL,MSL是maximum segment lifetime,BSD实现中的数值为30秒。IP头里面有TTL,MSL就是TTL为255级时报文也不会超过的最大生存时间。
须要TIME_WAIT的缘由一是由于回复给对方FIN的ACK报文可能会丢失,从而使得对方再一次发送FIN报文,如果TCP链接立刻退至CLOSED状态,对于第二次到来的FIN就会发送RST报文。
第二个缘由是让TCP连接expire掉,由于网络上可能还有残留的旧的TCP连接的数据,这些数据都要做废,2个MSL是由于有两个方向的数据做废时间,在TIME_WAIT结束之前,旧的TCP占用的端口号不能使用。
• TCP滑动窗口
接收窗口
接收方滑动窗口的左面是已确认的序号,窗口内部是可以接收的序号,右边是不能接收的序号。
窗口合拢:接收方判断某个序号之前的报文都已经收到,将该序号的报文移至接收缓冲区,并回复ACK报文,滑动窗口的左边缘相右合拢。
窗口张开:当应用进程从接收缓冲区中取出数据,滑动窗口的右边缘向右扩张。
若窗口的大小为0,代表缓存已满,当应用程序从缓存中取走数据后,接收方会宣告更大的窗口,发送方才能发送数据。接收方宣告的滑动窗口的大小和接收方的接收缓冲区有关,能够经过SO_RCVBUF来调节接收缓冲区的大小。
发送窗口
接收方宣告本身的接收窗口的大小,发送方以此做为发送窗口的大小。
发送窗口左面是已发送已确认的报文,发送窗口内的左半部是已发送但未确认的报文,发送窗口内的有半部是能够发送可是尚未发送的报文,发送窗口右面的不能够发送。
发送方获得接收方的ACK以后,发送窗口会右移。
发送方还有一个拥塞窗口的限制,用于避免网络拥塞。实际可以发送的数据大小为发送窗口和拥塞窗口二者的最小值。
接收缓冲区能够经过/proc/sys/net/ipv4/tcp_rmem查看和修改
发送缓冲区能够经过/proc/sys/net/ipv4/tcp_wmem查看和修改
• TCP超时重传
超时的时间判断并不固定,而是根据网络情况时时跟新的,TCP会测量往返时间RTT,并经过均值方差等运算求出RTO(下一次超时时间)。
超时以后TCP会重传,每一的RTO为上一次的两倍,超过必定重传次数以后,再也不重发,认为TCP连接已断。
• TCP慢启动与拥塞避免
TCP中有两个参数cwnd(拥塞窗口大小)和ssthtresh(慢启动门限)
慢启动算法时,cwnd呈指数增长;拥塞避免算法时,cwnd呈线性增长。
发送方可以发送的数据上限为发送窗口和cwnd的最小值。
拥塞窗口cwnd是发送方的流控,而发送窗口(由接收方的通告)是是接收方的流控。
慢启动和拥塞避免在一块儿实现,经过与慢启动门限ssthresh比较判断使用慢启动仍是拥塞避免算法。
1.初始化 cwnd 为 1 个报文段,ssthresh 为 65535 个字节。
2.TCP 输出例程的输出不能超过 cwnd 和接收方通告窗口的大小。
3.当拥塞发生时(超时或收到重复ACK),ssthresh 被设置为当前窗口大小的一半(cwnd 和接收方通告窗口大小的最小值,但最少为 2 个报文段)。若是是超时引发了拥塞,则 cwnd 被设置为 1 个报文段。
4.当新的数据被对方确认时,就增长 cwnd,但增长的方法取决于正在进行慢启动或拥塞避免算法。若是 cwnd 小于或等于 ssthresh,则进行慢启动,不然正在进行拥塞避免。慢启动一直持续到咱们回到当拥塞发生时所处位置的一半时候才中止,而后转为执行拥塞避免。
总之,拥塞窗口比较小的时候启用慢启动算法,较大的时候启用拥塞避免算法。
拥塞窗口是发送方的流量控制,而接收窗口则是接收方的流量控制。前者是发送方对网络拥塞的估 计,后者则与接收方的缓存大小有关。
• TCP快速重传与快速恢复
发送方收到三个重复的ACK报文以后认为丢包,从而不等的超时而立刻重传;只收到一个或两个重复的ACK报文被认为只是由于网络传输的无序致使的。
因为不需等到重传定时器超时,因此叫作快速重传,重传之后拥塞窗口采用拥塞避免算法,这又叫作快速恢复算法。
• TCP Nagle算法
Nagle算法是为了尽量发送大块数据,避免网络中充斥着许多小数据块。
Nagle算法的基本定义是任意时刻,发送方最多只能有一个未被确认的小段。小段是小于MSS的报文,如有其余小段须要发送,则要等待ACK到来。Nagle算法带来延迟,禁用可加上TCP_NODELAY选项。
• TCP定时器
坚持定时器
接收方发送0窗口通告则发送方不能发送数据直到接收方发送非0窗口,非0窗口一般在一个不含数据的ACK中,若是这个ACK掉了,则没有确认和重传机制。因此发送方有一个坚持定时器,周期性查询接受方的窗口是否增大。
保活定时器
发送接收双方长时间不传输数据,可是也要知道对方是否还存在,因此利用保活定时器来探寻。
创建链接定时器
Connect以后必定时间没有对SYN的ACK报文,则中止尝试。
重传定时器
根据RTT的测量有关
延迟ACK定时器
ACK不立刻回复,和数据发送。
FIN_WAIT_2定时器
在收到FIN的ACK以后由FIN_WAIT_1变为FIN_WAIT_2,等待对方的FIN,假设不使用TCP半打开,必定时间后关闭链接
TIME_WAIT定时器
收到对方FIN以后发送了ACK,等待必定时间。这一是为了防止对方的FIN发现超时并重发了,二是为了使旧的TCP连接上的数据无效。定为2MSL能够保证数据无效,由于最大TTL也生存不了这么长时间。
• TCP控制块
每种TCP状态都有一个控制块pcb的链表,好比有处于监听状态的pcb链表、有处于稳定(established)状态的pcb链表。每一个TCP连接用一个或多个pcb来描述,而且随着状态的变化,pcb可能挂在不一样的链表上。内核每收到一个TCP报文,会判断它是属于哪一个pcb的,并作相应处理;如果不属于任何pcb,则回复RST报文。
根据滑动窗口,内核会维护几种报文链表,好比unsend链表,unacked链表,ooseq链表(接受的无序链表)。在接收的时候,报文并不能确保必定是按顺序到来,因此收到报文的序号并不必定等于接收方以前发送的ACK,那这个报文就挂在ooseq上,后面收到的TCP报文也按顺序插在这个链表上。当等于接收方以前发送的ACK的那个序号来临时,可能使ooseq上的报文变得有序,从而能够交给上层(原先已经正确传输的包不用再传,这是SACK?)。
注:摘自LWIP
• TCP优化
最小化报文传输的延时,禁用Nagle算法
最小化系统调用的负载,减小socket系统调用
调节 TCP 窗口,socketopt
throughput = window_size / RTT
window_size 最好等于或大于 BDP = link_bandwidth * RTT,可是过大会浪费内存
BDP是Bandwidth Delay Product,用来计算理论上最优的 TCP socket 缓冲区大小
动态优化协议栈(调整proc/sys/net下参数),可是这对整个系统产生影响
• socket应用程序
摘自UNIX Network Programming, Volume 1
• socket地址表示
IPv4地址表示
struct in_addr {
in_addr_t s_addr; /* 32-bit IPv4 address */
/* network byte ordered */
};
struct sockaddr_in {
uint8_t sin_len; /* length of structure (16) */
sa_family_t sin_family; /* AF_INET */
in_port_t sin_port; /* 16-bit TCP or UDP port number */
/* network byte ordered */
struct in_addr sin_addr; /* 32-bit IPv4 address */
/* network byte ordered */
char sin_zero[8]; /* unused */
};
通用socket地址表示
struct sockaddr {
uint8_t sa_len;
sa_family_t sa_family; /* address family: AF_xxx value */
char sa_data[14]; /* protocol-specific address */
};
在bind中就将其转化为通用的socket地址
bind(sockfd, (struct sockaddr *) &serv, sizeof(serv));
由于各个地址类型长度不一样,传递时要指明不一样地址类型的长度。
地址转换
将地址字符串(eg:”192.168.1.2”)转换成地址数据结构
int inet_pton(int family, const char *strptr, void *addrptr);
const char *inet_ntop(int family, const void *addrptr, char *strptr, size_t len);
它们能够同时支持IPv4和IPv6,替换了原先的inet_aton、inet_ntoa和inet_addr
int inet_aton(const char * cp,struct in_addr *inp);
char * inet_ntoa(struct in_addr in);
unsigned long int inet_addr(const char *cp);
• socket()函数
int socket(int domain,int type,int protocol);
AF_KEY是用于IPsec的
AF_ROUTE是和路由有关的
Linux支持PF_PACKET支持对数据链路层的直接访问。
AF_XXX和PF_XXX没有区别
socket()返回的是文件描述符
• bind()函数
int bind(int sockfd,struct sockaddr * my_addr,int addrlen);
bind()指明了所用的地址和端口,不然的话可让内核随机指定地址和端口:
IPv4随机指定地址
struct sockaddr_in servaddr;
servaddr.sin_addr.s_addr = htonl (INADDR_ANY); /* wildcard */
IPv6随机指定地址
struct sockaddr_in6 serv;
serv.sin6_addr = in6addr_any; /* wildcard */
随机指定端口:serv.sin_port = 0;
若要得到随机指定的地址和端口,能够经过getsockname()进行。
• listen()函数
int listen(int sockfd,int backlog);
listen()使得内核能够接收链接到这个socket(IP地址+端口)的TCP连接。它使得TCP状态机从CLOSED转到LISTEN。第二个参数代表内核能够队列缓存多少个输入的连接。
内核会为处于listening状态的socket维护两个队列,一个是已经完成了三次握手的队列(TCP连接处于TCP状态机中的ESTABLISHED状态),一个是尚未完成三次握手的队列(TCP连接处于TCP状态机中的SYN_RCVD状态)。
当三次握手完成后,TCP连接就创建了,将这个成员从未完成队列已到完成队列(accept()是阻塞的,若已完成队列有连接,则返回的已完成队列的首个成员)。未完成队列中的成员有75秒生存时间。listen()的第二个参数指的是这两个队列的成员总数。
如果队列已满,server对新进来的连接不予处理,client的connect()会从新尝试连接。
有一种DOS(denial of service)攻击叫SYN flooding,它是某个clinet疯狂的发送SYN,尝试与server创建连接,那server队列满了以后,正常的lient的连接请求就不能处理了。
• connect()函数
int connect (int sockfd,struct sockaddr * serv_addr,int addrlen);
connect()由client调用,它发送SYN报文,尝试进行TCP连接创建的三次握手。client不用bind(),内核会随机指定一个端口和IP地址。若是一时没有收到对方的回复,connect()会继续尝试三次握手(即发送SYN报文),若是75秒后都没有响应,则返回超时错误。
若是对方没有开启用于server的进程,也就是没有listerning,那对方收到SYN以后会恢复RST报文。
几种出错的case:
子网中没有192.168.1.100
solaris % daytimetcpcli 192.168.1.100
connect error: Connection timed out
server没有开启相应进程
solaris % daytimetcpcli 192.168.1.5
connect error: Connection refused
路由器找不到主机
solaris % daytimetcpcli 192.3.4.5
connect error: No route to host
Normal 0 7.8 磅 0 2 false false false EN-US ZH-CN X-NONE /* Style Definitions */ table.MsoNormalTable {mso-style-name:普通表格; mso-tstyle-rowband-size:0; mso-tstyle-colband-size:0; mso-style-noshow:yes; mso-style-priority:99; mso-style-parent:""; mso-padding-alt:0cm 5.4pt 0cm 5.4pt; mso-para-margin:0cm; mso-para-margin-bottom:.0001pt; mso-pagination:widow-orphan; font-size:10.0pt; font-family:"Times New Roman","serif";}
UDP若要收取ICMP的错误好比(“port unreachable”)的话,须要先connect()。
• accept()函数
int accept(int sockfd,struct sockaddr * addr,int * addrlen);
accept()从已完成队列中取出首个成员并返回新的socket文件描述符,用于表示新的TCP连接。若是已完成队列为空,则accept()会挂起(即sleep)。
第一个参数是listen()用的socket文件描述符,accept()返回的是新的socket文件描述符。通常来讲,server建立一个socket文件描述符,它的生命周期为server的生命周期。accept()返回的socket文件描述符的生命周期在处理完client的连接以后就结束了(经过close())。
这种server处理完一个client的连接以后再从已完成队列中取出下一个client的连接处理。因此,server只能同时处理一个client。若须要作到并发处理clients,则要用到fork()。
• fork()函数
pid_t fork(void);
fork()建立了一个新的进程,它是一次调用,两次返回的。一次返回到parent进程,一次返回到child进程。返回给parent进程的fork()返回值是child进程的pid(process ID),返回给child进程的fork()返回值是0。child进程能够经过getppid()得到parent进程的pid。child进程共享全部在parent进程中打开的文件描述符。
fork()另外一种应用是后跟exec(),shell上就是这种的典型应用。exec()有几种变种,但做用都是加载新的程序,并执行新程序的main函数。但在socket中的应用一般为了并发的处理client,这样的server为:
由于fork()有不一样的返回值,parent进程由于返回值不为0因此就直接运行到close(connfd),以后有循环到了accept处等待下一个连接。child进程会执行if条件内的语句。
这里parent进程close了一次connfd,child进程close了listenfd的缘由是:文件是有参考计数的,即有多少个进程占用了已打开的文件描述符。fork()返回以后,parent、child进程各自占用了一个connfd和listenfd。一次close()只是减小文件的引用计数,直到引用计数为止,才会关闭文件。
若child进程退出了,它成为zombie的状态,并由内核发SIGCHLD信号给其parent进程,而后parent进程处理SIGCHLD信号并经过wait()或waitpid()将zombie状态的child进程的资源释放干净。之因此会有zombie状态的目的是让parent进程获取已退出child进程的信息。若parent进程也退出了,则parent及其下child进程的zombie状态由init进程来处理。
在parent进程中添加对SIGCHLD信号的处理函数:
Signal (SIGCHLD, sig_chld);
void sig_chld(int signo)
{
pid_t pid;
int stat;
pid = wait(&stat);
printf("child %d terminated\n", pid);
return;//will interrupt system calls
}
信号处理函数的返回会中断系统调用,系统调用检测到有中断产生,就返回-1并设置errno为EINTR。但是这个信号处理不该该影响咱们的socket系统调用,因此经常在socket程序能够看到对errno的判断:
for ( ; ; ) {
clilen = sizeof (cliaddr);
if ( (connfd = accept (listenfd, (SA *) &cliaddr, &clilen)) < 0) {
if (errno == EINTR)
continue; /* back to for () */
else
err_sys ("accept error");
}
• close()函数
close(int sockfd);
close()函数将socket文件描述符的引用计数减一,当引用计数为0时关闭socket文件并终止一条TCP连接(即发送FIN报文)。shutdown()也有相似的做用,但不递减文件描述符的引用计数。TCP提供了链接的一端在结束它的发送后还能接收来自另外一端数据的能力,这就是TCP的半关闭(收到对方的FIN包只意味着对方不会再发送任何消息)。
若是不调用close(),系统会用尽文件描述符,更重要的是TCP连接得不到终止。
一般应用程序退出时,内核会帮助关闭还没有关闭文件描述符(内核会帮助发送FIN报文)。
SO_LINGER改变TCP关闭时对 socket缓冲区的残留数据操做的行为。
• write()/read()函数
write()
在阻塞write()的状况下,内核会将应用程序的数据拷贝到TCP发送缓冲区,当发送缓冲区的容量不够时,应用程序进程被挂起,直到应用程序的数据所有拷贝完成以后write()才返回。write()的返回仅代表了应用程序能够从新使用应用程序数据的内存空间。
在非阻塞write()的状况下,若是发送缓冲区有空余,就返回已写入发送缓冲区的数据字节数(称为不足计数);若是发送发送缓冲区根本没有任何空余,则返回EWORLDBLOCK。对于UDP来讲,它其实没有真正的发送缓冲区,只是将应用程序拷贝到内核分配的空间,因此不会有缓冲区不够的说法。
发送以后,数据会保存在TCP的发送缓冲区直到收到对端发来的ACK信号。数据链路层有一个发送队列,当发送队列已满的话,错误会返回到协议栈,可是应用程序并不知晓。
read()
在阻塞read()的状况下,若接收缓冲区中为空,该进程将被挂起。对于UDP来讲,到达一个UDP数据报后唤醒进程(SOCK_DGRAM);对于TCP来讲,只要到达一些数据就会唤醒进程(SOCK_STREAM)。
非阻塞read()的状况下,若接收缓冲区中为空(TCP缓冲区无任何数据或者UDP缓冲区不存在一个UDP包),则返回EWOURLDBLOCK。
TCP包头的push标志指示接收端应尽快将数据提交给应用层。若是send函数提交的待发送数据量较小,例如小于MSS,那么协议层会将该报文中的TCP头部的push字段置为1;若是待发送的数据量较大,须要拆成多个数据段发送时,协议层只会将最后一个分段报文的TCP头部的push字段置1。收到带有push标志的TCP报文会促使read()返回。
参考:http://topic.csdn.net/u/20090428/13/4fd54186-d70a-4ff7-9b57-4af83f225e90.html
TCP异常
write()/read()可以反映TCP连接的异常状况,但这一般是异步的!
在收到对方的FIN报文后,本方的read()就会返回0(0对TCP来讲是EOF,0对UDP来讲是收到一个0长度的报文)。本方也仍然能够调用write(),由于TCP协议是支持半关闭的。但问题是对方发来的FIN多是应用程序主动调用close()来发的,也多是对方应用程序被kill掉由内核调用来发的,若是是后者,本方的发送就会使得对方回复RST,本方就会有errno=ECONNRESET的错误。若是继续对已经收到RST的socket调用write(),本方进程就会收到SIGPIPE,errno=EPIPE。
若是对方的机器挂了(连FIN报文都没发送),本方的先调用write()而后阻塞在read()上,TCP发送了数据以后收不到ACK报文,再尝试了重传12次以后(大约9分钟),read()返回错误,errno= ETIMEDOUT/EHOSTUNREACH/ENETUNREACH。没有调用read()就不返回错误?即便返回了错误,离write()的调用也好久了,因此是异步的。
参考:http://www.cppblog.com/elva/archive/2008/09/10/61544.html
http://www.cnblogs.com/promise6522/archive/2012/03/03/2377935.html
封装读写函数
read()、write()有可能过早返回,为此,编写封装函数,每次读写n个字节。若读写函数返回负数,表示有错误或者被signal打断,此时检查errno的值判断缘由,如果由于EINTR的话,则继续进行。
v\:* {behavior:url(#default#VML);} o\:* {behavior:url(#default#VML);} w\:* {behavior:url(#default#VML);} .shape {behavior:url(#default#VML);} Normal 0 7.8 磅 0 2 false false false EN-US ZH-CN X-NONE /* Style Definitions */ table.MsoNormalTable {mso-style-name:普通表格; mso-tstyle-rowband-size:0; mso-tstyle-colband-size:0; mso-style-noshow:yes; mso-style-priority:99; mso-style-parent:""; mso-padding-alt:0cm 5.4pt 0cm 5.4pt; mso-para-margin:0cm; mso-para-margin-bottom:.0001pt; mso-pagination:widow-orphan; font-size:10.0pt; font-family:"Times New Roman","serif";}
不一样的读写函数
UNIX Network Programming, Volume 1
Linux TCP IP 协议栈分析
LwIP协议栈源码详解
http://topic.csdn.net/u/20090428/13/4fd54186-d70a-4ff7-9b57-4af83f225e90.html
http://www.cppblog.com/elva/archive/2008/09/10/61544.html
http://www.cnblogs.com/promise6522/archive/2012/03/03/2377935.html