5.18 Data Format
In our example, the server never examines the
request that it receives from the client. The server just reads all
the data up through and including the newline and sends it back to
the client, looking for only the newline. This is an exception, not
the rule, and normally we must worry about the format of the data
exchanged between the client and server.
Example: Passing Text Strings between
Client and Server
Let's modify our server so that it still reads a
line of text from the client, but the server now expects that line
to contain two integers separated by white space, and the server
returns the sum of those two integers. Our client and server
main functions remain the same, as does our
str_cli function. All that changes is our
str_echo function, which we show in Figure 5.17.
Figure 5.17
str_echo function that adds two numbers.
tcpcliserv/str_ech08.c
1 #include "unp.h"
2 void
3 str_echo(int sockfd)
4 {
5 long arg1, arg2;
6 ssize_t n;
7 char line[MAXLINE];
8 for ( ; ; ) {
9 if ( (n = Readline(sockfd, line, MAXLINE)) == 0)
10 return; /* connection closed by other end */
11 if (sscanf(line, "%ld%ld", &arg1, &arg2) == 2)
12 snprintf(line, sizeof(line), "%ld\n", arg1 + arg2);
13 else
14 snprintf(line, sizeof(line), "input error\n");
15 n = strlen(line);
16 Writen(sockfd, line, n);
17 }
18 }
11鈥?4
We call sscanf to convert the two arguments from text
strings to long integers, and then snprintf is called to
convert the result into a text string.
This new client and server work fine, regardless
of the byte ordering of the client and server hosts.
Example: Passing Binary Structures
between Client and Server
We now modify our client and server to pass
binary values across the socket, instead of text strings. We will
see that this does not work when the client and server are run on
hosts with different byte orders, or on hosts that do not agree on
the size of a long integer (Figure 1.17).
Our client and server main functions do
not change. We define one structure for the two arguments, another
structure for the result, and place both definitions in our
sum.h header, shown in Figure 5.18. Figure 5.19 shows the str_cli
function.
Figure 5.18
sum.h header.
tcpcliserv/sum.h
1 struct args {
2 long arg1;
3 long arg2;
4 };
5 struct result {
6 long sum;
7 };
Figure 5.19
str_cli function which sends two binary integers to
server.
tcpcliserv/str_cli09.c
1 #include "unp.h"
2 #include "sum.h"
3 void
4 str_cli(FILE *fp, int sockfd)
5 {
6 char sendline[MAXLINE];
7 struct args args;
8 struct result result;
9 while (Fgets(sendline, MAXLINE, fp) != NULL) {
10 if (sscanf(sendline, "%ld%ld", &args.arg1, &args.arg2) != 2) {
11 printf("invalid input: %s", sendline);
12 continue;
13 }
14 Writen(sockfd, &args, sizeof(args));
15 if (Readn(sockfd, &result, sizeof(result)) == 0)
16 err_quit("str_cli: server terminated prematurely");
17 printf("%ld\n", result.sum);
18 }
19 }
10鈥?4
sscanf converts the two arguments from text strings to
binary, and we call writen to send the structure to the
server.
15鈥?7
We call readn to read the reply, and print the result
using printf.
Figure
5.20 shows our str_echo function.
Figure 5.20
str_echo function that adds two binary integers.
tcpcliserv/str_ech09.c
1 #include "unp.h"
2 #include "sum.h"
3 void
4 str_echo(int sockfd)
5 {
6 ssize_t n;
7 struct args args;
8 struct result result;
9 for ( ; ; ) {
10 if ( (n = Readn(sockfd, &args, sizeof(args))) == 0)
11 return; /* connection closed by other end */
12 result.sum = args.arg1 + args.arg2;
13 Writen(sockfd, &result, sizeof (result));
14 }
15 }
9鈥?4
We read the arguments by calling readn, calculate and
store the sum, and call writen to send back the result
structure.
If we run the client and server on two machines
of the same architecture, say two SPARC machines, everything works
fine. Here is the client interaction:
solaris % :tcpcli09 12.106.32.254
|
|
11
22
|
we type this
|
33
|
this is the server's
reply
|
-11
-44
|
|
-55
|
But when the client and server are on two
machines of different architectures (say the server is on the
big-endian SPARC system freebsd and the client is on the
little endian Intel system linux), it does not work.
linux % tcpcli09 206.168.112.96
|
1 2
|
we type this
|
3
|
and it works
|
-22
-77
|
then we type
this
|
-16777314
|
and it does not
work
|
The problem is that the two binary integers are
sent across the socket in little-endian format by the client, but
interpreted as big-endian integers by the server. We see that it
appears to work for positive integers but fails for negative
integers (see Exercise 5.8).
There are really three potential problems with this example:
-
Different
implementations store binary numbers in different formats. The most
common formats are big-endian and little-endian, as we described in
Section 3.4.
-
Different
implementations can store the same C datatype differently. For
example, most 32-bit Unix systems use 32 bits for a long
but 64-bit systems typically use 64 bits for the same datatype
(Figure 1.17). There
is no guarantee that a short, int, or
long is of any certain size.
-
Different
implementations pack structures differently, depending on the
number of bits used for the various datatypes and the alignment
restrictions of the machine. Therefore, it is never wise to send
binary structures across a socket.
There are two common solutions to this data
format problem:
-
Pass all numeric
data as text strings. This is what we did in Figure 5.17. This assumes that both hosts
have the same character set.
-
Explicitly
define the binary formats of the supported datatypes (number of
bits, big- or little-endian) and pass all data between the client
and server in this format. RPC packages normally use this
technique. RFC 1832 [Srinivasan 1995] describes the External Data Representation (XDR) standard
that is used with the Sun RPC package.
|