Week
3

Week 3 in Review


In the final week of teaching yourself how to use Perl, you've learned about the extensive Perl function library and about built-in system variables and options. The pair of programs in Listings R3.1 and R3.2 use some of the features you've learned about during this week.

These programs provide a simple "chat" service. The first program, the chat server, establishes connections with clients and passes messages back and forth. The second program, the chat client, enables users to establish connections to this server and send messages to the other users running the chat program.

Each message that a user enters is sent to all the clients currently running the chat program. To quit chatting, the user enters quit.

The server program can be called with the -m (for monitor) option. When -m is specified, each message sent by a client is displayed by the server as it is sent.


Listing R3.1. The chat server program.

1:   #!/usr/local/bin/perl -s

2:   

3:   # get port from command line, or use 2000 as default

4:   if ($#ARGV == -1) {

5:           $port = 2000;

6:   } else {

7:           $port = $ARGV[0];

8:   }

9:   if (getservbyport($port, "tcp")) {

10:          die ("can't access port $port\n");

11:  }

12:  

13:  # initialization stuff

14:  $nextport = $port + 1;

15:  $maxclient = 0;

16:  

17:  # establish main socket connection:  clients use this to

18:  # get ports for their own connections

19:  ($d1, $d2, $prototype) = getprotobyname ("tcp");

20:  $hostname = 'hostname';

21:  chop ($hostname);

22:  ($d1, $d2, $d3, $d4, $serverraddr) = gethostbyname ($hostname);

23:  $serveraddr = pack ("Sna4x8", 2, $port, $serverraddr);

24:  socket (SSOCKET, 2, 1, $prototype) ||

25:          die ("No main server socket\n");

26:  bind (SSOCKET, $serveraddr) ||

27:          die ("Can't bind main server socket\n");

28:  listen (SSOCKET, 5) ||

29:          die ("Can't listen on main server socket\n");

30:  select (STDOUT);

31:  $| = 1;

32:  

33:  while (1) {

34:          # listen for clients

35:          ($clientaddr = accept (SOCKET, SSOCKET)) ||

36:                  die ("Can't accept connection to main socket\n");

37:          select (SOCKET);

38:          $| = 1;

39:          # find ports for new client

40:          $recvport = $nextport;

41:          while (getservbyport($recvport, "tcp")) {

42:                  $recvport++;

43:          }

44:          $nextport = $recvport + 1;

45:          print SOCKET ("$recvport\n");

46:          $sendport = $nextport;

47:          while (getservbyport($sendport, "tcp")) {

48:                  $sendport++;

49:          }

50:          $nextport = $sendport + 1;

51:          # send ports to client

52:          print SOCKET ("$sendport\n");

53:          print SOCKET ("$$\n");

54:          close (SOCKET);

55:          # now connect for this client: first receive, then send

56:          socket (C1SOCKET, 2, 1, $prototype) ||

57:                  die ("No receive client socket\n");

58:          $msgaddr = pack ("Sna4x8", 2, $recvport, $serverraddr);

59:          bind (C1SOCKET, $msgaddr) ||

60:                  die ("Can't bind receive client\n");

61:          listen (C1SOCKET, 1) ||

62:                  die ("Can't listen for receive client\n");

63:          $rsockname = "CRSOCKET" . $maxclient;

64:          ($clientaddr = accept ($rsockname, C1SOCKET)) ||

65:                  die ("Can't accept receive client\n");

66:          socket (C2SOCKET, 2, 1, $prototype) ||

67:                  die ("No send client socket\n");

68:          $msgaddr = pack ("Sna4x8", 2, $sendport, $serverraddr);

69:          bind (C2SOCKET, $msgaddr) ||

70:                  die ("Can't bind send client\n");

71:          listen (C2SOCKET, 1) ||

72:                  die ("Can't listen for send client\n");

73:          $ssockname = "CSSOCKET" . $maxclient;

74:          ($clientaddr = accept ($ssockname, C2SOCKET)) ||

75:                  die ("Can't accept send client\n");

76:          select ($ssockname);

77:          $| = 1;

78:          # when a new client is created, we have to kill all the

79:          # existing children and start new ones, so that all

80:          # of the sockets are known to all of the clients

81:          for ($i = 0; $i <= $maxclient-1; $i++) {

82:                  kill (2, $procids[$i]);

83:          }

84:          for ($i = 0; $i <= $maxclient; $i++) {

85:                  if ($child = fork()) {

86:                          # parent: continue forking

87:                          $procids[$i] = $child;

88:                  } else {

89:                          # child: communicate with this client

90:                          &talk_to_client ($i, $maxclient);

91:                          exit(0);

92:                  }

93:          }

94:          # once we're done forking, go back and listen for

95:          # more clients

96:          $maxclient += 1;

97:  }

98:  

99:  sub talk_to_client {

100:         local ($clientnum, $maxclient) = @_;

101:         local ($msg, $i, $count, $rsockname, $sockname);

102: 

103:         # get read socket for this client

104:         $rsockname = "CRSOCKET" . $clientnum;

105:         while (1) {

106:                 $msg = <$rsockname>;

107:                 last if ($msg eq "quit");

108:                 if ($m) {

109:                         select (STDOUT);

110:                         print ("$msg");

111:                 }

112: 

113:                 # send message to all other clients

114:                 for ($i = 0; $i <= $maxclient; $i++) {

115:                         $sockname = "CSSOCKET" . $i;

116:                         select ($sockname);

117:                         print ("$msg");

118:                 }

119:         }

120: }


This program starts off by obtaining the number of the port to be used for the main socket connection. This port number is assumed to be the first argument on the command line; if no port number is supplied, 2000 is used. Lines 9-11 call getservbyport to check whether this port number is mentioned in the /etc/services file. If it is, the port number is reserved for use by some other program and can't be used here.

Next, lines 17-29 define a socket using the specified port number. Client programs use this socket to establish connections to the server. Note, in particular, that lines 20-21 read the machine name by calling the UNIX hostname command. This enables you to move this program to another machine without having to edit it.

Once the main socket has been established, the server is ready to listen for clients. When a client establishes a connection, the server finds two unused port numbers and sends them to the client. These ports will be used to establish two new socket connections-one for reading and one for writing-which will be used by the server and this particular client. This leaves the main socket free to establish connections with other clients. Lines 44-54 handle the task of obtaining the ports and sending them to the client; lines 55-77 then establish connections to the client using the sockets. (Note that line 53 also sends the process ID of the chat server to the client. This enables the client to kill off the server if something horrible happens.)

The chat server communicates with a client by spawning a child process that handles the task of receiving a message from that client and sending it to the other clients. One child process is defined for each client. The call to fork in line 85 creates a child process.

When a child process is created, it knows the names of the file variables corresponding to the sockets defined for each of the existing clients. However, if a new client appears, the existing child processes cannot send messages to the new client because they cannot access its sockets (because they are created after the children were spawned). To ensure that the existing clients can send messages to the new clients, the server program does the following:

  1. It kills all the child processes that communicate with clients. Lines 81-83 accomplish this task by calling kill.
  2. The server creates a new child process for each client. Lines 84-93 perform this task.

At this point, each client can talk to every other client because all of the socket connections are known by each child. (Recall that when a program splits into parent and child processes, each process has a copy of all the variables that have been defined to this point.)

The chat server uses the global variable $maxclient to keep track of how many clients are on the machine. This ensures that the correct number of child processes are created.

Each child process created by fork calls the subroutine talk_to_client, which reads messages from the client and then sends them to all the other clients via the "send" socket connections. Line 106 reads a message from the client. Line 107 checks whether the message is in fact quit; if it is, the subroutine (and the process) terminates. Lines 108-111 then print the message if the -m option is specified. Finally, lines 113-118 send the message to the other clients.

NOTE
The chat server does not make any attempt to clean up after itself or to reuse sockets closed by clients. This means that this particular program can't be used by too many clients at a time, or by too many different clients.
If you are looking for a challenging exercise, try modifying this program to close sockets when clients are finished with them. (This is a challenging exercise because the child process somehow has to tell the main server that the socket can be closed. You can do it, but it's not easy!)

Now that you've seen how the chat server works, Listing R3.2 shows the chat client program. Users run this program to establish a connection with the chat server and to chat with other users.


Listing R3.2. The chat client program.

1:   #!/usr/local/bin/perl

2:   

3:   # obtain the server port from the command line;

4:   # use 2000 as the default

5:   if ($#ARGV == -1) {

6:           $servport = 2000;

7:   } else {

8:           $servport = $ARGV[0];

9:   }

10:  

11:  # obtain the server machine name from the command line;

12:  # use "silver" as the default

13:  if ($#ARGV < 1) {

14:          $servname = "silver";

15:  } else {

16:          $servname = $ARGV[1];

17:  }

18:  # establish socket connection with server to obtain

19:  # ports for this client

20:  if (getservbyport($servport, "tcp")) {

21:          die ("can't access port $servport\n");

22:  }

23:  ($d1, $d2, $prototype) = getprotobyname ("tcp");

24:  $hostname = 'hostname';

25:  chop ($hostname);

26:  ($d1, $d2, $d3, $d4, $clientraddr) = gethostbyname ($hostname);

27:  ($d1, $d2, $d3, $d4, $serverraddr) = gethostbyname ($servname);

28:  $clientaddr = pack ("Sna4x8", 2, 0, $clientraddr);

29:  $serveraddr = pack ("Sna4x8", 2, $servport, $serverraddr);

30:  socket (SOCKET, 2, 1, $prototype) ||

31:          die ("No server socket\n");

32:  bind (SOCKET, $clientaddr) ||

33:          die ("Can't bind server socket\n");

34:  connect (SOCKET, $serveraddr) ||

35:          die ("Can't connect to server\n");

36:  $sendport = <SOCKET>;

37:  $recvport = <SOCKET>;

38:  $serverid = <SOCKET>;

39:  close (SOCKET);

40:  chop ($sendport);

41:  chop ($recvport);

42:  

43:  # use returned ports to create sockets for this client:

44:  # first socket is send, the second is receive

45:  $conncaddr = pack ("Sna4x8", 2, 0, $clientraddr);

46:  $connsaddr = pack ("Sna4x8", 2, $sendport, $serverraddr);

47:  socket (SSOCKET, 2, 1, $prototype) ||

48:          &nuke ("No send socket");

49:  bind (SSOCKET, $conncaddr) ||

50:          &nuke ("Can't bind send socket");

51:  connect (SSOCKET, $connsaddr) ||

52:          &nuke ("Can't connect to send socket");

53:  $connraddr = pack ("Sna4x8", 2, $recvport, $serverraddr);

54:  socket (RSOCKET, 2, 1, $prototype) ||

55:          &nuke ("No receive socket");

56:  bind (RSOCKET, $conncaddr) ||

57:          &nuke ("Can't bind receive socket");

58:  connect (RSOCKET, $connraddr) ||

59:          &nuke ("Can't connect to receive socket");

60:  select (SSOCKET);

61:  $| = 1;

62:  select (STDOUT);

63:  $| = 1;

64:  

65:  # now, we're ready to go:  prompt for user name

66:  select (STDOUT);

67:  print ("Welcome to chat!  Who are you? ");

68:  $username = <STDIN>;

69:  chop ($username);

70:  print ("Type 'quit' to exit chat.\n");

71:  $child = fork();

72:  if ($child == 0) {

73:          # child: receive messages

74:          &receive_msgs();

75:          exit(0);

76:  }

77:  # parent: send messages

78:  while (1) {

79:          # prompt for message

80:          select (STDOUT);

81:          $msg = <STDIN>;

82:          chop ($msg);

83:          # send message to server

84:          select (SSOCKET);

85:          if ($msg eq "quit") {

86:                  print ($msg);

87:                  last;

88:          }

89:          if ($msg !~ /^\s*$/) {

90:                  print ($username . ": " . $msg . "\n");

91:          }

92:  }

93:  kill (9, $child);

94:  close (RSOCKET);

95:  close (SSOCKET);

96:  

97:  sub receive_msgs {

98:          local ($msg);

99:  

100:         while (1) {

101:                 $msg = <RSOCKET>;

102:                 select (STDOUT);

103:                 print ("$msg");

104:         }

105: }

106: 

107: sub nuke {

108:         local ($errmsg) = @_;

109: 

110:         kill (-9, $serverid);

111:         die ("$errmsg\n");

112: }


This program starts off by obtaining the port number of the main socket connection employed by the chat server. This port number can be supplied on the command line; if it is not, the chat program uses 2000 as the port number.

The chat program then obtains the name of the machine on which the chat server is running. This name also can be supplied on the command line; if it isn't, the chat program assumes the machine is a local machine named silver.

Lines 23-41 establish a connection to the main server socket and receive the port numbers for the sockets to be used by this particular client-server connection. The client also receives the process ID of the server.

Once the port numbers have been received, the chat program can connect to the chat server by establishing two socket connections with the server: one to send messages and another to receive them. Lines 43-63 accomplish this task.

When these socket connections have been established, the chat program is ready to send messages. Lines 67-68 ask for a name by which you can identify yourself to the other users on the system. Once this name has been read in, the chat program splits itself in two by calling fork. The parent process handles the sending of messages, and the child handles messages received by other clients. This ensures that sending and receiving can take place at the same time.

Lines 77-95 send messages to the chat server. Line 81 prompts for a line of input. If the line of input is the message quit, the client program kills off its child, closes its socket connection, and exits. Otherwise, the message-along with the name of the user sending it-is transmitted to the chat server via the "write" socket. The server then sends it to all of the clients on the system.

The child process calls the subroutine receive_msgs, which handles the task of receiving messages from the other clients. Messages are received via the "read" socket and are printed as they are received.

If something goes radically wrong, the chat program calls the subroutine nuke, which kills both itself and the server program.