一、函数原型
recv函数用于socket通信中接收消息,接口定义如下:
ssize_t recv(int socket, void *buf, size_t len, int flags)
参数一:指定接收端套接字描述符;
参数二:指向一个缓冲区,该缓冲区用来存放recv函数接收到的数据;
参数三:指明buf的长度;
参数四:一般置为0;
返回值:失败时,返回值小于0;超时或对端主动关闭,返回值等于0;成功时,返回值是返回接收数据的长度。
send函数用于socket通信中发送消息,接口定义如下:
ssize_t send(int socket, const void *buf, size_t len, int flags);
参数一:指定发送端套接字描述符;
参数二:指明一个存放应用程序要发送数据的缓冲区;
参数三:指明实际要发送的数据的字节数;
参数四:一般置0;
返回值:失败时,返回值小于0;超时或对端主动关闭,返回值等于0;成功时,返回值是返回发送数据的长度。
二、TCP socket中的buffer
每个TCP socket在内核中都有一个发送缓冲区和一个接受缓冲区,TCP的全双工工作模式以及TCP的流量和拥塞控制便依赖于这两个独立的buffer以及buffer的填充状态。
接受缓冲区把数据缓存入内核,如果没有调用read()系统调用的话,数据会一直缓存在socket的接受缓冲区内。不管进程是否调用recv()读取socket,对等端发来的数据都会经由内核接受并且缓存到socket的内核接受缓冲区之中。recv()所做的工作,就是把内核缓冲区中的数据拷贝到应用层用户的buffer里面,并返回拷贝的字节数。(注意:是拷贝,不是像read那样读取之后,清空接受缓冲区内的数据。)
进程调用send()发送数据的时候,将数据拷贝到socket的内核发送缓冲区之中,然后返回拷贝的字节数。send()返回之时,数据不一定会发送到对等端去,send()仅仅是把应用层buffer的数据拷贝到socket的内核发送缓冲区中,发送是TCP的事情。(注意:这里也是拷贝,不是像write那样发送之后,清空发送缓冲区内的数据。)
接受缓冲区被TCP用来缓存网络上接收到的数据,一直保存到应用进程读走为止。如果应用进程一直没有读取,接受缓冲区满了以后,发生的动作是:接收端通知发送端,接收窗口关闭(win=0)。这个便是滑动窗口上的实现。保证TCP套接口接受缓冲区不会溢出,从而保证了TCP是可靠传输。因为对方不允许发出超过所通告窗口大小的数据。这就是TCP的流量控制,如果对方无视窗口大小而发出了超过窗口大小的数据,则接收方TCP将丢弃它。
三、send()的工作原理
send()函数只负责将数据提交给协议层。当调用该函数时,send()先比较待发送数据的长度和套接字的发送缓冲区的长度:
· 当待拷贝数据的长度大于发送缓冲区的长度时,该函数返回SOCKET_ERROR;
· 当待拷贝数据的长度小于或等于发送缓冲区的长度时,那么send先检查协议是否正在发送发送套接字的发送缓冲区中的数据:
如果是就等待协议把数据发送完,再进行拷贝;
如果协议还没有开始发送套接字的发送缓冲区中的数据或者该发送缓冲区中没有数据,那么send就比较该发送缓冲区中的剩余空间和待拷贝数据的长度:
如果待拷贝数据的长度大于剩余空间的大小,send就一直等待协议把该发送缓冲区中的数据发完;
如果待拷贝数据的长度小于剩余空间大小,send就仅仅把buf中的数据拷贝到剩余空间中。(注意:并不是send把该套接字的发送缓冲区中数据传到连接的另一端,而是协议传的,send仅仅是把数据拷贝到该发送缓冲区的剩余空间里面。)
如果send函数拷贝成功,就返回实际拷贝的字节数;如果拷贝的过程中出现错误,send就返回SOCKET_ERROR;如果send在等待协议传送数据时网络断开的话,那么send函数也返回SOCKET_ERROR。
要注意,send函数把buffer中的数据成功拷贝到套接字的发送缓冲区中的剩余空间里面后,它就返回了,但是此时这些数据并不一定马上被传到连接的另一端。如果协议在后续的传输过程中出现网络错误的话,那么下一个socket函数就会返回SOCKET_ERROR。(每一个除send外的socket函数在执行的最开始总要先等待套接字的发送缓冲区的数据被协议传送完毕才能继续,如果在等待时出现网络错误,那么该socket函数就返回SOCKET_ERROR。)
四、recv()的工作原理
recv先检查套接字的接收缓冲区,如果该接收缓冲区中没有数据或者协议正在接收数据,那么recv就一直等待,直到协议把数据接收完毕。当协议把数据接收完毕,recv函数就把套接字的接收缓冲区中的数据拷贝到用户层的buffer中,(注意:协议接收到的数据可能大于buffer的长度,所以在这种情况下,要调用几次recv函数才能把套接字接收缓冲区中的数据拷贝完。)recv函数仅仅是拷贝数据,真正的接收数据是协议来完成的。
recv函数返回其实际拷贝的字节数。如果recv在拷贝时出错,那么就返回SOCKET_ERROR;如果recv函数在等待协议接收数据时网络中断了,那么它返回0。对方优雅的关闭socket并不影响本地recv的正常接收数据,如果协议缓冲区内没有数据,recv返回0,指示对方关闭;如果协议缓冲区有数据,则返回对应数据(可能需要多次recv),在最后一次recv时,返回0,指示对方关闭。
五、应用
在处理粘包问题的时候,其中一种方法是在包尾加上’\n’。加上‘\n’以后读取数据过程如下:
//@ssize_t:返回读的长度,若ssize_t < count,则表示读失败。
//@buf:接收数据内存首地址
//@count:接收数据长度
ssize_t readn(int fd, const void* buf, size_t count){
size_t nletf = count;
ssize_t nread;
char *bufp = (char *)buf;
while(nleft > 0){
if((nread = read(fd, bufp, nleft)) < 0){
if(errno == EINTR)
continue; //如果是中断,则继续读。
return -1;
}else if(nread == 0){ //若对方已关闭
return count - nleft; //返回读到的字节数
}
bufp += nread;
nleft -= nread;
}
return count;
}
ssize_t recv_peek(int sockfd, void *buf, size_t len){
while(1){
int ret = recv(sockfd, buf, len, MSG_PEEK); //MSG_PEEK,仅把tcp 接收缓冲区中的数据读取到buf中,并不把已读取的数据从tcp 接收缓冲区中移除,再次调用recv仍然可以读到刚才读到的数据。
if(ret == -1 && errno == EINTR)
continue;
return ret;
}
}
//@maxline 一行最大数
//先提前peek一下缓冲区,如果有数据,则从缓冲区中拷贝数据
//1、缓冲区中的数据带\n
//2、缓冲区中的数据不带\n
ssize_t readline(int sockfd, void *buf, size_t maxline){
int ret;
int nread;
char *bufp = buf;
int nleft = maxline;
int count = 0;
while(1){
//看一下缓冲区中有没有数据,并不移除内核缓冲区中的数据
ret = recv_peek(sockfd, bufp, nleft);
if(ret < 0) //失败
return ret;
else if(ret == 0) //对方已关闭
return ret;
nread = ret;
int i;
for(i = 0; i < nread; i++){
if(bufp[i] == '\n'){ //若缓冲区有\n
ret = readn(sockfd, bufp, i+1) ; //读走数据
if(ret != i+1)
exit(EXIT_FAILURE);
return ret + count; //有\n就返回,并返回读走的数据
}
}
if(nread > nleft) //如果读到的数据大于一行最大数, 异常处理
exit(EXIT_FAILURE);
nleft -= nread; //若缓冲区没有\n,把剩余的数据读走。
ret = readn(sockfd, bufp, nread);
if(ret != nread)
exit(EXIT_FAILURE);
bufp += nread; //bufp指针后移,再接着偷看(recv_peek)缓冲区数据,直到遇到\n
count += nread;
}
return -1;
}
在readline函数中,我们先用recv_peek”偷窥“ 一下现在缓冲区有多少个字符并读取到bufp,然后查看是否存在换行符’\n’。
如果存在,则使用readn连同换行符一起读取(清空缓冲区);
如果不存在,也清空一下缓冲区, 且移动bufp的位置,回到while循环开头,再次窥看。
注意,当我们调用readn读取数据时,那部分缓冲区是会被清空的,因为readn调用了read函数。
还需注意一点是,如果第二次才读取到了’\n’,则先用count保存了第一次读取的字符个数,然后返回的ret需加上原先的数据大小。
六、补充
在进行TCP协议传输的时候,要注意数据流传输的特点,recv和send不一定时一一对应的,也就是说并不是send一次,就一定recv一次就接收完,有可能send一次,recv多次才接收完,也有可能send多次,一次recv就接收完了。
TCP协议会保证数据的有序完整的传输,但是如何去正确完整的处理每一条信息,是程序员的事情。例如,服务器在循环recv,recv的缓冲区大小为100byte,客户端在循环send,每次send 6byte数据,则recv每次收到的数据可能为6byte, 12byte, 18byte,这是随机的,编程的时候注意正确处理。