Skip to main content

Build Your Own IRC Client

Learn the IRC protocol by writing a simple client from scratch

The IRC Protocol

IRC is one of the simplest network protocols to implement. At its core, it's just text messages sent over a TCP connection. Each message is a single line terminated by \r\n (carriage return + line feed). To build a minimal IRC client, you only need to understand a handful of commands:

A typical connection flow looks like this: open a TCP socket to the server on port 6667 (or 6697 for TLS), send NICK and USER to register, respond to PING messages to stay connected, then JOIN channels and send PRIVMSG to chat.

Every example below connects to Libera Chat, joins #test, sends a message, and then disconnects. They all handle the PING/PONG keep-alive that servers require.

Python

Python's built-in socket module makes this straightforward. This example uses SSL/TLS for a secure connection.

import socket
import ssl

server = "irc.libera.chat"
port = 6697
nick = "mybot"
channel = "#test"

sock = socket.create_connection((server, port))
irc = ssl.create_default_context().wrap_socket(sock, server_hostname=server)

def send(msg):
    irc.send(f"{msg}\r\n".encode())

send(f"NICK {nick}")
send(f"USER {nick} 0 * :{nick}")

buf = b""
while True:
    buf += irc.recv(4096)
    while b"\r\n" in buf:
        line, buf = buf.split(b"\r\n", 1)
        line = line.decode("utf-8", errors="replace")
        print(line)

        if line.startswith("PING"):
            send("PONG" + line[4:])

        # 001 = RPL_WELCOME, meaning registration is complete
        if " 001 " in line:
            send(f"JOIN {channel}")
            send(f"PRIVMSG {channel} :Hello from Python!")
            send("QUIT :Bye")
            irc.close()
            exit()

JavaScript (Node.js)

Node.js provides the tls module for secure TCP connections.

const tls = require("tls");

const server = "irc.libera.chat";
const port = 6697;
const nick = "mybot";
const channel = "#test";

const socket = tls.connect(port, server, () => {
  send(`NICK ${nick}`);
  send(`USER ${nick} 0 * :${nick}`);
});

function send(msg) {
  socket.write(msg + "\r\n");
}

let buffer = "";
socket.setEncoding("utf8");
socket.on("data", (data) => {
  buffer += data;
  const lines = buffer.split("\r\n");
  buffer = lines.pop();

  for (const line of lines) {
    console.log(line);

    if (line.startsWith("PING")) {
      send("PONG" + line.slice(4));
    }

    // 001 = RPL_WELCOME
    if (line.includes(" 001 ")) {
      send(`JOIN ${channel}`);
      send(`PRIVMSG ${channel} :Hello from Node.js!`);
      send("QUIT :Bye");
    }
  }
});

Go

Go's standard library has everything needed with crypto/tls and bufio.

package main

import (
	"bufio"
	"crypto/tls"
	"fmt"
	"strings"
)

func main() {
	server := "irc.libera.chat:6697"
	nick := "mybot"
	channel := "#test"

	conn, err := tls.Dial("tcp", server, nil)
	if err != nil {
		panic(err)
	}
	defer conn.Close()

	send := func(msg string) {
		fmt.Fprintf(conn, "%s\r\n", msg)
	}

	send("NICK " + nick)
	send("USER " + nick + " 0 * :" + nick)

	scanner := bufio.NewScanner(conn)
	for scanner.Scan() {
		line := scanner.Text()
		fmt.Println(line)

		if strings.HasPrefix(line, "PING") {
			send("PONG" + line[4:])
		}

		// 001 = RPL_WELCOME
		if strings.Contains(line, " 001 ") {
			send("JOIN " + channel)
			send("PRIVMSG " + channel + " :Hello from Go!")
			send("QUIT :Bye")
			return
		}
	}
}

Rust

Using Rust's standard library with the native-tls crate for TLS support.

use native_tls::TlsConnector;
use std::io::{BufRead, BufReader, Write};
use std::net::TcpStream;

fn send(stream: &mut impl Write, msg: &str) {
    write!(stream, "{msg}\r\n").unwrap();
    stream.flush().unwrap();
}

fn main() {
    let server = "irc.libera.chat";
    let port = 6697;
    let nick = "mybot";
    let channel = "#test";

    let connector = TlsConnector::new().unwrap();
    let tcp = TcpStream::connect(format!("{server}:{port}")).unwrap();
    let tls = connector.connect(server, tcp).unwrap();

    let mut reader = BufReader::new(tls);

    send(reader.get_mut(), &format!("NICK {nick}"));
    send(reader.get_mut(), &format!("USER {nick} 0 * :{nick}"));

    let mut line = String::new();
    while reader.read_line(&mut line).unwrap() > 0 {
        print!("{line}");

        if line.starts_with("PING") {
            let pong = format!("PONG{}", &line[4..].trim());
            send(reader.get_mut(), &pong);
        }

        // 001 = RPL_WELCOME
        if line.contains(" 001 ") {
            send(reader.get_mut(), &format!("JOIN {channel}"));
            send(reader.get_mut(), &format!("PRIVMSG {channel} :Hello from Rust!"));
            send(reader.get_mut(), "QUIT :Bye");
            break;
        }

        line.clear();
    }
}

// Cargo.toml:
// [dependencies]
// native-tls = "0.2"

C

A plain TCP example using POSIX sockets. For production use, you would add TLS via OpenSSL.

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <netdb.h>
#include <sys/socket.h>

static int irc_fd;

void irc_send(const char *msg) {
    dprintf(irc_fd, "%s\r\n", msg);
}

/* Strip trailing \r\n */
void chomp(char *s) {
    size_t len = strlen(s);
    while (len > 0 && (s[len - 1] == '\r' || s[len - 1] == '\n'))
        s[--len] = '\0';
}

int main(void) {
    const char *server = "irc.libera.chat";
    const char *port = "6667";
    const char *nick = "mybot";
    const char *channel = "#test";

    struct addrinfo hints = { .ai_socktype = SOCK_STREAM }, *res;
    getaddrinfo(server, port, &hints, &res);

    irc_fd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
    connect(irc_fd, res->ai_addr, res->ai_addrlen);
    freeaddrinfo(res);

    char buf[512];
    snprintf(buf, sizeof(buf), "NICK %s", nick);
    irc_send(buf);
    snprintf(buf, sizeof(buf), "USER %s 0 * :%s", nick, nick);
    irc_send(buf);

    char line[512];
    FILE *fp = fdopen(dup(irc_fd), "r");
    while (fgets(line, sizeof(line), fp)) {
        chomp(line);
        printf("%s\n", line);

        if (strncmp(line, "PING", 4) == 0) {
            snprintf(buf, sizeof(buf), "PONG%s", line + 4);
            irc_send(buf);
        }

        /* 001 = RPL_WELCOME */
        if (strstr(line, " 001 ")) {
            snprintf(buf, sizeof(buf), "JOIN %s", channel);
            irc_send(buf);
            snprintf(buf, sizeof(buf), "PRIVMSG %s :Hello from C!", channel);
            irc_send(buf);
            irc_send("QUIT :Bye");
            break;
        }
    }

    fclose(fp);
    close(irc_fd);
    return 0;
}

/* Compile: gcc -o irc irc.c */

Java

Java's SSLSocketFactory provides TLS out of the box.

import javax.net.ssl.SSLSocketFactory;
import java.io.*;

public class IrcClient {
    static OutputStream raw;

    static void send(String msg) throws Exception {
        raw.write((msg + "\r\n").getBytes("UTF-8"));
        raw.flush();
    }

    public static void main(String[] args) throws Exception {
        String server = "irc.libera.chat";
        int port = 6697;
        String nick = "mybot";
        String channel = "#test";

        var socket = SSLSocketFactory.getDefault()
            .createSocket(server, port);
        raw = socket.getOutputStream();
        var in = new BufferedReader(
            new InputStreamReader(socket.getInputStream()));

        send("NICK " + nick);
        send("USER " + nick + " 0 * :" + nick);

        String line;
        while ((line = in.readLine()) != null) {
            System.out.println(line);

            if (line.startsWith("PING")) {
                send("PONG" + line.substring(4));
            }

            // 001 = RPL_WELCOME
            if (line.contains(" 001 ")) {
                send("JOIN " + channel);
                send("PRIVMSG " + channel + " :Hello from Java!");
                send("QUIT :Bye");
                break;
            }
        }

        socket.close();
    }
}

Ruby

Ruby's openssl standard library handles TLS connections.

require "socket"
require "openssl"

server = "irc.libera.chat"
port = 6697
nick = "mybot"
channel = "#test"

tcp = TCPSocket.new(server, port)
ctx = OpenSSL::SSL::SSLContext.new
irc = OpenSSL::SSL::SSLSocket.new(tcp, ctx)
irc.connect

def irc_send(conn, msg)
  conn.write("#{msg}\r\n")
end

irc_send(irc, "NICK #{nick}")
irc_send(irc, "USER #{nick} 0 * :#{nick}")

while line = irc.gets
  line.chomp!
  puts line

  if line.start_with?("PING")
    irc_send(irc, "PONG#{line[4..]}")
  end

  # 001 = RPL_WELCOME
  if line.include?(" 001 ")
    irc_send(irc, "JOIN #{channel}")
    irc_send(irc, "PRIVMSG #{channel} :Hello from Ruby!")
    irc_send(irc, "QUIT :Bye")
    break
  end
end

irc.close

C#

Using .NET's SslStream for a secure connection.

using System.Net.Security;
using System.Net.Sockets;
using System.Text;

var server = "irc.libera.chat";
var port = 6697;
var nick = "mybot";
var channel = "#test";

using var tcp = new TcpClient(server, port);
using var ssl = new SslStream(tcp.GetStream());
ssl.AuthenticateAsClient(server);

using var reader = new StreamReader(ssl, Encoding.UTF8);

void Send(string msg)
{
    var bytes = Encoding.UTF8.GetBytes(msg + "\r\n");
    ssl.Write(bytes);
    ssl.Flush();
}

Send($"NICK {nick}");
Send($"USER {nick} 0 * :{nick}");

string? line;
while ((line = reader.ReadLine()) != null)
{
    Console.WriteLine(line);

    if (line.StartsWith("PING"))
        Send("PONG" + line[4..]);

    // 001 = RPL_WELCOME
    if (line.Contains(" 001 "))
    {
        Send($"JOIN {channel}");
        Send($"PRIVMSG {channel} :Hello from C#!");
        Send("QUIT :Bye");
        break;
    }
}

PHP

PHP can open TLS sockets using the ssl:// stream wrapper.

<?php

$server = "irc.libera.chat";
$port = 6697;
$nick = "mybot";
$channel = "#test";

$irc = stream_socket_client("ssl://{$server}:{$port}");

function irc_send($conn, string $msg): void {
    fwrite($conn, "{$msg}\r\n");
}

irc_send($irc, "NICK {$nick}");
irc_send($irc, "USER {$nick} 0 * :{$nick}");

while ($line = fgets($irc)) {
    $line = rtrim($line, "\r\n");
    echo $line . "\n";

    if (str_starts_with($line, "PING")) {
        irc_send($irc, "PONG" . substr($line, 4));
    }

    // 001 = RPL_WELCOME
    if (str_contains($line, " 001 ")) {
        irc_send($irc, "JOIN {$channel}");
        irc_send($irc, "PRIVMSG {$channel} :Hello from PHP!");
        irc_send($irc, "QUIT :Bye");
        break;
    }
}

fclose($irc);

Next Steps

These examples cover the bare minimum. A real IRC client would need to handle:

Or Just Use Simple IRC Client

Don't want to build your own? Simple IRC Client is ready to go — clean, modern, and open source.