网络模型(1)—— 基础篇

来源:互联网 发布:mac地址查询手机型号 编辑:程序博客网 时间:2024/05/12 04:40

文章出处:http://www.cppblog.com/CppExplore/archive/2008/03/14/44509.html

作者:CppExplore

 

全文针对linux环境。tcp/udp两种server种,tcp相对较复杂也相对比较常用。本文就从tcp server开始讲起。先从基本说起,看一个单线程的网络模型,处理流程如下:

 

[c-sharp] view plaincopy
  1. socket-->bind-->listen-->[accept-->read-->write-->close]-->close  

[]中代码循环运行, []外是对监听socket的处理, []内的是对accept返回的客户socket的处理。这些系统调用的参数以及需要的头文件等,只需要在linux下man就好。


一、 注意事项

(1) 包裹宏使用。这些系统调用返回-1表示失败。检测系统调用的返回值是个好习惯,应该说必须检测,如果系统调用总是成功的话,它为何又要有返回值呢?。每次检查的话,代码写起来又很是罗唆,并且容易遗漏检测。使用宏包裹系统调用或者使用包裹函数是不错的方案。下面给出几个预定义包裹宏:

[c-sharp] view plaincopy
  1. #define NOERROR_FUNC(func,opt) if((func)<0) /  
  2.  { /  
  3.   printf("Line[%d] error[%d:%s]/n",__LINE__,errno,strerror(errno)); /  
  4.   opt; /  
  5.  }  
  6. #define NOERROR_FUNC_1(func) NOERROR_FUNC(func,return -1)  
  7. #define NOERROR_FUNC_NULL(func) NOERROR_FUNC(func,return NULL)  

    不知道strerror?,刚说了,去linux下:man strerror
以后使用就可以类似于这样:

[c-sharp] view plaincopy
  1. NOERROR_FUNC_1((fd=socket(AF_INET,SOCKET_STREAM,0)));  
  2. NOERROR_FUNC_1(bind(fd,(struct sockaddr *)&serverAddr,sizeof(struct sockaddr_in)));  

(2) 不能返回失败的错误。大多数阻塞式系统调用要处理EINTR错误,另accept还要处理ECONNABORTED。与(1)同样道理,预定义宏如下:

[c-sharp] view plaincopy
  1. #define NOERROR_FUNC_BUT_ERR(func,opt,err,erropt) if((func)<0) /  
  2.  { /  
  3.   printf("Line[%d] error[%d:%s]/n",__LINE__,errno,strerror(errno)); /  
  4.   if(errno==err) { erropt;} /  
  5.   else {opt;} /  
  6.  }  
  7. #define NOERROR_FUNC_BUT_ERR_2(func,opt,err1,err2,erropt) if((func)<0) /  
  8.  { /  
  9.   printf("Line[%d] error[%d:%s]/n",__LINE__,errno,strerror(errno)); /  
  10.   if(errno==err1||errno==err2) { erropt;} /  
  11.   else {opt;} /  
  12.  }  

调用accept的代码就可以如此写:

[c-sharp] view plaincopy
  1. while(1)  
  2.  {  
  3.   client_sockfd=accept(fd,(struct sockaddr *)&clientAddr,&lenAddr);  
  4.   NOERROR_FUNC_BUT_ERR_2(client_sockfd,retun -1,EINTR,ECONNABORTED,continue);  
  5.   ... ...  

(3) 涉及到的系统调用分两类:

1.  从用户态到内核态,该类系统调用使用值参数, 有:bind / setsockopt / connect;

2.  从内核态到用户态,该类系统调用使用值—结果参数,有:accept / getsockopt。

看下两者函数原型,从用户态到内核态:

[c-sharp] view plaincopy
  1. int setsockopt(int s, int level, int optname, const void *optval, socklen_t optlen);  
  2. int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen);  
  3. int bind(int sockfd,struct sockaddr *Addr,socklen_t addrlen);  

 

从内核态到用户态:

[c-sharp] view plaincopy
  1. int getsockopt(int s, int level, int optname, void *optval, socklen_t *optlen);  
  2. int accept(int sockfd,struct sockaddr *Addr,socklen_t *addrlen);  

 

看最后一个参数,从用户态到内核态只要告诉内核参数长度的值就可以了,因此是值方式

从内核态到用户态,要事先准备好变量保存内核态返回的结果的长度值,因此是指针方式。称之为值—结果参数

 

二、 系统调用

(1)  socket

(2)  bind

把socket绑定到一个地址,首先要指明其地址, 如下:

[c-sharp] view plaincopy
  1. struct sockaddr_in addr;  
  2. addr.sin_family=AF_INET;//协议类型  
  3. addr.sin_port=htons(5000);//端口地址  
  4. addr.sin_addr.s_addr=htonl(INADDR_ANY);//此处表示任意ip(主机有多个网卡,则将环路地址127.0.0.1以及各网卡ip都指定)。  
  5. NOERROR_FUNC_1(bind(fd,(struct sockaddr *)addr,sizeof(struct sockaddr_in)));  

    创建ipv4协议的地址,使用5000端口,接收任何地址的connect, 把该地址和fd绑定。

注意:

1、 地址声明的时候使用struct sockaddr_in, 使用的时候总是强制转换为struct sockaddr。

2、 struct sockaddr_in结构中端口和ip都必须是网络序

3、 除任意ip地址为常量外,一般习惯用点分字符串表示ip地址,而addr.sin_addr.s_addr要使用网络序整型。

因此有两个函数可以在字符串和网络序ip地址之间做转换

[c-sharp] view plaincopy
  1. const char *inet_ntop(int af, const void *src,char *dst, socklen_t cnt);  
  2. int inet_pton(int af, const char *src, void *dst);  

    这里是需要网络序,因此使用ton (to net) 那个函数,比如:

[c-sharp] view plaincopy
  1. NOERROR_FUNC_1(inet_pton(AF_INET,"172.168.0.45", &addr.sin_addr.s_addr));  

(3) setsockopt

[c-sharp] view plaincopy
  1. long val;  
  2. socklen_t len=sizeof(val);  
  3. NOERROR_FUNC_1(setsockopt(fd,SOL_SOCKET,SO_REUSEADDR,&(val=1),len));  

    给socket设置选项,常用的不多SO_REUSEADDR是一个,服务器一般使用,其他还有SO_RCVBUFSO_SNDBUF。accept返回的对端socket继承监听socket的发送缓存,接收缓存选项。一般也不需要设置SO_RCVBUF, SO_SNDBUF,默认的足够了,带宽很大的情况下,需要设置,以免其成为瓶颈, 貌似默认的是8092字节哦,还有要在listen前设置

(4) listen

[c-sharp] view plaincopy
  1. NOERROR_FUNC_1(listen(fd,SOMAXCONN));  

    把fd从主动端口变为被动端口,等待client connect。第二个参数是表示三次握手中队列以及完成了三次握手等待accept系统函数来取得的队列的值相加,有的系统不是简单的相加,还有一个系数,也就是如果设置5,系数是2,那么两个队列的和就是10。如果队列满,而accept没取来(很忙的情况下,来不及调用accept),再有连接来就要被拒绝掉,要想系统能处理超大爆发的连接,就加大这个参数值,加快accept的处理。SOMAXCONN表示取系统允许的最大值

(5) accept

阻塞式调用,需要处理EINTR(被信号终止)ECONNABORTED(返回前client异常终止)处理方式就是重新accept

(6) read

[c-sharp] view plaincopy
  1. int read(int fd,char *buf,size_t len);  

    这是针对文件描述符的一个系统调用,socket也属于文件描述符。tcp协议中传输的数据都是流字节,没有什么结束符的标志,只能由协议提供结束方式,比如http协议使用"/r/n/r/n"或者"/n/n"标识一条信令结束,这样的话,我们只能一个字节一个字节的读取,然后结合已经读取的字节,判断是否应该结束读。而网络模型中要提高性能,一个重要方面就是要减少系统调用的次数因此tcp中都要使用缓存区一次读取尽可能多的数据,然后再从该缓存区一个字节一个字节的读取,缓存区数据被读完而没有到结束位置的时候,再次调用系统调用read。返回值为0表示对端正常关闭,大于0表示读取到的字节数。示例见最后例子。
(7) write

[c-sharp] view plaincopy
  1. int write(int fd,char *buf,size_t len);  

    两个需要注意的地方:

1、 对EINTR处理。防止被信号中断,没有正确写入需求的字符数。

2、 signal(SIGPIPE, SIG_IGN);这句代码的意思是忽略SIGPIPE信号。
write写被重置(对端意外关闭)的套接口,产生SIGPIPE信号,不处理的话程序被终止。忽略的话,继续写会产生EPIPE错误,检查write系统调用的返回结果就好了。示例见最后例子。
signal的使用,man下就看到了,回调函数的原型等都有,SIG_IGN也会出现,呵呵。
(8) close就不说了

(9) fcntl

要对socket设置为非阻塞方式,setsockopt没有提供相应的选项,只能用fcntl函数设置

[c-sharp] view plaincopy
  1. int flags;  
  2. NOERROR_FUNC_1(flags=fcntl(client_sockfd,F_GETFL,0));  
  3. NOERROR_FUNC_1(fcntl(client_sockfd,F_SETFL,flags|O_NONBLOCK));  

    多路分离I/O(select / poll / epoll)通常设置为肥阻塞方式。

设置为阻塞方式(默认方式)代码:

[c-sharp] view plaincopy
  1. int flags;  
  2. NOERROR_FUNC_1(flags=fcntl(client_sockfd,F_GETFL,0));  
  3. NOERROR_FUNC_1(fcntl(client_sockfd,F_SETFL,flags&~O_NONBLOCK));  

    对于阻塞方式的套接口,如果要避免read write永远阻塞,设置等待时间的方式有3种:信号方式,不推荐,不说了;select方式,每次调用read前调用select监视该套接口是否在指定时间内可写,超时select返回0,这样每次执行read都要调用两个系统调用,不推荐;最后就是设置套接口选项SO_RECVTIMEO和SO_SNDTIMEO,其实这个也不推荐,总之不推荐阻塞式的方式,呵呵。实用的网络模型都是多路分离的

    非阻塞方式下的connect函数要说下,当然是就客户端而言,connect后如果没有立即返回连接成功的话,把这个socket加入select的 fd_set(poll的pollfd,epoll的EPOLL_CTL_ADD操作),要监视是否可写事件,可写的时候用getsockopt获取SO_ERROR选项,如果非负(其实就是0值)就标示connect成功,否则就是失败。EPOLL中测试结果是connect失败的返回事件是EPOLLERR|EPOLLHUP,并不是加入时的EPOLLOUT,成功的时候是EPOLLOUT。

 

三、 示例

最后给个单线程的服务器,虽说没什么实用意义,不过就象“hello world!”,入门第一课。
这个例子,读取数据,回写response,关闭clientfd。不管read write是否出错,都执行close,因此代码很简单。
先来main函数:

[c-sharp] view plaincopy
  1. int main()  
  2. {  
  3.     int server_sockfd;  
  4.     int client_sockfd;  
  5.     struct sockaddr_in serverAddr;  
  6.     struct sockaddr_in clientAddr;  
  7.     size_t lenAddr;  
  8.         int val;  
  9.   
  10.     memset(&serverAddr,0,sizeof(serverAddr));  
  11.     serverAddr.sin_family=AF_INET;  
  12.     serverAddr.sin_port=htons(5000);  
  13.     serverAddr.sin_addr.s_addr=htonl(INADDR_ANY);  
  14.   
  15.     NOERROR_FUNC_1((server_sockfd=socket(AF_INET,SOCK_STREAM,0)));  
  16.     NOERROR_FUNC_1(setsockopt(server_sockfd,SOL_SOCKET,SO_REUSEADDR,&(val=1),sizeof(val)));  
  17.     NOERROR_FUNC_1(bind(server_sockfd,(struct sockaddr *)&serverAddr,sizeof(struct sockaddr_in)));  
  18.     NOERROR_FUNC_1(listen(server_sockfd,SOMAXCONN));  
  19.     
  20.     const static char * response="HTTP/1.1 200 OK/r/n/r/n";  
  21.     char buf[BUF_LEN];  
  22.     signal(SIGPIPE, SIG_IGN);  
  23.     while(1)  
  24.     {  
  25.         client_sockfd=accept(server_sockfd,(struct sockaddr *)&clientAddr,&lenAddr);  
  26.         NOERROR_FUNC_BUT_ERR_2(client_sockfd,return -1,EINTR,ECONNABORTED,continue);  
  27.         BuffCache cache;  
  28.         if(read_double_enter(client_sockfd,buf,BUF_LEN,&cache)>0)  
  29.             writen(client_sockfd,response,19);  
  30.         close(client_sockfd);  
  31.     }  
  32.     close(server_sockfd);  
  33.     return 0;  
  34. }  

下面是包含的头文件和宏:

[c-sharp] view plaincopy
  1. #include <unistd.h>  
  2. #include <sys/types.h>  
  3. #include <sys/socket.h>  
  4. #include <arpa/inet.h>  
  5. #include <stdio.h>  
  6. #include <errno.h>  
  7. #include <signal.h>  
  8. #include <stdlib.h>  
  9. #include <string.h>  
  10. #include <stdarg.h>  
  11.  
  12.  
  13. #define NOERROR_FUNC(func,opt) if((func)<0) /  
  14.     { /  
  15.         printf("Line[%d] error[%d:%s]/n",__LINE__,errno,strerror(errno)); /  
  16.         opt; /  
  17.     }  
  18. #define NOERROR_FUNC_BUT_ERR(func,opt,err,erropt) if((func)<0) /  
  19.     { /  
  20.         printf("Line[%d] error[%d:%s]/n",__LINE__,errno,strerror(errno)); /  
  21.         if(errno==err) { erropt;} /  
  22.         else {opt;} /  
  23.     }  
  24. #define NOERROR_FUNC_BUT_ERR_2(func,opt,err1,err2,erropt) if((func)<0) /  
  25.     { /  
  26.         printf("Line[%d] error[%d:%s]/n",__LINE__,errno,strerror(errno)); /  
  27.         if(errno==err1||errno==err2) { erropt;} /  
  28.         else {opt;} /  
  29.     }  
  30.  
  31. #define NOERROR_FUNC_1(func) NOERROR_FUNC(func,return -1)  
  32. #define NOERROR_FUNC_NULL(func) NOERROR_FUNC(func,return NULL)  
  33.  
  34. #define BUF_LEN 1024  

 

下面是缓存区和读写代码:

[c-sharp] view plaincopy
  1. class BuffCache  
  2. {  
  3. public:  
  4.     BuffCache():count(0){}  
  5.     int read_socket(int fd,char * pCh)  
  6.     {  
  7.         if(count<=0)  
  8.         {  
  9.         again:  
  10.             if((count=read(fd,buf,BUF_LEN))<0)  
  11.             {  
  12.                 if(errno==EINTR)  
  13.                     goto again;  
  14.                 *pCh='/0';  
  15.                 return -1;  
  16.             }  
  17.             else if(count==0)  
  18.             {  
  19.                 *pCh='/0';  
  20.                 return 0;  
  21.             }  
  22.             ptrBuf=buf;  
  23.         }  
  24.         count--;  
  25.         *pCh=*(ptrBuf++);  
  26.         return 1;  
  27.     }  
  28. private:  
  29.     char buf[BUF_LEN];  
  30.     char * ptrBuf;  
  31.     int count;  
  32. };  
  33. inline int read_double_enter(int fd,char * pCh, int maxsize,BuffCache *cache)  
  34. {  
  35.     int i=0;  
  36.     char *ptr=pCh;  
  37.     int res=0;  
  38.     int sum=0;  
  39.     for(i=0;i<maxsize;i++)  
  40.     {  
  41.         if((res=cache->read_socket(fd,ptr))<0)  
  42.             return -1;  
  43.         else if(res==0)  
  44.         {  
  45.             *ptr='/0';  
  46.             return sum;  
  47.         }  
  48.         else  
  49.         {  
  50.             if(*ptr=='/n'&&  
  51.                 ((ptr-pCh>=1&&*(ptr-1)=='/n')||  
  52.                 (ptr-pCh>=3&&*(ptr-1)=='/r'&&*(ptr-2)=='/n'&&*(ptr-3)=='/r')))  
  53.             {  
  54.                 *(ptr+1)='/0';  
  55.                 return ++sum;  
  56.             }  
  57.         }      
  58.         ptr++;  
  59.         sum++;  
  60.     }  
  61. }  
  62.   
  63. inline int writen(int fd,const char * buf, int len)  
  64. {  
  65.     int count=0;  
  66.     int leftlen=len;  
  67.     const char * ptr=buf;  
  68.     while(leftlen>0)  
  69.     {  
  70.     again:  
  71.         NOERROR_FUNC_BUT_ERR((count=write(fd,ptr,leftlen)),return -1,EINTR,goto again);  
  72.         leftlen-=count;  
  73.         ptr+=count;  
  74.     }  
  75. }  

随便写的一个程序,凑合着看吧。


四、其它基础性知识的说明
(1)  read write外 还有recv send recvfrom sendto recvmsg sendmsg不说了
(2)  信号处理不说了
(3)  多路分离后面讲各种模型的时候详细写
(4)  信号方式的多路分离不细说了,在tcp中只能accept除使用信号SIGIO,但是该信号为非可靠信号,当大量client连接到来的时候,经常丢失信号,10并发都支持不了,实在没什么实际意义。

 

0 0
原创粉丝点击