July 22, 2008, 5 a.m.
posted by oxy
Item 15: Understand all your communications optionsJava communications APIs can be classified along three axes of interest: transport, format, and communication patterns. Every communications API must move across some sort of communications layer, what we call the transport layer. While virtually all of them end up traveling across TCP/IP (or, less frequently, its connectionless partner, UDP/IP), many of them take advantage of higher-level protocols that build on top of TCP/IP; one such example is everybody's favorite transport, HTTP. Every transport channel has its own unique aspects, however, so it's still worthwhile to differentiate between a communications API using raw TCP/IP as its transport and one that makes use of HTTP—for example, firewall products that can scan HTTP traffic will be happier working with a communications API that uses HTTP as its transport than one that uses raw TCP/IP. If necessary, Java can be extended to make use of other transport layers via JNI or the java.nio "New I/O" channels. For example, it's sometimes faster to make use of other operating system–specific IPC APIs, such as pipes, named pipes, and shared memory, than to go through TCP/IP. Some channels provide additional behavior, such as encryption, over traditional channels—for example, HTTP over SSL provides a "secure" HTTP channel, what we of course call HTTPS. The ability to move across a different channel is often exposed as a hook point (see Item 6) at the API level of the communication library. In order to pass data across a transport, the data needs to be in some wire-friendly format. Typically this means we need to either make sure only primitive types are passed across the wire (which are easy to turn into a wire-friendly format) or else turn what we see as fully formed objects in a nice spider web of object references into some kind of flat array of bytes that can be reconstituted into the spider web of object references on the other side. This is known as marshaling, and it's typically (although not always) the responsibility of the communications plumbing to do the data marshaling for you. The data, once marshaled, is often referred to as the payload, and it usually consists of both the data the programmer passes and additional information needed by the communications plumbing (which is sometimes referred to as the framing data). Two popular marshaling formats in Java are Java Object Serialization and XML. Serialization is popular since it provides all the behavior necessary to turn an arbitrary Serializable object into an ObjectStream without modification to the object in any way—all you need to do is implement the Serializable interface, and off you go. More importantly, Serialization is a completely lossless process. Doing the complete round-trip from object to serialized format back to object guarantees no loss of data. When marshaling into XML, a variety of formats are possible, but more and more XML marshaling is being done via the Simple Object Access Protocol (SOAP), or more recently, XML Schema, using schema types to define what the marshaled data should look like. Other marshaling formats are in use throughout the industry, such as CORBA's Internet Inter-Orb Protocol (IIOP) or the RELAX/NG XML Specification, and some formats remain entirely proprietary and closed. At the TCP level, all data sent over a socket is broken down into packets of data that are sent over the IP network and reassembled at the destination to be turned back into the original stream of data. Over time, however, we've come to rely on several abstractions—patterns of network communication—that help shield us programmers from the ugly realities of network communication. Two basic approaches to network communications have emerged. One is the now-familiar Remote Procedure Call (RPC) model, in which a programmer makes what looks like a local method or procedure call, leaving it up to the communications plumbing to marshal the parameters, send them over the transport, block until a response is received, unmarshal the response, and return the unmarshaled data (or throw the unmarshaled exception, if that was the result) to the caller. (This is why most RPC-style toolkits require a postcompilation step, such as RMI's rmic, which generates the local classes—often called proxies or stubs—that do all this work.) More generally known as request-response communication, RPC has found much favor with the programming community due to its conceptual familiarity: "I just call this method, and the rest is all magic until it gets to that method implementation over on the server." Fundamentally, however, the RPC request-response model is just one of several lower-level communications patterns built on the notion of "sending a message": in the request-response model, a sender sends a message to the recipient (the request, consisting of the marshaled parameters) and blocks until the recipient sends the expected message back (the response, consisting of the marshaled return value or fault code). Other (non-RPC) forms of request-response include the HTTP protocol itself, SQL, and even Telnet. When viewed this way, however, it becomes apparent that communication has more possibilities than just "send a message, block, receive a message." For example, I could send a message without blocking, send a message and expect zero to many messages back, send zero to many messages without expecting a response, and so on. In essence, we're just describing different ways to send a message. This concept of sending a message and the inherent flexibility that comes with it are what messaging communications APIs provide. While typically a bit more difficult to work with, in that more supporting code on your part is often required, messaging offers a number of capabilities an RPC-based request-response model cannot. Three such additional "patterns" of communication include solicit-notify, in which one party asks another party to send notifications (such as how electronic mailing lists work); fire-and-forget, also known as one-way or asynchronous calls, in which one party sends a message without waiting for a response; and asynchronous response, in which one party sends a request message expecting a reply but doesn't block waiting for the response, which comes in later. Some messaging systems also support the idea of broadcast messages, in which one message is flung out to multiple recipients. While messaging itself is a fundamental low-level networking concept, the idea of messaging and its commensurate flexibility has proven powerful enough to merit moving this approach to network communications up to the same level of abstraction as RPC. As a result, we can talk about messaging systems, or message-oriented middleware, which provides this same kind of functionality but at a higher level of abstraction—the Java Message Service, for example, is a specification that defines a standard Java API for working with such systems and defines how to send a message whose payload is a Serializable Java object, simple byte array, a String, a Java Map-implementing object, and so on. Having defined these two basic approaches to communications (RPC and messaging), we can go one level higher and begin classifying different architectural styles of network communications. A client/server architecture, for example, has one process—designated the server—that will be available to process requests on behalf of a process that initiates communication with it—the client. In a peer-to-peer architecture, however, generally there is no designated server, and processes communicate with one another freely, either side initiating the communication as desired. (See Item 16 for more on peer-to-peer architecture and discovery.) Note that neither client/server nor peer-to-peer architectures are inherently RPC-based or messaging-based; either one can make use of either approach just as easily. To show how the communications APIs supported by Java break down along the three axes of interest—transport, format, and communication patterns—let's examine each API in turn.
As you can see, the Java enterprise programmer (that's you) has a widespread set of choices for slinging data around between computers in a network. Which, of course, raises a question: How on earth are you supposed to decide? After all, any of them will ultimately get the job done—moving data from machine A to machine B—so obviously the decision has to be rooted in something other than just "will it work?" Any of these would work. The larger question is, "Which of these will work well for what I/we need to do?" Much of the decision-making process lies in identifying, to yourself, the particular context in which your communication needs to take place. Consider the following questions.
Certainly, there are other questions we could ask. We could even create a giant decision-making flowchart of communications technologies, but doing so starts to encroach on the value judgments that each architect and system designer will want to make differently. The point, simply, is to consider your communication needs carefully before committing to anything. (And remember, assuming you've used components in your system well, per Item 1, you can usually change and/or add new communications strategies without too much hassle.) |
- Comment