0.摘要
概念和技巧
-客户/服务器模型
-用管道来双向通信
-协同进程(coroutines)
-文件/进程的相似性
-什么是socket,为什么需要socket,如何使用socket
-网路服务
-用socket编写客户/服务器程序
相关系统调用和函数
-fdopen
-popen
-socket
-bind
-listen
-accept
-connect
1.一个简单的传输数据的结构
如图所示
-(1,2)磁盘/设备文件
用open命令连接,用read和write传送数据。
-管道
用pipe命令创建,用fork共享,用read和write传送数据
-Sockets
用socket,listen和connect连接,用read和write传送数据
接下里书中从bc出发来讲解管道通信与dc的交互。
2.bc:Unix中使用的计算器
1.bc并不是一个计算器
它是与一个dc的系统调用,通过管道来通信的,dc是一个基于栈的计算器.
2.从bc方法中得到的思想
(1)客户/服务器模型
bc/dc程序对是客户/服务器模型设计的一个实例。它们之间使用stdin和stdout进行通信
(2)双向通信
传统的Unix管道只是单向传送数据,但是bc和dc之间需要双向传递。
(3)永久性服务
bc只是让单一的dc进程处于运行状态,这不同于shell程序。bc程序持续不断和一个dc的同一个实例进行通信,把用户的输入转换成命令传给dc。
bc/dc对被称为协同进程(coroutines)以用来区别子程序(subroutines)。两种程序持续运行,当其中一个程序完成自己的工作后将把控制权传给另一个程序。bc的任务是分析输入及打印,而dc则负责计算。
2.1编写bc: pipe,fork,dup,exec
(1)创建两个管道
(2)创建一个进程来运行dc
(3)在新创建的过程中,重定向标准输入和标准输出到管道,然后运行exec dc
(4)在父进程中,读取并分析用户的输入,将命令传给dc,dc读取响应,并把响应传给用户。
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<sys/wait.h>
#define oops(m,x) {perror(m);exit(x);}
void be_bc(int *,int*);
void be_dc(int *,int*);
void main()
{
int pid, todc[2], fromdc[2];/*equipment*/
/*make two pipes*/
if(pipe(todc) == -1||pipe(fromdc) ==-1)
oops("pipe failed",1);
/*get a process for user interface*/
if((pid =fork())==-1)
oops("cannot fork",2);
if(pid == 0) /*child is dc*/
be_dc(todc,fromdc);
else{
be_bc(todc,fromdc); /*parent is ui*/
wait(NULL); /*wait for child*/
}
}
/*set up input and output*/
void be_dc(int *todc,int *fromdc)
{
/*set stdin from pipein*/
if(dup2(todc[0],0)==-1) /*copy read end to 0*/
oops("dc: cannot redirect stdin",3);
close(todc[0]);
close(todc[1]);
/*set stdout from pipeout*/
if(dup2(fromdc[1],1)==-1) /*copy write end to 1*/
oops("dc: cannot redirect stdout",4);
close(fromdc[0]);
close(fromdc[1]);
/*now execl dc with the -option*/
execlp("dc","dc",NULL);
//execlp("dc","dc","-",NULL);
oops("exec error",5);
}
/*read from stdin and convert into to RPN,send down pipe*/
void be_bc(int *todc,int *fromdc)
{
int num1,num2;
char operation[BUFSIZ];
FILE *foutput, *fpin, *fdopen();
char message[BUFSIZ];
/*setup*/
close(todc[0]);
close(fromdc[1]);
foutput = fdopen(todc[1],"w");
fpin = fdopen(fromdc[0],"r");
if(fpin == NULL ||foutput ==NULL)
{
perror("error convering pipes to streams");
}
while(fgets(message,BUFSIZ,stdin)!=NULL)
{
/*parse input*/
if(sscanf(message,"%d%[-+*/^]%d",&num1,operation,&num2)!=3){ //操作符的表示%[+-*/^]
printf("syntax error");
continue;
}
if(fprintf(foutput,"%d\n%d\n%c\np\n",num1,num2,*operation)==EOF)//波兰式,后缀式子
perror("Error writingaa");
fflush(foutput); //force a write to pipe
if(fgets(message,BUFSIZ,fpin)==NULL)
break;
printf("%d %c %d = %s",num1,*operation,num2,message);
}
fclose(foutput); /*close pipe*/
fclose(fpin); /*dc will see EOF*/
}
执行结果为
$ ./tinybc
3+3
3 + 3 = 6
3*3
3 * 3 = 9
可以看到以上的程序通过创建两个管道来实现双向通信的作用.
2.2.fdopen:让文件描述符像文件一样使用
fdopen与fopen类似,返回一个FILE*类型的值,不同的是函数的参数,fdopen函数参数是文件描述符,而非文件.
具体可以看man fdopen
3.popen:让进程看似文件
3.1.popen的功能
fopen打开一个指向文件的带缓存的连接.
fopen(<file path>,<operator>)
FILE*fp;
fp = fopen("file1","r");
c = getc(fp);
fgets(buf,len,fp);
fscanf(fp,"%d%d%s",&x,&y,x);
fclose(fp);
popen打开一个指向进程的带缓冲的连接.
popen(<file descriptor>,<operator>
FILE*fp;
fp = popen("ls","r");
fgets(buf,len,fp);
pclose(fp); /*close whtn done*/
注:打开FILE*必须要在不用的时候进行关闭
例如,下面给一个使用popen直接打开一个管道的程序.
/*demonstrates how to open a program for standard i/o*/
#include<stdio.h>
#include<stdlib.h>
int main()
{
FILE *fp;
char buf[100];
int i=0;
fp = popen("who|sort","r"); /*open the command*/
while(fgets(buf,100,fp)!=NULL) /*read from command */
printf("%3d %s",i++,buf); /*print data*/
pclose(fp); /*IMPORTANT!*/
return 0;
}
进程被创建之后必须要退出,这里的pclose中调用了wait函数来等待进程的结束.
3.2.实现popen:使用fdopen命令
见本书
3.3.访问数据:文件、应用程序接口(API)和服务器
方法1:从文件获取数据
方法2:从函数获取数据
方法3:从进程获取数据
bc/dc和popen例子显示了如何创建一个进程到另外一个进程的连接.
4.socket:与远端进程相连
管道的缺陷:在一个进程中被创建,通过fork来实现共享,管道只能连接相关的进程,也只能连接在一个主机之间。Unix提供另外一种进程间通信机制-socket.
socket允许不相关的进程间创建类似于管道的连接,甚至可以通过socket连接到其他的主机上的进程.
重要的概念:
1.客户和服务器:在unix中服务器是一个程序而不是一台电脑.服务器进程等待请求,处理请求,然后循环回去等待下一个请求.
2.主机名和端口
3.地址族
4.协议:客户端和服务端之间的服务规则.在时间服务器的例子中,协议很简单.
4.1.服务列表:众所周知的端口
可以使用命令more /etc/services
4.2.编写timeserv.c:时间服务器
步骤1:向内核申请一个socket
socket是一个通信端点。socket创建一个socket
三个参数:1.创建一个通信端点并返回一个标识符。2.指出了程序将要使用的数据流类型。3.函数中最后的参数protocol指的内核中网络代码所使用的协议。
步骤2:绑定地址到socket上,地址包括主机地址与端口号。
步骤3:在socket上,允许接入呼叫并设置队列长度为1(listen)
步骤4:等待/接收呼叫
步骤5:传输数据
步骤6:关闭连接
简单总结为:1.socket 2.bind 3.listen 4.accept 5.read/write 6.close
时间服务器:timeserv.c 一个socket-基于时间的日期服务器
#include<stdio.h>
#include<unistd.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<time.h>
#include<string.h>
#include<stdlib.h>
#include<netdb.h>
#include<arpa/inet.h>
#define oops(m,x) {perror(m);exit(x);}
#define HOSTLEN 256
#define PORTNUM 9999
void main()
{
struct sockaddr_in addr; //build our address here
struct hostent * host;
char hname[HOSTLEN]; //host name
int socket_id;
int socket_fd;
time_t curtime;
FILE *socket_fp;
/*1.create socket */
socket_id = socket(AF_INET,SOCK_STREAM,0);
if(socket_id == -1)
oops("socket ",1);
/*2.bind the address to socket. Address is host, port*/
bzero(&addr,sizeof(addr)); /*clear out struct*/
gethostname(hname,HOSTLEN); /*where am I?*/
host=gethostbyname(hname); /*get info about host*/
/*fill in host part*/
addr.sin_addr.s_addr = inet_addr("127.0.0.1");
addr.sin_family = AF_INET;
addr.sin_port = htons(PORTNUM); //change to the internet format
if(bind(socket_id,(struct sockaddr*)&addr,sizeof(addr))!=0)
oops("bind",2);
/*3.listen allow incoming calls with Qsize=1 on socket*/
if(listen(socket_id,1)!=0)
oops("listen",3);
/*main loop: accept ,write(),close()*/
while(1)
{
socket_fd = accept(socket_id,NULL,NULL);
printf("get a call\n");
if(socket_fd == -1)
oops("calls fail",4);
socket_fp = fdopen(socket_fd,"w");
if(socket_fp == NULL)
oops("fdopen",4);
curtime = time(NULL); /*get time*/
/*and convert to string*/
fprintf(socket_fp,"The time here si ...");
fprintf(socket_fp,"%s",ctime(&curtime));
fclose(socket_fp); /*release connection*/
}
}
步骤1:向内核申请一个socket
sockid = socket(int domain,int type,int protocol)
socket是一个通信端点.同时,socket又是产生呼叫和接收呼叫的地方.创建socket返回一个标识符.其中有很多类型的通信系统,每个被称为通信域.Internet本身就是一个域.其中socket可以指定SOCK_STREAM也有后面介绍的SOCK_DGRAM,最后的参数表示使用的协议类型.
步骤2:绑定地址到socket上,地址包括主机,端口
result = bind(int sockid,struct sockaddr * addrp, socklen_t addrlen)
在Internet域中,地之由主机和端口构成.其中端口是一个16位的数值,尽量取大一些,因为低位端口已经被其他程序占用.自己在写绑定地址的时候,需要首先初始化该sockaddr结构的成员,然后再填充具体的主机地址和端口号,最后填充地址族.
步骤3:在socket上,允许接入呼叫并设置队列长度为1
result = listen(int sockid,int qsize)
步骤4:等待/接收呼叫
fd = accept(int sockid,struct sockaddr*callerid,socklen_t*addrlenp)
accept阻塞当前进程,一直到指定socket上的接入连接被建立起来.之后操作socket返回的文件描述符,进行读写操作.
步骤5:传输数据
对于文件描述符进行读写操作.
步骤6:关闭连接.
使用close来关闭accept的标志,让一端关闭当前的标记后,如果再读,就可以得到不可读的结果.
~$ telnet 127.0.0.1 9999
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
The time here si ...Tue Jan 9 21:49:40 2018
Connection closed by foreign host.
$ ./timeserv
host name get a call
4.3编写timecInt.c:时间服务客户端
步骤1:向内核请求建立socket
步骤2:与服务器相连
步骤3和4:传送数据和挂断
4.4测试timeclnt.c
使用scp来拷贝
5. 另一种服务器:远程的ls
功能:在客户端输入查询目录,返回目录内文件.
1.设计远程ls系统
这个ls系统包含三个部分:
(1)协议
(2)客户端程序
(3)服务端程序
2.协议
协议包含有请求和应答.
存在问题:还存在一个读写的问题。
为了减少被破坏的风险,程序中必须确保接收到的字符串没有溢出输入 缓存,也没有溢出给命令设置的缓存并且接收的目录名不允许出现非法字符。`
软件精灵
很多服务器程序都以d结尾,如httpd,inetd,syslogd,和atd,这里的d表示精灵(daemon)的意思,因而如名叫syslogd的服务器程序实际上是系统日志精灵(system log daemon),即我们常说的守护进程。
可以通过
ps -el
ps -ax
来查看所有的运行中的进程,其中以d结尾的进程表示守护进程。