Logging on to Internet Relay Chat (IRC)

This month's simple and incredibly small tool lets your applications write output to IRC

In the first couple installments of this column, I alluded to revisiting an object pooling discussion I started back in June 1998. I was going to explain how to build a better pooling mechanism by using a combination of certain tools more recently discussed here. However, plans have changed. Other pooling discussions have surfaced (see the Resources section below) and, with Java 2's rising popularity, my design is no longer avant-garde. Thus, I won't revisit this issue; instead, I'll discuss how you can log on to IRC in order to monitor your application's debugging output.

An introduction to IRC

Internet Relay Chat (IRC) was once the de facto standard for realtime communications on the Internet. In a nutshell, there are IRC clients and IRC servers; you use a client to connect to a server. Once you are connected, you can join (or create) a channel. Other clients that enter the same channel may converse with each other via text messages broadcast by the server. The standard protocol for client/server communication is defined in Request for Comments (RFC) 1459 (see Resources).

Why log on to a channel?

A few years back, I was working on a project in which we had to remotely deploy a service whose output had to be monitored in realtime. Our biggest debate was over the method by which we would receive the service's output. We could have built our own multithreaded server-socket modules into the service so that we could connect to it via a home-brewed client and monitor its status. Back then, there wasn't much Java library support for standards like SNMP. By rolling our own solution, and essentially reinventing the wheel (a major software development faux pas), we would have created a maintenance nightmare. The protocol and service would have eventually been hacked, requiring us to rearchitect and redeploy the project.

My solution was to tackle the problem from the other side. What if the service initiated the connection? What if the service acted as a client to an already established standard? This would alleviate most of the headache. We wouldn't have to worry about designing and coding a multithreaded socket-monitoring server, which would use precious CPU cycles and inevitably turn into a security-maintenance issue.

We already had an IRC server set up so that telecommuters could electronically attend internal company meetings. My idea was to have the service connect to our IRC server, join a channel, and log its output. Thus, whenever we needed to check on the service's status, all we had to do was load up our favorite IRC client application, connect to the IRC server, and join the channel to which the service was sending its output. It worked out beautifully. It's been in production almost three years now and we've deployed half a dozen different types of services using the same solution.

Trimming the fat

To keep the examples simple and this article short, I have severely trimmed down the tool. The original version -- a mission-critical module -- had to be able to deal with server disconnections, network outages, channel management, and so forth. The version I will present to you simply demonstrates how to connect to the IRC server, join a channel, and dump some output. I leave the advanced features as an exercise for you.

On to the tool

The tool is comprised of two objects: IRCConnection and Channel. IRCConnection, as its name implies, takes care of connecting to the IRC server. Once you establish the connection, the Channel object is used to join a channel.

Connecting to the server

To connect to an IRC server, you have to know the server's name (DNS or IP address) and the port number. This information will be passed into the constructor of IRCConnection. Since the standard IRC port is 6667, you will provide an optional constructor that defaults to this port and thus only requires the server name. Here are the two constructors:

public class IRCConnection
{
  private String host;
  private int port;
  public IRCConnection( String host )
    throws UnknownHostException, IOException
  {
    this( host, 6667 );
  }
  public IRCConnection( String host, int port )
    throws UnknownHostException, IOException
  {
    this.host = host;
    this.port = port;
    connect();
    register();
  }

You'll notice that the main constructor calls two methods that haven't been identified yet. The first method is connect(), which opens up a socket to the server and stores a handle to the socket's output stream. The output stream will be used by the Channel object to send messages to the IRC server. Here's the connection method:

  private PrintStream out;
  private void connect()
    throws UnknownHostException, IOException
  {
    Socket socket = new Socket( host, port );
    out = new PrintStream( socket.getOutputStream() );
  }

The next method in the constructor, after connect(), is the call to register(). When a client connects to an IRC server, it must identify itself. The RFC explains all the fine details. At a minimum, the client must choose a nickname, which will uniquely identify the client's messages in the channel, and give it to the server. The client must also tell the server the location from which it is connecting. This information is conveyed by the USER and NICK commands. In the registration method below, I have hard-coded values for the nickname and client address:

  private void register()
  {
    String nickname = "test";
    String localhost = "localhost";
    out.println( "USER" + " " + nickname + " " + localhost + " " + host + " " + nickname );
    out.println( "NICK" + " " + nickname);
  }

In the interest of brevity and simplicity, I'm not checking any feedback from the IRC server here. The feedback would come in on the socket's input stream. For mission-critical versions of this tool, you would want to check the server feedback for possible errors; the server could be full, for instance, or your chosen nickname could already be in use.

Getting into a channel

Now that the client is connected to the server, it can join a channel and start sending messages. To join a channel, you have to add one more method -- getChannel -- to IRCConnection. This method simply instantiates a new Channel object and returns it. The constructor of Channel (discussed in a moment) takes the channel name and the socket output stream:

  public Channel getChannel( String name )
  {
    return( new Channel( name, out ) );
  }

The Channel object

As the code above implies, the Channel constructor takes a channel name and an output stream. The constructor then issues a JOIN command to the server, which signifies that the client wishes to join a channel:

public class Channel
{
  private String name;
  private PrintStream out;
  protected Channel( String name, PrintStream out )
  {
    this.name = name;
    this.out = out;
    out.println( "JOIN" + " " + "#" + name );
  }

Again, note that any feedback from the server is ignored here. It's possible that the channel could be full, locked, or even be blocking the client's domain. All of these are advanced topics outside this article's scope. Please see the RFC for details.

The Channel object then exposes one method for sending messages to the channel, println. This method simply takes the provided text, attaches the PRIVMSG command to the front of the text, and sends it off to the server:

  public void println( String msg )
  {
    out.println ( "PRIVMSG" + " " + "#" + name + " " + ":" + msg );
  }

Using the tool

Now that you have the tool, how do you use it? It's incredibly simple: instantiate an IRCConnection, retrieve a Channel object via the getChannel() method, and then send messages using the println() method:

  IRCConnection con = new IRCConnection( "irc.yourdomain.edu" );
  Channel cha = con.getChannel( "test" );
  cha.println( "i'm in the channel!" );

By the way, you can create multiple Channel objects and send different messages to different channels simultaneously.

A little bit of input

As you can imagine, debugging this tool is very difficult if you can't see what messages the IRC server is sending to the client. Fortunately, there is a simple solution. You can use a daemon thread to monitor the socket's input stream and dump any server messages to the console. I call this subtool InputDumper. The constructor simply takes a handle to an input stream:

public class InputDumper extends Thread
{
  private DataInputStream in;
  protected InputDumper( InputStream in )
  {
    this.in = new DataInputStream( in );
  }

Since you want this object to run as a thread, you override the run() method. The logic for this method is to simply loop forever and dump any input from the stream out to the console:

  public void run()
  {
    try
    {
      String msg;
      while ( ( msg = in.readLine() ) != null )
      {
        System.out.println( msg );
      }
    }
    catch( IOException e )
    {
      e.printStackTrace();
    }
  }

And that's the whole class. The final step is to integrate that class into the IRCConnection class. This integration will take place in the connect() method. First, you need a global reference to an InputDumper; then simply instantiate it, set it to a daemon, and start it up.

  private Thread t; // for debugging
  private void connect()
    throws UnknownHostException, IOException
  {
    Socket socket = new Socket( host, port );
    out = new PrintStream( socket.getOutputStream() );
    // for debugging
    t = new InputDumper( socket.getInputStream() );
    t.setDaemon( true );
    t.start();
  }

Now, when you run your application, you will see all the data that the IRC server sends to the client. This will help you see how the IRC protocol works, and let you catch the sorts of errors mentioned above.

Netiquette

Please be aware that automated clients connecting to IRC servers are commonly known as bots. Bots can be, and have been, used for abusive purposes, and are thus frowned upon by the mainstream IRC culture. With this in mind, be very careful while using this tool. Do not connect to public servers, and try to avoid popular channel names.

Conclusion

The IRC output tool is very simple, useful, and versatile. These small and simple classes let you easily add IRC connectability to any project. I hope you find this tool as intriguing and indispensable as I have. As always, comments, criticisms, and, especially, design-improvement suggestions are always welcome.

Thomas E. Davis is a Sun Certified Java Developer. He lives in sunny southern Florida, but spends most of his time indoors in front of the computer. He has spent the past three years designing and building large-scale, multitier distributed applications in Java.

Learn more about this topic