跳至主要内容

【转】C++/CLI程序进程之间的通讯

 现在,把大型软件项目分解为一些相交互的小程序似乎变得越来越普遍,程序各部分之间的通讯可使用某种类型的通讯协议,这些程序可能运行在不同的机器上、不同的操作系统中、以不同的语言编写,但也有可能只在同一台机器上,实际上,这些程序可看成是同一程序中的不同线程。而本文主要讨论C++/CLI程序间的通讯,当然,在此是讨论进程间通讯,而不是网络通讯。

  简介

  试想一个包含数据库查询功能的应用,通常有一个被称为服务端的程序,等待另一个被称为客户端程序发送请求,当接收到请求时,服务端执行相应功能,并把结果(或者错误信息)返回给客户端。在许多情况中,有着多个客户端,所有的请求都会在同一时间发送到同一服务端,这就要求服务端程序要更加高级、完善。

  在某些针对此任务的环境中,服务端程序可能只是众多程序中的一个程序,其他可能也是服务端或者客户端程序,实际上,如果我们的数据库服务端需要访问不存在于本机的文件,那么它就可能成为其他某个文件服务器的一个客户端。一个程序中可能会有一个服务线程及一个或多个客户线程,因此,我们需小心使用客户端及服务端这个术语,虽然它们表达了近似的抽象含义,但在具体实现上却大不相同。从一般的观点来看,客户端即为服务端所提供服务的"消费者",而服务端也能成为其他某些服务的客户端。

  服务端套接字

  让我们从一个具体有代表性的服务端程序开始(请看例1),此程序等待客户端发送一对整数,把它们相加之后返回结果给客户端。

  例1:

using namespace System;
using namespace System::IO;
using namespace System::Net;
using namespace System::Net::Sockets;

int main(array<String^>^ argv)
{
if (argv->Length != 1)
{
Console::WriteLine("Usage: Server port");
Environment::Exit(1);
}

int port = 0;

try
{
port = Int32::Parse(argv[0]);
}
catch (FormatException^ e)
{
Console::WriteLine("Port number {0} is ill-formed", argv[0]);
Environment::Exit(2);
}

/*1*/ if (port < IPEndPoint::MinPort || port > IPEndPoint::MaxPort)
{
Console::WriteLine("Port number must be in the range {0}-{1}",
IPEndPoint::MinPort, IPEndPoint::MaxPort);
Environment::Exit(3);
}

/*2*/ IPAddress^ ipAddress =
Dns::GetHostEntry(Dns::GetHostName())->AddressList[0];
/*3*/ IPEndPoint^ ipEndpoint = gcnew IPEndPoint(ipAddress, port);

/*4*/ Socket^ listenerSocket = gcnew Socket(AddressFamily::InterNetwork,
SocketType::Stream, ProtocolType::Tcp);

/*5*/ listenerSocket->Bind(ipEndpoint);

/*6*/ listenerSocket->Listen(1);
/*7*/ Console::WriteLine("Server listener blocking status is {0}",
listenerSocket->Blocking);

/*8*/ Socket^ serverSocket = listenerSocket->Accept();
Console::WriteLine("New connection accepted");
/*9*/ listenerSocket->Close();

/*10*/ NetworkStream^ netStream = gcnew NetworkStream(serverSocket);
/*11*/ BinaryReader^ br = gcnew BinaryReader(netStream);
/*12*/ BinaryWriter^ bw = gcnew BinaryWriter(netStream);

try
{
int value1, value2;
int result;

while (true)
{
/*13*/ value1 = br->ReadInt32();
/*14*/ value2 = br->ReadInt32();
Console::Write("Received values {0,3} and {1,3}",
value1, value2);

result = value1 + value2;
/*15*/ bw->Write(result);
Console::WriteLine(", sent result {0,3}", result);
}
}
/*16*/ catch (EndOfStreamException^ e)
{
}
/*17*/ catch (IOException^ e)
{
Console::WriteLine("IOException {0}", e);
}

/*18*/ serverSocket->Shutdown(SocketShutdown::Both);
/*19*/ serverSocket->Close();
/*20*/ netStream->Close();
Console::WriteLine("Shutting down server");
}

  此处与套接字相关的功能由命名空间System::Net和System::Net::Sockets提供,并且需要在生成期间引用System.dll程序集。另外,因为通过套接字的通讯涉及到流,所以还要用到System:IO机制。

  当程序执行时,服务端需要知道其用来监听客户端连接请求的端口号,在此,这个整数值通过命令行参数提供。一般来说,端口号在0-65535范围内,而0-1023保留给特定的用途,因此,服务端可用的端口号就为1024-65535。

  在标号1中,通过IPEndPoint类中的MinPort和MaxPort这两个公共静态字段,就可得到特定系统上可用的端口范围。

  而在标号2中,可得到我们自己的主机名,并解析到一个IpHostEntry,可从中取得本机的IP地址。接下来在标号3中,用IP地址和端口号创建了一个IPEndPoint对象,其可为某个连接提供某种服务。

  在标号4中,创建了一个Internet传输服务托管版本的套接字,一旦它被创建,就应通过Bind函数(标号5)绑定到一个特定的端点。接下来,套接字声明其已经开始服务,并监听连接请求(标号6)。传递给Listen的参数表明了请求队列中连接挂起的长度,因为我们只有一个客户端,所以在此1就足够了。

  套接字默认以阻塞模式创建,如标号7中所示,这意味着,它会一直等待连接请求。

  当从客户端接收到连接请求时,阻塞的套接字就会被唤醒,通过调用Accept(如标号8),接受请求并创建另一个套接字,并通过此套接字来与客户端通讯。我们看到,此时的服务端有两个套接字:一个用于监听客户连接请求,而另一个用于与连接的客户端通讯。当然,一个服务端在同一时间,可与多个客户端进行连接,且每个客户端都有一个套接字。

  在这个简单的例子中,我们只关心请求连接的第一个客户端,一旦连接上了,便可关闭此监听连接请求的套接字(参见标号9)。
在标号10-12中,我们用最近连接的套接字,建立了一个NetworkStream,连同两个读写函数一起,便可以从套接字接收请求,并返回结果。
服务端在此无限循环,读入一对整数,计算它们的和,并把结果返回给客户端。当服务端探测到输入流中的文件结束标志时(由客户端关闭了套接字),会抛出EndOfStreamException异常,并关闭I/O流和套接字,服务结束。

  标号18中的Socket::ShutDown调用将同时关闭套接字上的接收和发送功能,因为我们的服务端只需告之一个客户端它的关闭,所以此函数调用有点多余,但是,在服务端要过早地结束的某些情况下,这种做法还是有用的。

  为何要捕捉IOException异常的原因在标号17中,在此是为了处理客户端在关闭套接字之前的过早结束。
  前面所演示的服务端与客户端程序以简单的方式进行数值交换,如int,然而,程序很有可能也会需要发送与接收各种不同的用户自定义的对象类型,这就涉及到串行化。

  试想某些金融程序所涉及到的许多事务类型,如存款、转账、取款,每一种都与事务有关。在此,只需简单地设置好适当的串行化与反串行化机制,服务端就能处理多个客户端请求,并可返回这些事务的任意数量与任意组合。

  以下有一些练习来加深对此的了解:

  1、 如果一个服务端连接队列已满,那对新的客户端连接请求来说,会发生什么呢?

  2、 如果当客户端还有一个打开的套接字,而服务端此时却关闭了,会发生什么呢?反之呢?

  3、 试着运行一个服务端和两个客户端。我们前面说过,服务端只能处理一个客户端,为使服务端能同时处理多个客户端,需要进行多线程设计,建议对服务端作一些适当的修改,并用两个、三个、或更多客户端来测试。

  4、 以下是当有两个客户端运行时的输出:

Client 2600 4
Sent values 56 and 35, received result 91
Sent values 48 and 20, received result 68
Sent values 6 and 97, received result 103
Sent values 76 and 9, received result 85
Notified server we're shutting down
Shutting down client
Client 2600 2
Sent values 69 and 66, received result 135
Sent values 84 and 45, received result 129
Notified server we're shutting down
Shutting down client
Server 2600
Waiting for new connection request
New connection accepted
Started thread Thread-1
Waiting for new connection request
Executing thread Thread-1
Received values 56 and 35, sent result 91
New connection accepted
Started thread Thread-2
Waiting for new connection request
Executing thread Thread-2
Received values 69 and 66, sent result 135
Received values 48 and 20, sent result 68
Received values 84 and 45, sent result 129
Received values 6 and 97, sent result 103
Shutting down server thread Thread-2
Received values 76 and 9, sent result 85
Shutting down server thread Thread-1

评论

发表评论

此博客中的热门博文

【转】嵌入式开发中使用DDD进行调试

 在嵌入式程序开发过程中,程序员要进行大量的调试,以此验证程序的正确性,修改潜在的错误。调试器对于程序员来说是不可或缺的必备工具。在Linux环境中,有很多调试工具和调试辅助工具,例如GDB、XXGDB、RHIDE、XWPE、GVD和DDD等。其中,DDD是命令行调试器的图形前端,除了一般的程序调试功能以外,还具有交互式图形数据显示的功能。它在嵌入式应用开发中也十分出色。本文主要讲述DDD(Data Display Debugger)的使用方法。      1.认识DDD     GNU DDD是命令行调试程序,如GDB、DBX、WDB、Ladebug、JDB、XDB、Perl Debugger或Python Debugger的可视化图形前端。它特有的图形数据显示功能(Graphical Data Display)可以把数据结构按照图形的方式显示出来。DDD最初源于1990年Andreas Zeller编写的VSL结构化语言,后来经过一些程序员的努力,演化成今天的模样。DDD的功能非常强大,可以调试用C\C++、Ada、 Fortran、Pascal、Modula-2和Modula-3编写的程序;可以超文本方式浏览源代码;能够进行断点设置、回溯调试和历史纪录编辑;具有程序在终端运行的仿真窗口,并在远程主机上进行调试的能力;图形数据显示功能(Graphical Data Display)是创建该调试器的初衷之一,能够显示各种数据结构之间的关系,并将数据结构以图形化形式显示;具有GDB/DBX/XDB的命令行界面,包括完全的文本编辑、历史纪录、搜寻引擎。     DDD是开源软件,用户可以去 http://www.cs.tubs.de/softech/ddd/ 下载.rpm格式的DDD源码文件。      图1显示的是DDD的主窗口。它主要由选单栏、工具条、数据窗口、源文件窗口、机器码窗口、控制台和命令工具窗口等几部分组成。其中,数据窗口用于观察复杂的数据结构,在删除数据之后,显示仍然有效;源文件窗口显示源代码、断点和当前执行到达的位置,选择该窗口中的"Display"项,可以显示任意表达式的值;机...

【转】cs8900网卡的移植至基于linux2.6内核的s3c2410平台

cs8900网卡的移植至基于linux2.6内核的s3c2410平台(转) 2008-03-11 20:58 硬件环境:SBC-2410X开发板(CPU:S3C2410X) 内核版本:2.6.11.1 运行环境:Debian2.6.8 交叉编译环境:gcc-3.3.4-glibc-2.3.3 第一部分 网卡CS8900A驱动程序的移植 一、从网上将Linux内核源代码下载到本机上,并将其解压: #tar jxf linux-2.6.11.1.tar.bz2 二、打开内核顶层目录中的Makefile文件,这个文件中需要修改的内容包括以下两个方面。 (1)指定目标平台。 移植前:         ARCH?= $(SUBARCH) 移植后: ARCH            :=arm (2)指定交叉编译器。 移植前: CROSS_COMPILE ?= 移植后: CROSS_COMPILE   :=/opt/crosstool/arm-s3c2410-linux-gnu/gcc-3.3.4-glibc-2.3.3/bin/arm-s3c2410-linux-gnu- 注:这里假设编译器就放在本机的那个目录下。 三、添加驱动程序源代码,这涉及到以下几个方面。(1)、从网上下载了cs8900.c和cs8900.h两个针对2.6.7的内核的驱动程序源代码,将其放在drivers/net/arm/目录下面。 #cp cs8900.c ./drivers/net/arm/ #cp cs8900.h ./drivers/net/arm/ 并在cs8900_probe()函数中,memset (&priv,0,sizeof (cs8900_t));函数之后添加如下两条语句: __raw_writel(0x2211d110,S3C2410_BWSCON); __raw_writel(0x1f7c,S3C2410_BANKCON3); 注:其原因在"第二部分"解释。 (2)、修改drivers/net/arm/目录下的Kconfig文件,在最后添加如...

【转】U-boot代码分析

/* * S3C44B0 CPU启动代码。 * 蔡军生 2005/07/17 * */ /* * 包含配置文件。 */ #include <config.h> /* 添加版本说明. 2005/07/16 */ #include <version.h> /* * 中断向量表. */ /* × 声明标量是全局函数,CPU加电启动后,就从这里执行代码。 */ .globl _start _start:        b       reset                   /* 跳到复位中断开始位置。 */         add        pc, pc, #0x0c000000         add        pc, pc, #0x0c000000         add        pc, pc, #0x0c000000         add        pc, pc, #0x0c000000         add        pc, pc, #0x0c000000     ...