Socket(套接字)是操作系统提供的一种用于网络通信的API,它就像一个“电话插座”,程序可以通过它来发送和接收数据,实现不同计算机之间的通信。

我们将分为以下几个部分来讲解:
- 核心概念:理解Socket的基本原理。
- 基本流程:一个简单的客户端/服务器通信流程。
- 关键API函数:创建、连接、监听、接受、读写、关闭等。
- 一个完整的示例:一个简单的Echo Server(回显服务器)和客户端。
- 进阶话题:多线程/多进程处理、
select模型等。
核心概念
Socket是什么?
Socket是网络编程的“句柄”(Handle),当你创建一个Socket时,操作系统会返回一个整数,这个整数就是你用来进行后续网络操作的标识符。
两种主要的Socket类型:
- 流式套接字 (SOCK_STREAM):
- 协议:使用 TCP (Transmission Control Protocol)。
- 特点:面向连接、可靠、有序、不丢包,数据像一个字节流一样传输,适用于要求高可靠性的场景,如文件传输、网页浏览(HTTP/HTTPS)。
- 数据报套接字 (SOCK_DGRAM):
- 协议:使用 UDP (User Datagram Protocol)。
- 特点:无连接、不可靠、可能丢包、可能乱序,数据以独立的“数据报”形式发送,适用于对速度要求高、能容忍少量丢包的场景,如视频会议、在线游戏、DNS查询。
网络字节序
计算机内部存储数据有两种方式:大端序(高位字节在前)和小端序(低位字节在前),不同的CPU架构可能使用不同的字节序,为了保证网络通信中数据的一致性,规定网络传输必须使用大端序(也叫网络字节序)。
当你需要将一个32位的整数从主机字节序转换为网络字节序时,可以使用 htonl() (Host to Network Long),反之,用 ntohl(),对于16位整数,使用 htons() 和 ntohs()。

基本流程
服务器端流程 (以TCP为例)
- 创建套接字:调用
socket()函数,创建一个通信端点。 - 绑定地址和端口:调用
bind()函数,将套接字与一个特定的IP地址和端口号绑定,这样客户端才能知道连接到哪里。 - 监听连接:调用
listen()函数,将套接字设置为“监听”状态,等待客户端的连接请求。 - 接受连接:调用
accept()函数,阻塞等待客户端的连接,当一个客户端连接成功时,accept()会返回一个新的套接字,专门用于与这个客户端通信,而原来的监听套接字继续等待新的连接。 - 收发数据:使用
send()/write()和recv()/read()函数,与客户端进行数据交换。 - 关闭套接字:通信结束后,关闭用于通信的套接字和监听套接字。
客户端流程 (以TCP为例)
- 创建套接字:同样调用
socket()函数。 - 连接服务器:调用
connect()函数,向服务器的IP地址和端口号发起连接请求。 - 收发数据:连接成功后,使用
send()/write()和recv()/read()函数与服务器通信。 - 关闭套接字:通信结束后,关闭套接字。
关键API函数 (Linux/Unix风格)
在使用这些函数前,必须包含头文件:
#include <sys/socket.h> // 核心Socket函数 #include <netinet/in.h> // IPv4地址结构 #include <arpa/inet.h> // IP地址转换函数 (如inet_addr) #include <unistd.h> // close() 函数 #include <string.h> // memset(), bzero() 函数
| 函数 | 功能 | 参数说明 |
|---|---|---|
socket() |
创建一个Socket | domain (地址族, 如AF_INET), type (类型, 如SOCK_STREAM), protocol (协议, 如0) |
bind() |
绑定IP地址和端口 | sockfd (Socket描述符), addr (指向sockaddr结构体的指针), addrlen (地址长度) |
listen() |
开始监听连接 | sockfd (Socket描述符), backlog (等待连接的最大队列长度) |
accept() |
接受一个新连接 | sockfd (监听Socket描述符), addr (客户端地址), addrlen (地址长度指针) |
connect() |
向服务器发起连接 | sockfd (Socket描述符), addr (服务器地址), addrlen (地址长度) |
send() / write() |
通过Socket发送数据 | sockfd (Socket描述符), buf (数据缓冲区), len (数据长度), flags (通常为0) |
recv() / read() |
从Socket接收数据 | sockfd (Socket描述符), buf (数据缓冲区), len (缓冲区大小), flags (通常为0) |
close() |
关闭一个Socket描述符 | sockfd (要关闭的Socket描述符) |
重要结构体:struct sockaddr_in
这是用于IPv4地址的结构体,比通用的struct sockaddr更方便使用。
struct sockaddr_in {
short sin_family; // 地址族, AF_INET
unsigned short sin_port; // 端口号, 必须用htons()转换
struct in_addr sin_addr; // IP地址
char sin_zero[8]; // 填充字节, 使其与struct sockaddr大小一致
};
// struct in_addr 结构体
struct in_addr {
in_addr_t s_addr; // 32位IP地址, 必须用inet_addr()或inet_pton()转换
};
一个完整的示例
下面是一个简单的TCP Echo Server和Client,客户端发送任何消息,服务器都会原样返回。
服务器端代码 (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 (AF_INET for IPv4, SOCK_STREAM for TCP)
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
perror("socket failed");
exit(EXIT_FAILURE);
}
// 设置Socket选项,允许地址重用
if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) {
perror("setsockopt");
exit(EXIT_FAILURE);
}
// 2. 绑定地址和端口
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY; // 监听所有可用的网络接口
address.sin_port = htons(PORT); // 将端口号从主机字节序转换为网络字节序
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 with IP: %s and Port: %d\n",
inet_ntoa(address.sin_addr), ntohs(address.sin_port));
// 5. 收发数据
int valread;
while (1) {
valread = read(new_socket, buffer, BUFFER_SIZE);
if (valread <= 0) {
// 如果read返回0,表示客户端断开连接;如果返回-1,表示出错
printf("Client disconnected.\n");
break;
}
printf("Received from 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. 创建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 response: %s\n", buffer);
// 5. 关闭套接字
close(sock);
return 0;
}
如何编译和运行
-
编译: 在Linux或macOS终端中,使用
gcc进行编译,需要链接-pthread,因为一些系统可能需要它。
(图片来源网络,侵删)# 编译服务器 gcc server.c -o server # 编译客户端 gcc client.c -o client
-
运行:
-
先运行服务器:
./server
你会看到 "Server listening on port 8080..." 的输出。
-
再运行客户端(可以在另一个终端窗口中):
./client
客户端会连接到服务器,发送 "Hello from client",并打印出服务器的回显 "Hello from client"。
-
服务器端会打印出客户端的连接信息和接收到的消息。
-
进阶话题
多客户端处理
上面的服务器一次只能处理一个客户端,当accept()阻塞时,它无法处理新的连接,要处理多个客户端,常见的方法有:
- 多进程:父进程调用
accept()后,fork()一个子进程来处理与客户端的通信,父进程继续监听。 - 多线程:主线程调用
accept(),然后创建一个新的子线程来处理客户端通信,主线程继续监听。 - I/O多路复用:使用
select、poll或epoll等系统调用来监视多个Socket的状态,当某个Socket准备好读或写时,select会返回,程序再对它进行操作。epoll是Linux下最高效的I/O多路复用模型。
错误处理
在实际应用中,所有socket函数都应该进行错误检查。perror()是一个非常有用的函数,它会打印出当前错误信息。
Windows平台
如果你需要在Windows上运行,需要做以下调整:
- 包含不同的头文件:
#include <winsock2.h>和#include <ws2tcpip.h>。 - 在程序开始时,调用
WSAStartup()初始化Winsock库。 - 在程序结束时,调用
WSACleanup()清理Winsock库。 close()函数在Windows上是closesocket()。- 编译时需要链接
ws2_32.lib库。
希望这份详细的指南能帮助你理解C语言中的Socket编程!
