Linux嵌入式系统利用套接字编程(Socket Programming)实现网络通信的基础知识并附对一个简单实例的分析
目录
- 套接字编程的简介
- 套接字的基本概念
- 套接字编程的特点
- 套接字编程的主要步骤
- **服务端工作流程**:
- **客户端工作流程**:
- 服务端代码(使用 TCP 协议)示例代码及分析
- 服务端(使用 TCP 协议)的源代码
- 代码`int server_fd, new_socket;`分析(int server_fd、new_socket两个变量的作用)
- **1. `server_fd` 的作用**
- **2. `new_socket` 的作用**
- **具体工作流程举例**
- 代码`struct sockaddr_in server_addr, client_addr;`分析
- **1. `server_addr` 的作用**
- **2. `client_addr` 的作用**
- **两者的主要区别**
- **两者的使用场景**
- **`server_addr` 使用**:
- **`client_addr` 使用**:
- **结合代码解释**
- 小结
- “创建套接字”的代码分析
- **逐部分解析**
- **1. `socket()` 函数**
- **2. 参数详解**
- **(1) `AF_INET`**
- **(2) `SOCK_STREAM`**
- **(3) `0`**
- **结果**
- **示例:完整流程**
- **小结**
- “初始化地址结构体”的代码分析
- “绑定套接字”的代码分析
- **`bind()` 函数的定义**
- **为什么需要 `sizeof(server_addr)`?**
- **`sizeof(server_addr)` 的实际意义**
- 示例:
- 传递 `sizeof(server_addr)`:
- **错误示例**
- **小结**
- “监听连接”的代码分析(TCP协议才需要,UDP不需要)
- **函数原型**
- 参数说明:
- 返回值:
- **`listen()` 的作用**
- **参数 `backlog` 的意义**
- 队列的实际行为:
- 系统实际行为:
- **重要注意点**
- **小结**
- “接受客户端连接”的代码分析
- **`accept()` 函数的作用**
- **函数原型**
- 参数说明:
- 返回值:
- **代码解析**
- **逐部分解释:**
- **重点解读**
- **常见问题**
- **小结**
- “接收数据”的代码的分析
- **`recv()` 函数的作用**
- **函数原型**
- 参数说明:
- 返回值:
- **代码解析**
- **逐部分解释:**
- **`recv()` 的行为**
- **示例**:
- "发送响应"的代码
- 客户端代码(使用 TCP 协议)示例代码及分析
- 分析前的说明
- 客户端代码(使用 TCP 协议)的源代码
- “初始化服务器地址”的代码分析
- **`inet_pton()` 函数**
- **函数原型**
- 参数说明:
- 返回值:
- **代码解析**
- **逐部分解释:**
- **`inet_pton()` 与 `inet_ntop()` 的区别**
- **总结**
套接字编程的简介
套接字编程(Socket Programming)是一种网络编程方法,它通过操作系统提供的套接字(Socket)接口,允许程序之间在网络上进行通信。套接字可以被看作是网络通信的“端点”,它使得不同主机(甚至同一主机上的不同进程)之间能够通过网络协议进行数据交换。
套接字的基本概念
-
套接字(Socket):
- 是一种抽象的数据结构,表示网络通信的一个端点。
- 它封装了网络通信所需的相关信息,如 IP 地址、端口号、协议类型等。
-
分类:
- 流式套接字(Stream Socket):
- 使用 TCP 协议,提供面向连接的可靠通信。
- 特点:保证数据传输的顺序和完整性。
- 数据报套接字(Datagram Socket):
- 使用 UDP 协议,提供面向无连接的通信。
- 特点:不保证数据顺序和可靠性,传输速度快。
- 流式套接字(Stream Socket):
-
通信端点:
- 每个套接字通过以下信息唯一标识:
- IP 地址:指定通信的设备位置。
- 端口号:标识设备上具体的应用或服务。
- 每个套接字通过以下信息唯一标识:
套接字编程的特点
- 跨平台:套接字编程支持多种平台(Windows、Linux、嵌入式系统等)。
- 灵活性:可以选择 TCP 或 UDP,根据需求实现不同的通信方式。
- 复杂性:涉及字节序、协议、连接管理等细节。
套接字编程的主要步骤
以 TCP 为例,套接字编程通常分为客户端和服务器两部分,常用的步骤如下:
服务端工作流程:
-
创建套接字:
使用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,表示默认协议。
-
绑定套接字:
将套接字绑定到一个特定的 IP 地址和端口号。int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
-
监听连接:
服务端进入监听状态,等待客户端连接请求。int listen(int sockfd, int backlog);
backlog
:最大等待队列长度。
-
接受连接:
接收客户端的连接请求,并创建一个新的套接字用于通信。int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
-
数据传输:
使用send()
和recv()
进行数据发送和接收。 -
关闭连接:
使用close()
关闭套接字。
客户端工作流程:
-
创建套接字:
与服务端相同,使用socket()
函数创建套接字。 -
连接服务器:
使用connect()
函数向服务器发起连接请求。int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
-
数据传输:
使用send()
和recv()
进行数据交换。 -
关闭连接:
使用close()
关闭套接字。
服务端代码(使用 TCP 协议)示例代码及分析
服务端(使用 TCP 协议)的源代码
#include <stdio.h>
#include <string.h>
#include <arpa/inet.h>
#include <unistd.h>
int main() {
int server_fd, new_socket;
struct sockaddr_in server_addr, client_addr;
char buffer[1024] = {0};
socklen_t addr_len = sizeof(client_addr);
// 创建套接字
server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd == -1) {
perror("Socket creation failed");
return -1;
}
// 初始化地址结构体
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080); // 端口号 8080
server_addr.sin_addr.s_addr = INADDR_ANY; // 绑定到本地所有地址
// 绑定套接字
if (bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
perror("Bind failed");
close(server_fd);
return -1;
}
// 监听连接
if (listen(server_fd, 5) == -1) {
perror("Listen failed");
close(server_fd);
return -1;
}
printf("Server is listening on port 8080\n");
// 接受客户端连接
new_socket = accept(server_fd, (struct sockaddr*)&client_addr, &addr_len);
if (new_socket == -1) {
perror("Accept failed");
close(server_fd);
return -1;
}
// 接收数据
recv(new_socket, buffer, sizeof(buffer), 0);
printf("Received: %s\n", buffer);
// 发送响应
send(new_socket, "Hello, Client!", 14, 0);
// 关闭套接字
close(new_socket);
close(server_fd);
return 0;
}
代码int server_fd, new_socket;
分析(int server_fd、new_socket两个变量的作用)
int server_fd, new_socket;
在服务端代码中:
server_fd
和new_socket
是两个不同的文件描述符(File Descriptor),它们在服务端套接字的不同阶段分别扮演不同的角色。
1. server_fd
的作用
server_fd
是服务器的监听套接字,用于:
- 创建套接字:通过
socket()
函数创建。 - 绑定地址和端口:通过
bind()
将套接字绑定到特定的 IP 地址和端口。 - 监听连接请求:通过
listen()
进入监听状态,等待客户端连接。
server_fd
的主要职责是 监听客户端的连接请求,但它本身不用于与客户端通信。
2. new_socket
的作用
new_socket
是用来和客户端通信的套接字,由 accept()
函数返回。
- 当客户端发起连接请求时,
accept()
会从server_fd
监听的连接队列中取出一个连接,并创建一个新的套接字。 - 这个新的套接字(
new_socket
)表示 服务器与该客户端之间的连接。 - 通过
new_socket
,服务器可以与客户端进行数据传输。
每次有新的客户端连接时,accept()
都会返回一个新的套接字供服务器与该客户端通信,而 server_fd
继续负责监听其他客户端的连接请求。
具体工作流程举例
-
server_fd
:- 你可以把它看成是服务器的大门,负责接待来访者(客户端)。
- 它永远不会直接与来访者交谈,只负责接收来访者的请求并开门(监听和接受连接)。
-
new_socket
:- 你可以把它看成是为来访者安排的接待室。
- 每个来访者(客户端)都有自己专属的接待室(
new_socket
),服务器通过这个房间与来访者进行交谈(数据传输)。
代码struct sockaddr_in server_addr, client_addr;
分析
struct sockaddr_in server_addr, client_addr;
关于其中涉及到的结构体sockaddr_in的介绍,见我的另一篇博文 https://blog.csdn.net/wenhao_ir/article/details/144660421
弄清了结构体sockaddr_in的情况后,这里来说下server_addr,和client_addr的作用。
这里 server_addr
和 client_addr
是两个 sockaddr_in
类型的结构体,它们分别表示服务端和客户端的网络地址信息。两者在程序中有不同的作用。
1. server_addr
的作用
server_addr
用于描述 服务器的地址信息,包括:
-
IP 地址:
- 表示服务端在哪个网络接口上监听(例如:本地地址、特定 IP 地址等)。
- 通常设置为
INADDR_ANY
,表示监听所有本地IP地址。
-
端口号:
- 指定服务端监听的端口号,例如 8080。
- 客户端通过 IP 地址和这个端口号来连接服务器。
-
用途:
- 在调用
bind()
函数时,将server_addr
传递进去,把服务端的套接字绑定到特定的 IP 地址和端口上。
- 在调用
相关代码片段:
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET; // IPv4 地址族
server_addr.sin_port = htons(8080); // 监听端口号 8080
server_addr.sin_addr.s_addr = INADDR_ANY; // 监听所有本地IP地址
2. client_addr
的作用
client_addr
用于描述 客户端的地址信息,包括:
-
IP 地址:
- 表示客户端的来源 IP 地址。
- 当客户端连接到服务器时,服务器通过
accept()
函数获取客户端的 IP 地址。
-
端口号:
- 表示客户端的源端口号。
-
用途:
- 在
accept()
函数中用来存储连接的客户端的网络地址信息。 - 通过
client_addr
,服务器可以知道是哪一个客户端连接过来了。 - 例如,可以使用
inet_ntoa(client_addr.sin_addr)
将 IP 地址转换为字符串打印出来。
- 在
相关代码片段:
struct sockaddr_in client_addr;
socklen_t addr_len = sizeof(client_addr);
// accept() 会将客户端的地址信息存入 client_addr
int new_socket = accept(server_fd, (struct sockaddr*)&client_addr, &addr_len);
printf("Client connected from %s:%d\n",
inet_ntoa(client_addr.sin_addr), // 打印客户端 IP 地址
ntohs(client_addr.sin_port)); // 打印客户端端口号
两者的主要区别
字段 | server_addr | client_addr |
---|---|---|
含义 | 描述服务器的网络地址信息 | 描述连接到服务器的客户端的地址信息 |
作用 | 用于 bind() ,指定服务端监听的地址和端口 | 用于 accept() ,存储客户端的来源地址 |
设置方式 | 由服务器程序显式设置 | 由操作系统在客户端连接时自动填写 |
生命周期 | 服务端初始化时配置,贯穿程序运行 | 每次有新的客户端连接时更新 |
两者的使用场景
server_addr
使用:
- 在调用
bind()
之前,初始化服务端监听的 IP 和端口号。 - 服务端通过它告诉操作系统在哪个地址和端口监听连接。
client_addr
使用:
- 在调用
accept()
时,获取与服务器建立连接的客户端地址和端口。 - 可用于记录、打印日志或进行特定的客户端身份验证。
结合代码解释
在完整的服务端代码中,两者的作用可以直观看出:
int main() {
int server_fd, new_socket;
struct sockaddr_in server_addr, client_addr; // 定义两个结构体
char buffer[1024] = {0};
socklen_t addr_len = sizeof(client_addr);
// 创建套接字
server_fd = socket(AF_INET, SOCK_STREAM, 0);
// 配置 server_addr(服务端地址信息)
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080); // 服务端监听端口
server_addr.sin_addr.s_addr = INADDR_ANY; // 监听所有本地IP地址
// 绑定服务端地址
bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr));
// 进入监听状态
listen(server_fd, 5);
printf("Server is listening on port 8080\n");
// 接受客户端连接,获取客户端地址信息
new_socket = accept(server_fd, (struct sockaddr*)&client_addr, &addr_len);
printf("Client connected from %s:%d\n",
inet_ntoa(client_addr.sin_addr), // 获取客户端 IP 地址
ntohs(client_addr.sin_port)); // 获取客户端端口号
// 进行数据传输
recv(new_socket, buffer, sizeof(buffer), 0);
printf("Received: %s\n", buffer);
send(new_socket, "Hello, Client!", 14, 0);
close(new_socket);
close(server_fd);
return 0;
}
小结
server_addr
:用于配置服务器监听的地址和端口,是服务器主动设置的。client_addr
:用于保存客户端连接的地址和端口,是操作系统在accept()
中自动填写的。
“创建套接字”的代码分析
// 创建套接字
server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd == -1) {
perror("Socket creation failed");
return -1;
}
要理解这段代码关键是理解下面这句代码:
server_fd = socket(AF_INET, SOCK_STREAM, 0);
对上面这句代码的理解如下:
这句代码是用于创建一个套接字,具体含义如下:
server_fd = socket(AF_INET, SOCK_STREAM, 0);
逐部分解析
1. socket()
函数
- 作用:
socket()
函数用于创建一个套接字(socket)。 - 返回值:如果成功,返回一个文件描述符(整型值);如果失败,返回
-1
,并设置errno
表示错误原因。
套接字是网络通信的基本概念,表示一个通信端点。服务端和客户端都通过套接字来实现数据的发送和接收。
2. 参数详解
(1) AF_INET
- 表示 地址族(Address Family)。
AF_INET
指定使用 IPv4 协议。- 如果需要使用 IPv6,可以用
AF_INET6
。
(2) SOCK_STREAM
-
表示 套接字类型。
-
SOCK_STREAM
指定使用面向连接的流式套接字,即 TCP 协议。- 数据可靠、顺序传输。
- 提供双向字节流通信。
-
另一种常见的套接字类型是
SOCK_DGRAM
,用于 UDP(无连接协议)。
(3) 0
- 表示 协议编号。
- 通常为 0,表示根据前面的参数(
AF_INET
和SOCK_STREAM
)自动选择合适的协议。- 对于
AF_INET
和SOCK_STREAM
,协议默认是 TCP。 - 对于
AF_INET
和SOCK_DGRAM
,协议默认是 UDP。
- 对于
结果
-
成功:
- 创建一个支持 IPv4 和 TCP 协议的套接字。
- 返回的值是一个文件描述符(如
server_fd
),可以用来进一步操作套接字(如绑定地址、监听、接受连接等)。
-
失败:
- 返回
-1
,表示创建失败。 - 通常会检查
errno
的值来确定错误原因。
- 返回
示例:完整流程
以下代码演示了如何通过 socket()
创建一个 TCP 套接字并检查是否成功:
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
int main() {
int server_fd;
// 创建套接字
server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd == -1) {
perror("Socket creation failed");
exit(EXIT_FAILURE);
}
printf("Socket created successfully, fd: %d\n", server_fd);
// 后续可以绑定、监听等操作
close(server_fd);
return 0;
}
小结
server_fd = socket(AF_INET, SOCK_STREAM, 0);
- 创建一个 IPv4 地址族、面向连接(TCP)的套接字。
- 返回的文件描述符用于后续网络操作。
- 关键点:
AF_INET
指 IPv4。SOCK_STREAM
指 TCP 协议。0
表示默认协议。
- 常见错误:
- 系统资源不足。
- 权限问题(低端口绑定可能需要特权)。
“初始化地址结构体”的代码分析
// 初始化地址结构
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080); // 端口号 8080
server_addr.sin_addr.s_addr = INADDR_ANY; // 绑定到本地所有地址
如果读了对结构体sockaddr_in
的介绍(https://blog.csdn.net/wenhao_ir/article/details/144660421)
和上面对两个sockaddr_in
类型的实例server_addr、client_addr的介绍,就知道这几句代码的含义了,所以这里不再赘述了。
“绑定套接字”的代码分析
// 绑定套接字
if (bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
perror("Bind failed");
close(server_fd);
return -1;
}
关键是下面这句代码的理解:
bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr))
前两个参数没啥好说的,只是不明白为什么要有第3个参数sizeof(server_addr)
?
答:在服务端的代码中,调用 bind()
函数时,第 3 个参数 sizeof(server_addr)
的作用是 告诉操作系统 server_addr
结构体的大小。这是因为 bind()
函数需要知道绑定地址的信息有多大,以便正确解析和使用。
bind()
函数的定义
bind()
函数的原型如下(位于 <sys/socket.h>
中):
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd
:由socket()
创建的文件描述符,指定要绑定的套接字。addr
:指向包含绑定地址信息的结构体(通常是struct sockaddr_in
的指针)。addrlen
:指定绑定地址结构的大小,类型为socklen_t
。
为什么需要 sizeof(server_addr)
?
-
通用性:
addr
是一个通用指针(struct sockaddr *
),表示任意类型的套接字地址。- 不同协议族(如 IPv4、IPv6)使用不同的地址结构(如
sockaddr_in
或sockaddr_in6
),它们的大小可能不同。 addrlen
告诉操作系统传递的具体地址结构的大小,以便正确读取结构体中的内容。
-
安全性:
- 通过显式提供地址结构的大小,
bind()
函数可以防止读取越界或不完整的数据,确保代码的健壮性。
- 通过显式提供地址结构的大小,
-
兼容性:
- 未来或其他系统可能引入不同大小的地址结构,明确传递大小可以保证代码在多种环境下都能正确运行。
sizeof(server_addr)
的实际意义
在这里,sizeof(server_addr)
计算的是 struct sockaddr_in
的大小,因为 server_addr
是这个类型的变量。
示例:
struct sockaddr_in
的大小通常是:
struct sockaddr_in {
short sin_family; // 地址族,AF_INET
unsigned short sin_port; // 端口号
struct in_addr sin_addr; // IP 地址
char sin_zero[8]; // 填充位(为了对齐)
};
在大多数系统中,这个结构的大小为 16 字节。
传递 sizeof(server_addr)
:
- 操作系统会根据传递的大小,读取
server_addr
中的字段(例如sin_family
、sin_port
和sin_addr
)。 - 如果没有正确指定大小,操作系统可能读取错误的数据,导致程序行为异常。
错误示例
如果传递的大小错误,可能会导致以下问题:
- 如果大小 小于实际结构体大小:
- 操作系统可能只读取到部分数据,未读取的字段可能被视为未初始化,导致绑定失败或产生不可预测的行为。
- 如果大小 大于实际结构体大小:
- 操作系统可能尝试读取超出范围的数据,可能导致内存访问错误。
小结
sizeof(server_addr)
是为了告诉操作系统绑定地址的具体结构体大小。- 这保证了
bind()
函数能够安全且正确地解析传递的地址信息。 - 通常的实践是使用
sizeof(server_addr)
或类似的方法动态获取结构体大小,避免手动填写固定值(以减少错误和提高兼容性)。
“监听连接”的代码分析(TCP协议才需要,UDP不需要)
// 监听连接
if (listen(server_fd, 5) == -1) {
perror("Listen failed");
close(server_fd);
return -1;
}
printf("Server is listening on port 8080\n");
关键是对下面这句代码的理解:
listen(server_fd, 5)
答:这句代码是服务端套接字程序中的一个关键步骤,用于让套接字进入监听状态,以便接受来自客户端的连接请求。
注意:这里由于使用的协议是TCP协议,TCP 需要 listen() 来等待客户端连接请求,所以需要进入监听状态,如果是UDP协议,由于UDP协议是一种无连接的协议,所以不需要去监听客户端连接请求,也就不需要这里的代码。UDP 服务器使用 bind() 来绑定端口后,就可以直接通过 recvfrom() 接收数据包,而不需要监听端口。UDP 服务器通过 recvfrom() 接收所有发往该端口的数据包。
函数原型
listen()
函数的原型定义在 <sys/socket.h>
中:
int listen(int sockfd, int backlog);
参数说明:
-
sockfd
:- 表示服务端套接字的文件描述符(由
socket()
函数创建并经过bind()
函数绑定到指定地址和端口)。 - 它是需要进入监听状态的套接字。
- 表示服务端套接字的文件描述符(由
-
backlog
:- 指定待处理连接的最大数量(即未被
accept()
处理的连接请求队列的长度)。 - 当多个客户端同时发起连接时,系统会将这些连接请求存储在一个队列中,
backlog
就是该队列的最大长度。
- 指定待处理连接的最大数量(即未被
返回值:
- 成功返回
0
。 - 失败返回
-1
,并设置errno
以描述错误原因。
listen()
的作用
-
将套接字转换为被动套接字:
- 调用
listen()
后,套接字变成一个 监听套接字,用于接受客户端的连接请求。 - 监听套接字本身不用于数据传输,它只是一个连接管理工具。
- 实际的数据传输将由
accept()
返回的新的套接字完成。
- 调用
-
设置连接队列的大小:
- 如果客户端连接数量超过
backlog
值,超出的连接将被拒绝(或根据协议具体处理)。 - 一旦队列中有空位,新的连接请求可以重新排入队列。
- 如果客户端连接数量超过
参数 backlog
的意义
backlog
决定了服务端能够同时处理的连接请求的数量上限。
队列的实际行为:
-
队列分为两部分:
- 完全连接队列(已完成三次握手的连接请求)。
- 半连接队列(正在进行三次握手的连接请求)。
-
backlog
设置的值通常会影响完全连接队列的大小。
系统实际行为:
- 在某些系统中,
backlog
的值可能会被内核调整到一个上限(由系统参数决定)。 - 例如:
- 在 Linux 上,可以通过
/proc/sys/net/core/somaxconn
查看和修改backlog
的最大值(默认是 128)。
- 在 Linux 上,可以通过
重要注意点
-
必须在调用
listen()
前绑定套接字:- 在调用
listen()
前,必须使用bind()
函数将套接字绑定到具体的 IP 地址和端口号,否则无法监听。
- 在调用
-
backlog
的大小不是绝对值:- 实际队列大小可能会受到操作系统的限制。
- 例如,在 Linux 上,设置
listen(fd, 1000)
时,队列可能会被限制为系统参数somaxconn
的值(默认 128)。
-
多连接的处理:
- 如果
backlog
队列已满,操作系统通常会拒绝新的连接请求,客户端可能会收到连接失败的错误。
- 如果
小结
listen()
将一个套接字转变为 监听套接字,使其能够接受客户端连接请求。- 第二个参数
backlog
决定了连接请求队列的最大长度,但系统可能会对其值施加限制。 - 这是服务端套接字编程的关键步骤之一,配合后续的
accept()
函数实现对客户端连接的处理。
“接受客户端连接”的代码分析
// 接受客户端连接
new_socket = accept(server_fd, (struct sockaddr*)&client_addr, &addr_len);
if (new_socket == -1) {
perror("Accept failed");
close(server_fd);
return -1;
}
这段代码的理解关键是理解下面这句代码:
new_socket = accept(server_fd, (struct sockaddr*)&client_addr, &addr_len);
这句代码是服务端程序中处理客户端连接的关键一步。它从监听套接字 server_fd
的连接请求队列中取出一个连接,并为这个连接创建一个新的套接字 new_socket
。
accept()
函数的作用
accept()
函数的作用是:
- 从已完成连接(三次握手)的客户端队列中取出一个连接请求。
- 创建一个新的套接字,专门用于与这个客户端进行通信。
- 返回新套接字的文件描述符,供服务端使用。
函数原型
accept()
的函数原型如下(位于 <sys/socket.h>
中):
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数说明:
-
sockfd
:- 表示监听套接字的文件描述符(
server_fd
),由之前调用socket()
和bind()
创建并通过listen()
进入监听状态。 accept()
从该套接字的连接队列中取出一个连接。
- 表示监听套接字的文件描述符(
-
addr
:- 指向
struct sockaddr
类型的缓冲区,用于存储客户端的地址信息。 - 通常将其强制类型转换为
struct sockaddr_in*
(对于 IPv4)或其他地址结构类型。
- 指向
-
addrlen
:- 指向一个
socklen_t
类型的变量,用于存储addr
结构体的大小。 - 调用前需要将变量的值设置为
addr
缓冲区的大小;调用后,该变量会被更新为实际存储的地址信息大小。
- 指向一个
返回值:
- 成功时:返回一个新的文件描述符,表示与客户端连接的套接字。
- 失败时:返回
-1
,并设置errno
以描述错误原因。
代码解析
new_socket = accept(server_fd, (struct sockaddr*)&client_addr, &addr_len);
逐部分解释:
-
server_fd
:- 指定监听套接字(
server_fd
),从其连接队列中取出一个完成连接的客户端请求。
- 指定监听套接字(
-
(struct sockaddr*)&client_addr
:client_addr
是一个struct sockaddr_in
类型的变量,用于存储客户端的地址信息。- 使用
(struct sockaddr*)
进行类型转换,因为accept()
的参数类型是struct sockaddr*
。
-
&addr_len
:addr_len
是一个socklen_t
类型的变量,传递client_addr
结构体的大小。- 调用前,它的值是
sizeof(client_addr)
;调用后,它会被更新为实际填充的地址信息的大小(通常不会改变)。
-
返回值赋值给
new_socket
:accept()
返回的新套接字文件描述符(new_socket
)专门用于与该客户端通信。- 通过
new_socket
,服务端可以发送和接收数据。
重点解读
-
client_addr
的作用:client_addr
存储了客户端的地址信息,包括 IP 地址和端口号。- 可以通过工具函数(如
inet_ntop()
)将地址转换为人类可读的字符串。
-
new_socket
的作用:new_socket
是为某个具体的客户端连接创建的套接字。- 它与客户端的通信完全独立于
server_fd
。 - 每次调用
accept()
,都会返回一个新的文件描述符。
-
多个客户端连接:
- 如果有多个客户端连接,服务端可以多次调用
accept()
来逐个处理连接请求。
- 如果有多个客户端连接,服务端可以多次调用
常见问题
-
accept()
会阻塞吗?- 如果没有连接请求,
accept()
默认会阻塞,直到有客户端发起连接。 - 如果想避免阻塞,可以将套接字设置为非阻塞模式。
- 如果没有连接请求,
-
如果连接队列为空怎么办?
- 如果连接队列为空,
accept()
会阻塞(或在非阻塞模式下返回-1
,并设置errno
为EAGAIN
或EWOULDBLOCK
)。
- 如果连接队列为空,
-
addr
和addrlen
是否可以为NULL
?- 可以:
- 如果不关心客户端地址信息,可以将
addr
和addrlen
设置为NULL
。 - 但这样做无法获取客户端的 IP 地址和端口号。
- 如果不关心客户端地址信息,可以将
- 可以:
小结
accept()
从连接队列中取出一个客户端连接,为其创建一个新套接字。new_socket
是这个连接专用的套接字,可以用于数据收发。client_addr
并配合addr_len
可用于获取客户端的 IP 地址和端口号,帮助服务端识别连接的来源。
“接收数据”的代码的分析
// 接收数据
recv(new_socket, buffer, sizeof(buffer), 0);
printf("Received: %s\n", buffer);
要理解这段代码,关键是理解语句:
recv(new_socket, buffer, sizeof(buffer), 0);
答:这句代码是服务端程序中用于接收客户端发送的数据的一部分,具体调用了 recv()
函数来从客户端套接字 new_socket
接收数据。
recv()
函数的作用
recv()
函数用于从套接字中接收数据,它会阻塞等待数据的到来,直到接收到数据或发生错误。
函数原型
recv()
函数的原型如下(位于 <sys/socket.h>
中):
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
参数说明:
-
sockfd
:sockfd
是套接字文件描述符。在这里,new_socket
是通过accept()
函数返回的专用于与某个客户端通信的套接字。new_socket
用于从客户端接收数据。
-
buf
:buf
是一个指向缓冲区的指针,用于存储接收到的数据。- 在这段代码中,
buffer
是一个字符数组,存储从客户端接收到的数据。
-
len
:len
表示接收数据的最大长度。- 在这段代码中,
sizeof(buffer)
表示缓冲区的大小,即最多接收buffer
所能容纳的字节数。
-
flags
:flags
是控制接收行为的标志位。- 在这段代码中,设置为
0
,表示默认的行为,不使用特殊的标志。
返回值:
- 成功时,返回实际接收到的字节数(
ssize_t
类型)。 - 如果连接关闭,返回
0
。 - 如果出错,返回
-1
,并设置errno
表示错误原因。
代码解析
recv(new_socket, buffer, sizeof(buffer), 0);
逐部分解释:
-
new_socket
:- 这是与客户端连接的套接字,由
accept()
返回。 recv()
会从这个套接字接收数据。
- 这是与客户端连接的套接字,由
-
buffer
:buffer
是一个缓冲区,用来存储从客户端接收到的数据。- 在
recv()
中,buffer
是用来接收数据的地方,通常是一个字符数组或其他合适类型的数据结构。
-
sizeof(buffer)
:sizeof(buffer)
是buffer
缓冲区的大小,表示最多可以接收多少字节的数据。sizeof(buffer)
返回buffer
数组的字节数,它告诉recv()
最大接收字节数。- 如果客户端发送的数据超过了
sizeof(buffer)
的大小,则recv()
只会接收sizeof(buffer)
大小的数据,剩余的数据将会被丢弃。
-
0
:flags
参数设为0
表示使用默认的接收行为。- 在特殊情况下,可以设置不同的标志(例如
MSG_WAITALL
、MSG_PEEK
等)来改变接收的方式,但默认0
是最常用的。
recv()
的行为
recv()
会阻塞,直到从客户端接收到数据。- 如果客户端关闭了连接,
recv()
返回0
,表示连接已关闭。 - 如果发生错误,
recv()
返回-1
,并通过errno
返回错误代码。
示例:
以下是 recv()
的可能返回值及其含义:
- 返回正数(n):
- 成功接收了
n
字节的数据,n
小于或等于sizeof(buffer)
。
- 成功接收了
- 返回
0
:- 客户端已关闭连接(TCP 连接正常关闭)。
- 返回
-1
:- 出现错误,
errno
会指示错误原因(如EAGAIN
、ECONNRESET
等)。
- 出现错误,
"发送响应"的代码
// 发送响应
send(new_socket, "Hello, Client!", 14, 0);
这段代码的分析略,后面两个参数的意义和接收数据代码中的函数recv()
的意义一样。注意:字符串 "Hello, Client!"的长度刚好是14。
客户端代码(使用 TCP 协议)示例代码及分析
分析前的说明
这个示例代码与前面的服务端的示例代码有很多知识点是重合的,重合知识点的相关代码这里就不再分析了。
客户端代码(使用 TCP 协议)的源代码
#include <stdio.h>
#include <string.h>
#include <arpa/inet.h>
#include <unistd.h>
int main() {
int client_fd;
struct sockaddr_in server_addr;
char buffer[1024] = {0};
// 创建套接字
client_fd = socket(AF_INET, SOCK_STREAM, 0);
if (client_fd == -1) {
perror("Socket creation failed");
return -1;
}
// 初始化服务器地址
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080);
inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr);
// 连接服务器
if (connect(client_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
perror("Connection failed");
close(client_fd);
return -1;
}
// 发送数据
send(client_fd, "Hello, Server!", 14, 0);
// 接收响应
recv(client_fd, buffer, sizeof(buffer), 0);
printf("Received: %s\n", buffer);
// 关闭套接字
close(client_fd);
return 0;
}
“初始化服务器地址”的代码分析
// 初始化服务器地址
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080);
inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr);
关键是下面这句代码的理解:
inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr);
答:这句代码用于将一个 IPv4 地址(在本例中是 “127.0.0.1”)从 文本格式 转换为 二进制格式,并存储到 server_addr.sin_addr
中。inet_pton()
函数就是用来完成这项任务的。
inet_pton()
函数
inet_pton()
是 “网络地址文本到二进制”的缩写,用于将 IP 地址从人类可读的文本字符串转换为计算机能够处理的二进制格式。
函数原型
int inet_pton(int af, const char *src, void *dst);
参数说明:
-
af
:- 地址族(Address Family)。在这里是
AF_INET
,表示 IPv4 地址。 - 对于 IPv6 地址,可以使用
AF_INET6
。
- 地址族(Address Family)。在这里是
-
src
:- 目标 IP 地址的文本字符串。它是一个 以点分十进制表示的 IPv4 地址(如 “127.0.0.1”)或者 IPv6 地址(如果使用
AF_INET6
)。 - 在这句代码中,
"127.0.0.1"
是一个本地回环地址(localhost)。
- 目标 IP 地址的文本字符串。它是一个 以点分十进制表示的 IPv4 地址(如 “127.0.0.1”)或者 IPv6 地址(如果使用
-
dst
:- 指向存储转换结果的缓冲区。在这里,它是
server_addr.sin_addr
,它是一个struct in_addr
类型的变量。 sin_addr
是struct sockaddr_in
结构体的一个成员,表示与目标主机的连接相关的 IP 地址。
- 指向存储转换结果的缓冲区。在这里,它是
返回值:
- 成功时,返回
1
。 - 如果输入无效(例如不合法的 IP 地址),返回
0
。 - 出错时,返回
-1
,并设置errno
以指示错误原因。
代码解析
inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr);
逐部分解释:
-
AF_INET
:- 表示使用 IPv4 地址,这是套接字编程中常见的地址族。
- 对于 IPv6 地址,应该使用
AF_INET6
。
-
"127.0.0.1"
:- 这是一个 IPv4 地址 字符串,表示 本地回环地址。该地址用于指代本机(即客户端与服务端在同一台机器上通信)。
- 这种地址通常用于测试本地服务或程序。
-
&server_addr.sin_addr
:server_addr
是一个struct sockaddr_in
类型的变量,代表服务端的地址信息。sin_addr
是struct sockaddr_in
中的一个字段,专门用来存储 IPv4 地址,它的类型是struct in_addr
,后者是一个包含s_addr
字段的结构体,s_addr
用来存储二进制形式的 IP 地址。inet_pton()
函数将解析后的 二进制格式 IP 地址 存储到server_addr.sin_addr.s_addr
中。
inet_pton()
与 inet_ntop()
的区别
inet_pton()
(用于转换文本到二进制):将 IP 地址从 文本格式 转换为 二进制格式。inet_ntop()
(用于转换二进制到文本):将 IP 地址从 二进制格式 转换为 文本格式,常用于将sin_addr
或in_addr
转换回人类可读的字符串。
总结
inet_pton()
用于将 IPv4 地址(如"127.0.0.1"
)从 文本格式 转换为 二进制格式,并将其存储在server_addr.sin_addr
中,准备进行套接字连接。AF_INET
表示使用 IPv4 地址,sin_addr
是struct sockaddr_in
结构体中的字段,专门用于存储 IP 地址的二进制形式。- 这个过程确保了程序能正确地使用网络地址进行通信。