CoreTechED

Loading

Learn C socket programming: server and client implementation, TCP/UDP sockets, and the client-server model. Master network communication.

Socket Programming in C: Complete Guide

Socket Programming in C: A Comprehensive Tutorial

Socket programming is a method of enabling communication between two nodes over a network using sockets. One socket acts as a server, listening on a specific IP address and port, while another socket acts as a client, initiating a connection to the server. Socket programming follows a client-server architecture for data exchange.

This tutorial provides an in-depth exploration of socket programming in C: sockets, client-server model, server-side and client-side implementation, TCP/UDP sockets, common pitfalls, and best practices.


Conceptual Introduction & Fundamentals

What is a Socket?

A socket is an endpoint for communication between two processes over a network. It is a combination of an IP address and a port number.

Example: 192.168.1.1:8080

  • IP address: Identifies the machine
  • Port number: Identifies the specific application/process

Socket Types

Socket TypeProtocolCharacteristics
Stream Socket (SOCK_STREAM)TCPReliable, connection-oriented
Datagram Socket (SOCK_DGRAM)UDPUnreliable, connectionless

Client-Server Model

The client-server model is the architecture used in socket programming:

  • Server: Listens for incoming connections, processes requests, sends responses.
  • Client: Initiates connection to server, sends requests, receives responses.

Why Socket Programming Matters

  • Network communication: Enable data exchange between systems.
  • Distributed applications: Web servers, chat systems, streaming.
  • Real-time collaboration: Multiplayer games, collaborative tools.
  • Microservices: Inter-service communication.
  • IoT: Device-to-server communication.

Required Headers

c

#include <sys/socket.h> // Core socket functions #include <netinet/in.h> // sockaddr_in structure #include <arpa/inet.h> // inet_pton, inet_ntop #include <unistd.h> // close, read, write #include <string.h> // memset #include <stdio.h> // printf, perror #include <stdlib.h> // exit

Syntax & Core Mechanics

Server-Side Steps

1. Socket Creation

c

int sockfd = socket(domain, type, protocol);

Parameters:

  • domain: AF_INET (IPv4), AF_INET6 (IPv6), AF_LOCAL (same host)
  • type: SOCK_STREAM (TCP), SOCK_DGRAM (UDP)
  • protocol: Usually 0 (auto-selected)

2. Setsockopt (Optional)

Allows address/port reuse.

c

setsockopt(sockfd, level, optname, optval, optlen);

3. Bind

Binds socket to address and port.

c

bind(sockfd, (struct sockaddr*)&address, sizeof(address));

4. Listen

Puts socket in passive mode, waiting for connections.

c

listen(sockfd, backlog);

5. Accept

Accepts incoming connection, creates new socket.

c

int new_socket = accept(sockfd, (struct sockaddr*)&address, &addrlen);

6. Send/Receive

c

send(sockfd, buffer, length, flags); recv(sockfd, buffer, length, flags);

7. Close

c

close(sockfd);

Client-Side Steps

1. Socket Creation (same as server)

c

int client_fd = socket(AF_INET, SOCK_STREAM, 0);

2. Connect

c

connect(sockfd, (struct sockaddr*)&address, sizeof(address));

3. Send/Receive

c

send(sockfd, buffer, length, flags); recv(sockfd, buffer, length, flags);

4. Close

c

close(sockfd);

Full Practical Code Example with Explanation

This section provides complete server and client implementations demonstrating TCP socket communication.

Server Program: server.c

c

/** * server.c * TCP server that listens on port 8080. * Accepts client connections and exchanges messages. */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #define PORT 8080 #define BUFFER_SIZE 1024 int main(void) { int server_fd, client_fd; struct sockaddr_in address; int opt = 1; socklen_t addrlen = sizeof(address); char buffer[BUFFER_SIZE] = {0}; const char *response = "Hello from server"; /* Step 1: Create socket */ server_fd = socket(AF_INET, SOCK_STREAM, 0); if (server_fd < 0) { perror("Socket creation failed"); return EXIT_FAILURE; } printf("Socket created successfully.\n"); /* Step 2: Set socket options (reuse address/port) */ if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt)) < 0) { perror("setsockopt failed"); close(server_fd); return EXIT_FAILURE; } printf("Socket options set.\n"); /* Configure server address */ address.sin_family = AF_INET; address.sin_addr.s_addr = INADDR_ANY; // Bind to all interfaces address.sin_port = htons(PORT); /* Step 3: Bind socket to address */ if (bind(server_fd, (struct sockaddr*)&address, sizeof(address)) < 0) { perror("Bind failed"); close(server_fd); return EXIT_FAILURE; } printf("Bound to port %d.\n", PORT); /* Step 4: Listen for connections */ if (listen(server_fd, 3) < 0) { perror("Listen failed"); close(server_fd); return EXIT_FAILURE; } printf("Listening for connections...\n"); /* Step 5: Accept client connection */ client_fd = accept(server_fd, (struct sockaddr*)&address, &addrlen); if (client_fd < 0) { perror("Accept failed"); close(server_fd); return EXIT_FAILURE; } printf("Client connected.\n"); /* Step 6: Receive data from client */ ssize_t bytesRead = recv(client_fd, buffer, BUFFER_SIZE - 1, 0); if (bytesRead < 0) { perror("Receive failed"); close(client_fd); close(server_fd); return EXIT_FAILURE; } buffer[bytesRead] = '\0'; printf("Received from client: %s\n", buffer); /* Step 7: Send response to client */ ssize_t bytesSent = send(client_fd, response, strlen(response), 0); if (bytesSent < 0) { perror("Send failed"); close(client_fd); close(server_fd); return EXIT_FAILURE; } printf("Response sent: '%s'\n", response); /* Step 8: Close sockets */ close(client_fd); close(server_fd); printf("Connection closed.\n"); return EXIT_SUCCESS; }

Client Program: client.c

c

/** * client.c * TCP client that connects to server on port 8080. * Sends a message and receives a response. */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #define PORT 8080 #define BUFFER_SIZE 1024 int main(void) { int client_fd; struct sockaddr_in server_addr; char buffer[BUFFER_SIZE] = {0}; const char *message = "Hello from client"; /* Step 1: Create socket */ client_fd = socket(AF_INET, SOCK_STREAM, 0); if (client_fd < 0) { perror("Socket creation failed"); return EXIT_FAILURE; } printf("Socket created successfully.\n"); /* Configure server address */ server_addr.sin_family = AF_INET; server_addr.sin_port = htons(PORT); /* Convert IPv4 address from text to binary */ if (inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr) <= 0) { perror("Invalid address"); close(client_fd); return EXIT_FAILURE; } /* Step 2: Connect to server */ if (connect(client_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) { perror("Connection failed"); close(client_fd); return EXIT_FAILURE; } printf("Connected to server.\n"); /* Step 3: Send message to server */ ssize_t bytesSent = send(client_fd, message, strlen(message), 0); if (bytesSent < 0) { perror("Send failed"); close(client_fd); return EXIT_FAILURE; } printf("Message sent: '%s'\n", message); /* Step 4: Receive response from server */ ssize_t bytesRead = recv(client_fd, buffer, BUFFER_SIZE - 1, 0); if (bytesRead < 0) { perror("Receive failed"); close(client_fd); return EXIT_FAILURE; } buffer[bytesRead] = '\0'; printf("Received from server: %s\n", buffer); /* Step 5: Close socket */ close(client_fd); printf("Connection closed.\n"); return EXIT_SUCCESS; }

Compilation

bash

gcc -Wall -Wextra -std=c11 server.c -o server gcc -Wall -Wextra -std=c11 client.c -o client

Running the Programs

Terminal 1 (Server):

bash

./server

Terminal 2 (Client):

bash

./client

Expected Output

Server:

text

Socket created successfully. Socket options set. Bound to port 8080. Listening for connections... Client connected. Received from client: Hello from client Response sent: 'Hello from server' Connection closed.

Client:

text

Socket created successfully. Connected to server. Message sent: 'Hello from client' Received from server: Hello from server Connection closed.

Common Pitfalls, Edge Cases & Memory Considerations

1. Not Checking Return Values

Socket functions can fail; always check return values.

c

socket(AF_INET, SOCK_STREAM, 0); // No check

Solution: Check if (sockfd < 0) after creation.

2. Port Already in Use

Binding to a port that’s already in use fails.

Solution: Use SO_REUSEADDR or choose a different port.

3. Blocking Calls

By default, accept(), recv(), and connect() block indefinitely.

Solution: Use non-blocking mode with fcntl or select/poll.

4. Forgetting to Close Sockets

Leaving sockets open leaks resources.

Solution: Always close() sockets after use.

5. Byte Order Issues

Network byte order is big-endian; host may be little-endian.

Solution: Use htons, htonl, ntohs, ntohl for conversions.

6. Buffer Overflow

Using buffers without checking sizes.

c

char buffer[10]; recv(sockfd, buffer, 1024, 0); // BUG: overflow

Solution: Use sizeof(buffer) - 1 for receive size.

7. Not Handling Partial Sends/Receives

send and recv may transfer fewer bytes than requested.

Solution: Loop until all data is sent/received.

8. Incorrect Address Conversion

Using inet_addr (deprecated) instead of inet_pton.

Solution: Use inet_pton and inet_ntop.

9. Connection Refused

Client cannot connect if server is not running.

Solution: Check server status, IP address, and port.

10. Memory Considerations

  • Socket buffers consume kernel memory.
  • Large data transfers require adequate buffer sizes.
  • Use malloc for dynamic buffers when needed.
  • Close sockets to release resources.

Summary & Best Practices

Socket programming enables network communication between processes. Understanding the client-server model and proper socket management is essential for building distributed applications.

Key Takeaways

  • Sockets combine IP address and port number.
  • TCP (SOCK_STREAM) is reliable; UDP (SOCK_DGRAM) is faster.
  • Server: socket → bind → listen → accept → send/recv → close.
  • Client: socket → connect → send/recv → close.
  • Use htons/htonl for network byte order.
  • Always check return values and close sockets.

Best Practices

  1. Always check return values of socket functions.
  2. Use SO_REUSEADDR to avoid port binding issues.
  3. Use htons/htonl for network byte order conversions.
  4. Handle partial sends/receives with loops.
  5. Use adequate buffer sizes to prevent overflow.
  6. Set timeouts using setsockopt for non-blocking behavior.
  7. Close all sockets when done.
  8. Use inet_pton/inet_ntop for address conversion.
  9. Document protocol (message format) for clarity.
  10. Test with different scenarios: connection refused, timeout, large data.

By mastering socket programming, you’ll build robust networked applications that communicate efficiently across systems.