Linux高性能服务器编程学习笔记(一)高级I/O函数

高级I/O函数

6.1 pipe函数

pipe函数可用于创建一个管道,以实现进程间通信

#include<unistd.h>
int pipe(int fd[2]);
参数是包含两个int型整数的数组指针
成功返回0,并将一对打开的文件描述符写入数组,失败返回-1
如果要实现双向传输则需要打开两个管道
    read读取一个空管道或write写一个满的管道将阻塞
    但是可以设置关于阻塞的选项(第八章)

image

管道内部传输的数据是和TCP类似的字节流,两者概念相同,但是应用层能够往一个TCP连接中写入多少字节的数据取决于对方的接收通告窗口大小和本端的拥塞窗口大小,而管道本身具有容量限制,Linux 2.6.11开始大小默认为65536字节

可以使用fcntl函数修改管道容量

//创建双向管道的API
#include<sys/types.h>
#include<sys/socket.h>
int socketpair(int domain,int type,int protocol,int fd[2]);
domain只能只用AF_INET,所以只能在本地使用这个双向管道,fd可以又读又写
    成功返回0,失败返回-1

6.2 dup和dup2

这两个函数用于复制文件描述符

#include<unistd.h>
int dup(int file_dsescriptor);
int dup2(int file_descriptor_one,int file_descriptor_two);
//失败返回-1
/*
创建一个新的文件描述符且新的文件描述符和原有的文件描述符指向相同的文件,管道或网络连接,并且毒品返回的文件描述符是当前系统可用的最小的数值
dup2与dup不同的是:它返回不小于file_descriptor_two的值

通过dup创建的文件描述符并不继承原本文件描述符的属性(close-on-exec 和 non-blocking等)

*/
#include<sys/socket.h>
#include<netinet/in.h>
#include<assert.h>
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
#include<arpa/inet.h>


int main(int argc,char* argv[]){
    if(argc<=2){
        printf("Usage : %s ip_address port_number\n",basename(argv[0]));
        return 1;
    }
    //指定的ip地址(此代码中不适用随机的ip地址(INADDR_ANY))
    const char *ip = argv[1];
    int port = atoi(argv[2]);
    struct sockaddr_in address;
    bzero(&address,sizeof(address));
    address.sin_family = AF_INET;
    address.sin_port = htons(port);
// inet_pton用于将点分十进制ip地址转换为网络字节序ip地址且存入dst内存中
    inet_pton(AF_INET,ip,&address.sin_addr);

    int sock = socket(PF_INET,SOCK_STREAM,0);//生成服务器的socket文件描述符
    assert(sock>=0);
    //将socket地址分配给socket文件描述符(命名socket)
    int ret = bind(sock,(struct sockaddr*)&address,sizeof(address));
    assert(sock!=-1);
    ret = listen(sock,5);
    assert(ret!=-1);

    struct sockaddr_in clnt;
    socklen_t clnt_size = sizeof(clnt);
    int connfd = accept(sock,(struct sockaddr*)&clnt,&clnt_size);
    if(connfd<0){
        printf("errno is :%d\n",errno);
    }
    else{
        close(STDOUT_FILENO);
        dup(connfd);
        printf("abcd\n");
        close(connfd);

    }
    close(sock);
    return 0;
    
//先关闭标准输出文件描述符,再复制connfd,这样复制出来的文件描述符就是系统
// 当前可用最小值,即为之前的标准输出描述符即为1,这样使用printf输出的内容就会直接发送到
// 与客户端对应的socket上
// 此为CGI服务器的工作原理

}

image

客户端将接收并打印出abcd字符串

//客户端代码
#include<iostream>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<stdlib.h>
#include<stdio.h>
#include<string.h>


void error_handling(char *message);

// connect()请求连接函数


int main(int argc,char* argv[]){
    int sock;
    sockaddr_in serv_addr;
    char message[30];
    int str_len;

    if(argc!=3){
        printf("Usage:%s<ip> <port>\n",argv[0]);
        exit(1);
    }

    sock = socket(PF_INET,SOCK_STREAM,0);
    if(sock==-1){
        error_handling("socket() error");
    }

    memset(&serv_addr,0,sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
    serv_addr.sin_port = htons(atoi(argv[2]));

    if(connect(sock,(struct sockaddr*)&serv_addr,sizeof(serv_addr))==-1){
        error_handling("connect() error");
    }

    str_len = read(sock,message,sizeof(message)-1);
    if(str_len==-1){
        error_handling("read() error!");
    }

    printf("Message from server: %s\n",message);
    close(sock);
    return 0;
}
void error_handling(char *message){
    fputs(message,stderr);
    fputc('\n',stderr);
    exit(1);
}

6.3 readv,sendv

#include <sys/uio.h>

       ssize_t readv(int fd, const struct iovec *iov, int iovcnt);

       ssize_t writev(int fd, const struct iovec *iov, int iovcnt);

iovec描述一块内存区,count是vector数组的长度(即有几块内存需要从fd读出或写入fd)

成功时返回读出或写入的字节数

web服务器上的集中写:
Web服务器解析一个HTTP请求后,如果对应的文档存在且客户有相应的读取权限,则服务器需要发送HTTP应答报文:报文包括:

  1. 一个状态行
  2. 多个头部字段
  3. 一个空行
  4. 文档内容 其中前三部分大多数情况下放在同一内存块中,文档内容单独放入一个内存卡,且不许要将两个内存块拼接发送,使用writev可以同时写出
//此处省略头文件
#include<sys/socket.h>
#include<netinet/in.h>
#include<assert.h>
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
#include<arpa/inet.h>
#include<sys/sendfile.h>
#include<fcntl.h>
#include<string.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<sys/uio.h>

#define BUFFER_SIZE 1024
    //两种HTTP状态码和状态信息
static const char* status_line[2] = {"200 ok","500 Internal server error"};
    
int main(int argc,char* argv[])
{

    //指定的ip地址(此代码中不适用随机的ip地址(INADDR_ANY))
    const char *ip = argv[1];
    int port = atoi(argv[2]);
    //目标文件
    const char* file_name = argv[3];
    struct sockaddr_in address;
    bzero(&address,sizeof(address));
    address.sin_family = AF_INET;
    address.sin_port = htons(port);
// inet_pton用于将点分十进制ip地址转换为网络字节序ip地址且存入dst内存中
    inet_pton(AF_INET,ip,&address.sin_addr);

    int sock = socket(PF_INET,SOCK_STREAM,0);//生成服务器的socket文件描述符
    assert(sock>=0);
    //将socket地址分配给socket文件描述符(命名socket)
    int ret = bind(sock,(struct sockaddr*)&address,sizeof(address));
    assert(sock!=-1);
    ret = listen(sock,5);
    assert(ret!=-1);

    struct sockaddr_in clnt;
    socklen_t clnt_size = sizeof(clnt);
    int connfd = accept(sock,(struct sockaddr*)&clnt,&clnt_size);
    if(connfd<0){
        printf("errno is :%d\n",errno);
    }
    else{
        //获取连接成功
        //开辟内存空间用于保存状态行,头部字段,空行
        char head_buf[BUFFER_SIZE];
        memset(head_buf,'\0',sizeof(head_buf));
        //用于存放要获取的目标文件内容的应用程序缓存
        char * file_buf;
        //获取文件属性:(是否为目录/文件大小等属性)
        struct stat file_status;
        //记录文件是否有效
        bool valid = true;
        //缓存区head_buf用了多少字节空间
        int len = 0;
        if(stat(file_name,&file_status)<0){//返回-1目标文件不存在
            valid = false;
        }
        else{
            //查看是否为目录
            if(S_ISDIR(file_status.st_mode)){
                valid =false;
            }
            else if(file_status.st_mode&S_IROTH){//如果不是目录且当前用户有读取目标文件的权限
                //动态分配缓存区file_buf,指定大小为目标文件的大小+1,将内容读入file_buf
                int fd = open(file_name,O_RDONLY);
                file_buf = new char[file_status.st_size+1];
                memset(file_buf,'\0',file_status.st_size+1);
                if(read(fd,file_buf,file_status.st_size)<0){
                    valid = false;
                }
            }
            else{
                valid = false;
            }

        }
        //如果目标文件有效,则发送HTTP应答
        if(valid){
            //依次将head的三个部分加入header_buf
            ret = snprintf(head_buf,BUFFER_SIZE-1,"%s %s\r\n","HTTP/1.1",status_line[0]);
            len+=ret;
            ret = snprintf(head_buf+len,BUFFER_SIZE-1-len,"Content-Length:%d\r\n",file_status.st_size);
            len+=ret;
            ret = snprintf(head_buf+len,BUFFER_SIZE-1-len,"%s","\r\n");
            //使用writev将header_buf和file_buf的内容一起写出:
            struct iovec iv[2];
            iv[0].iov_base = head_buf;
            iv[0].iov_len = strlen(head_buf);
            iv[1].iov_base = file_buf;
            iv[1].iov_len = file_status.st_size;
            ret = writev(connfd,iv,2);
        }
        else{
            //文件无效
            ret = snprintf(head_buf,BUFFER_SIZE-1,"%s %s\r\n","HTTP/1.1",status_line[1]);
            len+=ret;
            ret = snprintf(head_buf+len,BUFFER_SIZE-1-len,"%s","\r\n");
            send(connfd,head_buf,strlen(head_buf),0);

        }
        close(connfd);
        delete [] file_buf;
    }   
    close(sock);
    return 0;
}

6.4 senfile函数

sendfile 几乎完全是为了在网络上传输文件设计的函数

因为它的in_fd必须是真实的文件,不能是socket和管道,out_fd必须是socket

off_set指定从读入文件的哪个位置,为NULL则从默认开始位置开始读,count是传递的字节数

#include <sys/sendfile.h>

   ssize_t  sendfile(int  out_fd,  int  in_fd,  off_t *offset,
   size_t count);
//使用sendfile传输文件
#include<sys/socket.h>
#include<netinet/in.h>
#include<assert.h>
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
#include<arpa/inet.h>
#include<sys/sendfile.h>
#include<fcntl.h>
#include<string.h>
#include<sys/stat.h>
#include<sys/types.h>
int main(int argc,char* argv[])
{   
    const char* ip = argv[1];
    int port = atoi(argv[2]);
    const char* file_name = argv[3];
    int filefd = open(file_name,O_RDONLY);
    assert(filefd>0);
    struct stat stat_buf;
    //fstat函数用来将参数fildes 所指的文件状态, 复制到参数buf 所指的结构中(struct stat). Fstat()与stat()作用完全相同, 不同处在于传入的参数为已打开的文件描述词
    fstat(filefd,&stat_buf);
        struct sockaddr_in address;
    bzero(&address,sizeof(address));
    address.sin_family = AF_INET;
    address.sin_port = htons(port);
// inet_pton用于将点分十进制ip地址转换为网络字节序ip地址且存入dst内存中
    inet_pton(AF_INET,ip,&address.sin_addr);

    int sock = socket(PF_INET,SOCK_STREAM,0);//生成服务器的socket文件描述符
    assert(sock>=0);
    //将socket地址分配给socket文件描述符(命名socket)
    int ret = bind(sock,(struct sockaddr*)&address,sizeof(address));
    assert(sock!=-1);
    ret = listen(sock,5);
    assert(ret!=-1);

    struct sockaddr_in clnt;
    socklen_t clnt_size = sizeof(clnt);
    int connfd = accept(sock,(struct sockaddr*)&clnt,&clnt_size);
    if(connfd<0){
        printf("errno is :%d\n",errno);
    }
    else{
        sendfile(connfd,filefd,NULL,stat_buf.st_size);
        close(connfd);
    }
    close(sock);
    return 0;
    
}

image

6.5 mmap,munmap

用于申请一段内存空间作为进程间通信的共享内存,也可以直接将文件映射到其中,munmap用于释放这段内存空间

#include <sys/mman.h>

       void *mmap(void *addr, size_t length, int prot, int flags,
                  int fd, off_t offset);

flags控制内存段内容被修改后程序的行为,其中MAP_PRIVATE和MAP_SHARED是互斥的

port可以取以下值的按位或

PORT_READ //内存段可读
PORT_WRITE//可写
PORT_EXEC //可执行
PORT_NONE //不能被访问

6.6 splice函数

用于在两个文件描述符之间移动数据(零拷贝操作)

#include<fcntl.h>
       ssize_t splice(int fd_in, loff_t *off_in, int fd_out,
                      loff_t *off_out, size_t len, unsigned int flags);
/*
fd_in是待输入数据的文件描述符,
    如果fd_in是管道文件描述符,off_in参数必须被设置为NULL。否则off_in表示从输入数据流的何处开始读取数据。此时,若off_in被设置为NULL,则表示从输入数据流的当前偏移位置读人﹔若off_in不为NULL,则它将指出具体的偏移位置。fd_out/off_out参数的含义与fd_in/off_in相同,不过用于输出数据流。len参数指定移动数据的长度;flags 参数则控制数据如何移动
*/

image

splice的参数中,fd_in/fd_out必须有一个是管道文件描述符

成功时返回移动字节的数量,0表示没有数据移动(例如从管道读取数据,但是该管道没有被写入任何数据时),失败返回-1,并设置errno

image

//server
//使用splice实现一个零拷贝的回声服务器,将客户端发送的数据返回给客户端
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<string.h>
#include<signal.h>
#include<errno.h>
#include<fcntl.h>

#define BUF_SIZE 1024
int main(int argc,char* argv[]){

                                                                                                                                                                      
    if(argc<=2){
        printf("usage\n");
        return 1;
    }
    const char* ip = argv[1];
    int port = atoi(argv[2]);
    struct sockaddr_in address;
    bzero(&address,sizeof(address));
    address.sin_family = AF_INET;
    address.sin_port = htons(port);
// inet_pton用于将点分十进制ip地址转换为网络字节序ip地址且存入dst内存中
    inet_pton(AF_INET,ip,&address.sin_addr);

    int sock = socket(PF_INET,SOCK_STREAM,0);//生成服务器的socket文件描述符
    assert(sock>=0);
    //将socket地址分配给socket文件描述符(命名socket)
    int ret = bind(sock,(struct sockaddr*)&address,sizeof(address));
    assert(sock!=-1);
    ret = listen(sock,5);
    assert(ret!=-1);
    //暂停20秒等待客户端连接和异常操作完成
    struct sockaddr_in client;
    socklen_t client_length = sizeof(client);
    int connfd = accept(sock,(struct sockaddr*)&client,&client_length);
    if(connfd<0){
        printf("errno is :%d\n",errno);
    }
    else{
        // char message[50];
        // read(sock,message,sizeof(message)-1);
        // puts(message);
        int pipefd[2];
        assert(ret!=-1);
        //创建管道
        ret =  pipe(pipefd);
        //将connfd上流入的客户数据定向到管道中
        ret = splice(connfd,NULL,pipefd[1],NULL,32768,SPLICE_F_MORE|SPLICE_F_MOVE);
        assert(ret!=-1);
        //将管道的输出重定向到connfd
        ret = splice(pipefd[0],NULL,connfd,NULL,32768,SPLICE_F_MORE|SPLICE_F_MOVE);
        assert(ret!=-1);
        
        close(connfd);

    }
    // msghdr
    close(sock);
    return 0;
//以上代码通过splice函数将客户端的内容读入到管道中,再使用splice函数读出该内容到客户端,实现echo
//不涉及send,recv操作,所以为设计用户空间和内核空间之间的数据拷贝

}
//clnt
#include<iostream>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<stdlib.h>
#include<stdio.h>
#include<string.h>

#define BUF_SIZE 30
void error_handling(char *message);

// connect()请求连接函数


int main(int argc,char* argv[]){
    int sock;
    sockaddr_in serv_addr;
    char message[30];
    int str_len;

    if(argc!=3){
        printf("Usage:%s<ip> <port>\n",argv[0]);
        exit(1);
    }

    sock = socket(PF_INET,SOCK_STREAM,0);
    if(sock==-1){
        error_handling("socket() error");
    }

    memset(&serv_addr,0,sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
    serv_addr.sin_port = htons(atoi(argv[2]));

    if(connect(sock,(struct sockaddr*)&serv_addr,sizeof(serv_addr))==-1){
        error_handling("connect() error");
    }

        fputs("Input message(Q/q to quit):",stdout);
        fgets(message,BUF_SIZE,stdin);
        if(!strcmp(message,"q\n")||!strcmp(message,"Q\n")){
            break;
        }
        printf("%s\n",message);
        write(sock,message,strlen(message));
        str_len = read(sock,message,BUF_SIZE-1);
        message[str_len] = 0;
        printf("Message from server:%s\n",message);
        close(sock);
    return 0;
}
void error_handling(char *message){
    fputs(message,stderr);
    fputc('\n',stderr);
    exit(1);
}

6.7 tee函数

在两个管道文件描述符之间复制数据,不消耗源文件描述符上的数据,即源文件描述符的数据仍可用于后续读操作,参数与splice基本相同,但是fd_in/fd_out都必须是管道文件描述符,成功返回复制的数据量(字节数),失败返回-1,返回0表示没有复制数据

//利用tee,splice函数实现同时输出到终端和文件的程序
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<string.h>
#include<signal.h>
#include<errno.h>
#include<fcntl.h>

int main(int argc,char* argv[])
{
    if(argc!=2){
        printf("usgae:%s <file>\n",argv[0]);
        return 1;
    }   
    int filefd = open(argv[1],O_CREAT|O_WRONLY|O_TRUNC,0666);
    assert(filefd>0);
    int pipefd_stdout[2];
    int ret = pipe(pipefd_stdout);
    assert(ret!=-1);
    int pipefd_file[2];
    ret = pipe(pipefd_file);
    assert(ret!=-1);
    
    //将标准输入的内容输入管道pipefd_stdout
    ret = splice(STDIN_FILENO,NULL,pipefd_stdout[1],NULL,32768,SPLICE_F_MORE|SPLICE_F_MOVE);
    //将pipefd_stdout管道输出复制到pipefd_file管道输入端
    ret = tee(pipefd_stdout[0],pipefd_file[1],32768,SPLICE_F_NONBLOCK);
    //将pipefd_file输出重定向到filefd上,从而将标准输入的内容写入文件
    ret = splice(pipefd_file[0],NULL,filefd,NULL,32768,SPLICE_F_MORE|SPLICE_F_MOVE);
    //将管道pipefd_stdout的输出定向到标准输出
    ret = splice(pipefd_stdout[0],NULL,STDOUT_FILENO,NULL,32768,SPLICE_F_MORE|SPLICE_F_MOVE);
    assert(ret!=-1);
    close(filefd);
    close(pipefd_stdout[0]);
    close(pipefd_stdout[1]);
    close(pipefd_file[0]);
    close(pipefd_file[1]);
    return 0;
}

image

image

6.8 fcntl函数

提供对文件描述符的各种控制操作,功能更多的是ioctl系统调用。但是fcntl是POSIX规范的首选方法

#include <unistd.h>
       #include <fcntl.h>

       int fcntl(int fd, int cmd, ... /* arg */ );

支持的操作和参数比较常用和重要的是:获取和设置文件描述符的状态标志

F_GETFL获取fd的状态标志,包括可由open系统调用设置的标志(O_CREATE,O_APPEND)和访问模式(O_RDONLY,O_WRONLY,O_RDWR)void返回fd的状态标志
F_SETFL设置fd 的状态标志,但是访问模式标志等不能修改long0

fcntl经常用于把一个文件描述符设置未非阻塞的

int setnonblocking(int fd){
	int old_option = fcntl(fd,F_GETFL);//获取文件描述符旧的状态标志
    int new_option = old_option|O_NONBLOCK;//设置非阻塞标志
    fcntl(fd,F_SETFL,new_option);
    return old_option;//以便日后回复旧的状态标志
}

image

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值