引言:网络数据能够正常到达用户并且被接收是进行网络传输的根本目的,网络传输的数据发送和接收有多种方案,本文章就对通过向量接收和发送等数据传输方式,并且对多种I/O模型进详细分析介绍。
目录
一.I/O函数
1.1 recv和send
在网络编程中,recv
和 send
函数是用于在套接字上进行数据传输的两个基本函数。它们是在Unix-like系统中的Berkeley套接字 API(也称为BSD套接字API)的一部分,在Windows系统中也有对应的实现。
recv函数
recv
函数用于从连接的套接字接收数据。函数原型如下:
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
-
sockfd
:指定接收数据的套接字文件描述符。 -
buf
:指向缓冲区的指针,用于存放接收到的数据。 -
len
:指定缓冲区的大小,即最多接收的数据量。 -
flags
:指定接收数据的操作方式,可以是0或者以下一个或多个值的逻辑或:MSG_OOB
:接收带外数据(Out-of-Band Data)。MSG_PEEK
:查看数据但不从接收队列中移除数据。MSG_WAITALL
:等待所有请求的数据,直到请求的数量被接收为止。- 等等。
返回值:
- 成功时,返回接收到的字节数。
- 如果连接被对方关闭,返回0。
- 出错时,返回-1,并设置errno来指示错误。
send函数
send
函数用于向连接的套接字发送数据。函数原型如下:
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
参数说明:
-
sockfd
:指定发送数据的套接字文件描述符。 -
buf
:指向要发送数据的指针。 -
len
:指定要发送数据的长度。 -
flags
:指定发送数据的操作方式,可以是0或者以下一个或多个值的逻辑或:MSG_OOB
:发送带外数据。MSG_DONTROUTE
:发送数据时不通过网关。- 等等。
返回值:
- 成功时,返回发送的字节数。
- 出错时,返回-1,并设置errno来指示错误。
1.2 readv和writev函数
readv
和 writev
函数是 Unix 和类 Unix 操作系统中的系统调用,它们允许程序在一次操作中从多个缓冲区读取数据或将数据写入多个缓冲区。这些函数对于分散读(scatter read)和集中写(gather write)操作非常有用。
readv函数
readv
函数用于从文件描述符读取数据到多个缓冲区中。函数原型如下:
ssize_t readv(int fd, const struct iovec *iov, int iovcnt);
参数说明:
fd
:指定要读取数据的文件描述符。iov
:指向iovec
结构数组的指针,每个iovec
结构指定一个缓冲区。iovcnt
:指定iov
数组中iovec
结构的数量。
iovec
结构定义如下:
struct iovec {
void *iov_base; /* Starting address */
size_t iov_len; /* Number of bytes to transfer */
};
iov_base
:指向数据缓冲区的指针。iov_len
:指定缓冲区的长度。
返回值:
- 成功时,返回读取的总字节数。
- 如果遇到文件结束,返回0。
- 出错时,返回-1,并设置errno来指示错误。
writev函数
writev
函数用于将多个缓冲区的数据写入到文件描述符。函数原型如下:
ssize_t writev(int fd, const struct iovec *iov, int iovcnt);
参数说明:
fd
:指定要写入数据的文件描述符。iov
:指向iovec
结构数组的指针,每个iovec
结构指定一个数据源缓冲区。iovcnt
:指定iov
数组中iovec
结构的数量。
返回值:
- 成功时,返回写入的总字节数。
- 出错时,返回-1,并设置errno来指示错误。
1.3 recvmsg和recvmsg函数
recvmsg
和 sendmsg
函数是 Unix 和类 Unix 操作系统中的系统调用,它们提供了比 recv
和 send
更高级的接口,用于在套接字上发送和接收数据。这些函数支持更多的功能,如scatter/gather I/O、控制消息的发送和接收,以及带外数据。
recvmsg函数
recvmsg
函数用于从套接字接收数据,并可以接收辅助数据(ancillary data)。函数原型如下:
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
参数说明:
-
sockfd
:指定接收数据的套接字文件描述符。 -
msg
:指向msghdr
结构的指针,该结构包含了接收操作的所有参数。 -
flags
:指定接收数据的操作方式,可以是0或者以下一个或多个值的逻辑或:MSG_OOB
:接收带外数据。MSG_PEEK
:查看数据但不从接收队列中移除数据。MSG_WAITALL
:等待所有请求的数据,直到请求的数量被接收为止。- 等等。
msghdr
结构定义如下:
struct msghdr {
void *msg_name; /* Optional address */
socklen_t msg_namelen; /* Size of address */
struct iovec *msg_iov; /* Scatter/gather array */
int msg_iovlen; /* # elements in msg_iov */
void *msg_control; /* Ancillary data, see below */
socklen_t msg_controllen; /* Ancillary data buffer len */
int msg_flags; /* Flags on received message */
};
msg_name
和msg_namelen
:用于接收对方地址信息(仅用于面向连接的套接字)。msg_iov
和msg_iovlen
:指定分散读的缓冲区数组iovec
和其长度。msg_control
和msg_controllen
:用于接收辅助数据和控制信息。msg_flags
:接收消息的标志,由系统填充。
返回值:
- 成功时,返回接收到的字节数。
- 如果连接被对方关闭,返回0。
- 出错时,返回-1,并设置errno来指示错误。
sendmsg函数
sendmsg
函数用于向套接字发送数据,并可以发送辅助数据。函数原型如下:
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
参数说明:
-
sockfd
:指定发送数据的套接字文件描述符。 -
msg
:指向msghdr
结构的指针,该结构包含了发送操作的所有参数。 -
flags
:指定发送数据的操作方式,可以是0或者以下一个或多个值的逻辑或:MSG_OOB
:发送带外数据。MSG_DONTROUTE
:发送数据时不通过网关。- 等等。
返回值:
- 成功时,返回发送的字节数。
- 出错时,返回-1,并设置errno来指示错误。
1.4 客户—服务器实例
这个服务器的功能主要是返回客户端输入的数据长度,其中使用了I/O函数中的readv和writev。
服务器端代码:
(我这个代码涉及到许多前面的基础知识,需要了解的就可以看我上一篇博客:
写文章-CSDN创作中心socket套接字函数-CSDN博客写文章-CSDN创作中心)
#include<t_stdio.h>
#include<t_file.h>
#include<stdlib.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <arpa/inet.h>
#include<unistd.h>
static struct iovec*vs=NULL;
void sig_process(int signo){//信号处理函数
printf("catch a exit signal..\n");
free(vs);
_exit(0);
}
#define port 8888//端口号
#define backlog 2//最大监听数量
//业务处理逻辑
void process_conn_server(int s){
char buffer [30];
ssize_t size=0; //向量的缓冲区
//申请3个向量空间
struct iovec * v = (struct iovec *)malloc(3*sizeof(struct iovec));
if(!v) E_MSG("malloc",-1);
vs = v;//挂接全局变量,易于释放管理
v[0].iov_base= buffer;//每个向量十个地址空间
v[1].iov_base= buffer+10;
v[2].iov_base= buffer+20;
v[0].iov_len = v[1].iov_len = v[2].iov_len = 10;//初始化长度为10
for(;;){
size = readv(s, v , 3);// 从套接字中读取数据放入数据缓冲区
if(size == 0){
return ;
}
//构建响应字符
sprintf(v[0].iov_base,"%d",size);
sprintf(v[1].iov_base,"bytes alter");
sprintf(v[2].iov_base,"ogether\n");
//设置写入数据的长度
v[0].iov_len = strlen(v[0].iov_base);
v[1].iov_len = strlen(v[1].iov_base);
v[2].iov_len = strlen(v[2].iov_base);
writev(s , v , 3);//发送给客户端
}
}
int main(int argc ,char * argv[])
{
int ss ,sc;//服务器和客户端的套接字文件描述符
struct sockaddr_in server_addr;//服务器地址结构
struct sockaddr_in client_addr;//客户端地址结构
pid_t pid;//创建子进程进行分叉进行
signal(SIGINT,sig_process);//添加sigint信号到信号掩码
signal(SIGPIPE,sig_process);
ss = socket(AF_INET,SOCK_STREAM,0);// 创建服务器套接字
if(ss < 0) E_MSG("socket",-1);
bzero(&server_addr,sizeof(server_addr));//将地址清零
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htons(INADDR_ANY);//本地地址
server_addr.sin_port=htons(port);//设置端口
//绑定本地到套接字符
int err = bind(ss,(struct sockaddr * )&server_addr,sizeof(server_addr));
if(err < 0)E_MSG("bind",-1);
err = listen(ss,backlog);//创建监听队列
for(;;){
int addrlen = sizeof(struct sockaddr);
sc = accept(ss,(struct sockaddr * )&client_addr,&addrlen);//获取用户套接字
if(sc < 0)continue;//客户端出错,结束这次循环,继续监听客户;
pid = fork();//创建新的子进程来处理当前连接客户端;
if(pid==0){
close(ss);// 子进程中关闭服务端
process_conn_server(sc);
}
else {
close(sc);//父进程关闭客户端连接,继续监听;
}
}
return 0;
}
客户端代码:
#include<t_stdio.h>
#include<t_file.h>
#include<stdlib.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <arpa/inet.h>
#include<unistd.h>
#define port 8888
static int s;
static struct iovec*vc=NULL;
void sig_proccess(int signo){
printf("catch a exit signal..\n");
free(vc);
_exit(0);
}
void sig_pipe(int signo){
printf("catch a sigpipe signal..\n");
free(vc);
_exit(0);
}
//业务处理函数
void process_conn_client(int s){
char buffer [30];
ssize_t size=0; //向量的缓冲区
//申请3个向量空间
struct iovec * v = (struct iovec *)malloc(3*sizeof(struct iovec));
if(!v) E_MSG("malloc",-1);
vc = v;//挂接全局变量,易于释放管理
v[0].iov_base= buffer;//每个向量十个地址空间
v[1].iov_base= buffer+10;
v[2].iov_base= buffer+20;
v[0].iov_len = v[1].iov_len = v[2].iov_len = 10;//初始化长度为10
int i=0;
for(;;){
//从标准输入中读取数据放入缓冲区buffer中
size = read(0,v[0].iov_base,10);//只有十个字节
if(size>0){
v[0].iov_len = size;
writev(s,v,1);//发送给服务器
v[0].iov_len = v[1].iov_len = v[2].iov_len = 10;
size=readv(s,v,3);//从服务器读取数据
for(i = 0;i<3;i++){
if(v[i].iov_len > 0)
{
write(1,v[i].iov_base,v[i].iov_len);//将服务区处理后数据输出
}
}
}
}
}
int main(int argc ,char * argv[])
{
struct sockaddr_in server_addr;//服务器结构地址
int err;
if(argc == 1){
printf("pls input server addr\n");
return 0;
}//没有输入指令参数
signal(SIGINT,sig_proccess);
signal(SIGPIPE,sig_pipe);//设置信号掩码
s = socket(AF_INET,SOCK_STREAM,0);//创建客户端套接字
if(s < 0)E_MSG("socket",-1);
//设置服务器地址
bzero(&server_addr,sizeof(server_addr));//将地址清0
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htons(INADDR_ANY);//本地地址
server_addr.sin_port=htons(port);//设置端口
inet_pton(AF_INET,argv[1],&server_addr.sin_addr);//将用户输入的字符串ip转化为二进制ip
connect(s,(struct sockaddr *)&server_addr,sizeof(server_addr));//连接服务器
process_conn_client(s);
return 0;
}
对于这两个代码,大家可以仔细阅读,我写了非常详细的注释,包括信号的处理,业务逻辑的处理等等,可以将我们整个基础网络知识串联起来!
二.I/O模型介绍
网络编程中的I/O模型主要影响程序如何处理网络套接字的读写操作。以下是几种常见的I/O模型:
2.1 阻塞I/O(Blocking I/O)
阻塞I/O模型是最简单的I/O模型。当发起一个I/O操作(如读或写)时,如果数据未准备好,程序会阻塞直到操作完成。在这段时间内,程序无法执行其他任务。
// 阻塞读
char buffer[1024];
read(socket, buffer, sizeof(buffer));
2.2非阻塞I/O(Non-blocking I/O)
在非阻塞I/O模型中,I/O操作不会阻塞程序。如果数据未准备好,I/O操作会立即返回一个错误码(通常是EAGAIN
或EWOULDBLOCK
)。程序需要定期轮询检查数据是否准备好.
// 设置套接字为非阻塞模式
int flags = fcntl(socket, F_GETFL, 0);
fcntl(socket, F_SETFL, flags | O_NONBLOCK);
// 非阻塞读
char buffer[1024];
while (read(socket, buffer, sizeof(buffer)) == -1 && errno == EAGAIN) {
// 数据未准备好,继续做其他事情
}
2.3I/O多路复用(I/O Multiplexing)
I/O多路复用允许程序同时监视多个文件描述符,等待一个或多个变得“就绪”。这可以通过select
、poll
、epoll
(Linux特有)或kqueue
(BSD特有)系统调用来实现。
// 使用select进行I/O多路复用
fd_set readfds;
FD_ZERO(&readfds);
FD_SET(socket, &readfds);
select(socket + 1, &readfds, NULL, NULL, NULL);
if (FD_ISSET(socket, &readfds)) {
char buffer[1024];
read(socket, buffer, sizeof(buffer));
}
2.4信号驱动I/O(Signal-driven I/O)
信号驱动I/O模型中,程序通过sigaction
系统调用请求内核在文件描述符就绪时发送一个信号。当数据准备好时,内核会发送一个SIGIO信号,程序可以在信号处理函数中处理数据。
// 设置信号处理函数
struct sigaction sa;
sa.sa_handler = io_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART;
sigaction(SIGIO, &sa, NULL);
// 设置套接字的所有者
fcntl(socket, F_SETOWN, getpid());
// 开启信号驱动I/O
int flags = fcntl(socket, F_GETFL, 0);
fcntl(socket, F_SETFL, flags | O_ASYNC);
2.5异步I/O(Asynchronous I/O)
异步I/O模型中,程序发起I/O操作后,立即返回,无需等待数据准备好。当操作完成时,程序会收到通知。这种模型通常通过aio_read
、aio_write
等系统调用来实现。
// 异步读
aiocb aio;
memset(&aio, 0, sizeof(aio));
aio.aio_fildes = socket;
aio.aio_buf = buffer;
aio.aio_nbytes = sizeof(buffer);
aio.aio_offset = 0;
aio_read(&aio);
每种I/O模型都有其适用的场景。例如,阻塞I/O适用于简单应用,非阻塞I/O和I/O多路复用适用于需要处理多个套接字的应用,而异步I/O适用于需要高性能和低延迟的应用。在选择合适的I/O模型时,需要考虑应用的特定需求和性能。
三.监视函数
3.1select和pselect
在网络编程中,select
和 pselect
函数是用于多路复用(I/O multiplexing)的系统调用,它们允许程序同时监视多个文件描述符,以判断它们是否准备好进行读、写或异常(错误)操作。
select函数
select
函数的原型如下:
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
参数说明:
nfds
:需要检查的最高文件描述符加1。通常设置为最大的文件描述符加上1。readfds
:指向一组文件描述符的指针,这些文件描述符需要检查是否准备好读取数据。writefds
:指向一组文件描述符的指针,这些文件描述符需要检查是否准备好写入数据。exceptfds
:指向一组文件描述符的指针,这些文件描述符需要检查是否发生异常。timeout
:指向timeval
结构的指针,用于指定select
应该阻塞等待的最大时间。如果设置为NULL
,select
将无限期地等待;如果设置为0,select
将立即返回。
timeval
结构定义如下:
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
返回值:
- 成功时,返回准备好的文件描述符的数量。
- 如果超时,返回0。
- 出错时,返回-1,并设置errno来指示错误。
使用 select
时,需要使用 FD_ZERO
、FD_SET
和 FD_CLR
宏来操作 fd_set
集合。
pselect函数
pselect
函数与 select
类似,但它提供了一些额外的功能,特别是在处理信号方面。pselect
的原型如下:
int pselect(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, const struct timespec *timeout, const sigset_t *sigmask);
参数说明:
- 前四个参数与
select
函数相同。 timeout
:指向timespec
结构的指针,用于指定pselect
应该阻塞等待的最大时间。timespec
结构与timeval
类似,但它使用秒和纳秒来表示时间。sigmask
:指向sigset_t
类型的一个信号集的指针,用于在pselect
调用期间临时替换进程的信号掩码。这意味着pselect
在等待文件描述符就绪时会阻塞指定的信号。
timespec
结构定义如下:
struct timespec {
time_t tv_sec; /* seconds */
long tv_nsec; /* nanoseconds */
};
返回值:
- 成功时,返回准备好的文件描述符的数量。
- 如果超时,返回0。
- 出错时,返回-1,并设置errno来指示错误。
例子:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/select.h>
int main() {
fd_set readfds;
struct timeval timeout;
int result;
FD_ZERO(&readfds);
FD_SET(0, &readfds); // 监视标准输入(文件描述符0)
timeout.tv_sec = 5; // 设置超时时间为5秒
timeout.tv_usec = 0;
result = select(1, &readfds, NULL, NULL, &timeout);
if (result == -1) {
perror("select");
exit(EXIT_FAILURE);
} else if (result > 0) {
printf("Data is available to read.\n");
} else {
printf("Timeout occurred.\n");
}
return 0;
}
在这个例子中,我们使用 select
来监视标准输入(文件描述符0),设置超时时间为5秒。如果在这段时间内有数据可读,select
将返回大于0的值;如果超时,将返回0;如果发生错误,将返回-1。
3.2poll函数和ppoll函数
poll
和 ppoll
函数是 Unix 和类 Unix 操作系统中的系统调用,用于多路复用(I/O multiplexing),允许程序同时监视多个文件描述符的读写状态。与 select
和 pselect
函数相比,poll
和 ppoll
提供了更丰富的接口和更好的性能。
poll函数
poll
函数的原型如下:
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
参数说明:
fds
:指向一个pollfd
结构数组的指针,每个结构描述了一个要监视的文件描述符。nfds
:指定fds
数组中pollfd
结构的数量。timeout
:指定poll
应该阻塞等待的最大时间(毫秒)。如果设置为-1,poll
将无限期地等待;如果设置为0,poll
将立即返回。
pollfd
结构定义如下:
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events */
short revents; /* returned events */
};
fd
:要监视的文件描述符。events
:请求监视的 events(通过位掩码指定)。revents
:返回时,表示实际发生的事件(通过位掩码指定)。
events
和 revents
可以是以下标志的组合(定义在 <poll.h>
头文件中):
POLLIN
:有数据可读。POLLOUT
:可以写数据。POLLERR
:发生错误。POLLHUP
:挂起。POLLNVAL
:无效的请求。
返回值:
- 成功时,返回准备好的文件描述符的数量。
- 如果超时,返回0。
- 出错时,返回-1,并设置errno来指示错误。
ppoll函数
ppoll
函数与 poll
类似,但它提供了一些额外的功能,特别是在处理信号方面。ppoll
的原型如下:
int ppoll(struct pollfd *fds, nfds_t nfds, const struct timespec *timeout, const sigset_t *sigmask);
参数说明:
- 前三个参数与
poll
函数相同。 timeout
:指向timespec
结构的指针,用于指定ppoll
应该阻塞等待的最大时间。timespec
结构与timeval
类似,但它使用秒和纳秒来表示时间。sigmask
:指向sigset_t
类型的一个信号集的指针,用于在ppoll
调用期间临时替换进程的信号掩码。这意味着ppoll
在等待文件描述符就绪时会阻塞指定的信号。
返回值:
- 成功时,返回准备好的文件描述符的数量。
- 如果超时,返回0。
- 出错时,返回-1,并设置errno来指示错误。
例子:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <poll.h>
int main() {
struct pollfd fds[1];
int result;
fds[0].fd = 0; // 监视标准输入(文件描述符0)
fds[0].events = POLLIN; // 监视可读事件
result = poll(fds, 1, 5000); // 设置超时时间为5000毫秒
if (result == -1) {
perror("poll");
exit(EXIT_FAILURE);
} else if (result > 0) {
if (fds[0].revents & POLLIN) {
printf("Data is available to read.\n");
}
} else {
printf("Timeout occurred.\n");
}
return 0;
}
在这个例子中,我们使用 poll
来监视标准输入(文件描述符0),设置超时时间为5000毫秒。如果在这段时间内有数据可读,poll
将返回大于0的值;如果超时,将返回0;如果发生错误,将返回-1。
四.总结
这篇文章对数据I/O进行了整体的介绍,包括recv ,send, readv , writev,recmsg , sendmsg等I/O函数,并且给了很多例子,来介绍I/O的各种模型及其使用,相信经过这篇文章的学习,大家对网络编程中的I/O操作能有个整体认知!!!