核心概念
在开始之前,我们需要理解几个核心概念:

- Socket (套接字):它就像一个电话插座,是网络通信的端点,程序通过 Socket 发送和接收数据。
- IP 地址:网络上设备的唯一地址,服务端需要绑定到一个 IP 地址上,以便客户端知道连接到哪里。
- 端口号:IP 地址标识了计算机,端口号则标识了计算机上的具体应用程序(服务),Web 服务通常使用 80 端口。
- TCP (传输控制协议):一种面向连接的、可靠的协议,在通信前,客户端和服务器必须先建立一个连接(三次握手),之后数据可以按顺序、无差错地传输。
- 流程:
- 服务器:创建 Socket -> 绑定 IP 和端口 -> 监听连接 -> 接受客户端连接 -> 与客户端收发数据 -> 关闭连接。
- 客户端:创建 Socket -> 连接服务器 -> 与服务器收发数据 -> 关闭连接。
准备工作:编译和链接
在 Linux/macOS 系统上,Socket 相关的函数位于 libsocket 库中,通常不需要额外链接,但在 Windows 上,需要链接 Ws2_32.lib。
- Linux/macOS 编译命令:
gcc server.c -o server gcc client.c -o client
- Windows 编译命令 (使用 MinGW):
gcc server.c -o server.exe -lws2_32 gcc client.c -o client.exe -lws2_32
第一部分:服务器端代码
服务器的主要任务是“等待”并“响应”客户端的请求。
代码逻辑:
- 创建套接字 (
socket) - 绑定地址和端口 (
bind):告诉操作系统这个 Socket 使用的 IP 和端口。 - 监听连接 (
listen):将 Socket 设置为监听模式,等待客户端连接。 - 接受连接 (
accept):从等待队列中取出一个客户端连接请求,并创建一个新的 Socket 专门与这个客户端通信。 - 收发数据 (
send/recv):通过新创建的 Socket 与客户端进行数据交互。 - 关闭套接字 (
close):关闭通信套接字和监听套接字。
server.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h> // 用于 read, write, close
#include <sys/socket.h> // 用于 socket, bind, listen, accept
#include <netinet/in.h> // 用于 struct sockaddr_in
#include <arpa/inet.h> // 用于 inet_addr
#define PORT 8080
#define BUFFER_SIZE 1024
int main() {
int server_fd, new_socket;
struct sockaddr_in address;
int opt = 1;
int addrlen = sizeof(address);
char buffer[BUFFER_SIZE] = {0};
// 1. 创建套接字文件描述符
// AF_INET: IPv4
// SOCK_STREAM: TCP
// 0: 自动选择协议
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
perror("socket failed");
exit(EXIT_FAILURE);
}
// 设置套接字选项,允许地址重用
if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) {
perror("setsockopt");
exit(EXIT_FAILURE);
}
// 2. 绑定地址和端口
address.sin_family = AF_INET; // IPv4
address.sin_addr.s_addr = INADDR_ANY; // 监听所有可用的网络接口
address.sin_port = htons(PORT); // 将端口号从主机字节序转换为网络字节序
// 绑定套接字到地址和端口
if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
perror("bind failed");
exit(EXIT_FAILURE);
}
printf("Server bound on port %d\n", PORT);
// 3. 监听连接
// SOMAXCONN 是系统允许的最大连接请求数
if (listen(server_fd, SOMAXCONN) < 0) {
perror("listen");
exit(EXIT_FAILURE);
}
printf("Server is listening for connections...\n");
// 4. 接受连接
// accept 会阻塞,直到有客户端连接
if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
perror("accept");
exit(EXIT_FAILURE);
}
printf("Connection accepted from %s:%d\n", inet_ntoa(address.sin_addr), ntohs(address.sin_port));
// 5. 与客户端收发数据
int valread;
// 从客户端读取数据
valread = read(new_socket, buffer, BUFFER_SIZE);
printf("Client says: %s\n", buffer);
// 向客户端发送数据
char *response = "Hello from server!";
send(new_socket, response, strlen(response), 0);
printf("Hello message sent\n");
// 6. 关闭套接字
close(new_socket);
close(server_fd);
return 0;
}
第二部分:客户端代码
客户端的主要任务是“发起”连接并与服务器“通信”。
代码逻辑:
- 创建套接字 (
socket) - 连接服务器 (
connect):指定服务器的 IP 地址和端口号,发起连接请求。 - 收发数据 (
send/recv):通过已连接的 Socket 与服务器进行数据交互。 - 关闭套接字 (
close)
client.c
#include <stdio.h>
#include <stdlib.h>
#include <string.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() {
int sock = 0;
struct sockaddr_in serv_addr;
char *hello = "Hello from client";
char buffer[BUFFER_SIZE] = {0};
// 1. 创建套接字
if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
printf("\n Socket creation error \n");
return -1;
}
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(PORT);
// 将 IPv4 地址从文本转换为二进制形式
if (inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr) <= 0) {
printf("\nInvalid address/ Address not supported \n");
return -1;
}
// 2. 连接服务器
// connect 会阻塞,直到连接成功或失败
if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
printf("\nConnection Failed \n");
return -1;
}
printf("Connected to server on port %d\n", PORT);
// 3. 向服务器发送数据
send(sock, hello, strlen(hello), 0);
printf("Hello message sent\n");
// 4. 从服务器读取数据
int valread = read(sock, buffer, BUFFER_SIZE);
printf("Server says: %s\n", buffer);
// 5. 关闭套接字
close(sock);
return 0;
}
第三部分:如何运行和测试
-
编译代码:分别编译服务器和客户端。
(图片来源网络,侵删)gcc server.c -o server gcc client.c -o client
-
启动服务器:在一个终端窗口中运行服务器,它会启动并等待连接。
./server
你会看到输出:
Server bound on port 8080 Server is listening for connections... -
启动客户端:在另一个终端窗口中运行客户端,客户端会连接到服务器。
./client
你会看到客户端的输出:
Connected to server on port 8080 Hello message sent Server says: Hello from server! -
观察服务器终端:在服务器终端,你会看到它接受了连接并收到了消息。
Server bound on port 8080 Server is listening for connections... Connection accepted from 127.0.0.1:54321 // 端口号可能不同 Client says: Hello from client Hello message sent
第四部分:代码解释与重要函数
头文件
<sys/socket.h>:核心 Socket 函数。<netinet/in.h>:定义了sockaddr_in结构体,用于处理 IPv4 地址和端口。<arpa/inet.h>:提供了 IP 地址转换函数,如inet_addr(已弃用) 和inet_pton(推荐)。
关键函数和结构体
-
socket(int domain, int type, int protocol)domain:AF_INET(IPv4),AF_INET6(IPv6),AF_UNIX(本地域)。type:SOCK_STREAM(TCP),SOCK_DGRAM(UDP)。protocol: 通常设为 0,让系统自动选择。
-
*`bind(int sockfd, const struct sockaddr addr, socklen_t addrlen)`**
sockfd:socket()返回的文件描述符。addr: 指向sockaddr结构体的指针,对于 IPv4,我们通常使用sockaddr_in并将其地址强制转换为sockaddr*。sockaddr_in结构体:sin_family: 地址族,设为AF_INET。sin_port: 16位端口号。必须用htons()转换为网络字节序(大端序)。sin_addr: 32位 IP 地址。INADDR_ANY表示监听所有网络接口。inet_pton()可将点分十进制字符串(如 "127.0.0.1")转换为网络字节序。
-
listen(int sockfd, int backlog)sockfd: 已绑定的 Socket。backlog: 请求队列的最大长度。
-
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen)sockfd: 处于监听状态的 Socket。addr和addrlen: 用于保存客户端的地址信息,如果不关心,可以设为NULL。- 返回值:一个新的 Socket 文件描述符,专门用于与这个已连接的客户端通信,原来的监听 Socket 继续监听新的连接请求。
-
*`connect(int sockfd, const struct sockaddr addr, socklen_t addrlen)`**
sockfd: 客户端创建的 Socket。addr: 指向服务器地址结构体的指针。
-
ssize_t read(int fd, void *buf, size_t count)ssize_t recv(int sockfd, void *buf, size_t len, int flags)- 从文件描述符
fd或已连接的 Socketsockfd读取最多count/len字节的数据到buf中。 read是通用 I/O 函数,recv是专用于 Socket 的函数,可以设置flags(如MSG_WAITALL)。- 如果对端关闭了连接,
read/recv会返回 0。
- 从文件描述符
-
ssize_t write(int fd, const void *buf, size_t count)ssize_t send(int sockfd, const void *buf, size_t len, int flags)- 将
buf中的最多count/len字节数据写入文件描述符fd或已连接的 Socketsockfd。 write是通用 I/O 函数,send是专用于 Socket 的函数,可以设置flags(如MSG_DONTROUTE)。
- 将
-
close(int fd)关闭文件描述符或 Socket,释放资源。
总结与进阶
- 阻塞 vs. 非阻塞:上述代码中的
accept,connect,read,recv都是阻塞的,意味着程序会暂停执行,直到操作完成,在实际应用中,你可能需要使用select,poll, 或epoll(Linux) 等机制来实现 I/O 多路复用,以便服务器能同时处理多个客户端。 - 错误处理:代码中的
perror是一个简单的错误处理方式,在生产环境中,你可能需要更健壮的错误恢复机制。 - 并发服务器:要实现一个可以同时与多个客户端通信的服务器,最简单的方式是在
accept之后创建一个新线程或新进程来处理与该客户端的通信,而主线程则继续调用accept等待下一个连接。
