核心概念简介
在开始之前,我们先简单理解几个核心概念:

(图片来源网络,侵删)
- Socket (套接字):可以看作是两个程序之间进行通信的“端点”,它就像一个电话插座,你把电话(程序)插上去,就可以和其他插着电话的插座进行通信。
- IP 地址:网络中设备的唯一地址,就像你家的门牌号。
0.0.1是本地回环地址,代表本机。 - 端口号:一台机器上可以运行多个网络服务,端口号用来区分这些服务,它就像公寓里的房间号。
80通常用于 HTTP 服务。 - TCP (传输控制协议):一种面向连接的、可靠的通信协议,通信前必须先建立连接(三次握手),就像打电话前要先拨号并对方接听一样。
- 通信流程:
- 服务器:创建一个监听 Socket -> 绑定 IP 和端口 -> 开始监听 -> 等待并接受客户端连接 -> 与客户端收发数据 -> 关闭连接。
- 客户端:创建一个 Socket -> 连接服务器的 IP 和端口 -> 与服务器收发数据 -> 关闭连接。
准备工作:编译环境
在 Linux 或 macOS 上,系统通常已经自带了必要的头文件和库,在 Windows 上,你可能需要一些额外的设置。
- Linux/macOS: 直接使用
gcc编译即可。 - Windows:
- 你需要一个 C 语言编译器,MinGW(包含在 MSYS2 或 Git for Windows 中)。
- 在编译时,需要链接
ws2_32.lib库,在命令行中,这通常通过-lws2_32参数实现。
实例代码
我们将创建两个文件:server.c 和 client.c。
服务器端代码 (server.c)
服务器会启动并等待客户端连接,当客户端连接后,它会接收客户端发来的消息,然后回复一个固定的消息。
// 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 和 htons
#include <arpa/inet.h> // 用于 inet_addr
#define PORT 8080
#define BUFFER_SIZE 1024
int main() {
int server_fd, client_socket;
struct sockaddr_in address;
int addrlen = sizeof(address);
char buffer[BUFFER_SIZE] = {0};
char *hello = "Hello from server";
// 1. 创建 socket (文件描述符)
// AF_INET: IPv4
// SOCK_STREAM: TCP
// 0: 默认协议 (IPPROTO_TCP)
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
perror("socket failed");
exit(EXIT_FAILURE);
}
// 设置 socket 选项,允许地址重用
// 这在快速重启服务器时很有用,可以避免 "Address already in use" 错误
int opt = 1;
if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) {
perror("setsockopt");
exit(EXIT_FAILURE);
}
address.sin_family = AF_INET; // IPv4
address.sin_addr.s_addr = INADDR_ANY; // 绑定到所有可用的网络接口
address.sin_port = htons(PORT); // 将端口号从主机字节序转换为网络字节序
// 2. 绑定 socket 到 IP 和端口
if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
perror("bind failed");
exit(EXIT_FAILURE);
}
// 3. 开始监听连接
// SOMAXCONN: 系统允许的最大连接数
if (listen(server_fd, SOMAXCONN) < 0) {
perror("listen");
exit(EXIT_FAILURE);
}
printf("Server listening on port %d...\n", PORT);
// 4. 接受一个客户端连接
// accept 会阻塞,直到有客户端连接
if ((client_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
perror("accept");
exit(EXIT_FAILURE);
}
printf("Client connected: %s:%d\n", inet_ntoa(address.sin_addr), ntohs(address.sin_port));
// 5. 从客户端读取数据
int valread = read(client_socket, buffer, BUFFER_SIZE);
printf("Client message: %s\n", buffer);
// 6. 向客户端发送数据
send(client_socket, hello, strlen(hello), 0);
printf("Hello message sent\n");
// 7. 关闭 socket
close(client_socket);
close(server_fd);
return 0;
}
客户端代码 (client.c)
客户端会连接到指定的服务器 IP 和端口,发送一条消息,然后等待并接收服务器的回复。

(图片来源网络,侵删)
// client.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h> // 用于 read, write, close
#include <sys/socket.h> // 用于 socket, connect
#include <netinet/in.h> // 用于 struct sockaddr_in 和 htons
#include <arpa/inet.h> // 用于 inet_addr
#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. 创建 socket
if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
printf("\n Socket creation error \n");
return -1;
}
serv_addr.sin_family = AF_INET; // IPv4
serv_addr.sin_port = htons(PORT); // 端口号
// 将 IPv4 地址从文本转换为二进制形式
// 这里我们连接本机服务器,所以用 127.0.0.1
if (inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr) <= 0) {
printf("\nInvalid address/ Address not supported \n");
return -1;
}
// 2. 连接服务器
if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
printf("\nConnection Failed \n");
return -1;
}
printf("Connected to server.\n");
// 3. 向服务器发送数据
send(sock, hello, strlen(hello), 0);
printf("Hello message sent\n");
// 4. 从服务器读取数据
int valread = read(sock, buffer, BUFFER_SIZE);
printf("Server message: %s\n", buffer);
// 5. 关闭 socket
close(sock);
return 0;
}
如何编译和运行
注意:你必须先启动服务器,然后再启动客户端,因为服务器在 accept() 处是阻塞的,它会一直等待客户端的到来。
步骤 1:编译代码
打开两个终端窗口。
在第一个终端(用于编译服务器):

(图片来源网络,侵删)
gcc server.c -o server
这会生成一个名为 server 的可执行文件。
在第二个终端(用于编译客户端):
gcc client.c -o client
这会生成一个名为 client 的可执行文件。
步骤 2:运行服务器
在第一个终端中运行服务器程序:
./server
你会看到输出:
Server listening on port 8080...
服务器正在等待客户端连接,程序会停在这里。
步骤 3:运行客户端
在第二个终端中运行客户端程序:
./client
你会看到客户端的输出:
Connected to server.
Hello message sent
Server message: Hello from server
步骤 4:观察服务器的输出
切换回第一个终端(服务器的终端),你会看到它之前等待的地方现在有了输出:
Server listening on port 8080...
Client connected: 127.0.0.1:54321 // 端口号可能会不同
Client message: Hello from client
Hello message sent
之后,服务器和客户端的终端都会正常退出,程序结束。
Windows 平台的特殊说明
如果你在 Windows 上使用 MinGW (GCC) 进行编译,命令稍有不同:
- 编译时需要链接
ws2_32库:- 服务器编译:
gcc server.c -o server.exe -lws2_32
- 客户端编译:
gcc client.c -o client.exe -lws2_32
- 服务器编译:
- 头文件和函数:
unistd.h在 Windows 上不存在,其功能由io.h和socket.h等提供,现代 MinGW 通常提供了unistd.h的兼容层。close()函数在 Windows 上通常用closesocket()替代,为了更好的跨平台性,可以使用#ifdef来区分,但在简单示例中,直接使用close()通常也能工作,因为 MinGW 的closeclosesocket的一个包装。- 你可能需要包含
<winsock2.h>和<ws2tcpip.h>,而不是<sys/socket.h>和<netinet/in.h>,但很多现代的 MinGW 工具链也提供了 POSIX 风格的头文件。
如果你使用 Visual Studio (MSVC),则项目设置和链接方式完全不同,需要配置项目的链接器依赖项。
代码中的关键函数解析
| 函数 | 作用 | 服务器端 | 客户端 |
|---|---|---|---|
socket() |
创建一个通信端点(套接字) | 是 | 是 |
bind() |
将套接字与特定的 IP 地址和端口号绑定 | 是 | 否 |
listen() |
将套接字设置为被动模式,准备接受连接请求 | 是 | 否 |
accept() |
接受一个连接请求,并返回一个新的套接字用于通信 | 是 | 否 |
connect() |
主动发起一个连接到指定的服务器 | 否 | 是 |
send() / write() |
通过套接字发送数据 | 是 | 是 |
recv() / read() |
通过套接字接收数据 | 是 | 是 |
close() / closesocket() |
关闭套接字,释放资源 | 是 | 是 |
htons() |
Host to Network Short (16位) | 是 | 是 |
inet_addr() / inet_pton() |
将点分十进制 IP 地址转换为网络字节序的二进制格式 | 否 | 是 |
这个例子是 Socket 编程的基石,理解了它,你就可以在此基础上进行更复杂的开发,例如处理多个客户端(多线程、select、poll、epoll)、处理更复杂的协议等。
