Joe's spitting in the sawdust Erlang tutorials
Tutorial number 3
Last edited 2003-10-02
Client in Erlang - Server in C
|
This tutorial shows you how to build a simple
client-server. All the code is here.
Sometimes I want to write a client-server application where the
client is written in Erlang and the server in C.
This happens relatively infrequently, since the pain of writing
a server in C is considerable. Every time I have to do this
I forget the horrendously horrible details of forking off parallel
processes in C and have to re-learn how socket and processes work in C.
IMHO this is very definitely you don't want to know stuff.
This tutorial has therefore been written to document how to write
a client in Erlang which talks to a server in C. Hopefully the code
might be useful to somebody.
Please, report all errors, omissions or improvements to the
author.
1. A simple server
2. A callback server
I want to make a client is Erlang and a server in C.
The client is the easy bit:
-module(client).
%% Socket client routines
%% Author: Joe Armstrong <joe@sics.se>
%% Date: 2003-10-02
-export([tests/1, test1/1, test2/1, test3/1]).
tests(Port) ->
spawn(fun() -> test1(Port) end),
spawn(fun() -> test2(Port) end),
spawn(fun() -> test3(Port) end).
%% send a single message to the server wait for
%% a reply and close the socket
test1(Port) ->
case gen_tcp:connect("localhost", Port, [binary,{packet, 2}]) of
{ok, Socket} ->
io:format("Socket=~p~n",[Socket]),
gen_tcp:send(Socket, "hello joe"),
Reply = wait_reply(Socket),
io:format("Reply 1 = ~p~n", [Reply]),
gen_tcp:close(Socket);
_ ->
error
end.
test2(Port) ->
case gen_tcp:connect("localhost", Port,
[binary,{packet, 2}]) of
{ok, Socket} ->
io:format("Socket=~p~n",[Socket]),
gen_tcp:send(Socket, "hello joe"),
Reply = wait_reply(Socket),
io:format("Reply 2 = ~p~n", [Reply]),
exit(1);
_ ->
error
end.
test3(Port) ->
case gen_tcp:connect("localhost", Port,
[binary,{packet, 2}]) of
{ok, Socket} ->
io:format("Socket=~p~n",[Socket]),
gen_tcp:send(Socket, [42|"hello joe"]),
Reply = wait_reply(Socket),
io:format("Reply 3 = ~p~n", [Reply]);
_ ->
error
end.
wait_reply(X) ->
receive
Reply ->
{value, Reply}
after 100000 ->
timeout
end.
|
This exports four routines:
- client:tests(Ports) evaluates the next three test
functions in parallel.
- client:test1(Port) Opens Port sends a
message to the port and waits for a reply and then closes the port.
- client:test2(Port) Opens Port sends a
message to the port and waits for a reply and then crashes.
- client:test3(Port) Opens Port sends a
message to the port which causes the server to crash and then waits for a replyfrom the server.
Note that in all cases the socket is opened with arguments
[binary,{packet, 2}]. The {packet,2}
directive means that all messages between the client and the server
are preceded by a two byte length count.
server1.c is my first attempt at a C server.
I have stuffed everything into a single file.
most of this code came from Stevens ...
/*
* Parallel socket server.
* Forks off a new process for every new connection.
* Adapted from Stevens - Unix Network Programming.
*
* Author: Joe Armstrong
* Date: 2003-10-02
* Usage:
* server <port>
*/
#include <stdio.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <varargs.h>
/* this buffer is used to store all the socket data */
char buf[65536];
/*VARARGS1*/
err_quit(va_alist)
va_dcl
{
va_list args;
char *fmt;
va_start(args);
fmt = va_arg(args, char *);
vfprintf(stderr, fmt, args);
fputc('\n', stderr);
va_end(args);
exit(1);
}
/*
* Read "n" bytes from a descriptor.
* Use in place of read() when fd is a stream socket.
*/
int readn(int fd, char *ptr, int nbytes)
{
int nleft, nread;
nleft = nbytes;
while (nleft > 0) {
nread = read(fd, ptr, nleft);
if (nread < 0)
return(nread); /* error, return < 0 */
else if (nread == 0)
break; /* EOF */
nleft -= nread;
ptr += nread;
}
return(nbytes - nleft); /* return >= 0 */
}
/*
* Write "n" bytes to a descriptor.
* Use in place of write() when fd is a stream socket.
*/
int writen(int fd, char *ptr, int nbytes)
{
int nleft, nwritten;
nleft = nbytes;
while (nleft > 0) {
nwritten = write(fd, ptr, nleft);
if (nwritten <= 0)
return(nwritten); /* error */
nleft -= nwritten;
ptr += nwritten;
}
return(nbytes - nleft);
}
main(int argc, char* argv[])
{
int port,sockfd, newsockfd, clilen, childpid, err;
struct sockaddr_in cli_addr, serv_addr;
if (argc != 2)
err_quit("usage: server <port>\n");
if ( (sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
err_quit("server: can't open stream socket");
port = atoi(argv[1]);
printf("opening port %d\n", port);
bzero((char *) &serv_addr, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(port);
if ((err = bind(sockfd, (struct sockaddr *) &serv_addr,
sizeof(serv_addr))) < 0)
err_quit("server: can't bind local address %d", err);
listen(sockfd, 5);
for ( ; ; ) {
clilen = sizeof(cli_addr);
newsockfd = accept(sockfd, (struct sockaddr *) &cli_addr, &clilen);
if (newsockfd < 0)
err_quit("server: accept error");
if ( (childpid = fork()) < 0)
err_quit("server: fork error");
else if (childpid == 0) {
close(sockfd);
handle(1, newsockfd, 0);
loop(newsockfd);
handle(3, newsockfd, 0);
exit(0);
}
close(newsockfd);
}
}
/* The socket protocol is 2 byte length then the data */
loop(int fd)
{
char *p;
int again, n, i;
while(1){
p = buf;
i = readn(fd, buf, 2);
if(i==0)
/* socket closed */
break;
else if (i == 2){
n = (*buf)*256 + *(buf+1);
i = readn(fd, buf, n);
if (i != n)
break;
handle(2, fd, n);
} else {
/* protocol error */
break;
}
}
}
/*
* handle *must* call gen_reply
*/
handle(int phase, int fd, int n)
{
int i;
switch (phase)
{
case 1:
printf("handle %d starting\n", getpid());
break;
case 2:
printf("handle %d received %d bytes:", getpid(), n);
for(i=0;i<n;i++)
putchar(buf[i]);
printf("\r\n");
/* just for fun crash if buf[0] = 42 */
if(buf[0] == 42)
exit(1);
strcpy(buf, "ack");
gen_reply(fd, buf, 3);
printf("handle %d sending ack\n", getpid());
break;
case 3:
printf("handle %d stopping\n", getpid());
break;
}
}
gen_reply(int fd, char *p, int n)
{
char out[2];
out[0] = n >> 8;
out[1] = n & 0xff;
writen(fd, out, 2);
writen(fd, p, n);
}
|
The salient point of this code is the function
handle(phase, fd, n). This is called as follows:
To run the client and server we need two windows.
In one shell window we run the server:
bash-2.05$ ./server1 1234
opening port 1234
|
In another window we start Erlang, and run the main test command:
bash-2.05$ erl
Erlang (BEAM) emulator version 5.2 [source] [hipe]
Eshell V5.2 (abort with ^G)
1> client:tests(1234).
<0.32.0>
Socket=#Port<0.29>
Socket=#Port<0.30>
Socket=#Port<0.31>
Reply 1 = {value,{tcp,#Port<0.29>,<<97,99,107>>}}
Reply 2 = {value,{tcp,#Port<0.30>,<<97,99,107>>}}
Reply 3 = {value,{tcp_closed,#Port<0.31>}}
|
In window one we can see what happened:
bash-2.05$ ./server1 1234
opening port 1234
handle 13411 starting
handle 13412 starting
handle 13411 received 9 bytes:hello joe
handle 13411 sending ack
handle 13412 received 9 bytes:hello joe
handle 13412 sending ack
handle 13411 stopping
handle 13412 stopping
handle 13413 starting
handle 13413 received 10 bytes:*hello joe
|
Note that because of the concurrency the output from the three
parallel processes is interleaved.
Also note that process 13413 crashed and thus there is no code to
say that it stopped. The crash was, however, detected by the Erlang
processes.
As a final tweak to the server program we break it into
two files gen_server.c and server2.c .
gen_sever.c is just our old friend server1.c where
I have abstracted out the handler function and a function pointer
instead of as a statically linked function.
The resulting code in server2.c is IMHO much easier
to understand :-)
Here is server2.c.
/*
* Parallel socket server.
* Author: Joe Armstrong <joe@sics.se>
* Date: 2003-10-02
*
* Usage:
* server2 <port>
*/
#include <stdio.h>
/* this buffer is used to communicate with the client */
char buf[65536];
/* handle will be called every time something happens
* phase = 1 means a connection is starting
* = 2 means the client has sent a message to the
* server. The length of the message is n
* and the data is in buf[0..n-1]
* The client *must* reply by calling
* gen_reply(int fd, char *p, int m)
* this returns the data in p[0..m-1] to the client
* = 3 means the client has disconnected
*/
my_handler(int phase, int fd, int n)
{
int i;
switch (phase)
{
case 1:
printf("handle %d starting\n", getpid());
break;
case 2:
printf("handle %d received %d bytes:", getpid(), n);
for(i=0;i<n;i++)
putchar(buf[i]);
printf("\r\n");
/* just for fun crash if buf[0] = 42 */
if(buf[0] == 42)
exit(1);
strcpy(buf, "ack");
gen_reply(fd, buf, 3);
printf("handle %d sending ack\n", getpid());
break;
case 3:
printf("handle %d stopping\n", getpid());
break;
}
}
main(int argc, char *argv[])
{
int port;
if (argc != 2)
err_quit("usage: server <port>\n");
port = atoi(argv[1]);
gen_server(port, my_handler);
}
|
|