aslain.dev
0%
01 Hizmetler 02 Hakkımda 03 Projeler 04 Stack 05 Blog 06 İletişim
← Tüm makaleler Game Server

C++ Socket Programming: Build a Simple TCP Server

Whether you are writing a game server, a chat service or a simple API, the foundation is always the same: two machines talking to each other over the network. On the C++ side, the lowest layer that does this work is the c++ socket API — the Berkeley sockets that have been standard since the 1980s and still run underneath the Linux kernel today. In this article we will build a simple TCP server from scratch, using only POSIX system calls and no external library, that listens for incoming clients, reads their data and sends a reply.

What is a socket, and why does TCP matter?

A socket is a communication endpoint provided by the operating system. From your program's point of view it behaves like a file descriptor: you read from it and write to it, and the kernel handles all the networking complexity in between. There are two main transport protocols:

  • TCP (SOCK_STREAM): connection-based, ordered and reliable. The bytes you send arrive complete and in the right order. Used wherever everything has to get through — chat, HTTP, game login.
  • UDP (SOCK_DGRAM): connectionless, fast but with no guarantees. Packets may be lost or reordered. Preferred where latency matters more than reliability, such as real-time game positions.

We focus on TCP here because it best illustrates the pattern people mean by a "simple server". The flow is always the same: socket()bind()listen()accept()recv()/send()close().

The headers you need

Socket programming on Linux requires a handful of POSIX headers. On Windows you would use Winsock (winsock2.h and WSAStartup); here we assume Linux/macOS.

#include <iostream>
#include <cstring>      // memset, strlen
#include <unistd.h>     // close, read, write
#include <sys/socket.h> // socket, bind, listen, accept
#include <netinet/in.h> // sockaddr_in, htons
#include <arpa/inet.h>  // inet_ntop

Creating the listening socket

The first step is to create an endpoint with the socket() call. AF_INET selects IPv4 and SOCK_STREAM selects TCP. The call returns -1 on failure; checking the return value of every system call is essential in socket programming.

int server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd == -1) {
    perror("socket");
    return 1;
}

// Prevents the "Address already in use" error when restarting the server
int opt = 1;
setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

The SO_REUSEADDR option is small but important: when you stop the server and start it again right away, the port may still be in TIME_WAIT and bind() fails. This option removes that obstacle.

Binding, listening and accepting

Now we bind the socket to an IP and port. We fill in the address with a sockaddr_in structure. The most critical point here is byte order: the port number is expected in big-endian on the wire, so we convert it with htons() (host-to-network short). INADDR_ANY means "listen on all network interfaces of this machine".

sockaddr_in addr{};
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY;   // all interfaces
addr.sin_port = htons(8080);         // port 8080 in network byte order

if (bind(server_fd, (sockaddr*)&addr, sizeof(addr)) == -1) {
    perror("bind");
    return 1;
}

if (listen(server_fd, SOMAXCONN) == -1) {  // backlog queue
    perror("listen");
    return 1;
}
std::cout << "Server listening on port 8080...\n";

The second argument to listen() is the size of the queue for pending connections; SOMAXCONN uses the largest reasonable value the system allows. We can now greet incoming clients one by one with accept(). This call blocks until a new connection arrives and returns a separate socket descriptor for each connection.

Reading, writing and building the loop

Once a connection is established we talk to the client with recv() and send() (equivalently, read()/write() work too). The most common mistake here is forgetting that recv() may return not as many bytes as you asked for, but as many as are available right now. The return value is the number of bytes read; 0 means the peer closed the connection and -1 means an error.

while (true) {
    sockaddr_in client{};
    socklen_t len = sizeof(client);
    int client_fd = accept(server_fd, (sockaddr*)&client, &len);
    if (client_fd == -1) { perror("accept"); continue; }

    char ip[INET_ADDRSTRLEN];
    inet_ntop(AF_INET, &client.sin_addr, ip, sizeof(ip));
    std::cout << "New connection: " << ip << "\n";

    char buffer[1024];
    ssize_t n = recv(client_fd, buffer, sizeof(buffer) - 1, 0);
    if (n > 0) {
        buffer[n] = '\0';
        std::cout << "Received: " << buffer;
        const char* reply = "Hello, got your message!\n";
        send(client_fd, reply, strlen(reply), 0);
    }
    close(client_fd);   // done with this client
}
close(server_fd);

This loop serves one client at a time. After compiling the program (g++ -std=c++17 server.cpp -o server) and running it, you can test it from another terminal by connecting with nc localhost 8080 or telnet localhost 8080 and typing a message.

What to watch for in production

The server above is perfect as a tutorial, but on its own it is not enough for the real world. A few points to keep in mind:

  • Concurrency: a single loop handles only one client at a time. For many clients you need to give each connection a thread, or — as a more scalable solution — build an epoll-based event loop.
  • Partial reads/writes: like recv(), send() may not send all the bytes at once either. You must call it again in a loop until the whole buffer is sent.
  • Signals: writing to a closed socket can kill the process with SIGPIPE; using the MSG_NOSIGNAL flag on the send() call is a good habit.
  • Message boundaries: TCP is a byte stream with no concept of "messages". In your own protocol you need to send the message length as a prefix or use a delimiter.

Frequently Asked Questions

Should I use TCP or UDP?

Use TCP when data must arrive complete and in order: login/authentication, chat, inventory operations. If losing a few packets is acceptable but low latency is critical (player positions, movement updates), UDP is a better fit. Many games use both together.

Does the same code work on Windows?

The logic is the same, but Windows needs the Winsock library: you call WSAStartup() at the start, the socket type is SOCKET instead of int, and you use closesocket() instead of close(). Writing a cross-platform layer to abstract these differences is a common approach.

Why does recv() return less data than I expected?

Because TCP is a stream, not packets. The kernel gives you whatever is in its buffer at that moment. To read a complete message you must call recv() again in a loop until you reach the expected number of bytes.

Need help with low-level network programming? If you want support with high-performance game and chat servers in C++, custom protocols and epoll-based architectures, get in touch with me — let us build your project on solid foundations together.

Bu kategorideki tüm yazılar →

Devamı için