Socket收发数据浅析

作为一个套接字描述符,它拥有两个缓冲区,分别为接收数据缓冲和发送数据缓冲区,当套接字有数据到达时,首先进入的就是接收数据缓冲区,然后应用程序从这个缓冲区中将数据读出来,这就是套接字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
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值