Socket Linux c 学习小结(一)
一、背景
初次接触服务器与客户端的知识,先从最简单的做起:了解linux中使用的计算器。linux中使用的计算器是最为简单的服务器与客户端协同工作的例子。
二、从管道到服务器与客户端
传统的Unix管道只是单方向的传送数据。打个比方,在linux命令行下使用cat /etc/passwd | grep Jack命令。这个命令将cat的输出结果通过管道传送到grep命令的标准输入中。但这只是一个单向的数据传送,grep的输出结果并不会进行回传。而若有两个进程A、B,他们两个协同进行工作,A进程将其输出结果传送给B进程,B进程处理数据完毕后再返回给A进程。这样,它们之间并不是单向的管道,而是一个双向的管道。B对A提供服务,而A是B的客户。A就叫做客户端,B叫做服务器.
补充
创建匿名管道的系统调用:
#include<unistd.h>
int pipe(int pipefd[2]);
匿名管道:单向的数据传送通道
pipefd[2] 为两端的文件描述符,需要注意的是,由于文件描述符只在进程的范围内有意义,换到另一个进程后就不再指向同一个文件了,所以,匿名管道两端的进程需要有相同的祖先:从而另它们的文件描述符指向同一个文件。
练习1:
一个简易版的bc 命令的实现
其实吧
bc 这个命令是把用户的输入进行解析,变成后缀表达式,然后借助 dc 这个命令去处理运算。
/*************************************************************************
> File Name: tinyBc.c
> Author:Gin.TaMa
> Mail:[email protected]
> Created Time: 2019年02月22日 星期五 10时17分53秒
************************************************************************/
#include<fcntl.h>
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include <sys/wait.h>
#define oops(m,x) {perror(m);exit(x);}
void be_dc(int*,int*);
void be_bc(int*,int*);
int main(){
int pid ,todc[2],fromdc[2]; /*equipmet*/
/* make two pipes */
if(pipe(todc) == -1||pipe(fromdc) == -1){
oops("pipe failed",1);
}
/* get a process for user inerface */
if((pid = fork()) == -1) {
oops("cannot fork",2);
}
if(pid == 0){
be_dc(todc,fromdc);
}else{
be_bc(todc,fromdc);
wait(NULL);
}
return 0;
}
/*
* set up stdin and stdou,then execl dc
*/
void be_dc(int* in,int* out){
/* setup stdin from pipein */
if(dup2(in[0],0) == -1){
oops("dc:cannot redirect stdin",3);
}
close(in[0]);
close(in[1]);
/* setup stdout to pipeout */
if(dup2(out[1],1) == -1){
oops("dc:cannot redirect stdout",4);
}
close(out[1]);
close(out[0]);
/* now execl dc with the - option */
execlp("dc","dc","-",NULL);
oops("cannot run dc",5);
}
void fatal(char * mess){
fprintf(stderr,"err: %s\n",mess);
exit(1);
}
void be_bc(int * todc,int * fromdc){
int num1,num2;
char operation[BUFSIZ],message[BUFSIZ],* fgets();
FILE *fout,*fpin,*fopen();
/* setup */
close(todc[0]);
close(fromdc[1]);
fout = fdopen(todc[1],"w");
fpin = fdopen(fromdc[0],"r");
while(printf("tinyBc: "),fgets(message,BUFSIZ,stdin)!=NULL){
if(sscanf(message,"%d%[-+*/^]%d",&num1,operation,&num2)!=3){
printf("syntax error\n");
continue;
}
if(fprintf(fout,"%d\n%d\n%c\np\n",num1,num2,*operation)==EOF){
fatal("Error writing");
}
fflush(fout);
if(fgets(message,BUFSIZ,fpin)==NULL){
break;
}
printf("%d %c %d = %s",num1,*operation,num2,message);
}
fclose(fout);
fclose(fpin);
}
上述程序的工作流程
+----------+ +--------+
stdin > 0 >==pipetodc======> |
| tinybc | | dc - |
stdout< 1 <===pipefromdc===< |
+----------+ +--------+
值得学习的地方:
-
main 函数的封装
... /* get a process for user inerface */ if((pid = fork()) == -1) { oops("cannot fork",2); } if(pid == 0){ be_dc(todc,fromdc); }else{ be_bc(todc,fromdc); wait(NULL); } ...
和 if ( (pid = fork()) == -1) 的使用
即 保留了Pid ,并 利用( ) == -1 来进行函数调用结果的判断
-
调用系统shell 的方式
execlp("dc","dc","-",NULL);
补充:
系统调用 exec() 组
int execve(const char* filename, char* const argv[], char* const envp[]);
这个函数将在 现在运行的 进程中以
argv
中存储的字符串为参数 来运行由filename
指向的可执行程序,并将envp
中存储的“变量=数值”对作为环境变量代入这个程序。根据传统,
argv
中的第一个参数必须与filename
一致,真正的参数从第二个开始。execve
会以filename
的程序覆盖现在进程中正在运行的程序,因此所有在execve
函数之后的行都只会在execve
失败之后被执行,所以我们可以通过在后面加入打印语句等方法标识
execve
运行失败。基于
execve()
的还有六个函数;他们都具有在现有的进程中打开某一可执行文件、覆盖现有程序执行的功能。int execl(const char* path, const char* arg, ...); int execlp(const char* file, const char* arg, ...); int execle(const char* path, const char* arg, ..., char* const envp[]); int execv(const char* path, char* const argv[]); int execvp(const char* file, char* const argv[]); int execvpe(const char* file, char* const argv[], char* const envp[]);
我们可以将这些函数表示为:“exec + l/v + p/e/pe”,
这六个函数主要有两处不同。 第一处不同(l/v)在参数中表现为“execl”类函数将所有参数作为一整个字符串代入,看函数原型也可以看出来,这是个变参函数。而“execv”类函数将参数分别放入一个字符串的数组中,将数组作为参数代入函数运行。
第二处不同(p/e/pe)表现为包含 p(代表 path)的函数可以模仿计算机中壳的功能,在“file”中不含“/”时应用环境变量 PATH 中的目录来寻找可执行文件,而包含 e(代表 environment)的函数可以像
execve
一样将“变量=数值”对作为环境变量代入程序。
execlp 就很好理解了: exec + l + p
-
fdopen 函数的使用:
fdopen取一个现存的文件描述符(我们可能从 open,dup,dup2,fcntl,pipe,socket,socketpair或accept函数得到此文件描述符) ,并使一个标准的I/O流与该描述符相结合。所以,该函数常用于由创建管道和网络通信通道函数获得的描述符。
-
sscanf(message,"%d%[-+*/^]%d",&num1,operation,&num2)!=3
补充:
这个函数的用法:
sscanf与scanf类似,都是用于输入的,只是后者以键盘(stdin)为输入源,前者以固定字符串为输入源。
第二个参数可以是一个或多个 {%[*] [width] [{h | I | I64 | L}]type | ’ ’ | ‘\t’ | ‘\n’ | 非%符号}
注:
1、 * 亦可用于格式中, (即 %*d 和 %s) 加了星号() 表示跳过此数据不读入. (也就是不把此数据读入参数中)
2、{a|b|c}表示a,b,c中选一,[d],表示可以有d也可以没有d。
3、width表示读取宽度。
4、{h | l | I64 | L}:参数的size,通常h表示单字节size,I表示2字节 size,L表示4字节size(double例外),l64表示8字节size。
5、type :这就很多了,就是%s,%d之类。
6、特别的:%*[width] [{h | l | I64 | L}]type 表示满足该条件的被过滤掉,不会向目标参数中写入值
失败返回0 ,否则返回格式化的参数个数
7、如果读取的字符串,不是以空格来分隔的话,就可以使用%[]。
函数将返回成功赋值的字段个数;返回值不包括已读取但未赋值的字段个数。 返回值为 0 表示没有将任何字段赋值。 如果在第一次读取之前到达字符串结尾,则返回EOF。
如果buffer或format是NULL调用指针,无效参数处理程序,如中所述参数验证。 如果允许继续执行,则这些函数返回 -1 并将errno设置为EINVAL。
成功则返回参数数目,失败则返回-1,错误原因存于errno中。
练习2:
实现一个popen:
popen 可以将进程当做文件一样操作。
/*************************************************************************
> File Name: popen.c
> Author:Gin.TaMa
> Mail:[email protected]
> Created Time: 2019年02月22日 星期五 14时02分18秒
************************************************************************/
#include<fcntl.h>
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include <sys/wait.h>
#include<signal.h>
#define READ 0
#define WRITE 1
FILE* myPopen(const char* command,const char* mode){
int pfp[2],pid;
FILE * fp;
int parent_end,child_end;
if(* mode == 'r'){
parent_end = READ;
child_end = WRITE;
}else if(* mode == 'w'){
parent_end = WRITE;
child_end = READ;
}else return NULL;
if(pipe(pfp) == -1)return NULL;
if((pid = fork()) == -1){
close(pfp[0]);
close(pfp[1]);
return NULL;
}
if(pid > 0){
if ( close(pfp[child_end]) == -1 )
return NULL;
return fdopen(pfp[parent_end],mode);
}
close(pfp[parent_end]);
dup2(pfp[child_end],child_end);
close(pfp[child_end]);
execl("/bin/sh","sh","-c",command,NULL);
exit(1);
}
int main(){
FILE *fp;
char buf[100];
int i = 0;
fp=myPopen("who|sort","r");
while(fgets(buf,100,fp)!=NULL){
printf("%3d %s",i++,buf);
}
pclose(fp);
return 0;
}
和上一个代码相比就蛮简单的啦。