1、socket缓冲区
每一个socket在被创建之后,系统都会给它分配两个缓冲区,即输入缓冲区和输出缓冲区。
(1)send函数并不是直接将数据传输到网络中,而是负责将数据写入输出缓冲区,数据从输出缓冲区发送到目标主机是由TCP协议完成的。数据写入到输出缓冲区之后,send函数就可以返回了,数据是否发送出去,是否发送成功,何时到达目标主机,都不由它负责了,而是由协议负责。
(2)recv函数也是一样的,它并不是直接从网络中获取数据,而是从输入缓冲区中读取数据。
输入输出缓冲区,系统会为每个socket都单独分配,并且是在socket创建的时候自动生成的。一般来说,默认的输入输出缓冲区大小为8K。套接字关闭的时候,输出缓冲区的数据不会丢失,会由协议发送到另一方;而输入缓冲区的数据则会丢失。
2、socket数据发送与接收问题
数据的发送和接收是独立的,并不是发送方执行一次send,接收方就执行一次recv。recv函数不管发送几次,都会从输入缓冲区尽可能多的获取数据。如果发送方发送了多次信息,接收方没来得及进行recv,则数据堆积在输入缓冲区中,取数据的时候会都取出来。换句话说,recv并不能判断数据包的结束位置。
send函数:
在数据进行发送的时候,需要先检查输出缓冲区的可用空间大小,如果可用空间大小小于要发送的数据长度,则send会被阻塞,直到缓冲区中的数据被发送到目标主机,有了足够的空间之后,send函数才会将数据写入输出缓冲区。
TCP协议正在将数据发送到网络上的时候,输出缓冲区会被锁定(生产者消费者问题),不允许写入,send函数会被阻塞,直到数据发送完,输出缓冲区解锁,此时send才能将数据写入到输出缓冲区。
要写入的数据大于输出缓冲区的最大长度的时候,要分多次写入,直到所有数据都被写到缓冲区之后,send函数才会返回。
recv函数:
函数先检查输入缓冲区,如果输入缓冲区中有数据,读取出缓冲区中的数据,否则的话,recv函数会被阻塞,等待网络上传来数据。如果读取的数据长度小于输出缓冲区中的数据长度,没法一次性将所有数据读出来,需要多次执行recv函数,才能将数据读取完毕。
3、完整的发送和接收数据代码
// 读取文件数据发送
const int _buffer_size = 1024;
char buffer[_buffer_size] = {0}; // 缓冲区
int nCount;
while( (nCount = fread(buffer, 1, _buffer_size, fp)) > 0 ){ // 当读取到文件末尾,fread() 会返回 0,结束循环send(sock, buffer, nCount, 0);
}
shutdown(sock, SHUT_WR); // 文件读取完毕,断开输出流,向对方发送FIN包
recv(sock, buffer, _buffer_size, 0); // 阻塞,等待对方接收完毕,此时recv()没有接收到对方数据,当对方关闭socket后,这边会收到FIN包,recv()就会返回,再执行后面的代码注意:读取完缓冲区中的数据 recv() 并不会返回 0,而是被阻塞,直到缓冲区中再次有数据。
recv() 返回 0 的唯一时机是收到FIN包,FIN 包表示数据传输完毕,计算机收到 FIN 包后表示对方不再向自己传输数据。
当调用 read()/recv() 函数时,如果缓冲区中没有数据,就会返回 0,表示读到了”socket文件的末尾“。
可以调用 shutdown() 来发送FIN包:server 端直接调用 close()/closesocket() 会使输出缓冲区中的数据
失效,文件内容很有可能没有传输完毕连接就断开了,而调用 shutdown() 会等待输出缓冲区中的数据传输完毕。// 收到数据写入文件,important!!
const int _buffer_size = 1024;
char buffer[_buffer_size] = {0}; // 缓冲区
int nCount;
while( (nCount = recv(sock, buffer, _buffer_size, 0)) > 0 ){fwrite(buffer, nCount, 1, fp);
}
close(sock); // 文件接收完毕后直接关闭套接字,无需调用shutdown()