核心概念
在开始编码前,先理解几个核心概念:
- Socket (套接字):可以看作是网络通信的端点,它是一个文件描述符,程序可以通过它来发送和接收数据。
- IP 地址:网络上设备的唯一标识,
0.0.1(本机地址)。 - 端口号:同一台主机上,不同应用程序的标识,一个 IP 地址可以对应多个端口,从而区分不同的服务。
- 协议:网络通信的规则,我们主要使用 TCP (流式套接字) 和 UDP (数据报套接字)。
- TCP (Transmission Control Protocol):面向连接的、可靠的协议,数据像水流一样,有序、无丢失地到达,适合要求高可靠性的场景,如文件传输、网页浏览。
- UDP (User Datagram Protocol):无连接的、不可靠的协议,数据像一个个包裹,发送出去但不保证到达或顺序,适合要求速度、能容忍少量丢包的场景,如视频会议、在线游戏。
- 字节序:计算机内存中多字节数据的存储顺序,网络协议规定使用大端序,而大多数 x86/x64 架构的电脑使用小端序,在发送包含多字节数据(如端口号、IP地址)时,需要进行转换,Linux/Unix 系统提供了
htons()(host to network short),htonl()(host to network long) 等函数来完成这个转换。
简单的 TCP 回显服务器与客户端
这个例子是最经典的入门程序,客户端发送一条消息,服务器原样返回这条消息。
服务器端代码 (server.c)
服务器的工作流程是:
- 创建套接字 (
socket)。 - 绑定 IP 地址和端口号 (
bind)。 - 监听连接 (
listen)。 - 接受客户端连接 (
accept)。 - 与客户端收发数据 (
send/recv)。 - 关闭连接 (
close)。
#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 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, &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. 绑定套接字到指定的 IP 和端口
if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
perror("bind failed");
exit(EXIT_FAILURE);
}
// 3. 开始监听连接
if (listen(server_fd, 3) < 0) { // backlog=3, 允许的排队连接数
perror("listen");
exit(EXIT_FAILURE);
}
printf("Server listening on port %d...\n", PORT);
// 4. 接受一个新连接
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 = read(new_socket, buffer, BUFFER_SIZE);
printf("Client: %s\n", buffer);
// 6. 将数据回写给客户端
send(new_socket, buffer, valread, 0);
printf("Message sent back to client.\n");
// 7. 关闭套接字
close(new_socket);
close(server_fd);
return 0;
}
客户端代码 (client.c)
客户端的工作流程是:
- 创建套接字 (
socket)。 - 连接到服务器 (
connect)。 - 发送数据 (
send)。 - 接收数据 (
recv)。 - 关闭连接 (
close)。
#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. 连接到服务器
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: %s\n", buffer);
// 5. 关闭套接字
close(sock);
return 0;
}
如何编译和运行
-
保存代码:将上面的代码分别保存为
server.c和client.c。 -
编译:打开终端,使用
gcc进行编译。# 编译服务器 gcc server.c -o server # 编译客户端 gcc client.c -o client
-
运行:
- 第一步:在一个终端中运行服务器。
./server
你会看到
Server listening on port 8080...。 - 第二步:在另一个终端中运行客户端。
./client
你会看到客户端的输出:
Connected to server. Hello message sent Server: Hello from client - 第三步:回到服务器终端,你会看到服务器的输出:
Server listening on port 8080... Connection accepted from 127.0.0.1:54321 (端口号可能不同) Client: Hello from client Message sent back to client.
- 第一步:在一个终端中运行服务器。
支持多客户端的服务器(使用 fork)
上面的服务器只能处理一个客户端连接,当处理客户端时,它无法接受新的连接,一个简单的解决方案是使用 fork 系统调用,为每个客户端连接创建一个子进程来处理。
服务器端代码 (multi_server.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>
#include <sys/wait.h> // for waitpid
#define PORT 8081
#define BUFFER_SIZE 1024
void handle_client(int client_socket) {
char buffer[BUFFER_SIZE] = {0};
int valread;
// 读取客户端消息
valread = read(client_socket, buffer, BUFFER_SIZE);
printf("Client %d: %s\n", client_socket, buffer);
// 回显消息
send(client_socket, buffer, valread, 0);
printf("Echoed back to client %d\n", client_socket);
// 关闭客户端套接字
close(client_socket);
exit(0); // 子进程完成任务后退出
}
int main() {
int server_fd, new_socket;
struct sockaddr_in address;
int opt = 1;
int addrlen = sizeof(address);
// 1. 创建套接字
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
perror("socket failed");
exit(EXIT_FAILURE);
}
if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) {
perror("setsockopt");
exit(EXIT_FAILURE);
}
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(PORT);
// 2. 绑定
if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
perror("bind failed");
exit(EXIT_FAILURE);
}
// 3. 监听
if (listen(server_fd, 10) < 0) { // backlog=10
perror("listen");
exit(EXIT_FAILURE);
}
printf("Multi-process server listening on port %d...\n", PORT);
while (1) {
// 4. 接受连接
if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
perror("accept");
continue; // 继续接受下一个连接
}
printf("New connection accepted, socket fd: %d\n", new_socket);
// 5. 创建子进程来处理客户端
pid_t pid = fork();
if (pid < 0) {
perror("fork failed");
close(new_socket);
} else if (pid == 0) { // 子进程
close(server_fd); // 子进程不需要监听套接字
handle_client(new_socket); // 处理客户端
} else { // 父进程
close(new_socket); // 父进程不需要已连接的套接字
// 回收僵尸进程
waitpid(-1, NULL, WNOHANG);
}
}
return 0;
}
如何运行:
编译和运行方式与之前类似,你可以打开多个终端窗口,运行多个 ./client,每个客户端都能得到服务器的响应,服务器主进程会持续运行,并为每个新连接创建一个子进程。
总结与注意事项
- 错误处理:Socket 编程中,几乎所有系统调用都可能失败。检查它们的返回值至关重要,本例中使用了
perror来打印错误信息,这是调试的好习惯。 - 头文件:
<sys/socket.h>: 核心 Socket API。<netinet/in.h>: IP 地址和端口号的结构定义(sockaddr_in)以及字节序转换函数。<arpa/inet.h>:inet_pton和inet_ntop函数,用于 IP 地址的转换。<unistd.h>: 提供read,write,close,fork等函数。
- 文件描述符:在 Linux/Unix 中,Socket 被视为一种文件,可以使用
read()和write()来收发数据,也可以使用recv()和send(),它们提供了更多选项。 - 资源释放:程序结束时,一定要记得
close()所有打开的套接字,否则会导致文件描述符泄露。 - 防火墙:如果在真实机器上运行,请确保防火墙允许你使用的端口号(如 8080)的流量。
- 跨平台:本代码主要针对 Linux/Unix 环境,在 Windows 上,需要包含
<winsock2.h>并在使用前调用WSAStartup()初始化 Winsock,代码会有所不同。
这个实例涵盖了 C 语言 Socket 编程的核心要点,掌握了这些,你就可以开始构建自己的网络应用程序了。
