Socket收发数据浅析

来源:互联网 发布:百年树人网络研修平台 编辑:程序博客网 时间:2024/05/22 14:12

作为一个套接字描述符,它拥有两个缓冲区,分别为接收数据缓冲和发送数据缓冲区,当套接字有数据到达时,首先进入的就是接收数据缓冲区,然后应用程序从这个缓冲区中将数据读出来,这就是套接字recv的过程,应用程序调用send发送数据实际是把数据拷贝到发送数据缓冲区,再由系统在缓冲区的数据发送出去。缓冲区的大小可以用SetSocketOpt()设定,同时操作系统对它有一个默认大小。
当套接字接受数据缓冲区满了后,再有数据来的时候就进不去了,对于TCP连接,发送方send就会返回错误,发送失败需要重发。对于UDP,这个时候就丢包了。所以对于UDP这个缓冲区设置多大很关键。

下面详细讲讲send/recv两个函数 (非阻塞)

ssize_t recv(int socket, void *buffer, size_t length, int flags);

socket: 套接字描述符

buffer:用来存放recv函数接收到的数据的缓冲区

length: 指明需要接收的buffer长度

flags: 一般置为0

如果调用成功,Recv函数返回其实际copy的字节数,如果发生错误返回-1,通过erron获得错误信息,如果连接断开,则返回0。需要注意的是接收缓冲区的数据可能比buffer要大,所以只需调用recv把接收缓冲区的数据copy完,这个在后面会详细讲到。
ssize_t send(int socket, const void *buffer, size_t length, int flags); socket:套接字描述符。

buffer:需要发送的数据缓冲区

length: 实际要发送的数据字节数

flags: 一般设置为0

具体收发数据编码

有了上面的基础,接下来说说具体怎么编码。我们目前的网络模型大都是epoll,因为epoll模型会比select模型性能高很多, 尤其在大连接数的情况下。下面讲讲epoll的LT和ET模式(这里只讲TCP非阻塞模式):

LT模式:是默认的工作模式。只要套接字描述符可读或者可写,内核就一直通知你,然后你可以对这个可读或者可写的套接字进行IO操作。不用担心事件丢失的情况。
对于recv,只要每次IN事件来的时候调用recv读出数据就行了。但是对于out事件,一般情况下套接字发送缓冲区都是不满可写的,所以一直会有out事件通知。这边想到的做法是,当套接字描述符不可写的时候add该套接字out事件,一旦套接字可写,就会通知应用程序,这个时候来send buffer。发送成功后移除out事件。以下代码示例:

int size = send(fd, buff, bufflen, 0);    If (size < bufflen)    {        AddSendBuffer(buff+size,bufflen-size);  //写fd缓冲区已满,应用程序缓存发送数据        m_IsCanWrite = false;        struct epoll_event ev;        ev.data.ptr = this;        ev.events = EPOLLIN | EPOLLOUT | EPOLLERR | EPOLLHUP;        //之前是有监听IN事件        epoll_ctl(Server::GetServer().GetEpfd(), EPOLL_CTL_DEL, m_Socket, &ev);        //增加OUT事件        epoll_ctl(Server::GetServer().GetEpfd(), EPOLL_CTL_ADD, m_Socket, &ev); }//当fd的OUT事件来了,表示可写 继续写数据        if(SendData(m_WriteBuff,m_WriteSize) == 0) //发送缓存的数据        {            m_WriteSize = 0;            struct epoll_event ev;            ev.data.ptr = this;            ev.events = EPOLLIN | EPOLLERR | EPOLLHUP;            //发送成功移除OUT事件            epoll_ctl(Server::GetServer().GetEpfd(), EPOLL_CTL_DEL, m_Socket, &ev);            epoll_ctl(Server::GetServer().GetEpfd(), EPOLL_CTL_ADD, m_Socket, &ev);     }

ET模式:高速模式,系统调用比较少。当套接字可读产生一个IN事件后,只会通知应用程序一次,及时接收缓冲区内还有数据没读完,系统也不会再告诉应用程序,直到下次缓冲区内数据发生变化。所以有IN事件到来的时候需要循环读取缓冲区数据,直到recv()返回的大小小于请求的大小或者errno==EAGIN,表示缓冲区的数据已经处理完,如下简单代码示例:

while(1){    int buflen = recv(fd, buf, sizeof(buf), 0);    if(buflen < 0)    {        //所以当errno为EAGAIN时,表示当前缓冲区已无数据可读        if(errno == EAGAIN)            break;        else        {            ...;            return;        }    }    else if(buflen == 0) //这里表示对端的socket已正常关闭.    {        ...;        break;    }    //收到数据处理数据    ...}

对于send buffer,一般情况下都是可写的。当send返回-1 errno为EAGAIN时表示当前套接字不可写,这时候需要把要发送的数据缓存起来,等待套接字可写的时候再把数据发送出去(类似上述LT模式send buffer的代码)。需要注意的是ET模式,每次IN事件到来的时候如果当时套接字是可写的,也会附带一个out事件。
另外因为TCP是以字节流传输的,是无边界,存在粘包问题。通常我们应用程序的协议包是按照包长+包体的形式,当我们recv到一段buffer,对这段buffer进行解析,先读取包长,在根据包长读取包体。如果解析的包体不够前面得到的包长长度。需要缓存余下的buffer,和下次recv的数据进行合并再解析。如下代码示例:

//m_ReadBuff 为应用程序接收缓存//m_ReadSize 为缓存数据的大小//m_pReadBuff 指向m_ReadBuff+m_ReadSize位置的指针int ret = recv(fd, m_pReadBuff, sizeof(m_ReadBuff)-m_ReadSize, 0);If (ret > 0){    char* pStr = m_ReadBuff;    int size = ret + m_ReadSize; //需要解析的总大小为当前接收到的数据和之前缓存的数据    int offsize = sizeof(int);   //包长+包体形式,完整包前面4字节为包长    while(size > offsize)    {        int msgsize = *((int*)pStr);   //得到包长信息        if(msgsize > size)           //buffer总长度不足一个包的包长,返回继续recv        {            break;        }        const char* pBuff = pStr;        size -= msgsize+offsize;        pStr += msgsize+offsize;        ProcessPacket(pBuff,msgsize);  //获得一个完整包 进行处理    }    if (pStr != m_ReadBuff && size != 0) //剩余buffer不足一个完整包的时候,缓存到m_ReadBuff    {        memmove(m_ReadBuff, pStr,size);    }    m_pReadBuff = m_ReadBuff + size;  //m_pReadBuff 指向m_ReadBuff+m_ReadSize位置,下次recv从m_pReadBuff位置开始copy    m_ReadSize = size;}
0 0
原创粉丝点击