Part 2 Linux programming:chapter 15:套接字和标准I/O

第十五章:套接字和标准I/O

15.1 标准I/O函数

这里需要熟练掌握一些文件操作时使用的函数(fopen、feof、fgetc、fputs等)
啥是标准I/O函数呢?
下面列出一些常用的

  1. fopen fclose
FILE *fopen(const char *path,const char *mode);

其中,path是我们要打开的流,而mode就是我们打开文件的方式了,也就决定你所打开的文件将被怎样的去对待啦,有如下几种方式:

"r":只读方式打开,打开的文件必须存在。
"r+"  :读写方式打开,文件必须存在。
"w" : 只写方式打开,文件不存在则创建,文件存在则清空。
"w+" : 读写方式打开,文件不存在则创建,文件存在则清空。
"a" :  只写方式打开,追加的方式写到文件的尾部,文件不存在则创建。
"a+": 读写方式打开,文件不存在创建,从头开始读,从尾开始写。

  1. fgetc fputc
 功能:  从文件中读取字符。
 头文件: #include <stdio.h>
 函数原型: int fgetc(FILE *stream);
 返回值: 返回所读取的一个字节。如果读到文件末尾或者读取出错时返回EOF


 功能:  将字符写到文件中。
 头文件: #include <stdio.h>
 函数原型: int fputc(int c, FILE *stream);

 返回值: 在正常调用情况下,函数返回写入文件的字符的ASCII码值,出错时,返回EOF-1)。当正确写入一个字符或一个字节的数据后,文件内部写指针会自动后移一个字节的位置。EOF是在头文件 stdio.h中定义的宏。
  1. fgets fputs
 fgets();
 功能:  从文件中读取字符串 
 头文件: #include <stdio.h>
 函数原型: char *fgets(char *buf, int size, FILE *stream);
 参数说明: *s  字符型指针,指向用来存储所得数据的地址。
    size 整型数据,指明存储数据的大小。
    *stream 文件结构体指针,将要读取的文件流。
 返回值: 
 1.成功,则返回第一个参数buf;
 2.在读字符时遇到end-of-file,则eof指示器被设置,如果还没读入任何字符就遇到这种情况,则buf保持原来的内容,返回NULL3.如果发生读入错误,error指示器被设置,返回NULL,buf的值可能被改变.
 
fputs();
 功能:  向指定的文件写入一个字符串(不自动写入字符串结束标记符‘\0)
 头文件: #include <stdio.h>
 函数原型: int fputs(const char *s, FILE *stream);
 参数说明: *s  s是字符型指针,可以是字符串常量,或者存放字符串的数组首地址。
 返回值: 返回值为非负整数;否则返回EOF(符号常量,其值为-1)
  1. fread fwrite
fread();
 功能:  从文件流中读数据,最多读取nmemb个项,每个项size个字节
 头文件: #include <stdio.h>
 函数原型: size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
 参数说明: *ptr 用于接收数据的内存地址
    size 要读的每个数据项的字节数,单位是字节
    nmemb 要读nmemb个数据项,每个数据项size个字节.
    *stream 文件流
 返回值: 
 返回真实读取的项数,若大于nmemb则意味着产生了错误。另外,产生错误后,文件位置指示器是无法确定的。
 若其他stream或buffer为空指针,或在unicode模式中写入的字节数为奇数,
 此函数设置errno为EINVAL以及返回0.


fwrite();
 功能:  向文件写入一个数据块
 头文件: #include <stdio.h>
 函数原型: size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
 返回值: 返回实际写入的数据块数目
15.1.1 标准I/O 函数的两个优点
  1. 标准I/O函数具有良好的移植性(Portability)。
  2. 标准I/O函数可以利用缓冲提高性能、

关于移植性:
其实所有的标准函数都具有这种性质。为了支持所有操作系统(编译器),这些函数都是按照ANSI C标准定义的。这不仅仅局限于网络编程,而是适用于所有编程领域。

关于利用缓冲提高性能:
使用标准I/O函数时会得到额外的缓冲支持。之前说过,在创建套接字时操作系统会为套接字生成用于I/O的缓冲,这两者是什么关系呢?
如下图所示:
在这里插入图片描述
上图可以看出,使用标书nI/O函数传输数据时,经过了2个缓冲。
例如:通过fputs函数传输字符串“xxxxx”时,数据先传递到标准I/O函数缓冲。然后数据将移动到套接字输出缓冲中,最后将字符串发送到对方主机。

套接字中的缓冲主要是为了实现TCP协议而设立的,例如TCP传输中数据没有接收到需要再次传递,这是就从保存有数据的输出缓冲中读取数据。

另一方面,使用标准I/O函数缓冲的主要目的是:为了提高性能。

那么如何提高性能呢?使用缓冲可以提高性能?为啥?
通常需要传输的数据越多,有无缓冲带来的性能差异越大
我们换个角度说,性能的提高可以从下面两个方面进程描述:

  1. 传输的数据量
  2. 数据向输出缓冲移动的次数

首先从数据量角度:
先提个问题:一个字节数据发送10次(10个数据包)与累计10个字节发送1尺的情况分别传输了多少个字节?
发送数据时使用的数据包中包含有头信息,头信息与数据大小无关,是按照一定的格式填入的。也就是说,假设一个头信息占用40个字节(实际更大):
前者:10 + 40 * 10 = 410 字节 后者:40 * 1 + 10 = 50字节。可以看到,这两种方式需要传递的数据量有很大的差别。

接着从向输出缓冲移动的次数:
前者移动10次花费的时间将近后者移动一次花费的10倍。

15.1.2 标准I/O 函数与系统函数之间的性能对比

上面讲了为什么缓冲能够提升性能。下面我们实际来测试一下,看看效果是否和分析的一样。
分别利用标准I/O函数和系统函数编写文件复制程序,这主要是为了检验缓冲提高性能的程度。首先是利用系统函数复制文件的示例。

syscpy.c

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#define BUF_SIZE 3

int main(int argc, char *argv[])
{
	int fd1, fd2, len;
	char buf[BUF_SIZE];

	fd1=open("news.txt", O_RDONLY);
	fd2=open("cpy.txt", O_WRONLY|O_CREAT|O_TRUNC);

	while((len=read(fd1, buf, sizeof(buf)))>0)
		write(fd2, buf, len);

	close(fd1);
	close(fd2);
	return 0;
}

下面是采用标准I/O函数复制文件的代码:
利用fputs和fgets函数复制文件,因此这是一种基于缓冲的复制。

stdcpy.c

#include <stdio.h>
#define BUF_SIZE 3

int main(int argc, char *argv[])
{
	FILE* fp1;
	FILE* fp2;
	char buf[BUF_SIZE];

	fp1 = fopen("news.txt","r");
	fp2 = fopen("cyp.txt", "w");

	while(fgets(buf, BUF_SIZE, fp1) != NULL)
		fputs(buf, fp2);

	fclose(fp1);
	fclose(fp1);
	return 0;
}

这两种方法在文件不大的情况下差异并不明显,如果文件越大,差别越明显。

15.1.3 标准I/O 函数的几个缺点
  1. 不容易进行双向通信
  2. 又是可能频繁调用fflush函数
  3. 需要FILE结构体指针的形式返回文件描述符

在c语言中,打开文件时,如果希望同时进行读写操作,应该以 r+、w+、a+模式打开。
但是由于缓冲,每次切换读写工作状态时应该刁颖fflush函数。 这也会影响基于缓冲的性能提高。 而且为了使用标准I/O函数,需要FILE结构体指针。而创建套接字时默认返回的是文件描述符。因此需要将文件描述符转化为FILE指针。

15.2 使用标准I/O函数

如前所述,创建套接字时返回文件描述符,而为了使用标准I/O函数,只能将其转换为FILE结构体指针。下面介绍转换方法。

15.2.1 利用fdopen函数转换为FILE结构体指针

可以通过fdopen函数将创建套接字时返回的文件描述符转换为标准I/O函数中使用的FILE结构体指针

#include <stdio.h>

FILE* fdopen(int fildes, const char* mode);
-> 成功时返回转换为FILE结构体指针,失败时返回NULL.

fildes:需要转换的文件描述符
mode:将创建的FILE结构体指针的模式(mode)信息

上面第二个参数 mode与fopen函数中的打开模式相同。常用的参数有
读模式:“r” 和 写模式:“w”。下面简单使用一下

desto.c

#include <stdio.h>
#define BUF_SIZE 3

int main(int argc, char *argv[])
{
	FILE* fp1;
	FILE* fp2;
	char buf[BUF_SIZE];

	fp1 = fopen("news.txt","r");
	fp2 = fopen("cyp.txt", "w");

	while(fgets(buf, BUF_SIZE, fp1) != NULL)
		fputs(buf, fp2);

	fclose(fp1);
	fclose(fp1);
	return 0;
}

在这里插入图片描述
捋一下思路:

  1. 使用open函数得到的int类型的文件描述符
  2. 使用fdopen函数将上一步得到的文件描述符进行转换,得到FILE* 类型的结构体指针
  3. 利用新得到的结构体指针,使用系统函数fputs进行写入。
15.2.2 利用fileno 函数转换为文件描述符

接下来介绍与fdopen函数提供相反功能的函数,如下

#include <stdio.h>

int fileno(FILE* stream);
->成功时返回转换后的文件描述符,失败时返回-1

这个用法很简单,向该函数传递FILE指针参数返回相应文件描述符,接下来给出fileno函数的调用示例。

todes.c

#include <stdio.h>
#include <fcntl.h>

int main(void)
{
	FILE *fp;
	int fd = open("data.dat",O_WRONLY|O_CREAT|O_TRUNC);
	if(fd == -1)
	{
		fputs("file open error", stdout);
		return -1;
	}

	printf("First file descripor:%d \n", fd);
	fp = fdopen(fd,"w");
	fputs("tcp ip socket programming\n",fp);

	printf("Second file descriptor : %d\n", fileno(fp));
	fclose(fp);
	return 0;
}

在这里插入图片描述

15.3 基于套接字的标准I/O函数使用

前面说了标准I/O函数的优缺点,以及文件描述符转换为FILE指针的方法。
下面将配合套接字进行操作,修改第四章的回声服务器以及回声客户端的代码,改为基于标准I/O函数的数据交换形式。未修改的代码看这里 第四章

服务器端:echo_stdserv.c

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

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

int main(int argc, char *argv[])
{

	int serv_sock, clnt_sock;
	char message[BUF_SIZE];
	int str_len, i;

	struct sockaddr_in serv_adr;
	struct sockaddr_in clnt_adr;
	socklen_t clnt_adr_sz;
	FILE * readfp;
	FILE * writefp;

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

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

	memset(&serv_adr, 0 , sizeof(serv_adr));
	serv_adr.sin_family = AF_INET;
	serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
	serv_adr.sin_port = htons(atoi(argv[1]));

	if(bind(serv_sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr)) == -1){
		error_handling("bind error");
	}

	if(listen(serv_sock, 5) == -1){
		error_handling("listen error");
	}

	clnt_adr_sz = sizeof(clnt_adr);

	for(int i = 0;i < 5;i++)
	{
		clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_adr, &clnt_adr_sz);
		if(clnt_sock == -1){
			error_handling("accept error");
		}
		else{
			printf("Connected client %d\n", clnt_sock);
		}

		readfp = fdopen(clnt_sock, "r");
		writefp = fdopen(clnt_sock, "w");

		while(!feof(readfp))
		{
			fgets(message, BUF_SIZE, readfp);
			fputs(message, writefp);

			fflush(writefp);
		}

		fclose(readfp);
		fclose(writefp);

	}

	close(serv_sock);
	return 0;
}

void error_handling(char *message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

客户端:echo_client.c

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

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

int main(int argc, char *argv[])
{
	int sock;
	char message[BUF_SIZE];
	int str_len;
	struct sockaddr_in serv_adr;
	FILE * readfp;
	FILE * writefp;

	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_adr, 0, sizeof(serv_adr));
	serv_adr.sin_family=AF_INET;
	serv_adr.sin_addr.s_addr=inet_addr(argv[1]);
	serv_adr.sin_port=htons(atoi(argv[2]));

	if(connect(sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr)) == -1)
	{
		error_handling("connect error");
	}
	else
	{
		puts("connected......");
	}

	readfp = fdopen(sock, "r");
	writefp = fdopen(sock, "w");

	while(1)
	{
		fputs("Input message(Q to quit): ", stdout);
		fgets(message, BUF_SIZE, stdin);		// 1

		if(!strcmp(message,"q\n") || !strcmp(message,"Q\n"))
			break;

		fputs(message, writefp);				// 2
		fflush(writefp);
		fgets(message, BUF_SIZE, readfp);		// 3
		printf("Messgae from server: %s\n", message);

	}

	fclose(writefp);
	fclose(readfp);

	return 0;
}

void error_handling(char *message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

在这里插入图片描述

看起来和使用write 函数以及 read函数实现的效果是完全一样的,而且代码看起来好像简单了起来= - =

fflush函数是用来将输入或输出缓冲中的数据全部输出的函数。
上面的过程中,调用基于字符串fgets 、fputs函数提供服务,并调用fflush函数。标准I/O函数为了提高性能,内部提供额外的缓冲。 因此如果不调用fflush函数则无法保证立即将数据传输到客户端。

第四章的回声客户端在接受到数据后,需要将数据转化为字符串(数据的尾部插入0),但是上面代码中却没有这一过程。

因为使用标准I/O函数后可以按字符串单位进行数据交换。

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值