目录
- 引言:什么是 Socket?
- Socket 编程基础概念
- 网络字节序
- 套接字类型
- 基本流程
- 核心 API 函数详解
socket(): 创建套接字bind(): 绑定地址和端口listen(): 监听连接 (服务器)accept(): 接受连接 (服务器)connect(): 发起连接 (客户端)send()/recv(): 发送和接收数据close(): 关闭套接字
- 实战案例:简单的回显服务器与客户端
- 服务器端代码 (
server.c) - 客户端代码 (
client.c) - 如何编译和运行
- 服务器端代码 (
- 进阶主题与注意事项
recv()的阻塞问题- 使用
select()实现多路复用 - 错误处理
- 代码可移植性 (Windows)
- 总结与资源
引言:什么是 Socket?
你可以把 Socket(套接字) 想象成一个通信的“终端”或“插座”,它是一组接口,是应用程序与网络协议栈之间的桥梁,通过 Socket,一个程序可以发送或接收数据,这些数据可以在同一台计算机的不同进程间传输,也可以在网络中不同计算机的进程间传输。

Socket 编程就是利用这些接口来实现网络通信的编程方式,它是最底层的网络通信方式之一,是构建更高级网络服务(如 HTTP, FTP, SMTP)的基础。
Socket 编程基础概念
网络字节序
计算机在内存中存储多字节数据(如 int, long)时有两种方式:
- 大端序: 高位字节存储在低地址,低位字节存储在高地址。
- 小端序: 低位字节存储在低地址,高位字节存储在高地址。
不同的操作系统可能使用不同的字节序,为了确保网络通信中数据能被正确解析,网络协议规定统一使用大端序,在发送数据前,如果主机是小端序,需要将数据转换成网络字节序;在接收数据后,如果主机是小端序,需要转换回主机字节序。
htons(): Host to Network Short (16位)htonl(): Host to Network Long (32位)ntohs(): Network to Host Shortntohl(): Network to Host Long
套接字类型
主要有三种类型:

-
流式套接字:
SOCK_STREAM- 特点: 面向连接,提供可靠的、有序的、双向的字节流服务。
- 协议: 通常基于 TCP (Transmission Control Protocol)。
- 适用场景: 要求数据不丢失、不重复、按序到达的场景,如文件传输、网页浏览。
-
数据报套接字:
SOCK_DGRAM- 特点: 无连接,提供“尽力而为”的数据报服务,不保证顺序,不保证不丢失,也不保证不重复。
- 协议: 通常基于 UDP (User Datagram Protocol)。
- 适用场景: 对实时性要求高,能容忍少量丢包的场景,如视频会议、在线游戏、DNS查询。
-
原始套接字:
SOCK_RAW- 特点: 可以直接访问底层协议(如 IP, ICMP),可以构造自己的数据包。
- 适用场景: 网络诊断、安全工具开发等,通常需要 root 权限。
基本流程
网络通信模型分为服务器端和客户端,它们的流程是不同的。

服务器端流程 (TCP为例):
socket()-> 创建套接字bind()-> 绑定 IP 地址和端口号listen()-> 开始监听,等待客户端连接accept()-> 阻塞,等待并接受客户端连接,返回一个新的套接字用于通信send()/recv()-> 与客户端通过新套接字收发数据close()-> 关闭通信套接字- (可选)
close()-> 关闭监听套接字
客户端流程 (TCP为例):
socket()-> 创建套接字connect()-> 主动连接服务器的 IP 地址和端口号send()/recv()-> 与服务器收发数据close()-> 关闭套接字
核心 API 函数详解
在使用这些函数前,需要包含必要的头文件:
#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, htons 等函数 #include <errno.h> // 提供 errno
socket()
创建一个套接字,返回一个文件描述符(一个整数)。
int socket(int domain, int type, int protocol);
domain: 地址族,最常用的是AF_INET(IPv4) 和AF_INET6(IPv6)。type: 套接字类型,如SOCK_STREAM(TCP) 或SOCK_DGRAM(UDP)。protocol: 协议,通常设为 0,系统会自动根据type选择。- 返回值: 成功返回套接字描述符(整数),失败返回 -1。
bind()
将套接字与本机的 IP 地址和端口号绑定。
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd:socket()返回的套接字描述符。addr: 指向sockaddr结构体的指针,实际编程中,我们更常用sockaddr_in结构体,并通过指针转换。struct sockaddr_in { short sin_family; // 地址族, AF_INET unsigned short sin_port; // 端口号, 需用 htons() 转换 struct in_addr sin_addr; // IP 地址 char sin_zero[8]; // 填充字段, 保持与 sockaddr 结构体大小一致 }; struct in_addr { unsigned long s_addr; // IP 地址, 用 inet_addr() 或 inet_pton() 转换 };addrlen:addr结构体的长度,sizeof(struct sockaddr_in)。- 返回值: 成功返回 0,失败返回 -1。
listen()
仅用于服务器端,将套接字设置为被动监听模式,准备接受客户端连接。
int listen(int sockfd, int backlog);
sockfd: 已绑定地址的套接字描述符。backlog: 等待连接队列的最大长度。- 返回值: 成功返回 0,失败返回 -1。
accept()
仅用于服务器端,从等待连接的队列中取出一个已完成的连接,并返回一个新的套接字用于与这个客户端通信,原套接字继续监听。
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
sockfd: 处于监听状态的套接字描述符。addr: 用于存放客户端的地址信息(可选,可设为 NULL)。addrlen: 指向addr长度的指针(可选,可设为 NULL)。- 返回值: 成功返回新的套接字描述符,失败返回 -1。
connect()
仅用于客户端,向服务器发起连接请求。
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd: 客户端创建的套接字描述符。addr: 指向服务器sockaddr_in结构体的指针。addrlen:addr结构体的长度。- 返回值: 成功返回 0,失败返回 -1。
send() / recv()
用于通过已连接的套接字发送和接收数据,它们是对 write() 和 read() 的封装,提供了更多功能。
// 发送数据 ssize_t send(int sockfd, const void *buf, size_t len, int flags); // 接收数据 ssize_t recv(int sockfd, void *buf, size_t len, int flags);
sockfd: 已连接的套接字描述符。buf: 发送/接收数据的缓冲区。len: 缓冲区的长度。flags: 通常设为 0。MSG_OOB: 处理带外数据。MSG_PEEK: 查看数据但不从缓冲区移除。
- 返回值:
- 成功时,
send()返回实际发送的字节数,recv()返回实际接收到的字节数。 - 如果返回 0,表示连接已关闭(对
recv()有效)。 - 如果返回 -1,表示发生错误。
- 成功时,
close()
关闭套接字,释放资源。
int close(int sockfd);
实战案例:简单的回显服务器与客户端
这个案例将实现一个简单的 TCP 服务器,它会将客户端发来的任何字符串原样返回(回显)。
服务器端代码 (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. 创建套接字
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);
}
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, 3) < 0) {
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("Client connected: %s:%d\n", inet_ntoa(address.sin_addr), ntohs(address.sin_port));
// 5. 与客户端通信
int valread;
while ((valread = read(new_socket, buffer, BUFFER_SIZE)) > 0) {
printf("Received: %s", buffer);
send(new_socket, buffer, valread, 0); // 回显数据
memset(buffer, 0, BUFFER_SIZE); // 清空缓冲区
}
if (valread == 0) {
printf("Client disconnected.\n");
} else if (valread < 0) {
perror("read");
}
// 6. 关闭套接字
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 *message = "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);
// 将 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;
}
// 3. 发送数据
send(sock, message, strlen(message), 0);
printf("Message sent: %s\n", message);
// 4. 接收服务器回显
int valread = read(sock, buffer, BUFFER_SIZE);
printf("Server echoed: %s\n", buffer);
// 5. 关闭套接字
close(sock);
return 0;
}
如何编译和运行
-
保存代码: 将上述两段代码分别保存为
server.c和client.c。 -
编译: 打开两个终端,分别编译服务器和客户端。
# 终端 1: 编译服务器 gcc server.c -o server # 终端 2: 编译客户端 gcc client.c -o client
-
运行:
- 在终端 1 中运行服务器:
./server
你会看到
Server listening on port 8080...。 - 在终端 2 中运行客户端:
./client
客户端会发送消息并打印回显,然后退出,服务器会打印客户端连接和断开的信息。
- 在终端 1 中运行服务器:
进阶主题与注意事项
recv() 的阻塞问题
recv() (和 read()) 在没有数据可读时会阻塞,即程序会暂停执行,直到有数据到达或连接关闭,这在简单的例子中没问题,但在复杂的程序中,我们不希望一个连接阻塞整个程序,解决方法包括:
- 多线程/多进程: 为每个客户端连接创建一个新的线程或进程。
- I/O 多路复用: 使用
select(),poll()或epoll()(Linux) 同时监视多个套接字,哪个套接字准备好读写,就处理哪个,这是高性能服务器的常用技术。
使用 select() 实现多路复用
select() 允许你监视一组文件描述符(包括套接字),等待其中一个变为“就绪”状态(可读、可写或出现异常)。
// 伪代码示例
fd_set read_fds;
int max_fd;
// 初始化套接字列表和 max_fd
// ...
while (1) {
FD_ZERO(&read_fds); // 清空集合
FD_SET(server_sock, &read_fds); // 添加监听套接字
// ... 添加所有客户端套接字
max_fd = server_sock; // 找出最大的套接字描述符
select(max_fd + 1, &read_fds, NULL, NULL, NULL); // 阻塞等待
if (FD_ISSET(server_sock, &read_fds)) {
// 有新连接
int new_sock = accept(server_sock, ...);
// 将 new_sock 加入你的套接字列表
}
for (int i = 0; i <= max_fd; i++) {
if (FD_ISSET(i, &read_fds)) {
// 有客户端数据可读
int valread = read(i, buffer, BUFFER_SIZE);
if (valread == 0) {
// 客户端断开连接
close(i);
// 从列表中移除 i
} else {
// 处理数据
}
}
}
}
错误处理
网络编程充满了不确定性,必须对每个可能失败的系统调用进行检查,并根据错误类型(errno)采取相应措施。accept() 可能被信号中断,返回 EINTR 错误,此时可以重试。
代码可移植性 (Windows)
Windows 下的 Socket 编程与 Linux 有显著不同:
- 头文件: 需要包含
<winsock2.h>和<ws2tcpip.h>。 - 库: 需要链接
ws2_32.lib库。 - �化: 在使用任何 Socket 函数前,必须调用
WSAStartup()初始化 Winsock。 - 清理: 程序结束时,必须调用
WSACleanup()。 - 数据类型:
SOCKET类型,而不是int。 - 关闭: 使用
closesocket()而不是close()。
为了简化跨平台开发,可以使用第三方库如 libuv 或 Boost.Asio,它们封装了底层的差异。
总结与资源
Socket 编程是网络开发的基石,本教程涵盖了 TCP Socket 编程的核心流程和 API,并通过一个简单的回显示例展示了其应用,关键点在于理解服务器和客户端的不同角色,以及 socket, bind, listen, accept, connect, send, recv 这些函数的协同工作方式。
进一步学习资源:
- 书籍:
- 《Unix 网络编程 卷1:套接字联网API》 (经典中的经典,必读)
- 《TCP/IP详解 卷1:协议》
- 在线文档:
man手册页 (在 Linux 终端输入man 2 socket,man 2 bind等)- Beej's Guide to Network Programming (非常友好的在线教程,有中文版)
- 实践:
- 尝试实现一个 UDP 的回显服务器/客户端。
- 尝试用
select()改造服务器,使其能同时处理多个客户端。 - 实现一个简单的文件传输程序。
