作为一个套接字描述符,它拥有两个缓冲区,分别为接收数据缓冲和发送数据缓冲区,当套接字有数据到达时,首先进入的就是接收数据缓冲区,然后应用程序从这个缓冲区中将数据读出来,这就是套接字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;
}