一、问题提出
在Delphi的工具模板的Internet页上,提供了开发Internet应用程序的构件,其中TClinetSocket和TServerSocket这两个构件用于开发基于客户/服务器(Client/Server)的应用软件,但是,仅使用这两个构件开发的软件,只能在Windwows系统中使用,为了能在Windows系统与Linux系统之间建立通信,必须在Linux系统中另外开发一个进程。
二、实现方法
Linux拥有POSIX.1标准库函数,利用socket()、bind()、listen()这几个库函数可以非常方便地实现服务器/客户机模型。由于在Windows环境下的网络应用系统编程接口Windows Socket支持TCP/IP通信,因此,很容易利用Socket在Windows系统与Linux系统之间建立通信。
下面用一个例子说明实现方法。
在Linux系统建立一个服务器进程,完成以下工作:
1、打开一个已知的监听端口。
2、在监听端口上监听客户机的连接请求,当有一客户机请求连接时建立连接线路并返回通信文件描述符。
3、父进程创建一子进程,父进程关闭通信文件描述符并继续监听端口上的客户机的连接请求。
4、子进程通过通信文件描述符与客户机进行通信:首先读取客户机请求,然后根据不同请求发送不同内容,通信结束后终止子进程并关闭通信文件描述符。
在Windows系统建立一个客户机程序,完成以下工作:
1、输入试卷代号及服务器名。
2、进行连接,连接失败给出错误信息。
3、连接成功后,向服务器发送请求信息,并接收服务器信息。
4、在客户端程序窗口显示服务器信息,并终止连接。
三、服务器程序
在Linux中设计网络程序,以下函数是核心库函数,它们的用法如下:
1、socket()
调用方式:
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain,int type,int protocol);
简要说明:
该函数为通信创建一个端口,正常调用将返回一个文件描述符,错误调用将返回-1并设置errno。domain参数有两种选择:AF_UNIX与AF_INET,其中AF_INET为Internet通信协议。type参数也有两种选择:SOCK_STREAM用于TCP,SOCK_DGRAM用于UDP。protocol参数通常为0。
2、bind()
调用方式:
#include <sys/types.h>
#include <sys/socket.h>
int bind(int s,const struct sockaddr *address,size_t address_len);
简要说明:
该函数把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为端口号,sin_addr将置为INADDR_ANY。这三个值设置完成后*address参数才有意义。在编写代码时,应先设置*address参数内部各成员变量的值,再调用bind。
3、listen()
调用方式:
#include <sys/types.h>
#include <sys/socket.h>
int listen(int s,int backlog);
简要说明:
该函数使socket端口能够接受从客户机来的连接请求,正常调用返回0,出错返回-1。s参数为socket产生的文件描述符,backlog为所能接受客户机的最大数目。socket,bind,listen三个函数的综合调用最终在服务器上产生一个能接受客户机请求的监听文件描述符s。
4、accept()
调用方式:
#include <sys/types.h>
#include <sys/socket.h>
int accept(int s,struct sockaddr *address,int *address_len);
简要说明:
当有客户机发出连接请求时,此函数初始化这个连接。正常调用返回与客户机通信的通信文件描述符,出错返回-1。参数s为socket调用返回的文件描述符,address将用来存储客户机的信息,此信息由accept填入,当与客户机连接时,客户机的地址与端口将填到此处。address_len是客户机地址长度的字节数,也由accept填入。
5、htons()
调用方式:
#include <netinet/in.h>
uint16_t hotns(uint16_t host16bitvalue);
简要说明:
将16位整数的主机字节序转换成网络字节序。在网络编程中常用于16位端口号的转换。
6、fork()
调用方式:
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
简要说明:
fork的作用是拷贝父进程的内存映象来创建子进程,两个进程将接着fork后的指令继续执行。 事实上它返回两个进程控制号,对于父进程它返回子进程的进程ID,对于子进程它返回0。
可用下边的代码调用fork:
pid_t childpid;
if((childpid=fork())==-1){
perror("The fork failed");
exit(1);
}
else if(childpid==0){
调用子进程;
}
else if(childpid>0){
调用父进程;
}
服务器程序代码如下:
//server.c源程序
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <stdio.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
int main(void)
{
int listenfd,communfd,n;
struct sockaddr_in servaddr;
pid_t childpid;
char recieve[1024],buf[1024];
if((listenfd=socket(AF_INET,SOCK_STREAM,0))==-1)
{
puts("Create socket error!\n");
exit(1);
}
servaddr.sin_family=AF_INET;
servaddr.sin_addr.s_addr=INADDR_ANY;
servaddr.sin_port=htons(6623); //必须与客户机程序中设置的端口号一致
if(bind(listenfd,(struct sockaddr *)&servaddr,sizeof(servaddr))==-1)
{
puts("Bind error!\n");
exit(1);
}
if(listen(listenfd,200)==-1)
{
puts("Listen error!\n");
exit(1);
}
while(communfd=accept(listenfd,(struct sockaddr *)NULL,NULL))
{
if((childpid=fork())==-1)
{
puts("Fork error!");
exit(1);
}
else if(childpid==0)
{
n=read(communfd,recieve,1024);
if((n==1)&&(recieve[0]>='A'&&recieve[0]<='C'))
{
if(recieve[0]=='A')
{
strcpy(buf,"你好!A");
}
if(recieve[0]=='B')
{
strcpy(buf,"你好!B");
}
if(recieve[0]=='C')
{
strcpy(buf,"你好!C");
}
write(communfd,buf,strlen(buf));
}
close(communfd);
break;
}
else if(childpid>0)
{
close(communfd);
}
}
exit(0);
}
四、客户机程序设计
新建工程(Client),对主窗体的属性设置如下:
AutoSize:=False 不能改变大小
Borderlcons.biMaximize:=False 不能最大化
Position:=poDesktopCenter 运行后窗口处在桌面中央
把表1中列出的构件加到主窗体中。
对Memo1属性设置如下:
ReadOnly:=True 数据只读
对ClientSocket1的属性设置如下:
Port:=6623,每个Socket对应一个端口号,必须与服务器程序中设置的端口号一致。
其它使用缺省设置。
ClientSocket1的响应事件:
OnConnect事件
根据用户提供的试卷代号(Edit1.Text)发送连接请求数据。
表1 客户端窗体中的构件
构件 |
Name |
Caption |
说明 |
Tbutton |
Button1 |
连接 |
|
Tbutton |
Button2 |
退出 |
|
Tmemo |
Memo1 |
|
信息显示窗口 |
TclientSocket |
ClientSocket1 |
|
Socket插口 |
Tlabel |
Label1 |
试卷代号: |
|
Tedit |
Edit1 |
|
|
Tlabel |
Label2 |
服务器名: |
|
Tedit |
Edit2 |
|
|
OnRead事件
接收下服务器端数据,并在信息窗口中显示,终止与服务器的连接,OnError事件
显示错误信息。
OnDisconnect事件
显示服务器关闭信息。
Button1的OnClick事件
如果试卷代号,服务器名为空,则显示错误信息,否则设置ClientSocket1.Host为Edit2.Text(服务器名),激活ClientSocket1。
Button2的OnClick事件
显示关闭信息,根据用户的选择关闭客户机程序。
客户机程序清单如下:
unit main;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
ScktComp, StdCtrls, ExtCtrls;
type
TForm1 = class(TForm)
Button1: TButton;
Button2: TButton;
ClientSocket1: TClientSocket;
Edit2: TEdit;
Edit1: TEdit;
Label1: TLabel;
Label2: TLabel;
Memo1: TMemo;
procedure Button1Click(Sender: TObject);
procedure ClientSocket1Connect(Sender: TObject;
Socket: TCustomWinSocket);
procedure Button2Click(Sender: TObject);
procedure ClientSocket1Read(Sender: TObject; Socket: TCustomWinSocket);
procedure ClientSocket1Disconnect(Sender: TObject;
Socket: TCustomWinSocket);
procedure ClientSocket1Error(Sender: TObject; Socket: TCustomWinSocket;
ErrorEvent: TErrorEvent; var ErrorCode: Integer);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
key:integer=0;
implementation
{$R *.DFM}
procedure TForm1.Button1Click(Sender: TObject);
begin
Edit1.Text:=Trim(Edit1.Text);
Edit2.Text:=Trim(Edit2.Text);
if (Length(Edit1.Text)=1) and (Length(Edit2.Text)> 0) then
begin
with ClientSocket1 do
begin
Host := Edit2.Text;
Active := True;
end;
end
else
MessageBox(0,'输入项内容有误!','错误信息窗口',MB_OK);
end;
procedure TForm1.ClientSocket1Connect(Sender: TObject;
Socket: TCustomWinSocket);
begin
Socket.Sendtext(Edit1.Text);
end;
procedure TForm1.Button2Click(Sender: TObject);
begin
if (MessageBox(0,'确定要退出系统吗?','退出窗口',MB_YESNO)=6) then
Close;
end;
procedure TForm1.ClientSocket1Read(Sender: TObject;
Socket: TCustomWinSocket);
var
sa:string;
begin
sa:=Socket.ReceiveText;
memo1.Lines.Add(sa);
key:=1;
ClientSocket1.Active:=false;
end;
procedure TForm1.ClientSocket1Disconnect(Sender: TObject;
Socket: TCustomWinSocket);
begin
if key=0 then
begin
MessageBox(0,'无法连接服务器!网上学习系统将关闭!','错误信息窗口',MB_OK);
// Close;
end;
key:=0;
end;
procedure TForm1.ClientSocket1Error(Sender: TObject;
Socket: TCustomWinSocket; ErrorEvent: TErrorEvent;
var ErrorCode: Integer);
begin
MessageBox(0,' 错误:无法连接服务器!','信息窗口',MB_OK);Errorcode:=0;
end;
end.
五、程序运行
在Linux中用gcc编译服务器源程序并取名为server,再运行,命令如下:
gcc -o server server.c
server &
在Windows中编译好Client,该程序必须在装有Windows系统并配有TCP/IP协议的联网计算机上运行,并确定可与Linux系统联网,运行Client程序时应注意服务器名的输入,如本地机没有设定IP地址,而使用DHCP获得,则在输入服务器名时不能用服务器的IP地址,只能用服务器的计算机名。该方法已在Delphi 4 Client/Server Suite及redhat 6.2上编译运行通过。
|