- 核心概念:理解什么是 Socket,以及通信的基本流程。
- 核心函数:介绍在 C 语言中使用 Socket 所需的主要函数。
- 一个完整的例子:分别实现一个简单的 TCP 服务器和客户端。
- UDP 编程简介:对比 TCP,介绍 UDP 的简单例子。
- 重要注意事项:如编译、错误处理、多客户端等。
核心概念
什么是 Socket?
你可以把 Socket 想象成一个“网络上的文件描述符”,在 C 语言中,文件描述符是一个整数,代表一个打开的文件、管道或 Socket,通过这个“句柄”,你可以对它进行读写操作,就像读写本地文件一样,只不过数据是通过网络传输的。

(图片来源网络,侵删)
通信模型(TCP)
我们以最常用的 TCP (传输控制协议) 为例,它提供了面向连接的、可靠的、基于字节流的服务。
服务器端流程:
- 创建套接字 (
socket):创建一个新的通信端点。 - 绑定地址和端口 (
bind):将套接字与一个特定的 IP 地址和端口号绑定,这样客户端才能找到它。 - 监听连接 (
listen):让套接字进入被动监听状态,等待客户端的连接请求。 - 接受连接 (
accept):从等待队列中取出一个已完成的连接请求,创建一个新的套接字与该客户端进行通信,原来的监听套接字继续监听新的连接。 - 收发数据 (
send/recv):通过新创建的套接字与客户端进行数据交互。 - 关闭套接字 (
close):通信结束后,关闭套接字,释放资源。
客户端流程:
- 创建套接字 (
socket):创建一个新的通信端点。 - 连接服务器 (
connect):主动向服务器的 IP 地址和端口号发起连接请求。 - 收发数据 (
send/recv):连接建立后,通过套接字与服务器进行数据交互。 - 关闭套接字 (
close):通信结束后,关闭套接字。
一个形象的比喻:

(图片来源网络,侵删)
- 服务器:就像一个电话总机(
bind+listen),总机本身不直接与外部通话,它负责接听所有来电。 - 客户端:就像一个打电话的人(
connect),拨打总机的号码。 accept:总机接线员拿起一个电话,分配一个分机给这个来电者,从此,这个分机(新的socket)就专门负责与这个来电者通话,而总机可以继续接听下一个电话。
核心函数
在使用这些函数前,需要包含必要的头文件:
#include <sys/socket.h> // 核心 Socket 函数 #include <netinet/in.h> // IP 地址和端口结构体 #include <arpa/inet.h> // IP 地址转换函数 (inet_pton) #include <unistd.h> // close 函数 #include <string.h> // memset, strlen 函数 #include <stdio.h> // perror, printf 函数
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结构体的指针,包含了要绑定的地址和端口。addrlen:addr结构体的长度。sockaddr_in结构体 (用于 IPv4):struct sockaddr_in { short sin_family; // 地址族,AF_INET unsigned short sin_port; // 端口号,需要用 htons() 转换 struct in_addr sin_addr; // IP 地址,用 inet_pton() 填充 char sin_zero[8]; // 填充0,使结构体大小与 sockaddr 一致 };htons()函数:Host to Network Short,将主机字节序的端口号转换为网络字节序(大端序),网络字节序是大端序。
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结构体的指针。addrlen:addr结构体的长度。- 返回值:成功返回 0,失败返回 -1。
send() / recv()
通过已连接的套接字发送和接收数据。
// 发送 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: 已连接的套接字描述符(服务器端是accept()返回的,客户端是connect()使用的)。buf: 数据缓冲区。len: 要发送/接收的数据长度。flags: 通常设为 0。- 返回值:成功返回实际发送/接收的字节数;对于
recv(),如果对方关闭了连接,返回 0;失败返回 -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);
}
// 设置套接字选项,允许地址重用(避免 TIME_WAIT 状态导致端口占用)
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, 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("Connection accepted from %s:%d\n", inet_ntoa(address.sin_addr), ntohs(address.sin_port));
// 5. 收发数据
int valread;
while(1) {
valread = recv(new_socket, buffer, BUFFER_SIZE, 0);
if (valread <= 0) {
// valread == 0 表示客户端关闭了连接
// valread < 0 表示发生错误
break;
}
printf("Client: %s", buffer);
// 回显消息给客户端
send(new_socket, buffer, valread, 0);
memset(buffer, 0, BUFFER_SIZE); // 清空缓冲区
}
// 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 *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 = recv(sock, buffer, BUFFER_SIZE, 0);
printf("Server: %s\n", buffer);
// 5. 关闭套接字
close(sock);
return 0;
}
如何编译和运行
- 打开两个终端。
- 在第一个终端编译并运行服务器:
gcc server.c -o server ./server
你会看到 "Server listening on port 8080..."。
- 在第二个终端编译并运行客户端:
gcc client.c -o client ./client
客户端会连接服务器,发送一条消息,并打印出服务器的回显,服务器终端也会显示收到的消息。
UDP 编程简介
UDP 是无连接的、不可靠的、基于数据报的协议,它的编程模型比 TCP 简单,没有 listen() 和 accept() 步骤。
服务器端流程:
socket()bind()(绑定到端口)- 进入一个循环,使用
recvfrom()接收数据,sendto()发送数据。
客户端流程:
socket()- 进入一个循环,使用
sendto()发送数据,recvfrom()接收数据。
UDP 服务器端示例 (udp_server.c)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#define PORT 8080
#define BUFFER_SIZE 1024
int main() {
int sockfd;
char buffer[BUFFER_SIZE];
struct sockaddr_in servaddr, cliaddr;
// 创建 UDP 套接字
if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0 ) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
memset(&servaddr, 0, sizeof(servaddr));
memset(&cliaddr, 0, sizeof(cliaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = INADDR_ANY;
servaddr.sin_port = htons(PORT);
// 绑定
if ( bind(sockfd, (const struct sockaddr *)&servaddr, sizeof(servaddr)) < 0 ) {
perror("bind failed");
exit(EXIT_FAILURE);
}
printf("UDP Server waiting for messages...\n");
int len, n;
len = sizeof(cliaddr); // 重要:len 必须初始化
// 循环接收数据
while(1) {
n = recvfrom(sockfd, (char *)buffer, BUFFER_SIZE, 0, ( struct sockaddr *) &cliaddr, &len);
buffer[n] = '\0';
printf("Client : %s\n", buffer);
sendto(sockfd, (const char *)buffer, strlen(buffer), 0, (const struct sockaddr *) &cliaddr, len);
printf("Hello message sent.\n");
}
return 0;
}
重要注意事项
- 编译时链接库:在 Linux/macOS 上,上面的例子可以直接用
gcc编译,因为系统调用已经包含在标准库中,但在某些系统(如较旧的 Solaris)或 Windows 上,可能需要显式链接-lnsl -lsocket。# Windows 下可能需要 # gcc server.c -o server -lws2_32
- 错误处理:所有 Socket 函数都可能失败,必须检查它们的返回值,并根据错误码(
perror是一个好帮手)进行相应处理,忘记检查返回值是初学者最常见的错误。 - 字节序:网络字节序是大端序,在设置端口号时,必须使用
htons()(Host to Network Short) 或htonl()(Host to Network Long) 进行转换,IP 地址通过inet_pton()函数处理,它会自动处理字节序问题。 - 多客户端:上面的 TCP 服务器一次只能处理一个客户端,当
accept()返回后,原服务器套接字可以继续监听,而新套接字负责与客户端通信,要实现真正的并发服务器,需要使用多线程 (pthread) 或多进程 (fork)。 - 阻塞与非阻塞:默认情况下,
accept(),recv(),connect()等函数是阻塞的,程序会一直等待直到事件发生,可以使用fcntl()或ioctl()将套接字设置为非阻塞模式,或者使用select(),poll(),epoll等多路复用技术来管理多个套接字。
