网络基础 send/recv

来源:互联网 发布:未来软件客服电话 编辑:程序博客网 时间:2024/05/21 18:11

  网络基础_send( )/recv( )

一.  阻塞和非阻塞基本概念
 
       阻塞函数在完成其指定任务以前不允许程序调用另一个函数。当服务器运行到accept语句时,而没有客户端连接服务请求到来,服务器就会停止在accept语句上等待连接服务请求的到来。这种情况成为阻塞(blocking);而非阻塞操作则可以立即完成。若希望服务器仅仅注意检查是否有客户在等待连接,有就接收连接,否则就继续做其他事情,则可以通过将socket设置为非阻塞方式来实现。非阻塞socket在没有客户等待时就使accept调用立即返回。
       以下为服务器端非阻塞的例子:      
#include <unistd.h>#include <fcntl.h>  sockfd = socket(AF_INET,SOCK_STREAM,0);fcntl(sockfd,F_SETFL,O_NONBLOCK);while(1){sockConn = accept(socketSer,(SOCKADDR*)&addrClient,&len);if(INVALID_SOCKET == sockConn){int err = WSAGetLastError();if(WSAEWOULDBLOCK == err){Sleep(100);continue;}char buf[100];recv(sockConn,buf,100,0);}}
       通过设置socket为非阻塞 方式,可以实现“轮询”若干socket。 当企图从一个没有数据等待的非阻塞socket读入数据时,函数将立即返回,返回值为-1,并设置errno错误号为EWOULDBLOCK。但是有个问题,这种轮询会使得cpu一直忙等,从而降低性能,浪费系统资源。
      而调用select()会有效的解决这个问题,它允许你把进程本身挂起,而通知使系统内核监听所要求的一组文件描述符的任何活动,只要确认在任务被监控的文件描述符上出现活动,select()调用将返回指示该文件描述符已经准备好的信息,从而实现了为进程选出随机的变化,而不必由进程本身对输入进行测试而浪费CPU开销(关于异步I/O今后将重点介绍)
     
二.  Send和Recv的缓存长度
 
      一个包没有固定长度,以太网限制在46-1500字节,1500就是以太网的MTU(需要认识到一点,无论应用层发送的包长度有多大,链路层都会将包进行分片),超过这个量,TCP会为IP数据报设置偏移量进行分片传输,现在一般可以允许应用层设置8K的缓冲区,8K的数据由底层分片,而应用层看来只是一次性发送。


      socket将数据传输分为 流(TCP)和数据报(UDP)。两者有啥区别呢?
      TCP作为传输控制协议,发送的是流,发的包不是整包达到的,而是连续不断的到达的,而组包的工作就需要接收端去完成。而UDP传输的是数据报文,它一定是一整包到达,但是整包数据不宜过长。适用于一次性传输少量数据,对可靠性不高的应用环境。

      网络中究竟一次性能传输多大的数据量呢?是不是我们一次塞给网络多大的数据,网络就能传输多大的呢?网络中究竟一次性能传输多大的数据量呢?是不是我们一次塞给网络多大的数据,网络就能传输多大的呢?
发包的长度是我们自己决定的,这需要我们考虑业务的具体需求与当前网络的状况。对于TCP而言,发送的长度可以比较大,但是socket内核默认的收发缓冲区大小为几KB (用户可以通过调用SetSockOpt来该改变这个值)。对于UDP,设置的值不宜过大,因为UDP要是其中有一个分片丢失,那么接受方网络层将把整个发送包丢弃,这就形成丢包,如果一个UDP包很大,被切分的自然片数很多,容易丢失的概率也就越大,但是包也不能太小,太小了会影响业务效率。

       1. send( )函数返回了实际发送的长度,只要网络不断,函数绝对不会返回发送失败(对于阻塞的socket,无论一包数据多大,都能够发送,但一次传输不宜过大,否则容易造成socket缓冲的阻塞)。对于TCP,可以写一个消息循环发送;而UDP最好一次性一整包到达。
       2. recv( )函数,对于TCP,接收方先收这个包头信息,然后再收包数据。一次收齐整个包也可以,可要对结果是否收齐进行验证。这也就完成了组包过程。UDP,那你只能整包接收了。要是你提供的接收Buffer过小,TCP将返回实际接收的长度,余下的还可以收,而UDP不同的是,余下的数据被丢弃并返回WSAEMSGSIZE错误。注意TCP,要是你提供的Buffer佷大,那么可能收到的就是多个发包,你必须分离它们,还有就是当Buffer太小,而一次收不完Socket内部的数据,那么Socket接收事件(OnReceive),可能不会再触发,使用事件方式进行接收时,密切注意这点。这些特性就是体现了流和数据包的区别。
     发包的长度是我们自己决定的,这需要我们考虑业务的具体需求与当前网络的状况。对于TCP而言,发送的长度可以比较大,但是socket内核默认的收发缓冲区大小为几KB (用户可以通过调用SetSockOpt来该改变这个值)。对于UDP,设置的值不宜过大,因为UDP要是其中有一个分片丢失,那么接受方网络层将把整个发送包丢弃,这就形成丢包,如果一个UDP包很大,被切分的自然片数很多,容易丢失的概率也就越大,但是包也不能太小,太小了会影响业务效率


三.  send( )和recv( )工作原理

    Send( )函数作用就是将buffer中的数据拷贝到socket的发送缓冲区,只要拷贝成功,send( )函数就返回了,但是此时这些数据并不一定马上通过网络传输。对于非阻塞socket的send函数调用,需要先比较发送数据的长度bLen和套接字的发送缓冲区的长度sLen:如果bLen长度大于sLen的长度,该函数将会返回SOCKET_ERROR。对于阻塞socket的send调用,则会一直等待直到全部拷贝到socket发送缓冲区才返回。
     recv( )函数先检查socket的接收缓冲区,如果接收缓冲区中没有数据或者协议正在接收数据,那么recv就一直等待,直到协议把数据接收完毕(这里对于阻塞套接字而言)。recv函数然后把socket中接收缓冲区中的数据拷贝到buffer中。
     对方如果中途调用close( )正常的关闭socket,这并不影响另外一端recv的正常接收数据;如果协议缓冲区内没有数据,recv返回0,指示对方关闭;如果协议缓冲区有数据,则返回对应数据(可能需要多次recv),
在最后一次recv时,返回0,指示对方关闭

 
四.  send( )和recv( )的返回值

      1. recv( )的返回值:
      无论阻塞还是非阻塞,recv的返回值都没有区别:
  •       小于 0 出错
  •       等于0 连接关闭
  •       大于0 接收到数据大小
      但是,返回值 < 0时候,并且(errno == EINTR / errno == EWOULDBLOCK / errno == EAGAIN) 情况下认为连接是正常的,会继续接收。
  •       EAGAIN:套接字已标记为非阻塞,而接收操作被阻塞或者接收超时(本机的接收缓冲区中无数据)
  •       EBADF:sock不是有效的描述词
  •       ECONNREFUSE:(104) 远程主机阻绝网络连接,远程主机没有关闭socket就直接退出了
  •       EFAULT:内存空间访问出错
  •       EINTR:操作被信号中断
  •       EINVAL:参数无效
  •       ENOMEM:内存不足
  •       ENOTCONN:与面向连接关联的套接字尚未被连接上
  •      ENOTSOCK:sock索引的不是套接字

      以下代码是服务器端调用libevent的读事件回调(关于libevent,以后将讲解),调用该回调时,已经保证了有数据到来:

void main_Read(evutil_socket_t fd, short events, void *arg){    TFdState *state = arg;    int iUsedIdx = 0;    int result = 0;    memset(state->read_buf, 0 ,MAX_BUF_LEN);       while (1)    {        result = recv(fd, state->read_buf+iUsedIdx, MAX_BUF_LEN, 0);        if (result <= 0)  break;                  printf("result :%d\n", result);                 iUsedIdx = result;    }    printf("while out result: %d errno: %d\n", result, errno);       if(result == 0) //接收套接字关闭    {        free_fd_state(state);        printf("recv finished!\n");    }    if(result < 0)    {        if(errno == EAGAIN)        {          printf("errno == EAGAIN\n");          return ;        }        else        {          printf("else");          free_fd_state(state);        }    }}

      (1) 如果客户端发送完数据(数据长度为3930个字节)以后就调用close() 关闭 套接字;且服务器的监听套接字(listen socket,设置非阻塞模式) 则结果如下:
 
                    
       由上图的结果可以知道,由于缓冲区中没有数据,表示接受超时,将收到EWOULDBLOCK错误码;最后一次当客户端调用close()函数关闭套接字,recv()函数将返回0。因为关闭套接字时,需要经过两次握手,所以服务器仍然会调用libevent的读回调,recv此时返回0。             
 
        (2) 如果客户端发送完数据(数据长度为3930个字节)以后就,然后强制通过结束客户端进程(不是通过调用close()关闭套接字);且服务器的监听套接字(listen socket,设置非阻塞模式) 则结果如下:
 
                      
       可以发现,最后一次并不是超时错误,而是表示对方已经断开,而未调用close正常关闭套接字。如果对方没有显式地调用close()来关闭一个TCP连接,那么在本机的进程退出之前,OS会释放相关资源。但是这种情况下的关闭,只是发送一个RESET包就结束,因此recv返回-1,且errno为104 。

       (3)如果客户端发送数据以后,服务器的监听套接字为阻塞套接字,则结果如下:
          
       可以发如果对方不close套接字,或者不强制关闭,则recv一直阻塞(buff中没有数据),或者返回大于0的数(buffer中有数据)。需通过对方关闭或者异常才能返回错误,退出while循环。
         2. send( )的返回值
      当在一个非阻塞套接字上调用send( ),可能出现出返回值为-1,且errno为SOCKET_ERROR,在非阻塞socket上,send( )操作仅仅将数据拷贝到内核缓冲区就立刻返回,如果bLen小于sLen,则会出现该情况。
 
 
0 0
原创粉丝点击