第11章 连接到近端或远端的进程:服务器与Socket

1.编写bc(pipe、fork、dup、exec)

bc/dc程序对是客户/服务器模型程序设计的一个实例,bc/dc对被称之为协同进程。

(1)创建两个管道

(2)创建一个进程来运行dc

(3)在新创建的进程中,重定向标准输入和标准输出到管道,然后运行exec dc

(4)在父进程中,读取并分析用户的输入,将命令传给dc,dc读取响应,并把响应传给用户

/**	tinybc.c	* a tiny calculator that uses dc to do its work
 **			* demonstrates bidirectional pipes
 **			* input looks like number op number which
 **			  tinybc converts into number \n number \n op \n p
 **			  and passes result back to stdout
 **
 **		+-----------+                +----------+
 **	stdin  >0           >== pipetodc ====>          |
 **	        |  tinybc   |                |   dc -   |
 **	stdout <1           <== pipefromdc ==<          |
 **		+-----------+                +----------+
 **
 **			* program outline
 **				a. get two pipes
 **				b. fork (get another process)
 **				c. in the dc-to-be process,
 **					 connect stdin and out to pipes
 **					 then execl dc
 **				d. in the tinybc-process, no plumbing to do
 **					 just talk to human via normal i/o
 **					 and send stuff via pipe
 **				e. then close pipe and dc dies
 **                     * note: does not handle multiline answers
 **/
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>

#define	oops(m,x)	{ perror(m); exit(x); }

void be_dc(int in[2], int out[2]);
void be_bc(int todc[2], int fromdc[2]);

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 */
	}
}

void be_dc(int in[2], int out[2])
/*
 *	set up stdin and stdout, then execl dc
 */
{
    /* setup stdin from pipein	*/
	if ( dup2(in[0],0) == -1 )	/* copy read end to 0   */
		oops("dc: cannot redirect stdin",3);
	close(in[0]);			/* moved to fd 0	*/
	close(in[1]);			/* won't write here	*/

    /* setup stdout to pipeout	*/
	if ( dup2(out[1], 1) == -1 )	/* dupe write end to 1	*/
		oops("dc: cannot redirect stdout",4);
	close(out[1]);			/* moved to fd 1	*/
	close(out[0]);			/* won't read from here	*/

    /* now execl dc with the - option */
	execlp("dc", "dc", "-", NULL );
	oops("Cannot run dc", 5);
}

void be_bc(int todc[2], int fromdc[2])
/*
 *	read from stdin and convert into to RPN, send down pipe
 *	then read from other pipe and print to user
 *	Uses fdopen() to convert a file descriptor to a stream
 */
{
	int	num1, num2;
	char	operation[BUFSIZ], message[BUFSIZ];
	FILE	*fpout, *fpin;

	/* setup */
	close(todc[0]);			/* won't read from pipe to dc  */
	close(fromdc[1]);		/* won't write to pipe from dc */

	fpout = fdopen( todc[1],   "w" );	/* convert file desc-  */
	fpin  = fdopen( fromdc[0], "r" );	/* riptors to streams  */
	if ( fpout == NULL || fpin == NULL )
		perror("Error convering pipes to streams");

	/* main loop */
	while ( printf("tinybc: "), fgets(message,BUFSIZ,stdin) != NULL ){

		/* parse input */
		if ( sscanf(message,"%d%[-+*/^]%d",&num1,operation,&num2)!=3){
			printf("syntax error\n");
			continue;
		}

		if ( fprintf( fpout , "%d\n%d\n%c\np\n", num1, num2, 
				*operation ) == EOF )
					perror("Error writing");
		fflush(  fpout );
		if ( fgets( message, BUFSIZ, fpin ) == NULL )
			break;
		printf("%d %c %d = %s", num1, *operation , num2, message);
	}
	fclose(fpout);		/* close pipe		*/
	fclose(fpin);		/* dc will see EOF	*/
}

在程序中使用了fdopen,fdopen和fopen类似返回一个FILE *类型的值,不同的是此函数以文件描述符而非文件作为参数。

2.popen:让进程 看似文件

fopen打开一个指向文件的带缓存的连接:

FILE *fp;
fp = fopen("file1", "r");
c = getc(fp);
fgets(buf, len, fp);
fscanf(fp, "%d %d %s", &x, &y, &z);
fclose(fp);

popen打开一个指向进程的带缓冲的连接:

FILE *fp;
fp = popen("ls", "r");
fgets(buf, len, fp);
pclose(fp);

popen和fopen类似,popen的第一个参数是要打开的命令的名称,第二个参数可以是"r"或者是"w",但绝不会是"a"。popen和fopen以及fdopen的返回值类型都是FILE *,可以使用标准的缓存I/O操作来对其进行操作了。

/* popendemo.c
 *	demonstrates how to open a program for standard i/o
 *	important points:
 *		1. popen() returns a FILE *, just like fopen()
 *		2. the FILE * it returns can be read/written
 *		   with all the standard functions
 *		3. you need to use pclose() when done
 */
#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;
}

实现popen:使用fdopen命令

/*  popen.c -  a version of the Unix popen() library function
 *	FILE *popen( char *command, char *mode )
 *		command is a regular shell command
 *		mode is "r" or "w" 
 *		returns a stream attached to the command, or NULL
 *		execls "sh" "-c" command
 *    todo: what about signal handling for child process?
 */
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>

#define	READ	0
#define	WRITE	1
FILE *popen(const char *command, const char *mode);
void main()
{
	FILE	*fp;
	char	buf[BUFSIZ];

	fp = popen("ls","r");
	while( fgets(buf, BUFSIZ,fp) != NULL)
		fputs(buf, stdout);
}


FILE *popen(const char *command, const char *mode)
{
	int	pfp[2], pid;		/* the pipe and the process	*/
	FILE	*fdopen(), *fp;		/* fdopen makes a fd a stream	*/
	int	parent_end, child_end;	/* of pipe 			*/

	if ( *mode == 'r' ){		/* figure out direction		*/
		parent_end = READ;
		child_end = WRITE ;
	} else if ( *mode == 'w' ){
		parent_end = WRITE;
	       	child_end = READ ;
	} else return NULL ;

	if ( pipe(pfp) == -1 ) 			/* get a pipe		*/
		return NULL;
	if ( (pid = fork()) == -1 ){		/* and a process	*/
		close(pfp[0]);			/* or dispose of pipe	*/
		close(pfp[1]);
		return NULL;
	}

	/* --------------- parent code here ------------------- */
	/*   need to close one end and fdopen other end		*/

	if ( pid > 0 ){	
		if (close( pfp[child_end] ) == -1 )
			return NULL;
		return fdopen( pfp[parent_end] , mode);	/* same mode */
	}

	/* --------------- child code here --------------------- */
	/*   need to redirect stdin or stdout then exec the cmd	 */

	if ( close(pfp[parent_end]) == -1 )	/* close the other end	*/
		exit(1);			/* do NOT return	*/

	if ( dup2(pfp[child_end], child_end) == -1 )
		exit(1);

	if ( close(pfp[child_end]) == -1 )	/* done with this one	*/
		exit(1);
						/* all set to run cmd	*/
	execl( "/bin/sh", "sh", "-c", command, NULL );
	exit(1);
}

3.socket:与远端进程相连

socket允许不同进程,甚至不同主机进行通信。

socket通信涉及4个方面:

(1)客户和服务器

(2)主机名和端口

(3)地址族

(4)协议

在文件/etc/services中定义了众所周知的服务端口列表

(1)编写timeserv.c: 时间服务器

/* timeserv.c - a socket-based time of day server
 */

#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<netdb.h>
#include<time.h>
#include<strings.h>
#include<stdlib.h>

#define   PORTNUM  13000   /* our time service phone number */
#define   HOSTLEN  256
#define   oops(msg)      { perror(msg) ; exit(1) ; }

void main(int ac, char *av[])
{
	struct  sockaddr_in   saddr;   /* build our address here */
	struct	hostent		*hp;   /* this is part of our    */
	char	hostname[HOSTLEN];     /* address 	         */
	int	sock_id,sock_fd;       /* line id, file desc     */
	FILE	*sock_fp;              /* use socket as stream   */
	char    *ctime();              /* convert secs to string */
	time_t  thetime;               /* the time we report     */

      /*
       * Step 1: ask kernel for a socket
       */

	sock_id = socket( PF_INET, SOCK_STREAM, 0 );    /* get a socket */
	if ( sock_id == -1 ) 
		oops( "socket" );

      /*
       * Step 2: bind address to socket.  Address is host,port
       */

	bzero( (void *)&saddr, sizeof(saddr) ); /* clear out struct     */

	gethostname( hostname, HOSTLEN );       /* where am I ?         */
        printf("hostname: %s\n", hostname);
	hp = gethostbyname( hostname );         /* get info about host  */
	                                        /* fill in host part    */
	bcopy( (void *)hp->h_addr, (void *)&saddr.sin_addr, hp->h_length);
	saddr.sin_port = htons(PORTNUM);        /* fill in socket port  */
	saddr.sin_family = AF_INET ;            /* fill in addr family  */

	if ( bind(sock_id, (struct sockaddr *)&saddr, sizeof(saddr)) != 0 )
	       oops( "bind" );

      /*
       * Step 3: allow incoming calls with Qsize=1 on socket
       */

	if ( listen(sock_id, 1) != 0 ) 
		oops( "listen" );

      /*
       * main loop: accept(), write(), close()
       */

	while ( 1 ){
	       sock_fd = accept(sock_id, NULL, NULL); /* wait for call */
		   printf("Wow! got a call!\n");
	       if ( sock_fd == -1 )
		       oops( "accept" );       /* error getting calls  */

	       sock_fp = fdopen(sock_fd,"w");  /* we'll write to the   */
	       if ( sock_fp == NULL )          /* socket as a stream   */
		       oops( "fdopen" );       /* unless we can't      */

	       thetime = time(NULL);           /* get time             */
					       /* and convert to strng */
	       fprintf( sock_fp, "The time here is .." );
	       fprintf( sock_fp, "%s", ctime(&thetime) ); 
	       fclose( sock_fp );              /* release connection   */
	}
}

 程序解读:

struct sockaddr {  
     unsigned short sa_family;
   char sa_data[14];                  
   }; 
//sa_family是通信类型,最常用的值是 "AF_INET"
//sa_data14字节,包含套接字中的目标地址和端口信息

struct sockaddr 是一个通用地址结构,这是为了统一地址结构的表示方法,统一接口函数,使不同的地址结构可以被bind() , connect() 等函数调用;sa_data把目标地址和端口信息混在一起了

struct sockaddr_in { 
   short int sin_family;
   unsigned short int sin_port; 
     struct in_addr sin_addr;
     struct in_addr { 
            unsigned long s_addr;
     }
                
     unsigned char sin_zero[8];
}   

struct sockaddr_in中的in 表示internet,就是网络地址,这只是我们比较常用的地址结构,属于AF_INET地址族,他非常的常用
sockaddr_in结构体解决了sockaddr的缺陷,把port和addr 分开储存在两个变量中。

sin_port和sin_addr都必须是网络字节序,一般可视化的数字都是主机字节序。

网络字节顺序与本地字节顺序之间的转换函数:

    htonl()--"Host to Network Long"
    ntohl()--"Network to Host Long"
    htons()--"Host to Network Short"
    ntohs()--"Network to Host Short"    
int gethostname(char *name, size_t len);     //返回本地主机的标准主机名
struct hostent *gethostbyname(const char *name); //用域名或主机名获取IP地址

 struct hostent {
               char  *h_name;            /* official name of host */
               char **h_aliases;         /* alias list */
               int    h_addrtype;        /* host address type */
               int    h_length;          /* length of address */
               char **h_addr_list;       /* list of addresses */
           }

复制函数

void bcopy(const void *src, void *dest, size_t n);

步骤1:向内核申请一个socket

socket常用的协议族(domain)有,AF_INET(IPV4)、AF_INET6(IPV6)、AF_LOCAL(或称AF_UNIX,Unix域socket)、AF_ROUTE等等。协议族决定了socket的地址类型,在通信中必须采用对应的地址,如AF_INET决定了要用ipv4地址(32位的)与端口号(16位的)的组合、AF_UNIX决定了要用一个绝对路径名作为地址。

 Name                Purpose                          Man page
 AF_UNIX, AF_LOCAL   Local communication              unix(7)
 AF_INET             IPv4 Internet protocols          ip(7)
 AF_INET6            IPv6 Internet protocols          ipv6(7)
 AF_IPX              IPX - Novell protocols
 AF_NETLINK          Kernel user interface device     netlink(7)
 AF_X25              ITU-T X.25 / ISO-8208 protocol   x25(7)
 AF_AX25             Amateur radio AX.25 protocol
 AF_ATMPVC           Access to raw ATM PVCs
 AF_APPLETALK        AppleTalk                        ddp(7)
 AF_PACKET           Low level packet interface       packet(7)
 AF_ALG              Interface to kernel crypto API

参数type用于设置套接字通信的类型,主要有SOCKET_STREAM(流式套接字)、SOCK_DGRAM(数据包套接字)等

第3个参数protocol用于制定某个协议的特定类型,即type类型中的某个类型。通常某协议中只有一种特定类型,这样protocol参数仅能设置为0;但是有些协议有多种特定的类型,就需要设置这个参数来选择特定的类型。

步骤2:绑定地址到socket上,地址包括主机、端口

步骤3:在socket上,允许接入呼叫并设置队列长度为1

步骤4:等待/接收呼叫

步骤5:传输数据

步骤6:关闭连接

开启两个终端,程序运行的结果如下

(2)编写timecln.c:时间服务器客户端

/* timeclnt.c - a client for timeserv.c
 *              usage: timeclnt hostname portnumber
 */
#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<netdb.h>
#include<stdlib.h>
#include<string.h>

#define        oops(msg)       { perror(msg); exit(1); }

main(int ac, char *av[])
{
	struct sockaddr_in  servadd;        /* the number to call */
	struct hostent      *hp;            /* used to get number */
	int    sock_id, sock_fd;            /* the socket and fd  */
	char   message[BUFSIZ];             /* to receive message */
	int    messlen;                     /* for message length */

     /*
      * Step 1: Get a socket
      */

	sock_id = socket( AF_INET, SOCK_STREAM, 0 );    /* get a line   */
	if ( sock_id == -1 ) 
		oops( "socket" );          		/* or fail      */

     /*
      * Step 2: connect to server
      *         need to build address (host,port) of server  first
      */

	bzero( &servadd, sizeof( servadd ) );   /* zero the address     */

	hp = gethostbyname( av[1] );            /* lookup host's ip #   */
	if (hp == NULL) 
		oops(av[1]);            	/* or die               */
	bcopy(hp->h_addr, (struct sockaddr *)&servadd.sin_addr, hp->h_length);

	servadd.sin_port = htons(atoi(av[2]));  /* fill in port number  */

	servadd.sin_family = AF_INET ;          /* fill in socket type  */

						       /* now dial     */
	if ( connect(sock_id,(struct sockaddr *)&servadd, sizeof(servadd)) !=0)
	       oops( "connect" );

     /*
      * Step 3: transfer data from server, then hangup
      */

	messlen = read(sock_id, message, BUFSIZ);     /* read stuff   */
	if ( messlen == - 1 )
	       oops("read") ;
	if ( write( 1, message, messlen ) != messlen )  /* and write to */
	       oops( "write" );                        /* stdout       */
	close( sock_id );    
}

步骤1:向内核请求建立socket

步骤2:与服务器相连

步骤3和4:传送数据和挂断

运行结果

(3)另一种服务器:远程的ls

原理

实现rls系统的三要素:协议,客户端程序,服务器程序

协议

协议包含请求和应答。首先客户端发送一行包含有目录名称的请求。服务器读取该目录名之后打开并读取该目录,然后把文件发送到客户端。客户端循环地读取文件列表,直到服务器挂断连接产生结尾标志。

客户端程序:rls.c

/* rls.c - a client for a remote directory listing service
 *         usage: rls hostname directory
 */
#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<netdb.h>
#include<stdlib.h>
#include<string.h>

#define        oops(msg)       { perror(msg); exit(1); }
#define		PORTNUM		15000

void main(int ac, char *av[])
{
	struct sockaddr_in  servadd;        /* the number to call */
	struct hostent      *hp;            /* used to get number */
	int    sock_id, sock_fd;            /* the socket and fd  */
	char   buffer[BUFSIZ];              /* to receive message */
	int    n_read;                      /* for message length */

	if ( ac != 3 ) exit(1);

     /** Step 1: Get a socket **/

	sock_id = socket( AF_INET, SOCK_STREAM, 0 );    /* get a line   */
	if ( sock_id == -1 ) 
		oops( "socket" );          		/* or fail      */

     /** Step 2: connect to server **/

	bzero( &servadd, sizeof(servadd) );     /* zero the address     */
	hp = gethostbyname( av[1] );            /* lookup host's ip #   */
	if (hp == NULL) 
		oops(av[1]);            	/* or die               */
	bcopy(hp->h_addr, (struct sockaddr *)&servadd.sin_addr, hp->h_length);
	servadd.sin_port = htons(PORTNUM);      /* fill in port number  */
	servadd.sin_family = AF_INET ;          /* fill in socket type  */

	if ( connect(sock_id,(struct sockaddr *)&servadd, sizeof(servadd)) !=0)
	       oops( "connect" );

     /** Step 3: send directory name, then read back results **/


	if ( write(sock_id, av[2], strlen(av[2])) == -1)
		oops("write");
	if ( write(sock_id, "\n", 1) == -1 )
		oops("write");

	while( (n_read = read(sock_id, buffer, BUFSIZ)) > 0 )
		if ( write(1, buffer, n_read) == -1 )
			oops("write");
	close( sock_id );    
}

服务器程序:rlsd.c

/* rlsd.c - a remote ls server - with paranoia
 */

#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<netdb.h>
#include<time.h>
#include<strings.h>
#include<stdlib.h>

#define   PORTNUM  15000   /* our remote ls server port */
#define   HOSTLEN  256
#define   oops(msg)      { perror(msg) ; exit(1) ; }

void sanitize(char *str);

int main(int ac, char *av[])
{
	struct  sockaddr_in   saddr;   /* build our address here */
	struct	hostent		*hp;   /* this is part of our    */
	char	hostname[HOSTLEN];     /* address 	         */
	int	sock_id,sock_fd;       /* line id, file desc     */
	FILE	*sock_fpi, *sock_fpo;  /* streams for in and out */
	FILE	*pipe_fp;	       /* use popen to run ls    */
	char    dirname[BUFSIZ];       /* from client            */
	char    command[BUFSIZ];       /* for popen()            */
	int	dirlen, c;

      /** Step 1: ask kernel for a socket **/

	sock_id = socket( PF_INET, SOCK_STREAM, 0 );    /* get a socket */
	if ( sock_id == -1 ) 
		oops( "socket" );

      /** Step 2: bind address to socket.  Address is host,port **/

	bzero( (void *)&saddr, sizeof(saddr) ); /* clear out struct     */
	gethostname( hostname, HOSTLEN );       /* where am I ?         */
	hp = gethostbyname( hostname );         /* get info about host  */
	bcopy( (void *)hp->h_addr, (void *)&saddr.sin_addr, hp->h_length);
	saddr.sin_port = htons(PORTNUM);        /* fill in socket port  */
	saddr.sin_family = AF_INET ;            /* fill in addr family  */
	if ( bind(sock_id, (struct sockaddr *)&saddr, sizeof(saddr)) != 0 )
	       oops( "bind" );

      /** Step 3: allow incoming calls with Qsize=1 on socket **/

	if ( listen(sock_id, 1) != 0 ) 
		oops( "listen" );

      /*
       * main loop: accept(), write(), close()
       */

	while ( 1 ){
	       sock_fd = accept(sock_id, NULL, NULL); /* wait for call */
	       if ( sock_fd == -1 )
		       oops("accept");       
			printf("get a call\n");
	       /* open reading direction as buffered stream */
	       if( (sock_fpi = fdopen(sock_fd,"r")) == NULL )
		       oops("fdopen reading");

	       if ( fgets(dirname, BUFSIZ-5, sock_fpi) == NULL )
		       oops("reading dirname");
			printf("dirname: %s\n", dirname);
	       sanitize(dirname);

	       /* open writing direction as buffered stream */
	       if ( (sock_fpo = fdopen(sock_fd,"w")) == NULL )
		       oops("fdopen writing");

	       sprintf(command,"ls %s", dirname);
	       if ( (pipe_fp = popen(command, "r")) == NULL )
		       oops("popen");
	      
	       /* transfer data from ls to socket */
	       while( (c = getc(pipe_fp)) != EOF )
		       putc(c, sock_fpo);
	       pclose(pipe_fp);
	       fclose(sock_fpo);
	       fclose(sock_fpi);
       }
}

void sanitize(char *str)
/*
 * it would be very bad if someone passed us an dirname like
 * "; rm *"  and we naively created a command  "ls ; rm *"
 *
 * so..we remove everything but slashes and alphanumerics
 * There are nicer solutions, see exercises
 */
{
	char *src, *dest;

	for ( src = dest = str ; *src ; src++ )
		if ( *src == '/' || isalnum(*src) ) //isalnum判断字符变量c是否为字母或数字 
			*dest++ = *src;
	*dest = '\0';
}

程序运行结果

4.精灵程序

unix中很多服务器程序以d结尾,这里d表示精灵的意思,在你的系统中输入命令ps -el或者ps -ax就可以看到以字符d结尾的进程。大部分精灵进程都是在系统启动后就处于运行状态了。位于类似于/etc/rc.d目录中的shell脚本在后台启动了这些服务,它们的运行与终端相分离,时刻准备提供数据或者服务。

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值