Linux网络编程
无论是 Web 服务器处理网页请求、即时通讯工具的消息传递,还是在线游戏的数据同步,都离不开 Socket 的支持。Socket 的本质,是将复杂的网络通信抽象成一个简单的接口,让开发者可以通过像操作文件一样的方式实现数据的收发。这也是”Unix中万物皆文件”。本文将以 TCP Socket 编程为例,逐步拆解从服务器端到客户端的交互过程,解析系统调用的作用,建立清晰的理解框架。
什么是 Socket?
Socket 提供了一种通信的端点,就像电器与电源的插座。通过 Socket,两个设备之间可以建立通信链接。
程序员只需要按照 Socket 提供的接口进行操作,不需要关心底层的复杂网络协议。
Socket 的现代实现被称为 Berkeley Sockets,因为它起源于 Berkeley Unix(BSD Unix)中,是基于 Unix 操作系统设计的。Berkeley Sockets 的出现奠定了今天网络编程的基础。
- 在Web浏览器与Web服务器通信:浏览器通过 Socket 向服务器发送 HTTP 请求,服务器通过 Socket 返回 HTTP 响应。
- 在即时通讯工具中传递消息:客户端通过 Socket 向服务器发送消息,服务器通过 Socket 将消息推送给其他客户端。
- 在在线游戏中同步数据:客户端通过 Socket 向服务器发送操作指令,服务器通过 Socket 将操作结果推送给其他客户端。
工作原理,怎么打电话?
- 首先,我们需要一个电话,也就是创建一个 Socket 对象。
- 如果想给朋友打电话,首先要知道朋友的电话号码。Socket 通信也是一样,需要知道对方的 IP 地址和端口号。
IP类似于大楼的地址,而Socket端口号类似于房间号。即我们通过IP地址实现主机到主机的通信,通过端口号实现主机内的进程到进程的通信。
- 打通电话后,可以开始通话。你说一句话(发送数据),对方听到并回复(接收数据)。Socket 通信也是一样,建立连接后,可以开始传输数据。
- 通话结束后,挂断电话。Socket通信结束后,关闭 Socket。
TCP与UDP,就像是打电话和写信
Socket 通信可以基于两种协议:TCP 和 UDP。TCP需要建立连接,而UDP不需要建立连接。
- TCP(Transmission Control Protocol):面向连接的协议,提供可靠的数据传输。TCP 保证数据的顺序和完整性,适用于要求数据准确无误的场景(网页、聊天、文件传输)。
- UDP(User Datagram Protocol):面向无连接的协议,提供高效的数据传输。UDP 不保证数据的顺序和完整性,适用于实时性要求高的场景(在线游戏、直播)。
TCP Socket通信模型
TCP Socket 通信模型是一种客户端-服务器模型。
- 服务器(Server):被叫方(接线员)随时准备接电话。在程序中表示为一个服务器进程,而不是硬件服务器。
- 客户端(Client):主动拨打电话。在程序中表示为一个客户端进程。
双工文件描述符
使用TCP Socket进行通信后,客户端和服务器各自都会获得一个文件描述符,这使得网络通信就像在操作文件。
写操作即是发送字节给对方,而读操作就是接受对方发送的字节。
文件描述符是操作系统中对文件的引用,是一个非负整数。在Linux系统中,文件描述符0、1、2分别对应标准输入、标准输出和标准错误。
每次打开一个文件或资源,操作系统都会返回一个文件描述符,用于后续对文件的操作。
TCP服务器系统调用过程
- socket() –准备好电话啦☎️上述代码除了创建服务器的socket外,还对服务器地址进行了配置。配置过程的主要任务就是告诉操作系统,使用哪种地址类型,绑定到哪个IP地址和端口号。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16// 1. 创建服务器的 Socket
// socket() 函数创建一个 Socket,返回一个文件描述符
// 参数说明:
// - AF_INET: 表示使用 IPv4,AF_INET6 表示 IPv6
// - SOCK_STREAM: 表示使用 TCP(流式传输).SOCK_DGRAM 表示 UDP(数据报传输)
// - 0: 默认协议(通常是 TCP)
server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd == -1) { // 如果返回 -1,表示创建失败
perror("socket"); // 打印错误信息
exit(EXIT_FAILURE); // 退出程序
}
// 2. 配置服务器地址
server_addr.sin_family = AF_INET; // 地址族,AF_INET 表示 IPv4
server_addr.sin_addr.s_addr = INADDR_ANY; // 监听所有网络接口(本机 IP)
server_addr.sin_port = htons(PORT); // 将端口号转换为网络字节序
而这些信息都存储在struct sockaddr_in结构体中,通过该结构体的成员变量进行配置。在配置IP地址时用到了INADDR_ANY,它表示监听所有网络接口,即服务器将接受来自任何网络接口的连接请求。1
2
3
4
5
6struct sockaddr_in {
sa_family_t sin_family; // 地址族 (AF_INET 表示 IPv4)
in_port_t sin_port; // 16 位端口号(网络字节序)
struct in_addr sin_addr; // IPv4 地址
char sin_zero[8]; // 填充字段,保持与 struct sockaddr 大小一致
};网络接口是连接设备和网络的桥梁,每个接口可以有自己的IP地址,例如有线网卡、无线网卡等。一台服务器可能有多个网络接口,一个连接到内网,一个连接到外网。所以一台服务器可能有多个IP地址。
INADDR_ANY表示监听所有网络接口,而INADDR_LOOPBACK表示监听本地回环接口,即只接受来自本机的连接请求。
在配置端口号时,使用了htons()函数将端口号转换为网络字节序。网络字节序是大端字节序,即高位字节存储在低地址,低位字节存储在高地址。
bind() 给电话装上号码📞
1
2
3
4
5
6
7// 3. 绑定地址到服务器的 Socket
// bind() 函数将服务器地址绑定到服务器 Socket
if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
perror("bind"); // 如果失败,打印错误信息
close(server_fd); // 关闭文件描述符
exit(EXIT_FAILURE); // 退出程序
}listen() 准备接听来电🤭
1
2
3
4
5
6
7
8
9// 4. 开始监听端口
// listen() 函数使服务器进入监听状态,准备接受连接
// 第二个参数指定连接队列的最大长度(这里为 5)
if (listen(server_fd, 5) == -1) {
perror("listen"); // 如果失败,打印错误信息
close(server_fd); // 关闭文件描述符
exit(EXIT_FAILURE); // 退出程序
}
printf("Server is listening on port %d...\n", PORT);accept() 电话接通了🤩
1
2
3
4
5
6
7
8
9// 5. 接受客户端连接
// accept() 函数等待客户端连接,成功后返回一个新的文件描述符(用于与客户端通信)
client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &addr_len);
if (client_fd == -1) {
perror("accept"); // 如果失败,打印错误信息
close(server_fd); // 关闭文件描述符
exit(EXIT_FAILURE); // 退出程序
}
printf("Client connected!\n");(struct sockaddr *)&client_addr的含义
在网络编程中,许多函数(如 bind() 和 connect())需要传递地址参数,类型为 struct sockaddr *,而不是 struct sockaddr_in *。
struct sockaddr 是一个通用的地址结构,可以表示多种类型的地址(IPv4、IPv6 等)。 struct sockaddr_in 是 IPv4 专用的地址结构。
函数要求地址参数的类型为 struct sockaddr *,以便支持多种协议。
为了解决这个问题,可以使用类型转换将 struct sockaddr_in * 转换为 struct sockaddr *,这样就可以传递给函数了。
accept()函数会阻塞等待客户端连接,一旦有客户端连接,就会返回一个新的文件描述符,用于与客户端通信。
这两个文件描述符各司其职,server_fd用于监听客户端连接,而client_fd用于与客户端通信。
read() write() 通话中…🤔
1
2
3
4
5
6
7
8// 6. 接收客户端发送的消息
memset(buffer, 0, BUFFER_SIZE); // 清空缓冲区
read(client_fd, buffer, BUFFER_SIZE); // 从客户端接收数据,存入缓冲区
printf("Client says: %s\n", buffer); // 打印接收到的消息
// 7. 回复客户端
const char *response = "Hello from server!";
write(client_fd, response, strlen(response)); // 将消息发送给客户端close() 挂断电话👋
1
2
3
4// 8. 关闭文件描述符
close(client_fd); // 关闭客户端文件描述符
close(server_fd); // 关闭服务器文件描述符
return 0;
TCP客户端系统调用过程
对于客户端呢,和TCP服务器一样,都要先准备电话,所以socket系统调用与TCP服务器是完全一致的。
客户端要想连接服务器,不需要限定自己的IP地址和端口号,只需要知道服务器的IP地址和端口号即可。
IPv4的地址是32位的,通常用点分十进制表示,例如本机回送地址是”127.0.0.1”‘,如果用32位整数表示,就是0x7F000001。十进制就是127256^3+0256^2+0*256+1=2130706433。
为了方便,我们可以使用inet_addr库函数将点分十进制的IP地址转换为网络字节序的整数。
connect()的调用格式与bind很类似,但是connect后的文件描述符不是通过返回值获得的,而是通过参数sockfd传递的。connect成功后,通过read/write操作文件描述符socket就可以与服务器通信了。通信结束后,
同样通过close()关闭文件描述符。
1 | // 连接服务器 |