1. 图形客户端
任何图形程序的一个重要目标是维护一个响应式的用户界面。许多常见的图形化编程工具包的事件循环是单线程运行的,任何应用程序代码的延迟都会对用户体验产生显著的负面影响。应用程序通常别无选择,只能为可能阻塞的任何操作,创建单独的线程。
默认情况下,Ice调用使用类似于常规进程内函数调用的同步语义:调用线程阻塞直到调用完成。然而,由于Ice在后台执行网络活动,同步的Ice调用会带来额外的阻塞风险。举个例子,Ice运行时可能需要建立到服务器的连接,这可能需要一些时间。即使由于较早的调用而已经建立了连接,通过打开的连接发送消息也可能意外地阻塞(例如,由于网络中的问题)。
像图形聊天客户端这样需要非阻塞语义的程序必须使用Ice的异步编程模型(称为异步方法调用(AMI))。虽然这个模型需要一些额外的工作,但好处是值得的:Ice保证异步调用永远不会阻塞调用线程。因此,图形应用程序可以安全地从事件循环线程调用Ice,而不会对用户界面产生负面影响。
Ice提供Dispatcher工具,让应用程序控制执行请求派发的线程。聊天示例中的图形客户端使用此功能,以便ChatRoomCallback对象上的调用可以直接更新用户界面。
1.1. 创建一个回话
推客户端的首要任务之一是与Glacier2路由建立会话。这个过程通过使用Ice中包含的Glacier2辅助类来大大简化,我们的图形客户端大量的使用这些类。(下面显示的代码是从Java图形客户端调整的,但是C#和C++代码是相似的。)
在创建会话之前,我们需要配置调度程序,以确保在UI线程中执行请求调度:
initData.dispatcher = (runnable, connection) -> SwingUtilities.invokeLater(runnable);
如你所见,调度程序实现只是委托给Swing的invokeLater函数。
接下来,客户端创建一个Glacier2.SessionFactoryHelper的实例并传递一个回调对象:
Glacier2.SessionCallback callback = ...;
_factory = new Glacier2.SessionFactoryHelper(initData, callback);
调用回调函数,来通知应用程序有关Glacier2会话生命周期中的重大事件。
现在我们可以使用factory来创建一个会话:
Glacier2.SessionHelper session = _factory.connect(userName, password);
客户端调用connect来处理用户提供的帐户信息。返回值是一个新的Glacier2.SessionHelper对象,它负责管理会话并在必要时调用客户端的SessionCallback对象。例如,由于建立会话异步发生,以避免阻塞调用connect的线程,所以客户端不能开始使用会话,直到调用其connected的回调方法来指示请求成功完成:
public void
connected(SessionHelper session)
throws SessionNotExistException
{
// ...
_chat = Chat.ChatSessionPrx.uncheckedCast(_session.session());
Chat.ChatRoomCallbackPrx callback = Chat.ChatRoomCallbackPrx.uncheckedCast(
_session.addWithUUID(new ChatRoomCallbackI(coordinator)));
_chat.setCallbackAsync(callback).whenCompleteAsync(...);
}
SessionHelper方法session返回新创建的Glacier2会话对象的代理。我们将这个代理下载到我们前面看到的派生接口Chat::ChatSession。
接下来,代码创建将从聊天室接收事件的回调对象。有很多事情发生在这段简短的代码中。首先,我们实例化我们的回调服务类ChatRoomCallbackI,并将其传递给addWithUUID方法。这个方法通过对象适配器特别是回调对象来注册我们的服务,如果他不存在,则创建对象适配器。addWithUUID还确保与服务关联的对象标识,包含一个UUID,并符合Glacier2对回调对象的约定。最后,我们使用一个未经检查的转换将得到的回调代理缩小到Chat::ChatRoomCallback类型。
最后一步是加入聊天室,我们通过在会话上调用setCallback来完成聊天室。我们在这里使用异步调用来避免阻塞。
1.2. .NET客户端
.NET聊天客户端的用户界面是使用Windows Presentation Foundation(WPF)构建的。聊天客户端的图形元素是使用可扩展应用程序标记语言(XAML)来安排的,而应用程序逻辑是用C#实现的。为了避免阻塞.NET的事件循环,程序使用异步调用与聊天服务器进行通信。例如,下面的代码显示了,客户端如何调用发送操作的异步版本,来发布新类型的聊天消息:
public async void sendMessage(string message)
{
...
try
{
long timestamp = await _chat.sendAsync(message);
userSayEvent(timestamp, _username, message);
}
catch(Chat.InvalidMessageException ex)
{
appendMessage("<system-message> - " + ex.reason + Environment.NewLine);
}
catch(Exception)
{
destroySession();
}
}
sendAsync方法接受聊天消息作为唯一的参数,并返回一个.NET任务。然后,代码等待任务和.NET将安排任务完成后继续运行。完成后,我们像调用同步一样调度事件或处理错误。对于sendAsync的调用是由WPF线程构成的,继续将使用WPF线程执行,因为WPF SynchronizationContext使用其调度程序调度延续,因此可以从此处修改GUI组件。
1.3. C++客户端
C++聊天客户端的用户界面是使用Qt框架构建的。为了避免阻塞Qt的事件循环,程序使用异步调用来与聊天服务端通信。例如,下面的代码显示了客户端如何调用发送操作的异步版本,来发布新类型的聊天消息:
shared_ptr<ChatSessionPrx> session = ...
string message = ...
chat->sendAsync(message, [this, message](long long timestamp)
{
_coordinator->userSayEvent(timestamp, _username, message);
},
[this](std::exception_ptr eptr)
{
try
{
rethrow(eptr);
}
catch(const Chat::InvalidMessageException& e)
{
ostringstream os;
os << " - " << e.reason;
_coordinator->appendMessage(os.str());
}
catch(const Ice::Exception&)
{
_coordinator->destroySession(ex);
}
});
调用成功完成时,Ice调用response回调;该方法的参数对应于Slice操作的返回值和输出参数。在上面的例子中,response接收服务端分配的时间戳。请注意,response和exception回调是在UI线程中执行的,因为我们安装了一个Dispatcher,所以这些函数修改用户界面是安全的。