当前位置:Linux教程 - Linux - 如何编写Linux下的客户机/服务器软件

如何编写Linux下的客户机/服务器软件

  Linux以其源代码公开闻名于世,并以其稳定性和可靠性雄霸操作系统领域,在网络应用技术方面使用得更加广泛。很久以来它就是Windows的重要对手之一。随着网络时代的来临,Linux的这种优势已变得更加突出。本文将论述如何在Linux环境下利用Socket实现客户机/服务器通信。
随着网络技术的发展,网络结构已从过去的主机/终端型、对等型发展到现在广为使用的客户机/服务器型。客户机/服务器模型应用十分广泛,在Internet上WWW,E-mail,FTP等都是基于这种模型的。在面向连接的通信模式下,服务器打开监听端口,监听网络上其它客户机向该服务器发出的连接请求,当收到一个请求信号时与该客户机建立一个连接,之后两者进行交互式的通信。具体步骤可这样组织:

服务器:
1.打开一个已知的监听端口,如smtp为25、pop3为110、ftp为21、telnet为23等。
2.在监听端口上监听客户机的连接请求,如果有客户机请求连接则建立一个连接线路。
3.在连接线路上与客户机通信。
4.通信完毕后关闭连接线路并继续监听客户机的连接请求。

客户机:
1.向指定的服务器主机及端口发出连接请求。
2.当服务器建立连接线路后与服务器进行通信。
3.通信完毕后关闭连接线路。

Linux的许多特性都非常有助于网络程序设计:首先Linux拥有POSIX.1标准库函数,socket()、bind()、listen()这几个库函数可以非常方便地实现服务器/客户机模型,有关这几个库函数的使用说明将在后边介绍。其次Linux的进程管理也非常符合服务器的工作原理,所谓进程就是程序在内存中运行时的状态,可以说进程是动态的程序。在运行着Linux操作系统的计算机中,每一个进程都有一个创建它的父进程,而且它也能创建多个子进程。在服务器端我们可以用父进程去监听客户机的连接请求,当有客户机的连接请求时父进程创建一个子进程去建立连接线路并与客户机通信,而它本身可继续监听其它客户机的连接请求,这样就可避免当有一个客户机与服务器建立连接后服务器就不能再与其它客户机通信的问题。Linux的另一个特性是它秉承了UNIX设备无关性这一优秀特征,即它通过文件描述符实现了统一的设备接口,磁盘、显示终端、音频设备、打印设备甚至网络通信都使用统一的I/O调用。这三个特性将使Linux下的网络程序设计变得易如反掌。上述三个特性的综合利用将是这篇文章所要讲述的真谛所在。下边的客户机/服务器实现过程可以说明一二,注意与上文所述步骤的不同。

服务器:
1.打开一个已知的监听端口。
2.在监听端口上监听客户机的连接请求,当有一客户机请求连接时建立连接线路并返回通信文件描述符。
4.父进程创建一子进程,父进程关闭通信文件描述符并继续监听端口上的客户机连接请求。
3.子进程通过通信文件描述符与客户机进行通信,通信结束后终止子进程并关闭通信文件描述符。

客户机:
1.向指定的服务器主机及端口发出连接请求,请求成功将返回通信文件描述符。
2.通过通信文件描述符与服务器进行通信。
3.通信完毕后关闭通信文件描述符。


Linux的以下几个库函数是网络程序设计的核心部分,它们分别是:
(1)socket
调用方式:
#include
#include

int socket(int domain,int type,int protocol);

简要说明:
此函数为通信创建一个端口,正常调用将返回一个文件描述符,错误调用将返回-1。domain参数有两种选择:AF_UNIX与AF_INET,其中AF_INET为Internet通信协议。type参数也有两种选择:SOCK_STREAM用于TCP,SOCK_DGRAM用于UDP。protocol参数通常为0。可通过下列代码为基于TCP协议的Internet通信建立套接口传输端口:

#include
#include
#include
int sock;

if((sock=socket(AF_INET,SOCK_STREAM,0))==-1)
perror(""Could not create socket"");

(2)bind
调用方式:
#include
#include

int bind(int s,const struct sockaddr *address,size_t address_len);

简要说明:
bind英文含意是关联,捆绑。其目的就是把socket返回的套接口端口与网络上的物理位置相关联。
bind正常调用返回0,出错返回-1。此函数有三个参数:其中s为socket调用返回的文件描述符,*address设置了与网络上的物理位置相关的信息,它的类型是struct sockaddr,但在Internet上它是struct sockaddr_in。在socket.h中struct sockaddr_in定义为:
struct sockaddr_in{
short sin_family;
u_short sin_port;
struct in_addr sin_addr;
char sin_zero[8];
};
sin_family一般为AF_INET,sin_port为端口号,由于使用不同字节顺序的机器必须作转换,故应使用宏命令htons(host to network short)来转换端口号,sin_addr将置为INADDR_ANY。这三个值设置完成后*address参数才有意义。在编写代码时,应先设置*address参数内部各成员变量的值,再调用bind。

(3)listen
调用方式:
#include
#include

int listen(int s,int backlog);

简要说明:
本函数使socket端口能够接受从客户机来的连接请求,正常调用返回0,出错返回-1。
s参数为socket产生的文件描述符,backlog为所能接受客户机的最大数目。
socket,bind,listen 三个函数的综合调用最终在服务器上产生一个能接受客户机请求的监听文件描述符s。

(4)accept
调用方式:
#include
#include

int accept(int s,struct sockaddr *address,int *address_len);

简要说明:
当有客户机发出连接请求时,此函数初始化这个连接。正常调用返回与客户机通信的通信文件描述符,出错返回-1。参数s为socket调用返回的文件描述符,address将用来存储客户机的信息,此信息由accept填入,当与客户机连接时,客户机的地址与端口将填到此处。address_len是客户机地址长度的字节数,也由accept填入。

(5)connect
调用方式:
#include
#include

int connect(int s,struct sockaddr *address,size_t address_len);

简要说明:
客户机调用socket建立传输端口后,调用connect来建立与远程服务器相连的连接线路。
此函数的参数调用同bind。

(6)inet_addr
调用方式:
#include
#include
#include

in_addr_t inet_addr(const char *addstring);

简要说明:
此函数将字符串addstring表示的网络地址(如192.168.0.1)转换成32位的网络字节序二进制值,若成功返回32位二进制的网络字节序地址,若出错返回 INADDR_NONE。INADDR_NONE是32位均为1的值(即255.255.255.255,它是Internet的有限广播地址),故如果要转换的addstring是255.255.255.255,函数调用将失败。

(7)fork
调用方式:
#include
#include


pid_t fork(void);

简要说明:
fork的作用是拷贝父进程的内存映象来创建子进程,两个进程将接着fork后的指令继续执行。 事实上它返回两个进程控制号,对于父进程它返回子进程的进程ID,对于子进程它返回0。

可用下边的代码调用fork:

pid_t childpid;
if((childpid=fork())=-1){
perror(""The fork failed"");
exit(1);
}
else if(child==0){
调用子进程;
}
else if(child>0){
调用父进程;
}


以上介绍了网络编程的有关库函数的调用方法,下面举一个客户机/服务器程序的小例子具体说明如何设计网络程序。本例介绍如何查看服务器上的时间和日期,由于daytime服务器的通用端口为13,客户机程序将通过调用13号端口对服务器上的时间和日期进行操作。


/*timeserve.c*/
/*服务器程序伪代码如下:

打开daytime监听端口;
while(客户机与服务器成功连接——成功返回通信文件描述符)
{
fork()
子进程:
{
读出当前时间;
将当前时间写入通信文件描述符;
关闭通信文件描述符;
}
父进程:
关闭通信文件描述符;
}
*/

#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include

int main(int argc,char *argv[])
{
int listenfd,communfd;
struct sockaddr_in servaddr;
pid_t childpid;
time_t tick;
char buf[1024];

if((listenfd=socket(AF_INET,SOCK_STREAM,0))==-1)
{
perror(""Could not create socket"");
exit(1);
}

servaddr.sin_family=AF_INET;
servaddr.sin_addr.s_addr=INADDR_ANY;
servaddr.sin_port=htons(13);
if(bind(listenfd,(struct sockaddr *)&servaddr,sizeof(servaddr))==-1)
{
perror(""bind error"");
exit(1);
}
if(listen(listenfd,254)==-1)
{
perror(""listen error"");
exit(1);
}
while(communfd=accept(listenfd,(struct sockaddr*)NULL,NULL))
{
if((childpid=fork())==-1)
{
perror(""fork error"");
exit(1);
}
else if(childpid==0)
{
tick=time(NULL);
snprintf(buf,sizeof(buf),""%.24s "",ctime(&tick));
write(communfd,buf,strlen(buf));
close(communfd);
}
else if(childpid>0)
close(communfd);

}
exit(0);
}


/*timeclient.h*/
#include
#include
#include
#include
#include
#include
#include
#include
#include

int main(int argc,char *argv[])
{
int communfd,n;
struct sockaddr_in servaddr;
char recieve[1024],buf[1024];

if(argc!=2)
{
perror(""Usage: client "");
exit(1);
}
if((communfd=socket(AF_INET,SOCK_STREAM,0))==-1)
{
perror(""socket error"");
exit(1);
}
servaddr.sin_family=AF_INET;
servaddr.sin_port=htons(13);
if((servaddr.sin_addr.s_addr=inet_addr(argv[1]))==INADDR_NONE)
{
perror(""inet_addr error"");
exit(1);
}
if(connect(communfd,(struct sockaddr*)&servaddr,sizeof(servaddr))==-1)
{
perror(""connect error"");
exit(1);
}
while((n=read(communfd,recieve,1024))>0)
{
recieve[n]=0;
if(fputs(recieve,stdout)==EOF)
perror(""fputs error"");
}
close(communfd);
exit(0);
}

用gcc编译两个源程序分别取名为server和client,以根用户身份运行服务器程序(设服务器网络地址为192.168.0.1):
server &
然后运行客户机程序(设服务器网络地址为192.168.0.1):
client 192.168.0.1
在客户机上就会反映出服务器上当前的时间如(Tue Feb 29 21:46:19 2000)。

以上程序代码在redhat 6.0上试验通过。在程序代码中有关库函数snprintf、fputs、read、write、close的用法就不在这里说明了,如想了解这些库函数的调用方法可到我的网页http://lzdx.yeah. net/pro_unix.html去查找。在我的网页http://lzdx.yeah.net/pro_uici.html中有关于通用Internet接口(UICI)专用库的介绍,通用Internet接口(UICI)利用Socket库函数提供了一个简化的独立于传输的接口,它从整体上简化了网络程序设计过程。有兴趣的人可到那里去看看。最后祝愿我们每个人都能编写出自己的网络程序。