技术系列之 网络模型(一)基础篇

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

socket --> bind --> listen -->[ accept --> read --> write --> close] --> close

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

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

# define NOERROR_FUNC(func,opt) if((func)<0) /
 {  /
  
printf ( " Line[%d] error[%d:%s]/n " , __LINE__ , errno , strerror(errno));  /
  opt; 
/
 }
# define NOERROR_FUNC_1(func) NOERROR_FUNC(func,return -1)
#define NOERROR_FUNC_NULL(func) NOERROR_FUNC(func,return NULL)

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

NOERROR_FUNC_1((fd = socket(AF_INET,SOCKET_STREAM, 0 )));
NOERROR_FUNC_1(bind(fd,(struct sockaddr 
* ) & serverAddr,sizeof(struct sockaddr_in)));


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

# define NOERROR_FUNC_BUT_ERR(func,opt,err,erropt) if((func)<0) /
 {  /
  
printf ( " Line[%d] error[%d:%s]/n " , __LINE__ , errno , strerror(errno));  /
  
if (errno == err) { erropt;}  /
  
else  {opt;}  /
 }
# define NOERROR_FUNC_BUT_ERR_2(func,opt,err1,err2,erropt) if((func)<0) /
 {  /
  
printf ( " Line[%d] error[%d:%s]/n " , __LINE__ , errno , strerror(errno));  /
  
if (errno == err1 || errno == err2) { erropt;}  /
  
else  {opt;}  /
 }

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

while ( 1 )
 
{
  client_sockfd
= accept(fd,(struct sockaddr  * ) & clientAddr, & lenAddr);
  NOERROR_FUNC_BUT_ERR_2(client_sockfd,retun 
- 1 ,EINTR,ECONNABORTED, continue );

(3)涉及到系统调用分两类: 从用户态到内核态,该类系统调用使用值参数,有:bind/setsockopt/connect;从内核态到用户态,该类系统调用使用值-结果参数,有:accept/getsockopt。
看下两者函数原型,从用户态到内核态:

        int  setsockopt( int  s,  int  level,  int  optname,  const   void   * optval, socklen_t optlen);
       
int  connect( int  sockfd,  const  struct sockaddr  * serv_addr, socklen_t addrlen);
       
int  bind( int  sockfd,struct sockaddr  * Addr,socklen_t addrlen);

从内核态到用户态:

     int  getsockopt( int  s,  int  level,  int  optname,  void   * optval, socklen_t  * optlen);
    
int  accept( int  sockfd,struct sockaddr  * Addr,socklen_t  * addrlen);

看最后一个参数,从用户态到内核态只要告诉内核参数长度的值就可以了,因此是值方式。从内核态到用户态,要事先准备好变量保存内核态返回的结果长度值,因此是指针方式,称之为值-结果参数。

二、系统调用
(1)socket

int  fd;
   NOERROR_FUNC_1(fd= socket(AF_INET,SOCKET_STREAM, 0 ));

创建一个ipv4的tcp socket
(2)bind
把socket绑定到一个地址,首先要指明地址,如下:

struct sockaddr_in addr;
addr.sin_family
= AF_INET; // 协议类型
addr.sin_port = htons( 5000 ); // 端口地址
addr.sin_addr.s_addr = htonl(INADDR_ANY); // 此处表示任意ip(主机有多个网卡,则将环路地址127.0.0.1以及各网卡ip都指定)。
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都必须是网络序。htons把主机序的short int转化为网络序,htonl把主机序的long int转化为网络序。
3、除任意ip地址为常量外,一般习惯用点分字符串表示ip地址,而addr.sin_addr.s_addr要使用网络序整型。
因此有两个函数可以在字符串和网络序ip地址之间做转换:

    const   char   * inet_ntop( int  af,  const   void   * src, char   * dst, socklen_t cnt);
   
int  inet_pton( int  af,  const   char   * src,  void   * dst);

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

NOERROR_FUNC_1(inet_pton(AF_INET, " 172.168.0.45 " & addr.sin_addr.s_addr));

(3)setsockopt

long  val;
socklen_t len
= sizeof(val);
NOERROR_FUNC_1(setsockopt(fd,SOL_SOCKET,SO_REUSEADDR,
& (val = 1 ),len) );

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

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

int  read( int  fd, char   * buf,size_t len);

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

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函数设置。

int  flags;
NOERROR_FUNC_1(flags
= fcntl(client_sockfd,F_GETFL, 0 ));
NOERROR_FUNC_1(fcntl(client_sockfd,F_SETFL,flags
| O_NONBLOCK));

多路分离I/O(select/poll/epoll)通常设置为非阻塞方式。
设置为阻塞方式(默认方式)代码:

int  flags;
NOERROR_FUNC_1(flags
= fcntl(client_sockfd,F_GETFL, 0 ));
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函数:

int  main()
{
    
int  server_sockfd;
    
int  client_sockfd;
    struct sockaddr_in serverAddr;
    struct sockaddr_in clientAddr;
    size_t lenAddr;
        int  val;

    memset(
& serverAddr, 0 ,sizeof(serverAddr));
    serverAddr.sin_family
= AF_INET;
    serverAddr.sin_port
= htons( 5000 );
    serverAddr.sin_addr.s_addr
= htonl(INADDR_ANY);

    NOERROR_FUNC_1((server_sockfd
= socket(AF_INET,SOCK_STREAM, 0 )));
    NOERROR_FUNC_1(setsockopt(server_sockfd,SOL_SOCKET,SO_REUSEADDR,
& (val = 1 ),sizeof(val) ));
    NOERROR_FUNC_1(bind(server_sockfd,(struct sockaddr 
* ) & serverAddr,sizeof(struct sockaddr_in)));
    NOERROR_FUNC_1(listen(server_sockfd,SOMAXCONN));
 
 
    
const   static   char   *  response = " HTTP/1.1 200 OK/r/n/r/n " ;
    
char  buf[BUF_LEN];
    signal(SIGPIPE, SIG_IGN);
    
while ( 1 )
    
{
        client_sockfd
= accept(server_sockfd,(struct sockaddr  * ) & clientAddr, & lenAddr);
        NOERROR_FUNC_BUT_ERR_2(client_sockfd,
return   - 1 ,EINTR,ECONNABORTED, continue );
        BuffCache cache;
        
if (read_double_enter(client_sockfd,buf,BUF_LEN, & cache) > 0 )
            writen(client_sockfd,response,
19 );
        close(client_sockfd);
    }

    close(server_sockfd);
    
return   0 ;
}

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

#include  < unistd.h >
#include 
< sys / types.h >
#include 
< sys / socket.h >
#include 
< arpa / inet.h >
#include 
< stdio.h >
#include 
< errno.h >
#include 
< signal.h >
#include 
< stdlib.h >
#include 
< string.h >
#include 
< stdarg.h >


#define NOERROR_FUNC(func,opt) 
if ((func) < 0 ) /
    
{ /
        printf(
" Line[%d] error[%d:%s]/n " ,__LINE__,errno,strerror(errno)); /
        opt; /
    }

#define NOERROR_FUNC_BUT_ERR(func,opt,err,erropt) 
if ((func) < 0 ) /
    
{ /
        printf(
" Line[%d] error[%d:%s]/n " ,__LINE__,errno,strerror(errno)); /
        
if (errno == err)  { erropt;}  /
        
else   {opt;}  /
    }

#define NOERROR_FUNC_BUT_ERR_2(func,opt,err1,err2,erropt) 
if ((func) < 0 ) /
    
{ /
        printf(
" Line[%d] error[%d:%s]/n " ,__LINE__,errno,strerror(errno)); /
        
if (errno == err1 || errno == err2)  { erropt;}  /
        
else   {opt;}  /
    }


#define NOERROR_FUNC_1(func) NOERROR_FUNC(func,
return   - 1 )
#define NOERROR_FUNC_NULL(func) NOERROR_FUNC(func,
return  NULL)

#define BUF_LEN 
1024


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

class  BuffCache
{
public :
    BuffCache():count(
0 ) {}
    
int  read_socket( int  fd, char   *  pCh)
    
{
        
if (count <= 0 )
        
{
        again:
            
if ((count = read(fd,buf,BUF_LEN)) < 0 )
            
{
                
if (errno == EINTR)
                    
goto  again;
                
* pCh = ' /0 ' ;
                
return   - 1 ;
            }

            
else   if (count == 0 )
            
{
                
* pCh = ' /0 ' ;
                
return   0 ;
            }

            ptrBuf
= buf;
        }

        count
-- ;
        
* pCh =* (ptrBuf ++ );
        
return   1 ;
    }

private :
    
char  buf[BUF_LEN];
    
char   *  ptrBuf;
    
int  count;
}
;
inline 
int  read_double_enter( int  fd, char   *  pCh,  int  maxsize,BuffCache  * cache)
{
    
int  i = 0 ;
    
char   * ptr = pCh;
    
int  res = 0 ;
    
int  sum = 0 ;
    
for (i = 0 ;i < maxsize;i ++ )
    
{
        
if ((res = cache -> read_socket(fd,ptr)) < 0 )
            
return   - 1 ;
        
else   if (res == 0 )
        
{
            
* ptr = ' /0 ' ;
            
return  sum;
        }

        
else
        
{
            
if ( * ptr == ' /n ' &&
                ((ptr
- pCh >= 1 &&* (ptr - 1 ) == ' /n ' ) ||
                (ptr
- pCh >= 3 &&* (ptr - 1 ) == ' /r ' &&* (ptr - 2 ) == ' /n ' &&* (ptr - 3 ) == ' /r ' )))
            
{
                
* (ptr + 1 ) = ' /0 ' ;
                
return   ++ sum;
            }

        }
    
        ptr
++ ;
        sum
++ ;
    }

}


inline 
int  writen( int  fd, const   char   *  buf,  int  len)
{
    
int  count = 0 ;
    
int  leftlen = len;
    
const   char   *  ptr = buf;
    
while (leftlen > 0 )
    
{
    again:
        NOERROR_FUNC_BUT_ERR((count
= write(fd,ptr,leftlen)), return   - 1 ,EINTR, goto  again);
        leftlen
-= count;
        ptr
+= count;
    }

}

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值