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:
NICK <nickname>— set your nicknameUSER <username> 0 * :<realname>— identify yourself to the serverJOIN <#channel>— join a channelPRIVMSG <target> :<message>— send a message to a channel or userPING <token>/PONG <token>— keep-alive mechanism (reply to server PINGs)QUIT :<message>— disconnect from the server
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:
- Message parsing — properly parse the IRC message format (
:prefix COMMAND params :trailing) - Multiple channels — track joined channels and route messages accordingly
- Nick collision — handle
433 ERR_NICKNAMEINUSEby trying alternative nicknames - Reconnection — automatically reconnect on connection loss
- SASL authentication — log in to registered accounts via the IRCv3 SASL mechanism
- CAP negotiation — request IRCv3 capabilities for modern features like message history and tags
- User interface — a terminal UI, graphical interface, or web frontend for interactive use
References
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.