read和write也可以用于读写
socket
。但是socket编程接口也提供了几个专门用于socket数据读写的系统调用。他们增加了对数据读写的控制
TCP数据读写
理论
#include <sys/types.h>
#include <sys/socket.h>
/*
* 作用: 接收经指定的socket 传来的数据
* 参数: socket -- 已建立好连接的socket
* buff 指明存放数据的缓冲区
* len 指明缓冲区的长度
* flags 一般设置为0
* 返回值:
* (1)返回实际读取到的数据长度,他可能小于期望的len。因此我们可能需要多次调用recv才能读到完整的数据
* (2)返回0,表示对端已经关闭连接了
* (3) 返回-1,表示出错,将会设置error
*/
ssize_t recv(int sockfd, void *buff, size_t len, int flags);
/*
* 作用: 将数据由指定的socket 传给对方主机
* 参数: socket -- 已建立好连接的socket
* buff 指明存放数据的缓冲区
* len 指明缓冲区的长度
* flags 一般设置为0
* 返回值:
* (1)成功返回实际写入的长度
* (2)失败返回-1并设置errno
*/
ssize_t send(int sockfd, const void *buff, size_t len, int flags);
关于flag
每个TCP socket在内核中都有一个发送缓冲区和一个接收缓冲区,TCP的全双工的工作模式以及TCP的流量控制就是依赖于这两个独立的buffer以及buffer的填充状态。
-
接收缓冲区把数据缓存入内核,应用进程一直没有调用
recv()
进入读取的话,此数据就会一直缓存在相应socket
的的接收缓冲区内。也就是说,不管进程是否调用recv()
读取socket,对端发来的数据都会经由内核接收并且缓存到socket的内核接收缓冲区之中。recv()所做的工作,就是把内核缓冲区中的数据拷贝到应用层用户的buffer里面,然后返回。- 接收缓冲区被TCP用来缓存网络上来的数据,一直保存到应用进程读走为止
- 如果数据一直没有被应用程序读取,当接收缓冲区满了之后,发生的动作是:接收方通知发送发,接收窗口关闭(win = 0)。这个便是滑动窗口的实现。保证TCP套接口接收缓冲区不会溢出,从而保证了TCP是可靠传输。因为对方不允许发出超过所通告窗口大小的数据。如果对方无视窗口大小而发出了超过窗口大小的数据,则接收方TCP将丢弃它
-
进程调用
send()
发送数据的时候,最简单的情况(也是一般情况)就是将应用层buffer的数据拷贝到socket的内核发送缓冲区中,并不负责将数据发送给对端,发送数据是TCP的事。
同步send的工作原理
send
函数只负责将数据提交给协议层- 当调用该函数时,send先比较待发送数据的长度len_data和套接字s的发送缓冲区的长度len_buff
- len_data > len_buff:返回
SOCKET_ERROR
- len_data <= len_buff:先检查协议是否正在发送socket的发数缓冲区中的数据
- 如果是就等待协议把数据发送完
- 如果不是,再比较发送缓冲区的剩余空间len_buff_remain和len_data
* len_data > len_buff_remain:等待TCP协议把发送缓冲区的数据发送完
* len_data <= len_buff_remain:将待发送的数据copy到剩余空间中
* 如果copy
成功,返回实际copy的字节数
* 如果copy的时候出现错误,返回SOCKET_ERROR
- 如果send在等待协议时网络断开,返回
SOCKET_ERROR
。
- len_data > len_buff:返回
- 也就是说send函数把buf中的数据成功copy到s的发送缓冲的剩余空间里后它就返回了,但是此时这些数据并不一定马上被传到连接的另一端 。 如 果协议在后续的传送过程中出现网络错误的话,那么下一个Socket函数就会返回SOCKET_ERROR
- 另外:在Unix系统下,如果send在等待协议传送数据时网络断开,调用send的进程会接收到一个会收到一个SIGPIPE信号,进程对该信号的默认处理是进程终止。
同步recv的工作原理
- recv先检查是否TCP协议正在处理接收缓冲区的数据:
- 如果协议正在接收数据,那么recv就一直等待,直到协议把数据接收完毕
- 如果接收缓存区空闲,recv函数就把应用层的数据copy到接收缓冲区中(有可能待copy的数据比接收缓冲区的长度小,这时需要调用多次recv才能将数据copy到接收缓冲区中),recv函数返回其实际copy的字节数。如果recv在copy时出错,那么它返回SOCKET_ERROR;如果recv函数在等待协议接收数据时网络中断了,那么它返回0
在进行TCP协议传输的时候,要注意数据流传输的特点,recv和send不一定是一一对应的(一般情况下是一一对应),也就是说并不是send一次,就一定recv一次就接收完,有可能send一次,recv多次才接收完,也可能send多次,一次recv就接收完了。TCP协议会保证数据的有序完整的传输,但是如何去正确完整的处理每一条信息,是程序员的事情。
例如:服务器在循环recv,recv的缓冲区大小为100byte,客户端在循环send,每次send 6byte数据,则recv每次收到的数据可能为6byte,12byte,18byte,这是随机的,编程的时候注意正确的处理。
实践
查看socket发送缓冲区大小
$ cat /proc/sys/net/ipv4/tcp_wmem
4096 16384 4194304
第一个值是一个限制值,socket发送缓存区的最少字节数;
第二个值是默认值----16384(16K);
第三个值是一个限制值,socket发送缓存区的最大字节数;
proc文件系统下的值和sysctl中的值都是全局值,应用程序可根据需要在程序中使用setsockopt()对某个socket的发送缓冲区尺寸进行单独修改
UDP数据读写
//
ssize_t sendto(int sockfd, const void *buf, size_t len, unsigned int flags,
const struct sockaddr *dest_addr, int addrlen);
/*
* 因为UDP没有连接的概念,所以我们每次读取数据时都需要获取发送端的socket地址,也就是src_dest的内容
* addrlen指定该地址的长度
*/
ssize_t recvfrom(int sockfd,void *buf,size_t len,unsigned int flags, struct sockaddr *src_dest,socklen_t *addrlen);
recvfrom/sendto可以用用于TCP编程,只要把后面两个参数置NULL即可
通用数据读写
理论
recvmsg、sendmsg
即可用于读取TCP数据流,也可以用于读取UDP数据报。
#include <sys/socket.h>
struct msghdr {
void *msg_name; /* socket地址 */
socklen_t msg_namelen; /* socket地址长度 */
struct iovec *msg_iov; /* 分散的内存块 */
int msg_iovlen; /* 分散内存块的数量v */
void *msg_control; /* 指向辅助数组的起始位置 */
socklen_t msg_conntrollen; /* 辅助数据的大小 */
int msg_flags; /* 复制函数中的flags参数,并在调用过程中更新 */
}
#include <sys/uio.h>
struct iovec {
void *iov_base; /* 内存起始位置 */
size_t iov_len; /* size of buffer */
}
/*
* 功能:接收远程主机经指定的socket 传来的数据.
* 参数:
* sockfd -- 已建立好连线的socket, 如果利用UDP协议则不需经过连线操作.
* msg -- 待接收的数据
* flags -- 一般默认为0
* 返回值:成功则返回接收到的字符数, 失败则返回-1, 错误原因存于errno 中.
*/
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
/*
* 功能:将数据由指定的socket传给对方主机
* 参数:
* sockfd -- 已建立好连线的socket, 如果利用UDP协议则不需经过连线操作.
* msg -- 待发送的数据
* flags -- 一般默认为0
* 返回值:成功则返回接收到的字符数, 失败则返回-1, 错误原因存于errno 中.
*/
ssizt_t sendmsg(int sockfd, struct msghdr *msg, int flags);
recvmsg和sendmsg是最通用的I/O函数,只要设置好参数,read、readv、recv、recvfrom和write、writev、send、sendto等函数都可以对应换成这两个函数来调用。
关于msghdr :
msg_name
与msg_namelen
- 对于UDP通信,
msg_name
必须指向一个socket地址结构变量。它指定通信对方的socket地址。msg_namelen
为这个地址的长度 - 对于TCP通信,
msg_name
必须设置为NULL,msg_namelen
为0。因为对数据流socket而言,对方的地址已经知道。
- 对于UDP通信,
msg_iov
和msg_iovlen
两个成员用于指定数据缓冲区数组,即iovec
结构数组。而iovec
结构体封装了一块内存的起始位置和长度。msg_iovlen
指定这样的iovec
有多少个- 对于
recvmsg
:数据读取并存放在msg_iovlen
块分散的内存中,这些内存的位置和长度由msg_iov
指向的数组指定,这称为分散读 - 对于
sendmsg
:msg_iovlen
块分散的内存中的数据将被一起发送,这称为集中写
- 对于
msg_control
和msg_controllen
是用来设置辅助数据的位置和大小的,辅助数据(ancillary data)也叫作控制信息(control infomation)。这两个成员可以用来返回关于数据报文的其他指定信息,不过需要通过setsockopt函数指定要返回的辅助信息。对于sendmsg,这两项需要都设置成0,否则会导致发送数据失败msg_flags
成员无须设定,它会复制recvmsg/sendmsg
的flags参数的内容以影响数据读写过程。recvmsg
会在调用结束之前,将某些更新后的标志设置到msg_flag
中。
readv、writev
readv/writev
是recvmsg/sendmsg
的简化版,主要针对与文件IO(对read/write的优化)。具体可以参见这里
实践
sendmsg传递数据
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <error.h>
#include <errno.h>
#include <sys/socket.h>
#include <stdlib.h>
int main(int argc, char* argv[]){
int ret; /* 返回值 */
int sock[2]; /* 套接字对 */
struct msghdr msg;
struct iovec iov[1];
char send_buf[100] = "it is a test";
struct msghdr msgr;
struct iovec iovr[1];
char recv_buf[100];
/* 创建套接字对 */
ret = socketpair(AF_LOCAL,SOCK_STREAM,0,sock);
if(ret == -1){
printf("socketpair err\n");
return 1;
}
/* sock[1]发送数据到本地主机 */
bzero(&msg, sizeof(msg));
msg.msg_name = NULL;
msg.msg_namelen = 0;
iov[0].iov_base = send_buf;
iov[0].iov_len = sizeof(send_buf);
msg.msg_iov = iov;//要发送或接受数据设为iov
msg.msg_iovlen = 1;//1个元素
printf("开始发送数据:\n");
printf("发送的数据为: %s\n", send_buf);
ret = sendmsg(sock[1], &msg, 0 );
if(ret == -1 ){
printf("sendmsg err\n");
return -1;
}
printf("发送成功!\n");
/* 通过sock[0]接收发送过来的数据 */
bzero(&msg, sizeof(msg));
msgr.msg_name = NULL;
msgr.msg_namelen = 0;
iovr[0].iov_base = &recv_buf;
iovr[0].iov_len = sizeof(recv_buf);
msgr.msg_iov = iovr;
msgr.msg_iovlen = 1;
ret = recvmsg(sock[0], &msgr, 0);
if(ret == -1 ){
printf("recvmsg err\n");
return -1;
}
printf("接收成功!\n");
printf("收到数据为: %s\n", recv_buf);
/* 关闭sockets */
close(sock[0]);
close(sock[1]);
return EXIT_SUCCESS;
}
socketpair的用法和理解
TCP之深入浅出send和recv
socket中send和recv函数
高性能服务器编程
recvmsg和sendmsg函数