TCP协议和socket API 学习笔记

• TCP包头

   22397896_1347896554F32Q.png

ACK为1时,确认序号有效,表示期望收到的下一个序号,是上次成功收到的字节序加1。

SYN, FIN都占用一个序号。

 

• TCP连接的建立

22397896_1347896564rLZA.png

client通过connect()来建立TCP连接,connect()会发送SYN报文;

server通过bind()、listen()、accept()来接受一个TCP连接,listen()会处理三次握手。

SYN报文中会指明滑动窗口的初始大小,滑动窗口是TCP接收方的流控,表明了当前时刻接受方可以接收的数据大小。

SYN报文通常附带TCP选项,其中有        MSS大小,发送方会用接受方的MSS来分割数据。

 

• TCP连接的中止

22397896_13478965749tkR.png

发送方通过close()发送FIN报文,接收方收到FIN之后会传给应用程序,应用程序将其看作为EOF,使得read()/recv()返回0。之后,通常接收方也会调用close(),于是也发送一个FIN报文。

被动收到FIN的一方在发送自己的FIN之前还可以发送数据给先主动发送FIN的一方,这种成为half-close,更多信息参考shutdown()。

应用程序显明的调用close()会发送FIN报文(前提是文件描述符的引用参考已经递减为0)。另外,应用程序意外退出的时候(比如被kill掉),内核会关闭文件描述符,也会发送FIN报文。

 

• TCP连接的数据交互

22397896_1347896586T51d.png

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状态机

22397896_1347896605s37C.png

解释一下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滑动窗口

接收窗口

接收方滑动窗口的左面是已确认的序号,窗口内部是能够接收的序号,右边是不能接收的序号。

22397896_13478966183LTG.png

窗口合拢:接收方判断某个序号以前的报文都已经收到,将该序号的报文移至接收缓冲区,并回复ACK报文,滑动窗口的左边缘相右合拢。

窗口张开:当应用进程从接收缓冲区中取出数据,滑动窗口的右边缘向右扩张。

若窗口的大小为0,表明缓存已满,当应用程序从缓存中取走数据后,接收方会宣告更大的窗口,发送方才能发送数据。接收方宣告的滑动窗口的大小和接收方的接收缓冲区有关,可以通过SO_RCVBUF来调节接收缓冲区的大小。

发送窗口

接收方宣告自己的接收窗口的大小,发送方以此作为发送窗口的大小。

发送窗口左面是已发送已确认的报文,发送窗口内的左半部是已发送但未确认的报文,发送窗口内的有半部是可以发送但是还没有发送的报文,发送窗口右面的不可以发送。

发送方得到接收方的ACK之后,发送窗口会右移。

22397896_13478966325lh0.png

发送方还有一个拥塞窗口的限制,用于避免网络拥塞。实际能够发送的数据大小为发送窗口和拥塞窗口两者的最小值。

接收缓冲区可以通过/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报文被认为只是因为网络传输的无序导致的。

由于不需等到重传定时器超时,所以叫做快速重传,重传以后拥塞窗口采用拥塞避免算法,这又叫做快速恢复算法。

   22397896_13478966471QdO.png

 

• 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

  1. #include    "unp.h"
  2. int main(int argc, char **argv)
  3. {
  4.     int                    sockfd, n;
  5.     char                recvline[MAXLINE + 1];
  6.     struct sockaddr_in    servaddr;
  7.  
  8.     if (argc != 2)
  9.         err_quit("usage: a.out ");
  10.  
  11.     if ( (sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
  12.         err_sys("socket error");
  13.  
  14.     bzero(&servaddr, sizeof(servaddr));
  15.     servaddr.sin_family = AF_INET;
  16.     servaddr.sin_port = htons(13);    /* daytime server */
  17.     if (inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0)
  18.         err_quit("inet_pton error for %s", argv[1]);
  19.  
  20.     if (connect(sockfd, (SA *) &servaddr, sizeof(servaddr)) < 0)
  21.         err_sys("connect error");
  22.  
  23.     while ( (n = read(sockfd, recvline, MAXLINE)) > 0) {
  24.         recvline[n] = 0;    /* null terminate */
  25.         if (fputs(recvline, stdout) == EOF)
  26.             err_sys("fputs error");
  27.     }
  28.     if (n < 0)
  29.         err_sys("read error");
  30.  
  31.     exit(0);
  32. }
  33.  
  34. #include    "unp.h"
  35. #include    <time.h>
  36. int main(int argc, char **argv)
  37. {
  38.     int                    listenfd, connfd;
  39.     socklen_t            len;
  40.     struct sockaddr_in    servaddr, cliaddr;
  41.     char                buff[MAXLINE];
  42.     time_t                ticks;
  43.  
  44.     listenfd = Socket(AF_INET, SOCK_STREAM, 0);
  45.     bzero(&servaddr, sizeof(servaddr));
  46.     servaddr.sin_family = AF_INET;
  47.     servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
  48.     servaddr.sin_port = htons(13);    /* daytime server */
  49.  
  50.     Bind(listenfd, (SA *) &servaddr, sizeof(servaddr));
  51.     Listen(listenfd, LISTENQ);
  52.  
  53.     for ( ; ; ) {
  54.         len = sizeof(cliaddr);
  55.         connfd = Accept(listenfd, (SA *) &cliaddr, &len);
  56.         printf("connection from %s, port %d\n",
  57.              Inet_ntop(AF_INET, &cliaddr.sin_addr, buff, sizeof(buff)),
  58.              ntohs(cliaddr.sin_port));
  59.  
  60.         ticks = time(NULL);
  61.         snprintf(buff, sizeof(buff), "%.24s\r\n", ctime(&ticks));
  62.         Write(connfd, buff, strlen(buff));
  63.  
  64.         Close(connfd);
  65.     }
  66. }

       22397896_1347896665OXj4.png

 

• 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));

因为各个地址类型长度不同,传递时要指明不同地址类型的长度。

             22397896_1347896680912A.png

地址转换

将地址字符串(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);

22397896_1347896692sj78.png

 

• socket()函数

int socket(int domain,int type,int protocol);

22397896_13478967101W1x.png

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。第二个参数表明内核可以队列缓存多少个输入的链接。

             22397896_1347896725IRt0.png

内核会为处于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为:

  1. pid_t pid;
  2. int listenfd, connfd;
  3. listenfd = Socket( ... );
  4. Bind(listenfd, ... );
  5. Listen(listenfd, LISTENQ);
  6. for ( ; ; ) {
  7.     connfd = Accept (listenfd, ... ); /* probably blocks */
  8.     if( (pid = Fork()) == 0) {
  9.        Close(listenfd); /* child closes listening socket */
  10.        doit(connfd); /* process the request */
  11.        Close(connfd); /* done with this client */
  12.        exit(0); /* child terminates */
  13.     }
  14.     Close(connfd); /* parent closes connected socket */
  15. }

     因为fork()有不同的返回值,parent进程因为返回值不为0所以就直接运行到close(connfd),之后有循环到了accept处等待下一个链接。child进程会执行if条件内的语句。

这里parent进程close了一次connfd,child进程close了listenfd的原因是:文件是有参考计数的,即有多少个进程占用了已打开的文件描述符。fork()返回之后,parent、child进程各自占用了一个connfd和listenfd。一次close()只是减少文件的引用计数,直到引用计数为止,才会关闭文件。

     22397896_1347981754k5G1.jpg

若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的话,则继续进行。

  1. ssize_t readn(int fd, void *vptr, size_t n)     /* Read "n" bytes from a descriptor. */
  2. {
  3.     size_t    nleft;
  4.     ssize_t    nread;
  5.     char    *ptr;
  6.  
  7.     ptr = vptr;
  8.     nleft = n;
  9.     while (nleft > 0) {
  10.         if ( (nread = read(fd, ptr, nleft)) < 0) {
  11.             if (errno == EINTR)
  12.                 nread = 0;        /* and call read() again */
  13.             else
  14.                 return(-1);
  15.         } else if (nread == 0)
  16.             break;                /* EOF */
  17.  
  18.         nleft -= nread;
  19.         ptr += nread;
  20.     }
  21.     return(n - nleft);        /* return >= 0 */
  22. }
  23. ssize_t writen(int fd, const void *vptr, size_t n) /* Write "n" bytes to a descriptor. */
  24.  
  25. {
  26.     size_t        nleft;
  27.     ssize_t        nwritten;
  28.     const char    *ptr;
  29.  
  30.     ptr = vptr;
  31.     nleft = n;
  32.     while (nleft > 0) {
  33.         if ( (nwritten = write(fd, ptr, nleft)) <= 0) {
  34.             if (nwritten < 0 && errno == EINTR)
  35.                 nwritten = 0;        /* and call write() again */
  36.             else
  37.                 return(-1);            /* error */
  38.         }
  39.  
  40.         nleft -= nwritten;
  41.         ptr += nwritten;
  42.     }
  43.     return(n);
  44. }

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";}

不同的读写函数

        22397896_1347981818NC4b.png

• 参考

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

转载于:https://my.oschina.net/u/1474965/blog/838598

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值