Skip to content

Client-Server Model

What is Client-Server?

The client-server model divides computing between service requesters (clients) and service providers (servers):

Client-Server Architecture

┌──────────┐                         ┌──────────┐
│  Client  │ ──── Request ─────────▶ │  Server  │
│          │                         │          │
│ (browser)│ ◀─── Response ───────── │ (web app)│
└──────────┘                         └──────────┘

Client: Initiates requests, displays results
Server: Waits for requests, processes, responds

Characteristics

Client

  • Initiates communication
  • Sends requests
  • Waits for responses
  • Usually many clients per server
  • Examples: web browser, mobile app, API client

Server

  • Listens for connections
  • Processes requests
  • Returns responses
  • Serves many clients simultaneously
  • Examples: web server, database, API endpoint

Request-Response Cycle

1. Client establishes connection
   ┌────────┐                    ┌────────┐
   │ Client │ ═══ Connect ═════▶ │ Server │
   └────────┘                    └────────┘

2. Client sends request
   ┌────────┐                    ┌────────┐
   │ Client │ ──── Request ────▶ │ Server │
   │        │    (GET /page)     │        │
   └────────┘                    └────────┘

3. Server processes request
   ┌────────┐                    ┌────────┐
   │ Client │                    │ Server │
   │(waiting)                    │[Process]│
   └────────┘                    └────────┘

4. Server sends response
   ┌────────┐                    ┌────────┐
   │ Client │ ◀─── Response ──── │ Server │
   │        │   (HTML content)   │        │
   └────────┘                    └────────┘

5. Connection closes (or stays open for more)

Python Client Example

import socket

def simple_client(host, port, message):
    """Send a message to server and get response."""
    # Create socket
    client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    try:
        # Connect to server
        client_socket.connect((host, port))
        print(f"Connected to {host}:{port}")

        # Send data
        client_socket.send(message.encode('utf-8'))
        print(f"Sent: {message}")

        # Receive response
        response = client_socket.recv(4096).decode('utf-8')
        print(f"Received: {response}")

        return response
    finally:
        client_socket.close()

# Usage
# simple_client('localhost', 8080, 'Hello, Server!')

Python Server Example

import socket

def simple_server(host, port):
    """Simple echo server."""
    # Create socket
    server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

    # Bind to address
    server_socket.bind((host, port))

    # Listen for connections
    server_socket.listen(5)
    print(f"Server listening on {host}:{port}")

    while True:
        # Accept connection
        client_socket, client_address = server_socket.accept()
        print(f"Connection from {client_address}")

        try:
            # Receive data
            data = client_socket.recv(4096).decode('utf-8')
            print(f"Received: {data}")

            # Process and respond (echo with modification)
            response = f"Server received: {data}"
            client_socket.send(response.encode('utf-8'))
        finally:
            client_socket.close()

# Usage
# simple_server('localhost', 8080)

Handling Multiple Clients

Sequential (Blocking)

# One client at a time - doesn't scale!
while True:
    client = server.accept()
    handle_client(client)  # Blocks until complete

Threading

import socket
import threading

def handle_client(client_socket, address):
    """Handle a single client in its own thread."""
    try:
        data = client_socket.recv(4096)
        response = process(data)
        client_socket.send(response)
    finally:
        client_socket.close()

def threaded_server(host, port):
    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server.bind((host, port))
    server.listen(100)

    while True:
        client, address = server.accept()
        # Handle each client in separate thread
        thread = threading.Thread(target=handle_client, 
                                  args=(client, address))
        thread.start()

Async I/O

import asyncio

async def handle_client(reader, writer):
    """Handle client with async I/O."""
    data = await reader.read(4096)
    message = data.decode()

    response = f"Received: {message}"
    writer.write(response.encode())
    await writer.drain()

    writer.close()
    await writer.wait_closed()

async def async_server(host, port):
    server = await asyncio.start_server(
        handle_client, host, port
    )

    async with server:
        await server.serve_forever()

# asyncio.run(async_server('localhost', 8080))

Common Client-Server Patterns

Web Server (HTTP)

Browser                              Web Server
   │                                      │
   │──── GET /index.html ───────────────▶│
   │                                      │
   │◀──── 200 OK + HTML ──────────────────│
   │                                      │

Database

Application                          Database
   │                                      │
   │──── SELECT * FROM users ───────────▶│
   │                                      │
   │◀──── [user1, user2, ...] ────────────│
   │                                      │

API Server

Client App                           API Server
   │                                      │
   │──── POST /api/process ─────────────▶│
   │     {"data": [...]}                  │
   │                                      │
   │◀──── {"result": "done"} ─────────────│
   │                                      │

Stateless vs Stateful

Stateless

Server doesn't remember previous requests:

# Stateless: each request is independent
@app.route('/add')
def add():
    a = request.args.get('a')
    b = request.args.get('b')
    return str(int(a) + int(b))

# Client must send all info every time
# GET /add?a=5&b=3 → 8

Stateful

Server maintains state between requests:

# Stateful: server remembers session
sessions = {}

@app.route('/login')
def login():
    session_id = create_session(request.user)
    sessions[session_id] = {'user': request.user}
    return session_id

@app.route('/profile')
def profile():
    session_id = request.cookies.get('session')
    user_data = sessions.get(session_id)  # Retrieved from memory
    return user_data

Load Balancing

Distributing requests across multiple servers:

                    ┌────────────────┐
                    │ Load Balancer  │
                    └───────┬────────┘
                            │
          ┌─────────────────┼─────────────────┐
          ▼                 ▼                 ▼
    ┌──────────┐      ┌──────────┐      ┌──────────┐
    │ Server 1 │      │ Server 2 │      │ Server 3 │
    └──────────┘      └──────────┘      └──────────┘

Strategies:
  - Round Robin: Rotate through servers
  - Least Connections: Send to least busy
  - IP Hash: Same client → same server

Using HTTP Libraries

Client with Requests

import requests

# Simple GET
response = requests.get('https://api.example.com/data')
data = response.json()

# POST with data
response = requests.post(
    'https://api.example.com/process',
    json={'input': [1, 2, 3]}
)
result = response.json()

Server with Flask

from flask import Flask, request, jsonify

app = Flask(__name__)

@app.route('/process', methods=['POST'])
def process():
    data = request.json
    result = sum(data['input'])
    return jsonify({'result': result})

# app.run(host='0.0.0.0', port=5000)

Summary

Concept Description
Client Initiates requests, consumes services
Server Listens, processes, responds
Request Client message to server
Response Server reply to client
Stateless No memory between requests
Stateful Server maintains session state
Load Balancing Distributing load across servers

Key points:

  • Client-server is the foundation of web and distributed systems
  • Servers must handle concurrent clients (threading, async)
  • HTTP is the dominant client-server protocol for web
  • Stateless designs scale better
  • Python's socket for low-level, requests/flask for HTTP