如何编写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/r/n",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库函数提供了一个简化的独立于传输的接口,它从整体上简化了网络程序设计过程。有兴趣的人可到那里去看看。最后祝愿我们每个人都能编写出自己的网络程序。  
编 者 的 话 5 第1篇 基础篇 6 第1章 数据库原理与访问 7 1.1 数据库基本原理 7 1.1.1 概述 7 1.1.2 桌面数据库 7 1.1.3 对象数据库 8 1.1.4 关系数据库服务器 9 1.1.5 选择适用的数据库 9 1.2 数据库访问技术 10 1.2.1 概述 10 1.2.2 ODBC API 10 1.2.3 ODBC的MFC类 11 1.2.4 DAO与RDO 11 1.2.5 OLE DB与ADO 12 1.3 数据库操纵语言SQL 13 1.3.1 SQL命令 13 1.3.2 SQL从句 13 1.3.3 SQL运算符 14 1.3.4 SQL合计函数 14 1.4 小 结 14 第2章 COM与数据库访问 15 2.1 COM的基本原理 15 2.1.1 COM历史 16 2.1.2 COM结构 16 2.1.3 COM优势 17 2.1.4 COM接口 18 2.1.5 COM与数据库访问 19 2.1.6 COM与Internet 19 2.2 ACTIVEX的数据库访问 19 2.2.1 ActiveX简介 19 2.2.2 ActiveX对数据库访问的支持 20 2.3 ATL的数据库访问 20 2.3.1 ATL目标 20 2.3.2 ATL内容简介 22 2.3.3 ATL对数据库访问的支持 22 2.4 小 结 23 第3章 数据库开发过程 23 3.1 阶段1:调查与分析 24 3.2 阶段2:数据建模 24 3.3 阶段3:功能设计 24 3.4 阶段4:选择数据库系统 25 3.5 阶段5:选择数据库访问技术 25 3.6 阶段6:代码设计 25 3.7 阶段7:测试与调试 26 3.8 阶段8:发行产品 26 第4章 VC++数据库开发基础 26 4.1 VC++ 6.0工程创建向导 26 4.2 VC++ 6.0数据库新建工具 27 4.3 VC++ 6.0的数据库工程 29 4.4 小 结 31 第2篇 实例篇 32 第5章 ODBC API编程 33 5.1 了解ODBC API 34 5.2 ODBC API编程步骤 34 5.2.1 步骤1:连接数据源 34 5.2.2 步骤2:分配语句句柄 36 5.2.3 步骤3:准备并执行SQL语句 36 5.2.4 步骤4:获取结果集 37 5.2.5 步骤5:提交事务 38 5.2.6 步骤6:断开数据源连接并释放环境句柄 39 5.3 ODBC API编程实例 39 5.3.1 实例概述 39 5.3.2 实例实现过程 40 5.3.3 编译并运行ODBCDemo1工程 97 5.3.4 ODBCDemo1实例小结 98 5.4 本 章 小 结 99 第6章 MFC ODBC编程 100 6.1 了解MFC ODBC 100 6.1.1 CDatabase类 100 6.1.2 CRecordSet类 100 6.2 MFC ODBC数据库访问技术 101 6.2.1 记录查询 101 6.2.2 记录添加 102 6.2.3 记录删除 102 6.2.4 记录修改 102 6.2.5 撤销数据库更新操作 103 6.2.6 直接执行SQL语句 103 6.2.7 MFC ODBC的数据库操作过程 103 6.3 MFC ODBC编程实例 104 6.3.1 实例概述 104 6.3.2 实例实现过程 105 6.3.3 编译并运行ODBCDemo2工程 132 6.3.4 ODBCDemo2实例小结 137 6.4 本 章 小 结 137 第7章 DAO数据库编程 138 7.1 DAO的数据访问 138 7.1.1 DAO对象 138 7.1.2 MFC对DAO的支持 139 7.1.3 DAO与ODBC的比较 139 7.1.4 MFC的DAO类简介 139 7.2 DAO编程实例 142 7.2.1 实例概述 142 7.2.2 实例实现过程 143 7.2.3 运行DAODemo工程 167 7.2.4 DAODemo实例小结 171 7.3 小 结 172 第8章 OLE DB客户数据库编程 172 8.1 OLE DB原理 172 8.1.1 OLE DB与ODBC 172 8.1.2 OLE DB的结构 173 8.1.3 OLE DB的优越性 173 8.1.4 OLE DB对象 174 8.1.5 OLE DB客户模板结构 177 8.1.6 OLE DB客户模
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值