Unix网络编程学习笔记

声明
本笔记尽量不涉及SCTP、IPv6以及Unix平台特定的相关知识等.便于学习Winsock的读者阅读.
第一章 简介 
说明:相关内容请阅读本博客关于计算机网络的笔记http://t.cn/zjQjulJ,此处不再赘述.
第二章 传输层:TCP和UDP
用户数据包协议UDP
      用UDP进行网络编程所碰到的问题是 缺乏可靠性,我们也称UDP提供 无连接的(connectionless)服务,因为UDP客户与服务器不必存在长期的关系.
传输控制协议TCP
       TCP提供客户与服务器的连接.一个TCP客户建立与一个给定服务器的连接,并跨该连接与对应服务器交换数据,然后终止连接; TCP提供可靠性.当TCP向另一端发送数据时,它要求对端返回一个确认.如果确认没有收到,TCP自动重传数据并等待更长时间.在数次重传失败后,TCP才放弃.UDP提供不可靠的数据报传送.UDP本身不提供确认、序列号、RTT估算、超时及重传等机制; TCP提供流量控制.TCP总是告诉对端它能够接收多少字节的数据,这称为通告窗口; TCP连接是全双工的,这意味着在给定的连接上应用进程在任何时刻既可以发送也可以接收数据.
TIME_WAIT状态
(1)、实现终止TCP全双工链接的可靠性,即保证tcp连接可靠断开
      解释:必须处理连接终止序列四个分节中任何一个分节丢失的情况(例如,主动关闭一端发送的ACK丢失,则可能再次收到FIN,而重新发送ACK)
(2)、让老的重复分节在网络中消失
      解释:假设在一对IP及端口上有一个连接,关闭这个连接后立即又建立起新的连接,后一个连接为前一个的化身,因为IP和端口相同,TCP必须防止来自某个连接的老分组在连接终止后再现,从而影响新连接的数据交互,所以tcp不能给处于TIME_WAIT状态的连接启动新的化身,从而被误解成属于同一连接.
      至于时间规定为2MSL(最长分节生命期),TCP规定一个分节在网络中最大生存时间是MSL,这足够让某个方向上的分节最多存活MSL秒即被丢弃,另一个方向的应答最多存活MSL秒也被丢弃,通过这个规则,就能保证当成功建立一个TCP连接时,来自该链接的之前所有连接的老的重复分组在网络中已消失.
下图所示处于不同局域网的客户主机和服务器通过广域网连接:
下图所示并发服务器让子进程处理客户:
下图所示第二个客服与同一个服务器连接:
TCP输出 
      每一个TCP套接字有一个发送缓冲区,我们可以使用SO_SNDBUF套接字选项来更改缓冲区的大小.当某个应用进程调用Write函数时,内核从该应用进程的缓冲区复制所有数据到所写套接字的发送缓冲区.如果该套接字的发送缓冲区容不下该进程的所有数据该应用进程将被投入睡眠IF该套接字是阻塞的.内核将不从Write系统调用返回,直到应用进程的缓冲区中的所有数据都复制到套接字发送缓冲区.因此,从写一个TCP套接字的Write调用成功返回仅仅表示我们可以重新使用原来的应用进程缓冲区,并不表明对端的TCP或应用.
下图所示应用进程写TCP套接字时的设计的步骤和缓冲区
UDP输出
      这一次我们以虚线框展示套接字发送缓冲区,因为它实际上并不存在.任何UDP套接字都有发送缓冲区大小,不过它仅仅是可写到该套接字的UDP数据报的大小上限.如果一个应用进程写一个大于套接字缓冲区大小的数据报,内核将返回一个错误.既然UDP是不可靠的,它不必保存应用进程数据的一个副本,因此无需一个真正的发送缓冲区.从写一个UDP套接字的Write调用成功返回表示缩所写的数据报或其所有片段已被加入数据链路层的输出队列.如果该队列没有足够的空间存放该数据报或它的某个片段,内核会返回一个错误给应用进程.下图所示应用进程写UDP套接字时涉及的步骤与缓冲区
端口号
      众所周知的端口:0~1023它们绑定于一些服务,这些服务通常确定了某种协议如HTTP协议端口80; 经注册的端口:1024~49151它们松散的绑定一些服务,可动态分配; 动态和私用端口:49152~65535一般不为服务绑定这些端口,动态分配所用.
第三章 基本套接字编程见解
套接字地址结构
IPv4套接字地址结构通常也称为"网际套接字地址结构".IPv4的POXIS定义如下.
[cpp]  view plain copy
  1. #include<<netinet/in.h>  
  2. typedef uint32_t in_addr_t;  
  3. struct in_addr  
  4. {  
  5.  in_addr_t s_addr;            //即uint32_t s_addr 32位的IPv4网络字节序  
  6. };  
  7. typedef unsigned short int sa_family_t;  
  8. #define __SOCKADDR_COMMON(sa_prefix) sa_family_t sa_prefix##family  //C语言中#的用法  
  9. struct sockaddr  
  10. {  
  11.  __SOCKADDR_COMMON (sa_);     //即unsigned short int sin_family; 地址族  
  12.  char sa_data[14];            //特定的协议地址  
  13. };  
  14. struct sockaddr_in  
  15. {  
  16.  uint8_t sin_len;             //该结构体的长度  
  17.  __SOCKADDR_COMMON (sin_);    //即unsigned short int sin_family;协议族 IPv4为AF_INET  
  18.  in_port_t  sin_port;         //端口号  
  19.  struct     in_addr sin_addr; //32位IPv4网络字节序地址  
  20.  unsigned char sin_zero[sizeof (struct sockaddr) - __SOCKADDR_COMMON_SIZE - sizeof (in_port_t) - sizeof (struct in_addr)]; //填充对齐位  
  21. };  
  22. //一般情况下上述结构体可以具体化如下:  
  23. struct sockaddr_in  
  24. {  
  25.  uint8_t        sin_len;      //带符号8位整数地址结构长度  
  26.  sa_family_t    sin_family;   //协议族,IPv4为AF_INET  
  27.  in_port_t      sin_port;     //端口号  
  28.  struct in_addr sin_addr;     //32位IPv4网络字节序地址  
  29.  char           sin_zero[8];  //填充对齐位  
  30. };  
下图所示IPv4地址结构示意图+POSIX规范要求的数据类型
通用套接字地址结构
      存在通用套接字地址结构的原因是:当作为一个参数传递进任何套接字函数时,套接字地址结构总是以 引用的方式(也就是以指向该结构的指针指针)传递的.然而以这样的指针作为参数之一的任何套接字函数必须处理来自所支持的 任何协议族的套接字地址结构. 不同的协议有不同的套接字地址结构,函数的参数怎么声明这些套接字地址结构的指针类型是一个问题,于是就定义了一个通用套接字地址结构,所有需要套接字地址结构做参数的函数的这个形参都被声明为指向这个通用套接字地址结构的指针的类型.其他套接字地址结构的指针被强制转换为通用套接字地址结构的指针类型. 从应用程序开发的角度来看,这些通用套接字结构的唯一用途就是对指向特定于协议的套接字地址结构的指针执行类型强制转换 通用套接字地址结构体其定义如下:
[cpp]  view plain copy
  1. #include<sys/socket.h>  
  2. struct sockaddr   
  3. {  
  4.   uint8_t      sa_len;      //该结构体的长度  
  5.   sa_family_t  sa_family;   //地址族  
  6.   char         sa_data[14]; //特定的协议地址  
  7. };  
值-结果参数
      一个套接字函数传递一个套接字地址结构时候,该结构总以引用形式来传递,也就是说传递的指向该结构的一个指针,该结构的长度也作为一个参数来传递,不过其传递方式取决于该结构的传递方向:是从进程到内核,还是从内核到进程.如下图所示:

(1)、从进程到内核传递套接字结构函数:bind、connect和sendto.这些函数的一个参数是指向某个套接字地址结构的指针,另一个参数是该结构体的整数大小.例如:
[cpp]  view plain copy
  1. struct sockaddr_in serv; //定义一个套接字地址结构体变量  
  2. connect (sockfd, (struct sockaddr *) &serv, sizeof(serv));  
(2)、从内核到进程传递套接字地址结构的函数:accept、recvfrom、getsockname和getpeername.这4个函数的其中两个参数指向某个套接字结构体的指针和指向表示该结构体大小的整数变量的指针.例如
[cpp]  view plain copy
  1. struct sockaddr_un cli;                          //定义一个套接字地址结构体变量  
  2. socklen_t len = sizeof(cli);                     //该结构体的大小  
  3. getpeername(unixfd,(struct sockaddr *)&cli,&len);//len可能会被改变  
      把套接字地址结构大小这个参数从一个整数改为指向某个整数变量的指针,其原因在于:当函数被调用时,结构大小是一个值,他告诉内核该结构的大小,这样内核在写该结构时不至于越界;当函数返回时,结构大小又是一个结果,它告诉进程内核在该结构中究竟存储了多少信息.
字节排序函数
一、大端字节序:高字节在低地址. 二、小端字节序:低字节在高地址.三、网络字节序:网络字节序采用大端字节序 .四、主机字节序:本地主机使用的字节序,可能为大端或小端
下图所示16位整数的小端字节和大端字节示意图:
      因为套接字地址结构中的某些字段必须按照 网络字节序进行维护,所以必须关注 主机字节序和网络字节序之间的相互转换,这两种字节序之间的转换使用下面的4个函数:
[cpp]  view plain copy
  1. #include <netinet/in.h>  
  2. uint16_t htons(uint16_t host16bitvalue); //将16位主机字节序转化为相应的网络字节序  
  3. uint32_t htonl(uint32_t host32bitvalue); //将32位主机字节序转化为相应的网络字节序  
  4. uint16_t ntohs(uint16_t  net16bitvalue); //将16位网络字节序转化为相应的主机字节序  
  5. uint32_t ntohl(uint32_t  net32bitvalue); //将32位网络字节序转化为相应的主机字节序  
字节操纵函数
[cpp]  view plain copy
  1. #include <strings.h>  
  2. void   bzero(void *dest,size_t nbytes);                         //将字符串初始化为0  
  3. void   bcopy(const void *src,void *dest,size_t nbytes);         //拷贝字符串  
  4. int    bcmp(const void*ptrl,const void *ptr2,size_t nbytes);    //比较两个字符串,若相等则返回0  
  5. void  *memset(void *dest,const void *c,size_t len);             //作用是在一段内存块中填充某个给定的值,它是对较大的结构体或数组进行清零操作的一种最快方法  
  6. void  *memcpy(void *dest,const void *src,size_t nbytes);        //从源src所指的内存地址的起始位置开始拷贝nytes个字节到目标dest所指的内存地址的起始位置中  
  7. int    memcmp(const void *ptrl,const void *ptr2,size_t nbytes); //比较两个字符串的前nbytes个字符,若相等则返回0  
  8.                                                                 //补充说明:当源字节串与目标字节串重叠时,bcopy能够正确处理,但是memcpy的操作结果却不可知.  
inet_aton、inet_addr和inet_ntoa函数
[cpp]  view plain copy
  1. #include<arpa/inet.h>                      
  2. in_addr_t inet_addr(const char *strptr);//若成功,返回32位二进制的网络字节序地址                                        
  3. char *inet ntoa(struct in_addr inaddr); //将一个32位的网络字节序二进制IPv4地址转换成相应的点分十进制数串  
      说明:第一个函数inet_aton将strptr所指的C字符串转换成32位的网络字节序二进制值,并通过指针addrptr来存储.如果成功返回1,否则返回0,inte_addr进行相同的转换,返回值为32位的网络字节序二进制值,一般用inet_aton来代替代替inet_addr.函数inet_ntoa将以结构为参数,而不是指向结构的指针.
一个确定主机字节序即主机是大端还是小端的程序:
[cpp]  view plain copy
  1. #include "unp.h"  
  2. int main(int argc, char **argv)  
  3. {  
  4.  union                                  //注意这是共用体  
  5.  {  
  6.  short  s;  
  7.  char  c[sizeof(short)];  
  8.  }un;  
  9.  un.s = 0x0102;  
  10.  if (sizeof(short) == 2)   
  11.  {  
  12.  if (un.c[0] == 1 && un.c[1] == 2)      //高字节在高地址 低字节在低地址  
  13.   printf("大端\n");  
  14.  else if (un.c[0] == 2 && un.c[1] == 1) //低字节在高地址 高字节在低地址  
  15.   printf("小端\n");  
  16.  else  
  17.   printf("未知\n");  
  18.  }   
  19.  else  
  20.   printf("sizeof(short) = %d\n"sizeof(short));  
  21.  exit(0);  
  22. }  
 readn、writen和readline函数
     字节流套接字上的read和write函数所表现的行为不同于通常的文件I/O.字节流套接字上调用read或write输入或输出的字节数可能比请求的数量少,然而这不是出错状态.这个现象的原因在于内核中用于套接字的缓冲区可能已经达到了极限.此时所需的是调用者再次调用read个write函数,以输入或输出剩余的字节. 我们提供的以下三个函数是每当我们读或写一个字节流套接字时要使用的函数.
[cpp]  view plain copy
  1. //从一个描述符读取n个字节  
  2. ssize_t readn(int fd, void* vptr, size_t n)  
  3. {  
  4.  size_t  nleft = n;  //记录还剩下多少字节数没读取  
  5.  ssize_t nread;      //记录已经读取的字节数  
  6.  char*  ptr = vptr;  //指向要读取数据的指针  
  7.  while(nleft > 0)    //还有数据要读取  
  8.  {  
  9.   if(nread = read(fd,ptr,nleft) < 0)  
  10.    if(erron == EINTR)//系统被一个捕获的信号中断  
  11.     nread = 0;       //再次读取  
  12.    else  
  13.     return -1;       //返回  
  14.   else if(nread == 0)//没有出错但是也没有读取到数据  
  15.    break;            //再次读取  
  16.   nleft -= nread;    //计算剩下未读取的字节数  
  17.   ptr  += nread;     //移动指针到以读取数据的下一个位置  
  18.  }  
  19.  return (n-nleft);   //返回读取的字节数  
  20. }  
  21. /**************************************************************************************************/  
  22. //从一个描述符读文本行,一次一个字节  
  23. ssize_t readline(int fd, void* vptr, size_t maxlen)//一个字节一个字节地读取  
  24. {  
  25.  ssize_t  rc;        //每次读取的字符  
  26.  ssize_t  n;         //读取的次数也即读取字符串的长度  
  27.  char     c;         //  
  28.  char* ptr = vptr;   //指向要读取的数据的指针  
  29.  for(n = 1;n < maxlen; n++)  
  30.  {  
  31.   again:  
  32.   if((rc = read(fd,&c,1)) == 1)  
  33.   {  
  34.    *ptr++ = c;       //移动指针  
  35.    if(c == '\n')     //换行符  
  36.     break;           //跳出循环  
  37.    else if(rc == 0)  //结束  
  38.     *ptr = 0;        //字符串以0结尾  
  39.    return (n-1);     //返回读取的字节数 末尾的0不算  
  40.   }  
  41.   else  
  42.   {  
  43.    if(erron == EINTR)  
  44.     goto again;      //重新读取  
  45.    return (-1)  
  46.   }  
  47.  }  
  48.  *ptr=0;  
  49.  return n;  
  50. }  
  51. /**************************************************************************************************/  
  52. //往一个描述符写n个字节  
  53. ssize_t writen(ind fd, const void* vptr, size_t n)  
  54. {   
  55.  size_t nleft = n;        //还需要写入的字节数   
  56.  ssize_t nwritten;        //每次写入的字节数   
  57.  const char* ptr = vptr;  //指向要写入的数据的指针   
  58.  while(nleft > 0)   
  59.  {   
  60.   if((nwritten = write(fd,ptr,nleft)) <= 0)   
  61.   {   
  62.    if(nwritten < 0 && erron == EINTR)   
  63.     nwritten = 0;   
  64.    else return -1;   
  65.   }  
  66.    nleft -= nwritten;     //计算还需要写入的字节数   
  67.  ptr += nwritten;         //移动数据指针   
  68.   }   
  69.   return n;  
  70.  }  
第四章 基本TCP套接字编程
下图所示基本TCP客户服务器程序的套接字函数:

下面对涉及到的一些基本的函数做一个简要的说明.
[cpp]  view plain copy
  1. #include <sys/socket.h>  
  2. int socket(int family, int type, iny potocol);  
说明:socket函数在成功是返回一个非负整数值,它与文件描述符类似,我们把它称为套接字描述符,简称sockfd.
[cpp]  view plain copy
  1. int connect(int sockfd,const struct  sockaddr* servaddr,socklen_t addrlen);  
      说明:调用connect函数前不必非得要调用bind函数,因为如果需要的话,内核会确定源IP地址,并选择一个临时端口作为源端口.客服在调用connect函数将激发TCP三次握手过程,而且仅在连接建立成功或出错时才返回,其中出错返回可能有以下几种情况:
一、若TCP客户没有收到SYN包的响应,则返回ETIMEDOUT错误.如调用该函数时,内核发送一个SYN,如果无响应则等待6s后再发一个,如果仍无响应,则等待24s再发一个,若总共等了75s后仍未收到响应消息则返回该错误.
二、若客服的SYN响应是RST(表示复位),表明该服务器主机在我们指定的端口上没有进程等待.这是一种硬错误,客户收到RST包后马上返回ECONNREFUSED错误
三、若客户发出的SYN在中间的路由器上引发了一个"目的地不可到达"的ICMP错误,则认为是一种软错误.客服主机内核保存该消息,并按第一种情形所述的时间间隔继续发送SYN.若在某个规定的时间内没有收到回应,则将ICMP错 误作为EHOSTUNREACH或ENETUNREACH错误返回给进程.以下两种情况也是有可能的:一是按照本地系统的转发表,根本没有达到远程系统的路径;而是connect调用根本不等待就返回.
产生RST的三个条件:①、目的地为某端口的SYN到达,然而该端口上没有正在监听的服务器.②、TCP想取消一个已有的连接③、TCP接收到一个根本不存在的连接上的分节
[cpp]  view plain copy
  1. #include<sys/socket.h>  
  2. int bind(int sockfd,const struct sockaddr* myaddr,socklen_t addrlen);  
      说明:bind函数把一个本地协议地址赋予一个套接字.对于网际协议,协议地址是一个ip地址和一个端口号. 调用bind函数可以指定一个端口,或者指定一个地址,也可以两者都指定,还可以都不指定: 服务器在启动时候捆绑他们众所周知的端口; 进程可以把一个特定的IP地址捆绑到它的套接字上,不过这个IP地址必须属于其所在主机的网络接口之一.下图所示给bind函数指定要捆绑的IP地址和/或端口号产生的结果:
[cpp]  view plain copy
  1. #include<sys/socket.h>  
  2. int listen(int sockfd,int backlog);  
函数listen仅被TCP服务器调用,它做两件事件:
一.当函数socket创建一个套接字时,它被假设为一个主动套接字,也就是说,它是一个将调用connect发起连接的客户套接字.函数listen将未连接的套接字转换成一个被动套接字,指示内核应接受指向该套接字的连接请求.调用 listen函数导致套接字从CLOSED状态转换到LISTEN状态.
二.函数的第二个函数规定了内核为此套接字排队的最大连接个数
      要理解backlog参数,我们必须认识到内核为任何一个给定的监听套接字维护2个队列如下图所示:
一、未完成连接队列,每个这样的SYN分节对应其中一项:已由某个客服发出并到达服务器,而服务器正在等待完成相应的TCP三次握手过程.这些套接字处于SYN_RECV状态.
二、已完成连接队列,每个已完成TCP三次握手的客服对应其中一项.这些套接字 处于处于ESTABLISHED状态
关于两个队列的处理的一点说明:
①、在三路握手正常完成的前提下(也就是说没有丢失分节,从而没有重传),未完成连接队列的任何一项在其中的存留时间就是一个RTT,而RTT的值取决于特定的客户与服务器
②、当一个客户SYN到达时,若这些队列是满的,TCP就忽略该分节,也就是不发送RST
③、在三路握手完成后,但在服务器调用accept之前到达的数据应由服务器TCP排列,最大数据量为相应已连接套接字的接受缓冲区大小
[cpp]  view plain copy
  1. int accept(int sockfd, struct sockaddr* Cliaddr,socklen_t* Addrlen);  
      说明:参数Cliaddr和addrlen返回已连接的客户的协议地址.如果对客户的协议地址不感兴趣,可以置为空.参数Addrlen在函数调用的时候是传入的套接字地址结构的大小,函数返回时它的值是内核存放在该套接字地址结构中的确切字节数.如果accept成功,那么其返回值是由内核自动生成的一个全新描述符,代表与返回客户的TCP连接.一般我们称accept函数第一个参数为监听套接字描述符,称它的返回值为已连接套接字描述符accept函数最多返回一下三个值中的其中之一: 一个既可能是新的套接字描述符也可能是出错指示的整数、客户进程的协议地址以及该地址的大小.
[cpp]  view plain copy
  1. #include <sys/socket.h>  
  2. int close(int sockfd);  
  说明:通常的close函数也用来关闭套接字,并终止TCP连接.close一个TCP套接字的默认行为是把该套接字设置成已关闭,然后立即返回到调用进程.该套接字描述符不能再由调用进程使用.然而TCP将尝试发送已排队等待发送到对端的任何数据,发送完毕后发生的是正常的TCP连接终止序列.可以通过SO_LINGER套接字选项来改变TCP套接字的这种默认行为.下面对涉及到的一些基本的函数做一个简要的说明.   
[cpp]  view plain copy
  1. #include <sys/socket.h>  
  2. int getsockname(int sockfd, struct sockaddr *localaddr, socklen_t *addrlen);  
  3. int getpeername(int sockfd, struct sockaddr *peeraddr, socklen_t  *addrlen);  
 说明:需要这两个函数有如下的理由:
①、在一个没有调用bind的TCP客户上,connect成功返回后,getsockname用于返回内核赋予该连接的本地IP地址和本地端口号.
②、在以端口号0调用bind后,getsockname用于返回由内核赋予的本地端口号 
③、一旦连接建立,获取客户身份便可以调用getpeername.       ④、getsockname可以用于获取某个套接字的地址族
⑤、在一个以通配IP地址调用bind的TCP服务器上,与某个客户的连接一旦建立(accept成功返回),getsockname就可以用于返回由内核赋予该连接的本地IP地址,在这样的调用中,套接字描述符参数必须是已连接套接字的描述符,而不是监听套接字的描述符
⑥、.当一个服务器是由调用过accept的某个进程通过调用exec执行程序时,它能够获取客户身份的唯一途径便是调用getpeername
第五章 TCP客户/服务器程序示例
下图所示简单的回射客户服务器示意图:
TCP回射服务器程序
[cpp]  view plain copy
  1. int main()  
  2. {  
  3.  int   listenfd;                            //监听套接字  
  4.  int   connfd;                              //连接套接字  
  5.  pid_t childpid;                            //线程ID  
  6.  socklen_t clilen;                          //客户地址结构大小  
  7.  struct sockaddr_in servaddr;               //服务器地址结构信息  
  8.  struct sockaddr_in ciladdr;                //客户地址结构信息  
  9.  listenfd=Socket(AF_INET,SOCK_STREAM,0);    //创建套接字  
  10.  bzero(&servaddr,sizeof(servaddr));         //初始化  
  11.  servaddr.sin_family=AF_INET;               //设置本服务器的协议族  
  12.  servaddr.sin_addr.s_addr=htonl(INADDR_ANY);//设置本服务器的IP地址  
  13.  servaddr.sin_port=htons(SERV_PORT);        //设置本服务器的端口号  
  14.  Bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr)); //绑定套接字  
  15.  Listen(listedfd,LISTENQ);                  //开始监听  
  16.  while(1)                                   //死循环  
  17.  {  
  18.   clilen=sizeof(cliaddr);                   //计算结构体大小  
  19.   connfd=Accept(listenfd,(struct sockaddr*)&cliaddr,&clilen); //接受请求并记录下所请求客户的相关信息:IP地址+端口号  
  20.   if((childpid = Fork()) == 0)              //创建线程  
  21.   {  
  22.    Close(listenfd);                         //关闭连接  
  23.    str_echo(connfd);  
  24.    exit(0);  
  25.   }  
  26.   close(connfd);                            //关闭连接  
  27.  }  
  28. }  
  29. void str_echo(int sockfd)  
  30. {  
  31.  ssize_t n;  
  32.  char buf[MAXLINE];  
  33. again:  
  34.  while((n = read(sockfd,buf,MAXLINE))>0)  
  35.   Writen(sockfd,buf,n);  
  36.  if(n<0 && erron==EINTR)                    //出错了  
  37.   goto again;  
  38.  else if(n<0)  
  39.   err_sys("str_echo:read error");  
  40. }  
TCP回射客户程序
[cpp]  view plain copy
  1. int main(int argc, char** argv)  
  2. {  
  3.  int  sockfd;                                 //套接字  
  4.  struct sockaddr_in servaddr;                 //服务器地址结构信息变量  
  5.  if(argc!=2)  
  6.   err_quit("请输入点分十进制格式的IP地址");  
  7.  sockfd=Socket(AF_INET,SOCK_STREAM,0);        //创建一个连接  
  8.  bzero(&servaddr,sizeof(servaddr));           //填写远程服务器的相关信息  
  9.  servaddr.sin_family = AF_INET;               //协议族  
  10.  setvaddr.sin_port=htons(SERV_PORT);          //服务器端口号  
  11.  inet_pton(AF_INET,argv[1],&seraddr.sin_addr);//IP地址格式转换  
  12.  connect(sockfd,(struct sockaddr *)&servaddr,sizeof(servaddr));//向服务器发起连接请求  
  13.  str_cli(stdin,sockfd);  
  14.  exit(0);  
  15.  }  
  16. }  
  17. void str_cli(FILE* fp, int sockfd)  
  18. {  
  19.  char sendline[MAXLINE];  
  20.  char recvline[MAXLINE];  
  21.  while(Fgets(sendline,MAXLINE,fp)!=NULL)      //等待用户数据输入  
  22.  {  
  23.   Write(sockfd,recvline,strlen(sendline));    //向服务器发送用户所输入的数据  
  24.   if(Readline(sockfd,recvline,MAXLINE)==0)    //读取服务器发送过来的数据  
  25.    err_quit("str_cli:server treminated prematurely");  
  26.   Fputs(recvline,stdout);                     //输出  
  27.  }  
  28. }  
程序说明:运行服务器程序,可以用 netstat -a命令来查看服务器监听套接字的状态.
我们可以总结出正常终止客服和服务器的步骤:
1、当我们键入EOF字符时,fgets返回一个空指针,str_cli函数返回.      
2、当str_cli返回到客户的main函数时,main通过exit终止.
3、进程终止时的部分操作是关闭所有打开的描述字,因此客户打开的描述字由内核关闭.这导致客户TCP发送一个FIN给服务器,服务器TCP则以ACK响应,这是TCP连接终止的前半部操作.至此,服务器接口处于CLOSE_WAIT状态,客户套接字处于FIN_WAIT_2状态.
4、当服务器TCP接受FIN时,服务器子进程阻塞于readline调用,于是readline返回0.这导致str_echo函数返回服务器子进程的main函数
5、服务器子进程通过调用exit来终止.
6、服务器子进程打开的所有描述字随之关闭.由子进程关闭已连接套接字会引发TCP连接终止序列最后两个分节:一个是从服务器到客户的FIN;另一个是从客户到服务器的ACK.至此,连接完全终止,客户套接字进入TIME_WAIT状态
7、进程终止处理的另一部分内容是:在服务器子进程终止时,会给父进程发送一个SIGCHLD信号.该信号的缺省行为被忽略.
accept返回前连接中止
下图所示ESTABLISHED状态的连接在调用accept之前收到RST:

服务器进程终止
1.找到服务器的子进程并执行Kill命令.作为进程终止处理的部分工作,子进程中所有打开的描述字全部关闭.这就导致向客户发送一个FIN.而客户端TCP则响应一个ACK.
服务器主机崩溃
       如果客户端和服务器已经建立了连接的时候,此时服务器崩溃(和关机不一样的),如果这时客户端向服务器发送数据的时候,因为服务器已经不存在了,那么客户端就不能接受到服务器给客户端的ACK信息,这个时候,客户端建立的是TCP连接,就会重发数据报,而服务器对客户的数据分节根本没有响应,那么所返回的错误就是ETIMEDOUT.如果中间某个路由判断目的主机不可到达,从而响应一个"目的地不可达"ICMP消息,所返回的错误是EHOSTUNREACH或者ENETUNREACH.
服务器主机崩溃后重启
      当客户端和服务器已经建立连接的时候,服务器发生崩溃,重新启动的时候,丢失了原来和客户端的连接信息,这个时候,当客户端向服务器发送数据的时候(客户端并不知道,服务器已经忘记三次握手了),此时服务器发送RST数据报,就结束了客户端的发送.
TCP程序例子小结
      需要通信的客户/服务器程序在通信之前都要指定套接字对:本地IP地址,本地端口号,外地IP地址,外地端口.客户程序的本地IP地址和本地端口号通常是内核分配.服务程序的本地IP地址和端口号有bind函数指定.
数据格式
网络传递数据存在三个潜在问题:
(1)不同的实现以不同的格式存储二进制数,最常见的是大端字节序和小端字节序.
(2)不同的实现在存储相同的C数据类型上可能存在差异.例如对于short、int或long等整数类型都没有确切保证.
(3)不同的实现给结构打包的方式存在差异,取决于各种数据类型所用的位数以及机器的对齐限制.因此,穿越套接字传送二进制结构绝不是明智的.
解决上述问题的两个常用方法:
(1)把所有的数值数据作为文本串来传递.前提是客户和服务器机器具有相同的字符集.
(2)显式定义所支持数据类型的二进制格式(位数,大端或小端字节序),并以这样的格式在客户与服务器之间传递所有数据.
第六章 I/O复用:select和poll函数
       在第五章我们看到TCP客户同时处理两个输入:标准输入和TCP套接字.我们遇到的问题是就在客户阻塞于fgets调用期间,服务器进程会被杀死.服务器TCP虽然正确地给客户TCP发送了一个FIN,但是既然客户进程阻塞于从标准输入读入的过程,它将看不到这个EOF,直到从套接字读取为止.这样的进程需要一种预先告知内核的内力,使得内核一旦发现进程指定的一个或多个I/O条件就绪,它就通知进程.这个能力称为I/O复用.
       本文不会对select模型进行过多的分析,我们专门写一篇关于在Winsock中的select模型的文章.并从源码的角度对其做深入分析.敬请读者关注.
5种基本I/O模型:阻塞式I/O、非阻塞式I/O、I/O复用(select和poll)、信号驱动式I/O(SIGIO)、异步I/O
一个输入操作通常包括两个不同的阶段:等待数据准备; 从内核向进程复制数据. PS:对于一个套接字上的输入操作,第一步一般是等待数据到达网络.当分组到达时,它被拷贝到内核中的某个缓冲区.第二步是将数据从内核缓冲区拷贝到应用缓冲区
阻塞式I/O模型
      最流行的I/O模型是阻塞式I/O模型,默认情形下,所有的套接字都是阻塞的.下图所示阻塞式I/O模型:
      说明:我们使用UDP而不是TCP作为例子是原因在于就UDP而言,数据准备好读取的概念比较简单:要么这个数据报已经收到,要么还没有.上图中进程在从调用recvfrom开始到它返回的整段时间内被阻塞,其系统调用直到数据报到达应用教程的缓冲区或者发生错误才返回.,recvfrom成功返回后,应用进程开始数据处理.
非阻塞式I/O模型
进程把一个套接字设置成非阻塞是在通知内核:当所请求的I/O操作非得把本进程投入睡眠才能完成时,不能把本进程投入睡眠,而是返回一个错误.下图展示了我们即将考虑的例子.
      说明:前三次调用recvfrom时没有数据可以返回,因此内核转而立即返回一个EWOULDBLOCK错误,第四次调用recvfrom时已经有数据报准备好,它被复制到应用程序缓冲区,于是recvfrom成功返回.
      当一个应用程序使用了非阻塞模式的套接字,它需要使用一个循环来不听的测试是否一个文件描述符有数据可读(称做polling).应用程序不停的polling内核来检查是否I/O操作已经就绪.这么做往往耗费大量CPU时间,通常这种模型是在专门提供某一种功能的系统中才有.
I/O复用模型
有了I/O复用,我们就可以调用select或poll,阻塞在这两个系统调用中的某一个之上,而不是阻塞真正的I/O系统之上.下图展示了I/O复用模型.
      说明:我们阻塞于select调用,等待数据报套接字变为可读,当select返回套接字可读这一条件时,调用recvfrom把所读的数据复制到应用程序缓冲区内.另外使用select的优势在于我们可以等待多个描述符就绪.
信号驱动I/O模型
      我们可以用信号让内核在描述符就绪时发送SIGIO信号通知我们.下图展示了这种模型的概要.
      说明:无论如何处理SIGIO信号,这种模型的优势在于等待数据报到达期间进程不被阻塞.主循环可以继续执行,只要等待来自信号处理函数的通知:既可以是数据已处理好被处理,也可以是数据已准备被读取.
异步I/O模型
      相关函数的工作机制是:告知内核自动启动某个操作,并让内核在整个操作(包括将数据从内核复制到我们自己的缓冲区)完成后通知我们.异步I/O和信号驱动I/O的区别是:信号驱动I/O模式下,内核在操作可以被操作的时候通知给我们的应用程序发送SIGIO消息.异步I/O模式下,内核在所有的操作都已经被内核操作结束之后才会通知我们的应用程序.下图是一个展示了该模型的例子
select函数
      该函数允许进程指示内核等待多个事件的任何一个发生,并只在有一个或多个事件发生或经历一段指定的时间才唤醒它,也就是说我们调用select告知内核对哪些描述符(就读、写或异常条件)感兴趣以及等待多长时间,当然感兴趣的描述符可以不仅局限于套接字,任何描述符都可以用select测试.
[cpp]  view plain copy
  1. #include<sys/select.h>  
  2. #include<sys/time.h>  
  3. struct timeval  
  4. {  
  5.  longtv_sec;  
  6.  longtv_usec;  
  7. };  
  8. int select(int maxfdp1,fd_set* readset,fd_set* writeset,fd_set* exceptset,const struct timeval* timeout);  
说明:该函数返回所有描述符已就绪的总位数.参数maxfdp1说明了被测试的描述符的个数,它的值是要被测试的最大的描述符加1.最后一个参数有以下三种可能:
1、永远等待下去:仅在有一个描述字准备好I/O时才返回,为此,我们将参数timeout设置为空指针.
2、等待固定时间:在有一个描述字准备好I/O是返回,但不超过由timeout参数所指timeval结构中指定的秒数和微秒数.
3、根本不等待:检查描述字后立即返回,这称为轮询(polling).为了实现这一点,参数timeout必须指向结构timeval,且定时器的值(由结构timeval指定的秒数和微秒数)必须为0.
      PS:在前两者情况的等待中,如果进程捕获了一个信号并从信号处理程序返回,那么等待一般被中断.
描述符就绪条件
一、满足下面四个中任意条件,则一个套接字准备好读:
A、套接字接收缓冲区的数据字节数大于等于套接字接收缓冲区低水位标记的当前大小.对这样的套接字执行读操作不会阻塞并将返回一个大于0的值.可用SO_RCVLOWAT套接选项来设置低水位标记.对于TCP和UDP套按字,默认值为1.
B、该连接的读半部分关闭(接收到了FIN的TCP连接).对这样的套接字读操作不阻塞并返回0(EOF)
C、该套接字是一个监听套接字且已经完成的连接数不为0.对这样的套按字的accept通常不会阻塞
D、其上有一个套接字错误待处理.对这样的套按字的读操作将不阻塞并返回-1(错误),同时把errno设置成错误条件,这些待处理错误也可以通过指定SO_ERROR套接字选项调用getsockopt获取并清除.
二、满足下面四个条件中任意一个条件时,则一个套接字准备好写:
A、该套接字发送缓冲区的可用字节数大于等于套接字发送缓冲区低水位标记的当前大小.并且或者该套接已经连接,或者套按字不需要连接(UDP).如果我们把这套接字设置成非阻塞,写操作将不阻塞并返回一个正值(例如由传输层接受的字节数).可以使用SO_SNDLOWAT设置该套接字的低水位标记.对于TCP和UDP而言,其默认值通常为2048.
B、该连接的写半部关闭.对这样的套接写的写操作将产生SIGPIPE信号.
C、使用非阻塞式的connect的套按字已经建立连接,或者connect已经以失败告终.
D、其上有一个套接字错误等处理.对这样的套接字进行写操作会返回-,且,把ERROR设置成错误条件,可以通过指定SO_ERROR套按选项调用getsockopt获取并清除.
三、 如果一个套接字存在带外数据或者仍处于带外标记,那么它有异常条件待处理
注意:当某个套接字上发生错误时,它将由select标记为既可读又可写.
      接受低水位标记和发送低水位标记的目的在于:允许应用程序进程控制在select返回可读或可写条件之前有多少数据可读或有多大空间可用于写.举例来说,如果我们知道除非至少存在64个字节数据,否则我们的应用进程没有任何有效的工作可做,那么可以把接收低水位标记设置为64,以防少于64个字节的数据准备好读时select唤醒.任何UDP套接字只要其发送低水位标记小于等于发送缓冲区大小就总是可写,这是因为UDP套接字是不需要连接的.下图所示对以上相关内容做了一个总结:

str_cli函数
      现在我们可以使用select重写以前的str_cli函数了,这样的服务器一终止,客户就能马上得到通知.早先的版本存在的问题是:当套接字上发生某些事件时,客户可能阻塞与fgets调用.新版本改为阻塞与select调用,或是等待标准输入可读,或是等待套接字可读.下图展示了调用select所处理的各种条件.
客户套接字上的三个条件处理如下:
一、如果对端TCP发送数据,那么该套接字变为可读,并且read返回读入数据的字节数
二、如果对端发送一个FIN(对端进程终止),那么该套接字变为可读,并且read返回0(EOF)
三、如果对端发送一个RST(对端主机崩溃并重启),那么该套接字变为可读,并且read返回-1,而erron中有确切的代码错误
下面给出了这个版本的源代码.
[cpp]  view plain copy
  1. #include "unp.h"  
  2. void str_cli(FILE *fp, int sockfd)  
  3. {  
  4.  int   maxfdp1;  
  5.  fd_set  rset;  
  6.  char  sendline[MAXLINE], recvline[MAXLINE];  
  7.  FD_ZERO(&rset);  
  8.  for ( ; ; )   
  9.  {  
  10.   FD_SET(fileno(fp), &rset);  
  11.   FD_SET(sockfd, &rset);  
  12.   maxfdp1 = max(fileno(fp), sockfd) + 1;  
  13.   Select(maxfdp1, &rset, NULL, NULL, NULL);  
  14.   if (FD_ISSET(sockfd, &rset))   
  15.   {  
  16.    if (Readline(sockfd, recvline, MAXLINE) == 0)  
  17.     err_quit("str_cli: server terminated prematurely");  
  18.    Fputs(recvline, stdout);  
  19.   }  
  20.   if (FD_ISSET(fileno(fp), &rset)) //可读  
  21.   {  
  22.    if (Fgets(sendline, MAXLINE, fp) == NULL)  
  23.     return;  
  24.    Writen(sockfd, sendline, strlen(sendline));  
  25.   }  
  26.  }  
  27. }  
批量输入 这一小节暂时略过,以后再来补充. ....
shutdown函数
终止网络连接的正常方法是调用close,但close有两个限制可由函数shutdown来避免:
1、close将描述字的访问计数减1,仅在此计数为0时才关闭套接字.用shutdown我们可以激发TCP的正常连接终止序列,而不管访问计数.
2、close终止了数据传送的两个方向:读和写.由于TCP连接是全双工的,有很多时候我们要通知另一端我们已经完成了数据发送,即使那一端仍有许多数据要发送.
下图展示了这样的情况下典型的函数调用.


[cpp]  view plain copy
  1. #include<sys/socket.h>  
  2. int shutdown(intsockfd,inthowto);  
说明:该函数的行为依赖于howto参数的值:
SHUT_RD    关闭连接的读的这一半--套接字中不再有数据可接受,而且套接字接收缓冲区中的现有数据被丢弃.进程不能再对这样的套接字调用任何读函数.对一个TCP这样调用shutdown函数后,该套接字接收的来自对端的任何数据都被确认,然后悄然丢弃.
SHUT_WR   关闭连接的写一半--对于TCP套接字,这称为半关闭.当前留在套接字发送缓冲区的数据将被发送掉,后跟TCP的正常连接终止序列.我们已经说过,不管套接字描述符的引用计数是否为0,这样的写半部照样执行.
SHUT_RDWR 关闭连接读半部和写半部--这与调用shutdown两次等效.第一次指定SHUT_RD,第二次指定SHUT_WR
      下面给出了str_cli函数的改进版本.它使用了select和shutdown,其中前者只要服务器关闭那一端的连接就会通知我们,后者允许我们正确地处理批量输入.这个版本还废弃了以文本行为中心的代码,改而针对缓冲区进行操作.
[cpp]  view plain copy
  1. #include "unp.h"  
  2. void str_cli(FILE *fp, int sockfd)  
  3. {  
  4.  int   maxfdp1, stdineof;  
  5.  fd_set  rset;  
  6.  char  buf[MAXLINE];  
  7.  int  n;  
  8.  stdineof = 0;  
  9.  FD_ZERO(&rset);  
  10.  for ( ; ; )   
  11.  {  
  12.   if (stdineof == 0)  
  13.    FD_SET(fileno(fp), &rset);  
  14.   FD_SET(sockfd, &rset);  
  15.   maxfdp1 = max(fileno(fp), sockfd) + 1;  
  16.   Select(maxfdp1, &rset, NULL, NULL, NULL);  
  17.   if (FD_ISSET(sockfd, &rset))   
  18.   {  
  19.    if ( (n = Read(sockfd, buf, MAXLINE)) == 0)   
  20.    {  
  21.     if (stdineof == 1)  
  22.      return;  
  23.     else  
  24.      err_quit("str_cli: server terminated prematurely");  
  25.    }  
  26.    Write(fileno(stdout), buf, n);  
  27.   }  
  28.   if (FD_ISSET(fileno(fp), &rset))   
  29.   {   
  30.    if ( (n = Read(fileno(fp), buf, MAXLINE)) == 0)   
  31.   {  
  32.     stdineof = 1;  
  33.     Shutdown(sockfd, SHUT_WR);   
  34.     FD_CLR(fileno(fp), &rset);  
  35.     continue;  
  36.    }  
  37.    Writen(sockfd, buf, n);  
  38.   }  
  39.  }  
  40. }  
TCP回射服务器程序(修订版)
[cpp]  view plain copy
  1. #include "unp.h"  
  2. int main(int argc, char **argv)  
  3. {  
  4.  int i, maxi, maxfd, listenfd, connfd, sockfd;  
  5.  int nready, client[FD_SETSIZE];  
  6.  ssize_t n;  
  7.  fd_set rset, allset;  
  8.  char buf[MAXLINE];  
  9.  socklen_t clilen;  
  10.  struct sockaddr_in cliaddr, servaddr;  
  11.  listenfd = Socket(AF_INET, SOCK_STREAM, 0);  
  12.  bzero(&servaddr, sizeof(servaddr));  
  13.  servaddr.sin_family      = AF_INET;  
  14.  servaddr.sin_addr.s_addr = htonl(INADDR_ANY);  
  15.  servaddr.sin_port        = htons(SERV_PORT);  
  16.  Bind(listenfd, (SA *) &servaddr, sizeof(servaddr));  
  17.  Listen(listenfd, LISTENQ);  
  18.  maxfd = listenfd;  
  19.  maxi = -1;   
  20.  for (i = 0; i < FD_SETSIZE; i++)  
  21.   client[i] = -1;  
  22.  FD_ZERO(&allset);  
  23.  FD_SET(listenfd, &allset);  
  24.  for ( ; ; )   
  25. {  
  26.   rset = allset;   
  27.   nready = Select(maxfd+1, &rset, NULL, NULL, NULL);  
  28.   if (FD_ISSET(listenfd, &rset))   
  29.   {   
  30.    clilen = sizeof(cliaddr);  
  31.    connfd = Accept(listenfd, (SA *) &cliaddr, &clilen);  
  32. #ifdef NOTDEF  
  33.    printf("new client: %s, port %d\n",  
  34.      Inet_ntop(AF_INET, &cliaddr.sin_addr, 4, NULL),  
  35.      ntohs(cliaddr.sin_port));  
  36. #endif  
  37.    for (i = 0; i < FD_SETSIZE; i++)  
  38.     if (client[i] < 0)   
  39.     {  
  40.      client[i] = connfd;  
  41.      break;  
  42.     }  
  43.    if (i == FD_SETSIZE)  
  44.     err_quit("too many clients");  
  45.    FD_SET(connfd, &allset);   
  46.    if (connfd > maxfd)  
  47.     maxfd = connfd;   
  48.    if (i > maxi)  
  49.     maxi = i;   
  50.    if (--nready <= 0)  
  51.     continue;  
  52.   }  
  53.   for (i = 0; i <= maxi; i++)   
  54.   {   
  55.    if ( (sockfd = client[i]) < 0)  
  56.     continue;  
  57.    if (FD_ISSET(sockfd, &rset))   
  58.    {  
  59.     if ( (n = Read(sockfd, buf, MAXLINE)) == 0)   
  60.    {  
  61.      Close(sockfd);  
  62.      FD_CLR(sockfd, &allset);  
  63.      client[i] = -1;  
  64.     }   
  65.    else  
  66.      Writen(sockfd, buf, n);  
  67.     if (--nready <= 0)  
  68.      break;   
  69.    }  
  70.   }  
  71.  }  
  72. }  
本人郑重声明如下 一、本文来自CSDN博客,本文地址http://t.cn/zjm6I6n 二、All Rights Reserved. 任何个人或网站转载本文时不得移除本声明. 三、不得对文章进行修改,除非明确说明.同时欢迎大家评论转载和分享.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值