Client-Server
The Client-Server pattern is a distributed application structure that partitions tasks or workloads between providers of a resource or service, called servers, and requesters of that resource, called clients. It fundamentally separates concerns: clients focus on user interface and request logic, while servers focus on data storage, processing, and security. This separation allows for greater scalability, maintainability, and resource sharing.
Usage
The Client-Server pattern is ubiquitous in modern computing. It’s used in web applications (browsers as clients, web servers as servers), email systems (email clients like Outlook or Thunderbird, email servers like Exchange or Gmail), database systems (applications as clients, database management systems as servers), and file sharing (clients requesting files from a file server). It’s also a core principle in microservices architectures, where individual services act as servers providing specific functionalities to client applications. Cloud computing heavily relies on this pattern, with clients accessing resources and services hosted on remote servers.
Examples
-
Web Browsers and Web Servers: A web browser (the client) requests a webpage from a web server (like Apache or Nginx). The server processes the request, retrieves the necessary HTML, CSS, and JavaScript files, and sends them back to the browser for rendering. This is a classic example of the Client-Server pattern.
-
Database Applications and Database Servers: Applications like a customer relationship management (CRM) system (the client) interact with a database server (like MySQL, PostgreSQL, or Oracle). The CRM application sends queries to the database server to retrieve, update, or delete data. The database server handles the data management and returns the results to the application.
-
Email Clients and Email Servers: An email client (like Outlook or Thunderbird) connects to an email server (like Exchange or Gmail’s IMAP/SMTP servers). The client sends requests to retrieve emails, send new emails, or manage folders. The server handles the email storage, routing, and delivery.
Specimens
15 implementationsThe Client-Server pattern distributes application logic between servers providing resources or services and clients requesting those resources. This example uses Dart’s Isolate to simulate a simple server handling string reversal requests from clients. The server listens for messages on a ReceivePort, reverses the received string, and sends the result back via a SendPort provided by the client. This demonstrates asynchronous communication, a key aspect of client-server architectures. Dart’s isolates enable true concurrency, preventing blocking the main thread while processing requests, fitting the pattern’s need for responsiveness.
// server.dart
import 'dart:isolate';
void server(SendPort sendPort) {
ReceivePort receivePort = ReceivePort();
sendPort.send(receivePort.sendPort); // Send back the server's receive port
receivePort.listen((message) {
String input = message as String;
String reversed = input.split('').reversed.join('');
sendPort.send(reversed);
});
}
// client.dart
import 'dart:isolate';
void main() async {
Isolate.spawn(server, null);
ReceivePort receivePort = ReceivePort();
SendPort sendPort = receivePort.sendPort;
Isolate spawnedIsolate = await Isolate.spawn(server, null);
sendPort.post('hello');
receivePort.listen((message) {
print('Received from server: $message');
spawnedIsolate.kill(); // Terminate isolate after receiving the result
receivePort.close(); // Close the receiving port
});
}
The Client-Server pattern establishes a relationship where one component (the client) requests services from another component (the server). The server provides resources or performs actions based on the client’s requests. This example uses Scala’s Future to handle asynchronous communication, reflecting a common approach in Scala for concurrent operations and network interactions. We define a simple Server and Client where the server handles a string transformation and the client requests this service. The use of Future makes the client non-blocking while waiting for the server’s response. This aligns with Scala’s functional and concurrent-friendly nature.
import scala.concurrent.{Future, ExecutionContext}
import scala.util.{Success, Failure}
// Server Component
object Server {
def processRequest(request: String)(implicit ec: ExecutionContext): Future[String] = {
Future {
// Simulate some processing time
Thread.sleep(500)
s"Server processed: ${request.toUpperCase}"
}
}
}
// Client Component
object Client {
def sendRequest(request: String)(implicit ec: ExecutionContext): Unit = {
Server.processRequest(request) onComplete {
case Success(response) => println(s"Client received: $response")
case Failure(exception) => println(s"Client error: ${exception.getMessage}")
}
println("Client sent request and is non-blocking...")
}
}
// Main Program
object Main extends App {
import scala.concurrent.ExecutionContext.Implicits.global
Client.sendRequest("hello")
Client.sendRequest("world")
Thread.sleep(1000) // Allow futures to complete before exiting
}
The Client-Server pattern decouples a service provider (server) from its consumers (clients). The server provides resources or services, while clients request and utilize them. This promotes modularity, allowing the server to be updated or scaled independently of clients. Our PHP example simulates a simple time server. The server script receives a request, calculates the current time, and sends it back as a response. The client script makes an HTTP request to the server and displays the received time. This implementation uses PHP’s built-in HTTP request functions for the client and basic script execution for the server, fitting PHP’s common web-centric use case.
<?php
// server.php
// Simulate a time service
header('Content-Type: text/plain');
echo date('Y-m-d H:i:s');
?>
<?php
// client.php
<?php
$serverUrl = 'http://localhost/server.php'; // Adjust path if needed
$ch = curl_init($serverUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
if (curl_errno($ch)) {
echo 'Error: ' . curl_error($ch);
} else {
echo "Current time from server: " . $response;
}
curl_close($ch);
?>
The Client-Server pattern organizes a distributed application into two distinct parts: a server that provides a service or resource, and clients that request and consume that service. This example uses Ruby’s socket library to create a simple TCP-based server and client. The server listens for connections, receives data, processes it (here, simply echoing it back), and sends a response. The client connects to the server, sends data, receives the response, and prints it. This is common Ruby practice for networking tasks due to the library’s stability and widespread use. Separate files for client and server promote modularity.
# server.rb
require 'socket'
server = Socket.tcp_server('localhost', 20000)
loop do
client = server.accept
puts "Accepted connection from: #{client.peeraddr[3]}"
request = client.read
puts "Request received: #{request}"
response = "Echo: #{request}"
client.write(response)
client.close
puts "Connection closed."
rescue => e
puts "Error: #{e.message}"
end
# client.rb
require 'socket'
client = Socket.getaddrinfo('localhost', '20000', Socket::AF_INET, Socket::SOCK_STREAM).first
socket = Socket.new(client[0], client[1], client[2])
puts "Connected to server."
request = "Hello, Server!"
socket.write(request)
response = socket.read
puts "Response received: #{response}"
socket.close
puts "Connection closed."
The Client-Server pattern decouples a service provider (Server) from its consumers (Clients). The Server provides resources or functionalities, and Clients request and utilize them. This enables scalability, reusability, and independent development of client and server. Here, we simulate a simple temperature server using a TemperatureServer class that stores and provides the current temperature. A TemperatureClient requests this temperature. We use a simple protocol and a shared representation (Double) to define the communication. This is a very common and straightforward pattern in Swift networking applications, utilizing classes for state management and clear separation of concerns.
// TemperatureServer.swift
protocol TemperatureProvider {
func getCurrentTemperature() -> Double
}
class TemperatureServer: TemperatureProvider {
private var temperature: Double = 20.0
func getCurrentTemperature() -> Double {
return temperature
}
func setTemperature(newTemperature: Double) {
temperature = newTemperature
}
}
// TemperatureClient.swift
class TemperatureClient {
private let server: TemperatureProvider
init(server: TemperatureProvider) {
self.server = server
}
func displayTemperature() {
let temperature = server.getCurrentTemperature()
print("Current temperature: \(temperature)°C")
}
}
// Example Usage
let temperatureServer = TemperatureServer()
let temperatureClient = TemperatureClient(server: temperatureServer)
temperatureClient.displayTemperature() // Output: Current temperature: 20.0°C
temperatureServer.setTemperature(newTemperature: 25.5)
temperatureClient.displayTemperature() // Output: Current temperature: 25.5°C
The Client-Server pattern distributes applications into two parts: clients, which request resources or services, and servers, which provide those resources or services. This example uses Kotlin Coroutines and Channels for asynchronous communication. The server listens on a channel for client requests (strings) and responds with processed results (also strings). The client sends requests to the server’s channel and receives responses. This is idiomatic Kotlin due to its concise syntax, use of coroutines for lightweight concurrency, and reliance on channels for safe communication between concurrent routines. The use of launch creates concurrent scopes for both client and server, preventing blocking.
import kotlinx.coroutines.*
import kotlin.system.measureTimeMillis
// Server
fun server(channel: ReceiveChannel<String>, responseChannel: SendChannel<String>) = runBlocking {
for (message in channel) {
println("Server received: $message")
val result = processMessage(message)
responseChannel.send(result)
}
}
fun processMessage(message: String): String {
// Simulate some work
Thread.sleep(500)
return "Server processed: ${message.uppercase()}"
}
// Client
fun client(channel: SendChannel<String>, responseChannel: ReceiveChannel<String>) = runBlocking {
val messages = listOf("hello", "world", "kotlin")
val times = messages.map { message ->
val time = measureTimeMillis {
channel.send(message)
val response = responseChannel.receive()
println("Client received: $response")
}
time to message
}
println("Processing took: ${times.sumOf { it.first }}ms")
}
fun main() {
val requestChannel = Channel<String>()
val responseChannel = Channel<String>()
val serverJob = CoroutineScope(Dispatchers.Default).launch {
server(requestChannel, responseChannel)
}
val clientJob = CoroutineScope(Dispatchers.Default).launch {
client(responseChannel, requestChannel)
}
runBlocking{ serverJob.join() }
runBlocking{ clientJob.join() }
requestChannel.close()
responseChannel.close()
}
The Client-Server pattern involves dividing a system into two distinct parts: the client, which requests services, and the server, which provides those services. This example uses Rust’s standard library for TCP networking to implement a simple echo server and client. The server listens for connections on a specified port and echoes back any data received. The client connects to the server, sends a message, receives the echoed response, and prints it. This implementation prominently leverages Rust’s ownership and error handling systems, using Result for robust communication and ensuring memory safety throughout the network operations. Utilizing std::io for reading/writing is standard practice in Rust networking.
use std::io::{self, Read, Write};
use std::net::{TcpListener, TcpStream};
use std::thread;
// Server
fn handle_client(mut stream: TcpStream) -> io::Result<()> {
let mut buffer = [0; 1024];
loop {
let bytes_read = stream.read(&mut buffer)?;
if bytes_read == 0 {
return Ok(());
}
stream.write_all(&buffer[..bytes_read])?;
}
}
fn main() -> io::Result<()> {
let listener = TcpListener::bind("127.0.0.1:8080")?;
println!("Server listening on 127.0.0.1:8080");
for stream in listener.incoming() {
match stream {
Ok(stream) => {
thread::spawn(move || {
handle_client(stream).unwrap_or_else(|error| eprintln!("Error handling client: {}", error));
});
}
Err(e) => {
eprintln!("Connection failed: {}", e);
}
}
}
Ok(())
}
// Client (separate file for clarity - client.rs can be compiled and run independently)
// file: client.rs
use std::io::{self, Read, Write};
use std::net::TcpStream;
fn main() -> io::Result<()> {
let mut stream = TcpStream::connect("127.0.0.1:8080")?;
let message = "Hello, Server!";
stream.write(message.as_bytes())?;
println!("Sent: {}", message);
let mut buffer = [0; 1024];
let bytes_read = stream.read(&mut buffer)?;
let response = String::from_utf8_lossy(&buffer[..bytes_read]);
println!("Received: {}", response);
Ok(())
}
The Client-Server pattern is a distributed application structure that divides tasks between providers of a resource (servers) and requesters of that resource (clients). Here, a simple TCP server listens for connections and echoes back received messages. A client connects to the server, sends a message, receives the echoed response, and then disconnects. The Go implementation utilizes goroutines and channels for concurrent handling of multiple clients, a cornerstone of Go’s concurrency model. The net package provides the necessary tools for network communication, aligning with Go’s standard library approach for common tasks. Error handling is explicit, following Go’s convention of checking return values.
// server.go
package main
import (
"bufio"
"fmt"
"log"
"net"
)
func handleConnection(conn net.Conn) {
defer conn.Close()
reader := bufio.NewReader(conn)
for {
message, _ := reader.ReadString('\n')
fmt.Print("Message Received:", string(message))
conn.Write([]byte(message))
}
}
func main() {
ln, err := net.Listen("tcp", ":8080")
if err != nil {
log.Fatal(err)
}
defer ln.Close()
fmt.Println("Server listening on :8080")
for {
conn, err := ln.Accept()
if err != nil {
log.Println(err)
continue
}
go handleConnection(conn) // Handle each connection in a new goroutine
}
}
// client.go
package main
import (
"bufio"
"fmt"
"log"
"net"
)
func main() {
conn, err := net.Dial("tcp", "localhost:8080")
if err != nil {
log.Fatal("Error connecting:", err)
}
defer conn.Close()
reader := bufio.NewReader(conn)
fmt.Print("Enter message: ")
message, _ := reader.ReadString('\n')
conn.Write([]byte(message))
response, _ := reader.ReadString('\n')
fmt.Print("Server responded: ", string(response))
}
The Client-Server pattern involves separating an application into two parts: a server that provides a resource or service, and clients that request and utilize that service. This example uses basic sockets in C to demonstrate this pattern. The server listens for incoming connections, receives a request (a simple string), and sends back a response. The client connects to the server, sends the request, and prints the received response. This implementation is typical C style using low-level socket functions and explicit error handling, prioritizing clarity and portability over higher-level abstractions.
// server.c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#define PORT 8080
#define BUFFER_SIZE 1024
int main() {
int server_fd, new_socket;
struct sockaddr_in address;
int addrlen = sizeof(address);
char buffer[BUFFER_SIZE] = {0};
// Create socket file descriptor
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
perror("socket failed");
exit(EXIT_FAILURE);
}
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(PORT);
// Bind the socket
if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
perror("bind failed");
exit(EXIT_FAILURE);
}
// Listen for connections
if (listen(server_fd, 3) < 0) {
perror("listen failed");
exit(EXIT_FAILURE);
}
printf("Server listening on port %d...\n", PORT);
while(1) {
// Accept a connection
if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
perror("accept failed");
continue;
}
// Read data from the client
recv(new_socket, buffer, BUFFER_SIZE, 0);
printf("Received from client: %s\n", buffer);
// Send a response back to the client
const char *response = "Server received your message!";
send(new_socket, response, strlen(response), 0);
// Close the connection
close(new_socket);
}
return 0;
}
// client.c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#define PORT 8080
#define BUFFER_SIZE 1024
int main() {
int socket_fd;
struct sockaddr_in address;
char buffer[BUFFER_SIZE] = {0};
// Create socket file descriptor
if ((socket_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
perror("socket failed");
exit(EXIT_FAILURE);
}
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(PORT);
// Connect to the server
if (connect(socket_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
perror("connect failed");
exit(EXIT_FAILURE);
}
const char *request = "Hello, Server!";
// Send data to the server
send(socket_fd, request, strlen(request), 0);
// Receive data from the server
recv(socket_fd, buffer, BUFFER_SIZE, 0);
printf("Received from server: %s\n", buffer);
// Close the connection
close(socket_fd);
return 0;
}
The Client-Server pattern decouples a service provider (Server) from its consumers (Clients). The Server exposes functionalities, and Clients request those functionalities without needing to know the Server’s internal workings. This example uses basic sockets for communication. The Server listens for connections, receives messages, processes them (echoing them back in this case), and sends responses. The Client connects to the Server, sends a message, and receives the Server’s response. This follows C++’s typical network programming approach using system calls for socket management, which is a common and efficient pattern.
#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
// Server code
void run_server() {
int server_fd, new_socket;
struct sockaddr_in address;
int opt = 1;
int addrlen = sizeof(address);
char buffer[1024] = {0};
server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd == -1) {
perror("socket failed");
exit(EXIT_FAILURE);
}
// Attach socket to port 8080
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(8080);
if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
perror("bind failed");
exit(EXIT_FAILURE);
}
if (listen(server_fd, 3) < 0) {
perror("listen");
exit(EXIT_FAILURE);
}
new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen);
if (new_socket == -1) {
perror("accept");
exit(EXIT_FAILURE);
}
while (true) {
memset(buffer, 0, sizeof(buffer));
ssize_t bytes_received = recv(new_socket, buffer, sizeof(buffer), 0);
if (bytes_received <= 0) break;
std::cout << "Received: " << buffer << std::endl;
send(new_socket, buffer, bytes_received, 0); // Echo back
}
close(new_socket);
close(server_fd);
}
// Client code
void run_client() {
int socket_fd;
struct sockaddr_in server_address;
char buffer[1024] = {0};
socket_fd = socket(AF_INET, SOCK_STREAM, 0);
if (socket_fd == -1) {
perror("socket failed");
exit(EXIT_FAILURE);
}
server_address.sin_family = AF_INET;
server_address.sin_addr.s_addr = INADDR_LOOPBACK; //connect to localhost
server_address.sin_port = htons(8080);
if (connect(socket_fd, (struct sockaddr *)&server_address, sizeof(server_address)) < 0) {
perror("connect failed");
exit(EXIT_FAILURE);
}
std::string message = "Hello from Client!";
send(socket_fd, message.c_str(), message.length(), 0);
memset(buffer, 0, sizeof(buffer));
ssize_t bytes_received = recv(socket_fd, buffer, sizeof(buffer), 0);
if (bytes_received > 0) {
std::cout << "Received from Server: " << buffer << std::endl;
}
close(socket_fd);
}
int main(int argc, char const *argv[]) {
if (argc == 2 && std::string(argv[1]) == "server") {
run_server();
} else {
run_client();
}
return 0;
}
The Client-Server pattern decouples an application into two parts: a server that provides a resource or service, and a client that requests and consumes that resource. This improves modularity, scalability, and allows for different technologies to be used for each component. This C# example utilizes TcpListener for the server and TcpClient for the client to communicate over a network socket. The server listens for incoming connections, receives messages, processes them (in this simplified case, just echoing back), and sends a response. The client connects to the server, sends a message, receives the response, and then disconnects. This approach is common in C# network programming due to its straightforward implementation and strong support for threading.
// Server.cs
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
public class Server
{
public static void Main(string[] args)
{
TcpListener server = null;
try
{
Int32 port = 13000;
server = new TcpListener(IPAddress.Any, port);
server.Start();
Console.WriteLine($"Server listening on port {port}...");
while (true)
{
TcpClient client = server.AcceptTcpClient();
Console.WriteLine("Client connected.");
ThreadPool.QueueUserWorkItem(HandleClient, client);
}
}
catch (Exception e)
{
Console.WriteLine($"Error: {e.ToString()}");
}
finally
{
server?.Stop();
}
}
private static void HandleClient(object clientObj)
{
TcpClient client = (TcpClient)clientObj;
NetworkStream stream = client.GetStream();
byte[] bytes = new byte[1024];
try
{
int i = stream.Read(bytes, 0, bytes.Length);
string data = Encoding.ASCII.GetString(bytes, 0, i);
Console.WriteLine($"Received: {data}");
string response = "Server received: " + data;
byte[] msg = Encoding.ASCII.GetBytes(response);
stream.Write(msg, 0, msg.Length);
Console.WriteLine("Sent: " + response);
}
catch (Exception e)
{
Console.WriteLine($"Error handling client: {e.ToString()}");
}
finally
{
client.Close();
}
}
}
// Client.cs
using System;
using System.Net.Sockets;
using System.Text;
public class Client
{
public static void Main(string[] args)
{
try
{
TcpClient client = new TcpClient("127.0.0.1", 13000);
NetworkStream stream = client.GetStream();
string message = "Hello from the client!";
byte[] data = Encoding.ASCII.GetBytes(message);
stream.Write(data, 0, data.Length);
Console.WriteLine($"Sent: {message}");
byte[] bytes = new byte[1024];
int i = stream.Read(bytes, 0, bytes.Length);
string response = Encoding.ASCII.GetString(bytes, 0, i);
Console.WriteLine($"Received: {response}");
stream.Close();
client.Close();
}
catch (Exception e)
{
Console.WriteLine($"Error: {e.ToString()}");
}
}
}
The Client-Server pattern decouples an application into two distinct parts: a client requesting resources or services, and a server providing them. This promotes scalability, maintainability, and allows for independent evolution of each component. The code exemplifies this by creating a simple server using Node.js and Express that exposes an endpoint /greet. A client-side function then calls this endpoint using fetch to retrieve and display a greeting. TypeScript’s static typing enhances code reliability by ensuring data consistency between client requests and server responses. The asynchronous nature of fetch and Node.js’s event loop aligns with TypeScript’s async/await for clean, non-blocking I/O operations.
// server.ts
import express from 'express';
const app = express();
const port = 3000;
app.get('/greet', (req, res) => {
const name = req.query.name || 'World';
res.json({ message: `Hello, ${name}!` });
});
app.listen(port, () => {
console.log(`Server listening at http://localhost:${port}`);
});
// client.ts
async function greetServer(name: string) {
try {
const response = await fetch(`http://localhost:3000/greet?name=${name}`);
const data = await response.json();
console.log(data.message);
} catch (error) {
console.error('Error fetching greeting:', error);
}
}
greetServer('TypeScript');
The Client-Server pattern separates application functionality into distinct roles: a client requesting services and a server providing them. This improves modularity, scalability, and maintainability. Here, a simple HTTP server using Node.js’s built-in http module acts as the server. It listens for incoming requests and responds with a “Hello from Server!” message. A basic client script (included as a comment) makes a GET request to the server. This demonstrates asynchronous communication – a key aspect of client-server architecture in JavaScript, leveraging its event-loop model. Node.js’s module system and http’s callback-based API are utilized for a standard and efficient JavaScript implementation.
// Server (server.js)
const http = require('http');
const port = 3000;
const server = http.createServer((req, res) => {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.end('Hello from Server!');
});
server.listen(port, () => {
console.log(`Server running at http://localhost:${port}/`);
});
// Client (example usage - not runnable as-is, but illustrative)
/*
fetch('http://localhost:3000/')
.then(response => response.text())
.then(data => console.log(data)) // Output: Hello from Server!
.catch(error => console.error('Error:', error));
*/
The Client-Server pattern is a distributed application structure that divides tasks among providers of a resource, called servers, and requesters of that resource, called clients. Here, a simple example uses Python’s socket module to establish a TCP connection. The server listens for connections and responds with an uppercase version of the received data. The client connects to the server, sends a message, and prints the server’s response. This implementation follows Python’s clear syntax and utilizes standard library modules for networking, making it idiomatic and readable. The use of separate scripts clearly delineates the roles of the client and server.
# server.py
import socket
HOST = '127.0.0.1' # Standard loopback interface address (localhost)
PORT = 65432 # Port to listen on (non-privileged ports are > 1023)
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind((HOST, PORT))
s.listen()
print(f"Server listening on {HOST}:{PORT}")
conn, addr = s.accept()
with conn:
print(f"Connected by {addr}")
while True:
data = conn.recv(1024)
if not data:
break
decoded_data = data.decode()
print(f"Received: {decoded_data}")
conn.sendall(decoded_data.upper().encode())
print(f"Sent: {decoded_data.upper()}")
# client.py
import socket
HOST = '127.0.0.1' # The server's hostname or IP address
PORT = 65432 # The port used by the server
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.connect((HOST, PORT))
message = "Hello, Server!"
print(f"Sending: {message}")
s.sendall(message.encode())
data = s.recv(1024)
print(f"Received: {data.decode()}")
The Client-Server pattern decouples an application into two distinct parts: a client requesting services and a server providing those services. This allows for scalability, maintainability, and the potential for multiple clients to access the same server. This Java example uses sockets to establish communication. The server listens for client connections and processes requests (in this case, simply echoing back the client’s message). The client connects to the server, sends a message, and receives the response. The implementation utilizes Java’s networking classes following standard thread handling for concurrency. This aligns with Java’s robust network support and object-oriented approach for structuring networked applications.
import java.io.*;
import java.net.*;
// Server.java
class Server {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(9999);
System.out.println("Server listening on port 9999...");
while (true) {
Socket clientSocket = serverSocket.accept();
new Thread(new ClientHandler(clientSocket)).start();
}
}
}
class ClientHandler implements Runnable {
private final Socket clientSocket;
public ClientHandler(Socket socket) {
this.clientSocket = socket;
}
public void run() {
try {
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
String inputLine;
while ((inputLine = in.readLine()) != null) {
System.out.println("Received from client: " + inputLine);
out.println("Server received: " + inputLine);
}
} catch (IOException e) {
System.err.println("Error handling client: " + e.getMessage());
} finally {
try {
clientSocket.close();
} catch (IOException e) {
System.err.println("Error closing socket: " + e.getMessage());
}
}
}
}
// Client.java
class Client {
public static void main(String[] args) throws IOException {
Socket socket = new Socket("localhost", 9999);
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
String message = "Hello from the client!";
out.println(message);
System.out.println("Sent to server: " + message);
String response = in.readLine();
System.out.println("Received from server: " + response);
socket.close();
}
}