- 核心概念:了解 Socket 是什么,以及通信的基本流程。
- 基本 API 函数:介绍最常用的 Socket 函数。
- 一个简单的 TCP 服务器/客户端示例:通过代码演示如何实现一个回显服务器。
- 一个简单的 UDP 服务器/客户端示例:演示 UDP 通信。
- 多线程与
select模型:如何处理多个客户端连接。 - 编译与运行:如何在 Linux 和 Windows 上编译和运行这些代码。
核心概念
什么是 Socket?
Socket 可以看作是网络通信的“终端点”,它就像一个电话插孔,你把电话线(网络连接)插进去,就可以和另一个电话(另一个程序)进行通话。
两种主要的通信方式
- TCP (Transmission Control Protocol):面向连接的、可靠的通信。
- 特点:数据传输前需要先建立连接(三次握手),数据传输有序、无丢失、无重复,传输完毕后需要断开连接(四次挥手)。
- 类比:打电话,你必须先拨号等待对方接听,建立连接后才能说话,说完后挂断。
- 适用场景:文件传输(HTTP, FTP)、邮件发送(SMTP)等对可靠性要求高的场景。
- UDP (User Datagram Protocol):无连接的、不可靠的通信。
- 特点:不需要建立连接,直接发送数据包,速度快,但不保证数据包的顺序、是否丢失或重复。
- 类比:寄明信片,你写好地址直接扔进邮筒,但不能保证对方一定能收到,也不能保证按寄送顺序收到。
- 适用场景:视频会议、在线游戏、DNS 查询等对实时性要求高、能容忍少量丢包的场景。
网络字节序
计算机内部存储数据有两种方式:大端序(高位字节在前)和小端序(低位字节在前),不同架构的计算机可能使用不同的字节序,为了确保网络通信中数据能被正确解析,网络协议规定使用 大端序,在发送数据前,如果数据是多字节的(如 int, short),需要将其从主机字节序转换为网络字节序;接收数据后,再转换回主机字节序。
htons():Host to Network Short (16位)htonl():Host to Network Long (32位)ntohs():Network to Host Short (16位)ntohl():Network to Host Long (32位)
基本 API 函数 (以 TCP 为例)
服务器端流程
socket(): 创建一个套接字文件描述符。bind(): 将套接字与一个 IP 地址和端口号绑定。listen(): 将套接字设置为监听状态,等待客户端连接。accept(): 接受客户端的连接请求,返回一个新的套接字用于与该客户端通信。read()/recv(): 从已连接的套接字中读取数据。write()/send(): 向已连接的套接字中写入数据。close(): 关闭套接字。
客户端流程
socket(): 创建一个套接字文件描述符。connect(): 主动连接服务器的 IP 地址和端口号。read()/recv(): 从已连接的套接字中读取数据。write()/send(): 向已连接的套接字中写入数据。close(): 关闭套接字。
TCP 服务器/客户端示例
这是一个经典的“回显”(Echo)服务器:客户端发送什么消息,服务器就原样返回什么消息。
头文件
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> // 用于 read, write, close #include <sys/socket.h> #include <netinet/in.h> // 用于 sockaddr_in #include <arpa/inet.h> // 用于 inet_addr
服务器代码 (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>
#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. 创建 socket 文件描述符
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
perror("socket failed");
exit(EXIT_FAILURE);
}
// 2. 设置 socket 选项,允许地址重用
if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &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);
// 3. 绑定 socket 到地址和端口
if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
perror("bind failed");
exit(EXIT_FAILURE);
}
printf("Server listening on port %d...\n", PORT);
// 4. 开始监听,等待连接
if (listen(server_fd, 3) < 0) {
perror("listen");
exit(EXIT_FAILURE);
}
// 5. 接受客户端连接
if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
perror("accept");
exit(EXIT_FAILURE);
}
printf("Client connected with IP: %s and Port: %d\n",
inet_ntoa(address.sin_addr), ntohs(address.sin_port));
// 6. 读取客户端数据并发回
int valread;
while ((valread = read(new_socket, buffer, BUFFER_SIZE)) > 0) {
printf("Received from client: %s", buffer);
send(new_socket, buffer, valread, 0);
memset(buffer, 0, BUFFER_SIZE); // 清空缓冲区
}
// 7. 关闭 socket
close(new_socket);
close(server_fd);
return 0;
}
客户端代码 (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. 创建 socket
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);
// 将 IP 地址从文本转换为网络地址
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 echoed: %s\n", buffer);
// 5. 关闭 socket
close(sock);
return 0;
}
UDP 服务器/客户端示例
UDP 无需连接,流程更简单。
UDP 服务器代码 (udp_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>
#define PORT 8081
#define BUFFER_SIZE 1024
int main() {
int sock;
struct sockaddr_in serv_addr, cli_addr;
char buffer[BUFFER_SIZE] = {0};
int len, n;
// 1. 创建 UDP socket (SOCK_DGRAM)
if ((sock = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
memset(&serv_addr, 0, sizeof(serv_addr));
memset(&cli_addr, 0, sizeof(cli_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = INADDR_ANY;
serv_addr.sin_port = htons(PORT);
// 2. 绑定
if (bind(sock, (const struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
perror("bind failed");
exit(EXIT_FAILURE);
}
printf("UDP Server listening on port %d...\n", PORT);
len = sizeof(cli_addr);
// 3. 循环接收数据
while(1) {
n = recvfrom(sock, (char *)buffer, BUFFER_SIZE, 0, (struct sockaddr *)&cli_addr, &len);
buffer[n] = '\0';
printf("Client : %s\n", buffer);
sendto(sock, (const char *)buffer, n, 0, (struct sockaddr *)&cli_addr, len);
printf("Echoed back to client\n");
}
return 0;
}
UDP 客户端代码 (udp_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 8081
#define BUFFER_SIZE 1024
int main() {
int sock;
struct sockaddr_in serv_addr;
char *hello = "Hello from UDP client";
char buffer[BUFFER_SIZE] = {0};
struct sockaddr_in serv_addr_len;
// 1. 创建 UDP socket
if ((sock = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(PORT);
if (inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr) <= 0) {
perror("invalid address/ Address not supported");
exit(EXIT_FAILURE);
}
// 2. 发送数据 (不需要 connect)
sendto(sock, (const char *)hello, strlen(hello), 0, (const struct sockaddr *)&serv_addr, sizeof(serv_addr));
printf("Hello message sent.\n");
// 3. 接收服务器返回的数据
int n;
socklen_t len = sizeof(serv_addr);
n = recvfrom(sock, (char *)buffer, BUFFER_SIZE, 0, (struct sockaddr *)&serv_addr, &len);
buffer[n] = '\0';
printf("Server : %s\n", buffer);
close(sock);
return 0;
}
处理多个客户端
上面的 TCP 服务器一次只能处理一个客户端,要处理多个客户端,有几种常见模型:
- 多线程/多进程模型:每当
accept()一个新客户端,就创建一个新的线程或进程来处理与该客户端的通信,主线程则继续监听新的连接,这是最直观的方式。 - I/O 多路复用模型 (
select/poll/epoll):使用一个线程来管理所有套接字。select函数会阻塞,直到有一个或多个套接字准备好进行 I/O 操作,当select返回后,程序遍历所有套接字,对“就绪”的套接字进行读写操作。epoll是 Linux 下更高效的 I/O 多路复用机制。
select 模型简单示例 (伪代码):
fd_set read_fds;
int max_fd = server_socket;
while(1) {
FD_ZERO(&read_fds); // 清空集合
FD_SET(server_socket, &read_fds); // 添加监听 socket
// ... 将所有客户端 socket 也添加到 read_fds 中
// 并更新 max_fd
select(max_fd + 1, &read_fds, NULL, NULL, NULL); // 阻塞等待
if (FD_ISSET(server_socket, &read_fds)) {
// 有新连接,执行 accept
}
// ... 遍历所有客户端 socket
// FD_ISSET(client_socket, &read_fds) 为真,则进行 read
}
编译与运行
Linux / macOS
使用 gcc 编译,需要链接 -lpthread 如果使用了多线程。
# 编译 TCP 服务器和客户端 gcc server.c -o server gcc client.c -o client # 编译 UDP 服务器和客户端 gcc udp_server.c -o udp_server gcc udp_client.c -o udp_client # 运行 # 1. 打开一个终端,运行服务器 ./server # 2. 打开另一个终端,运行客户端 ./client
Windows
Windows 使用不同的头文件和库。
头文件:
#include <winsock2.h> #include <ws2tcpip.h> #pragma comment(lib, "ws2_32.lib") // 链接 Winsock 库
�和清理: 在程序开始时需要初始化 Winsock:
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
printf("WSAStartup failed\n");
return 1;
}
在程序结束时需要清理:
WSACleanup();
socket(), bind(), listen(), accept() 等函数在 Windows 下的名称和参数与 Linux 基本一致,但返回的错误码处理方式不同(通常使用 WSAGetLastError())。
编译: 使用 Visual Studio 的 cl.exe 或 MinGW 的 gcc。
# 使用 MinGW gcc server.c -o server.exe -lws2_32 gcc client.c -o client.exe -lws2_32
希望这个详细的教程能帮助你入门 C 语言 Socket 编程!
