Linux嵌入式系统利用套接字编程(Socket Programming)实现网络通信的基础知识并附对一个简单实例的分析

server/2024/12/26 17:05:33/

目录

  • 套接字编程的简介
  • 套接字的基本概念
  • 套接字编程的特点
  • 套接字编程的主要步骤
    • **服务端工作流程**:
    • **客户端工作流程**:
  • 服务端代码(使用 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)接口,允许程序之间在网络上进行通信。套接字可以被看作是网络通信的“端点”,它使得不同主机(甚至同一主机上的不同进程)之间能够通过网络协议进行数据交换。

套接字的基本概念

  1. 套接字(Socket)

    • 是一种抽象的数据结构,表示网络通信的一个端点。
    • 它封装了网络通信所需的相关信息,如 IP 地址、端口号、协议类型等。
  2. 分类

    • 流式套接字(Stream Socket)
      • 使用 TCP 协议,提供面向连接的可靠通信。
      • 特点:保证数据传输的顺序和完整性。
    • 数据报套接字(Datagram Socket)
      • 使用 UDP 协议,提供面向无连接的通信。
      • 特点:不保证数据顺序和可靠性,传输速度快。
  3. 通信端点

    • 每个套接字通过以下信息唯一标识:
      • IP 地址:指定通信的设备位置。
      • 端口号:标识设备上具体的应用或服务。

套接字编程的特点

  1. 跨平台套接字编程支持多种平台(Windows、Linux、嵌入式系统等)。
  2. 灵活性:可以选择 TCP 或 UDP,根据需求实现不同的通信方式。
  3. 复杂性:涉及字节序、协议、连接管理等细节。

套接字编程的主要步骤

以 TCP 为例,套接字编程通常分为客户端和服务器两部分,常用的步骤如下:

服务端工作流程

  1. 创建套接字
    使用 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,表示默认协议。
  2. 绑定套接字
    套接字绑定到一个特定的 IP 地址和端口号。

    int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
    
  3. 监听连接
    服务端进入监听状态,等待客户端连接请求。

    int listen(int sockfd, int backlog);
    
    • backlog:最大等待队列长度。
  4. 接受连接
    接收客户端的连接请求,并创建一个新的套接字用于通信。

    int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
    
  5. 数据传输
    使用 send()recv() 进行数据发送和接收。

  6. 关闭连接
    使用 close() 关闭套接字


客户端工作流程

  1. 创建套接字
    与服务端相同,使用 socket() 函数创建套接字

  2. 连接服务器
    使用 connect() 函数向服务器发起连接请求。

    int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
    
  3. 数据传输
    使用 send()recv() 进行数据交换。

  4. 关闭连接
    使用 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);           // 端口号 8080server_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_fdnew_socket 是两个不同的文件描述符(File Descriptor),它们在服务端套接字的不同阶段分别扮演不同的角色。

1. server_fd 的作用

server_fd 是服务器的监听套接字,用于:

  1. 创建套接字:通过 socket() 函数创建。
  2. 绑定地址和端口:通过 bind()套接字绑定到特定的 IP 地址和端口。
  3. 监听连接请求:通过 listen() 进入监听状态,等待客户端连接。

server_fd 的主要职责是 监听客户端的连接请求,但它本身不用于与客户端通信。


2. new_socket 的作用

new_socket 是用来和客户端通信的套接字,由 accept() 函数返回。

  1. 当客户端发起连接请求时,accept() 会从 server_fd 监听的连接队列中取出一个连接,并创建一个新的套接字
  2. 这个新的套接字new_socket)表示 服务器与该客户端之间的连接
  3. 通过 new_socket,服务器可以与客户端进行数据传输。

每次有新的客户端连接时,accept() 都会返回一个新的套接字供服务器与该客户端通信,而 server_fd 继续负责监听其他客户端的连接请求。


具体工作流程举例

  1. server_fd:

    • 你可以把它看成是服务器的大门,负责接待来访者(客户端)。
    • 它永远不会直接与来访者交谈,只负责接收来访者的请求并开门(监听和接受连接)。
  2. 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_addrclient_addr是两个 sockaddr_in 类型的结构体,它们分别表示服务端和客户端的网络地址信息。两者在程序中有不同的作用。

1. server_addr 的作用

server_addr 用于描述 服务器的地址信息,包括:

  1. IP 地址

    • 表示服务端在哪个网络接口上监听(例如:本地地址、特定 IP 地址等)。
    • 通常设置为 INADDR_ANY,表示监听所有本地IP地址。
  2. 端口号

    • 指定服务端监听的端口号,例如 8080。
    • 客户端通过 IP 地址和这个端口号来连接服务器。
  3. 用途

    • 在调用 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 用于描述 客户端的地址信息,包括:

  1. IP 地址

    • 表示客户端的来源 IP 地址。
    • 当客户端连接到服务器时,服务器通过 accept() 函数获取客户端的 IP 地址。
  2. 端口号

    • 表示客户端的源端口号。
  3. 用途

    • 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_addrclient_addr
含义描述服务器的网络地址信息描述连接到服务器的客户端的地址信息
作用用于 bind(),指定服务端监听的地址和端口用于 accept(),存储客户端的来源地址
设置方式由服务器程序显式设置由操作系统在客户端连接时自动填写
生命周期服务端初始化时配置,贯穿程序运行每次有新的客户端连接时更新

两者的使用场景

server_addr 使用
  1. 在调用 bind() 之前,初始化服务端监听的 IP 和端口号。
  2. 服务端通过它告诉操作系统在哪个地址和端口监听连接。
client_addr 使用
  1. 在调用 accept() 时,获取与服务器建立连接的客户端地址和端口。
  2. 可用于记录、打印日志或进行特定的客户端身份验证。

结合代码解释

在完整的服务端代码中,两者的作用可以直观看出:

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_INETSOCK_STREAM)自动选择合适的协议。
    • 对于 AF_INETSOCK_STREAM,协议默认是 TCP。
    • 对于 AF_INETSOCK_DGRAM,协议默认是 UDP。

结果

  1. 成功

    • 创建一个支持 IPv4 和 TCP 协议的套接字
    • 返回的值是一个文件描述符(如 server_fd),可以用来进一步操作套接字(如绑定地址、监听、接受连接等)。
  2. 失败

    • 返回 -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);           // 端口号 8080server_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)

  1. 通用性

    • addr 是一个通用指针(struct sockaddr *),表示任意类型的套接字地址。
    • 不同协议族(如 IPv4、IPv6)使用不同的地址结构(如 sockaddr_insockaddr_in6),它们的大小可能不同。
    • addrlen 告诉操作系统传递的具体地址结构的大小,以便正确读取结构体中的内容。
  2. 安全性

    • 通过显式提供地址结构的大小,bind() 函数可以防止读取越界或不完整的数据,确保代码的健壮性。
  3. 兼容性

    • 未来或其他系统可能引入不同大小的地址结构,明确传递大小可以保证代码在多种环境下都能正确运行。

sizeof(server_addr) 的实际意义

在这里,sizeof(server_addr) 计算的是 struct sockaddr_in 的大小,因为 server_addr 是这个类型的变量。

示例:

struct sockaddr_in 的大小通常是:

struct sockaddr_in {short            sin_family;   // 地址族,AF_INETunsigned short   sin_port;     // 端口号struct in_addr   sin_addr;     // IP 地址char             sin_zero[8];  // 填充位(为了对齐)
};

在大多数系统中,这个结构的大小为 16 字节。

传递 sizeof(server_addr)
  • 操作系统会根据传递的大小,读取 server_addr 中的字段(例如 sin_familysin_portsin_addr)。
  • 如果没有正确指定大小,操作系统可能读取错误的数据,导致程序行为异常。

错误示例

如果传递的大小错误,可能会导致以下问题:

  1. 如果大小 小于实际结构体大小
    • 操作系统可能只读取到部分数据,未读取的字段可能被视为未初始化,导致绑定失败或产生不可预测的行为。
  2. 如果大小 大于实际结构体大小
    • 操作系统可能尝试读取超出范围的数据,可能导致内存访问错误。

小结

  • 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);
参数说明:
  1. sockfd

    • 表示服务端套接字的文件描述符(由 socket() 函数创建并经过 bind() 函数绑定到指定地址和端口)。
    • 它是需要进入监听状态的套接字
  2. backlog

    • 指定待处理连接的最大数量(即未被 accept() 处理的连接请求队列的长度)。
    • 当多个客户端同时发起连接时,系统会将这些连接请求存储在一个队列中,backlog 就是该队列的最大长度。
返回值:
  • 成功返回 0
  • 失败返回 -1,并设置 errno 以描述错误原因。

listen() 的作用

  1. 套接字转换为被动套接字

    • 调用 listen() 后,套接字变成一个 监听套接字,用于接受客户端的连接请求。
    • 监听套接字本身不用于数据传输,它只是一个连接管理工具。
    • 实际的数据传输将由 accept() 返回的新的套接字完成。
  2. 设置连接队列的大小

    • 如果客户端连接数量超过 backlog 值,超出的连接将被拒绝(或根据协议具体处理)。
    • 一旦队列中有空位,新的连接请求可以重新排入队列。

参数 backlog 的意义

backlog 决定了服务端能够同时处理的连接请求的数量上限。

队列的实际行为:
  • 队列分为两部分:

    1. 完全连接队列(已完成三次握手的连接请求)。
    2. 半连接队列(正在进行三次握手的连接请求)。
  • backlog 设置的值通常会影响完全连接队列的大小。

系统实际行为:
  • 在某些系统中,backlog 的值可能会被内核调整到一个上限(由系统参数决定)。
  • 例如:
    • 在 Linux 上,可以通过 /proc/sys/net/core/somaxconn 查看和修改 backlog 的最大值(默认是 128)。

重要注意点

  1. 必须在调用 listen() 前绑定套接字

    • 在调用 listen() 前,必须使用 bind() 函数将套接字绑定到具体的 IP 地址和端口号,否则无法监听。
  2. backlog 的大小不是绝对值

    • 实际队列大小可能会受到操作系统的限制。
    • 例如,在 Linux 上,设置 listen(fd, 1000) 时,队列可能会被限制为系统参数 somaxconn 的值(默认 128)。
  3. 多连接的处理

    • 如果 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() 函数的作用是:

  1. 从已完成连接(三次握手)的客户端队列中取出一个连接请求。
  2. 创建一个新的套接字,专门用于与这个客户端进行通信。
  3. 返回新套接字的文件描述符,供服务端使用。

函数原型

accept() 的函数原型如下(位于 <sys/socket.h> 中):

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数说明:
  1. sockfd

    • 表示监听套接字的文件描述符(server_fd),由之前调用 socket()bind() 创建并通过 listen() 进入监听状态。
    • accept() 从该套接字的连接队列中取出一个连接。
  2. addr

    • 指向 struct sockaddr 类型的缓冲区,用于存储客户端的地址信息。
    • 通常将其强制类型转换为 struct sockaddr_in*(对于 IPv4)或其他地址结构类型。
  3. addrlen

    • 指向一个 socklen_t 类型的变量,用于存储 addr 结构体的大小。
    • 调用前需要将变量的值设置为 addr 缓冲区的大小;调用后,该变量会被更新为实际存储的地址信息大小。
返回值:
  • 成功时:返回一个新的文件描述符,表示与客户端连接的套接字
  • 失败时:返回 -1,并设置 errno 以描述错误原因。

代码解析

new_socket = accept(server_fd, (struct sockaddr*)&client_addr, &addr_len);
逐部分解释:
  1. server_fd

    • 指定监听套接字server_fd),从其连接队列中取出一个完成连接的客户端请求。
  2. (struct sockaddr*)&client_addr

    • client_addr 是一个 struct sockaddr_in 类型的变量,用于存储客户端的地址信息。
    • 使用 (struct sockaddr*) 进行类型转换,因为 accept() 的参数类型是 struct sockaddr*
  3. &addr_len

    • addr_len 是一个 socklen_t 类型的变量,传递 client_addr 结构体的大小。
    • 调用前,它的值是 sizeof(client_addr);调用后,它会被更新为实际填充的地址信息的大小(通常不会改变)。
  4. 返回值赋值给 new_socket

    • accept() 返回的新套接字文件描述符(new_socket)专门用于与该客户端通信。
    • 通过 new_socket,服务端可以发送和接收数据。

重点解读

  1. client_addr 的作用

    • client_addr 存储了客户端的地址信息,包括 IP 地址和端口号。
    • 可以通过工具函数(如 inet_ntop())将地址转换为人类可读的字符串。
  2. new_socket 的作用

    • new_socket 是为某个具体的客户端连接创建的套接字
    • 它与客户端的通信完全独立于 server_fd
    • 每次调用 accept(),都会返回一个新的文件描述符。
  3. 多个客户端连接

    • 如果有多个客户端连接,服务端可以多次调用 accept() 来逐个处理连接请求。

常见问题

  1. accept() 会阻塞吗?

    • 如果没有连接请求,accept() 默认会阻塞,直到有客户端发起连接。
    • 如果想避免阻塞,可以将套接字设置为非阻塞模式。
  2. 如果连接队列为空怎么办?

    • 如果连接队列为空,accept() 会阻塞(或在非阻塞模式下返回 -1,并设置 errnoEAGAINEWOULDBLOCK)。
  3. addraddrlen 是否可以为 NULL

    • 可以:
      • 如果不关心客户端地址信息,可以将 addraddrlen 设置为 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);
参数说明:
  1. sockfd

    • sockfd套接字文件描述符。在这里,new_socket 是通过 accept() 函数返回的专用于与某个客户端通信的套接字
    • new_socket 用于从客户端接收数据。
  2. buf

    • buf 是一个指向缓冲区的指针,用于存储接收到的数据。
    • 在这段代码中,buffer 是一个字符数组,存储从客户端接收到的数据。
  3. len

    • len 表示接收数据的最大长度。
    • 在这段代码中,sizeof(buffer) 表示缓冲区的大小,即最多接收 buffer 所能容纳的字节数。
  4. flags

    • flags 是控制接收行为的标志位。
    • 在这段代码中,设置为 0,表示默认的行为,不使用特殊的标志。
返回值:
  • 成功时,返回实际接收到的字节数(ssize_t 类型)。
  • 如果连接关闭,返回 0
  • 如果出错,返回 -1,并设置 errno 表示错误原因。

代码解析

recv(new_socket, buffer, sizeof(buffer), 0);
逐部分解释:
  1. new_socket

    • 这是与客户端连接的套接字,由 accept() 返回。
    • recv() 会从这个套接字接收数据。
  2. buffer

    • buffer 是一个缓冲区,用来存储从客户端接收到的数据。
    • recv() 中,buffer 是用来接收数据的地方,通常是一个字符数组或其他合适类型的数据结构。
  3. sizeof(buffer)

    • sizeof(buffer)buffer 缓冲区的大小,表示最多可以接收多少字节的数据。
    • sizeof(buffer) 返回 buffer 数组的字节数,它告诉 recv() 最大接收字节数。
    • 如果客户端发送的数据超过了 sizeof(buffer) 的大小,则 recv() 只会接收 sizeof(buffer) 大小的数据,剩余的数据将会被丢弃。
  4. 0

    • flags 参数设为 0 表示使用默认的接收行为。
    • 在特殊情况下,可以设置不同的标志(例如 MSG_WAITALLMSG_PEEK 等)来改变接收的方式,但默认 0 是最常用的。

recv() 的行为

  • recv() 会阻塞,直到从客户端接收到数据。
  • 如果客户端关闭了连接,recv() 返回 0,表示连接已关闭。
  • 如果发生错误,recv() 返回 -1,并通过 errno 返回错误代码。
示例

以下是 recv() 的可能返回值及其含义:

  • 返回正数(n)
    • 成功接收了 n 字节的数据,n 小于或等于 sizeof(buffer)
  • 返回 0
    • 客户端已关闭连接(TCP 连接正常关闭)。
  • 返回 -1
    • 出现错误,errno 会指示错误原因(如 EAGAINECONNRESET 等)。

"发送响应"的代码

    // 发送响应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);
参数说明:
  1. af

    • 地址族(Address Family)。在这里是 AF_INET,表示 IPv4 地址。
    • 对于 IPv6 地址,可以使用 AF_INET6
  2. src

    • 目标 IP 地址的文本字符串。它是一个 以点分十进制表示的 IPv4 地址(如 “127.0.0.1”)或者 IPv6 地址(如果使用 AF_INET6)。
    • 在这句代码中,"127.0.0.1" 是一个本地回环地址(localhost)。
  3. dst

    • 指向存储转换结果的缓冲区。在这里,它是 server_addr.sin_addr,它是一个 struct in_addr 类型的变量。
    • sin_addrstruct sockaddr_in 结构体的一个成员,表示与目标主机的连接相关的 IP 地址。
返回值:
  • 成功时,返回 1
  • 如果输入无效(例如不合法的 IP 地址),返回 0
  • 出错时,返回 -1,并设置 errno 以指示错误原因。

代码解析

inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr);
逐部分解释:
  1. AF_INET

    • 表示使用 IPv4 地址,这是套接字编程中常见的地址族。
    • 对于 IPv6 地址,应该使用 AF_INET6
  2. "127.0.0.1"

    • 这是一个 IPv4 地址 字符串,表示 本地回环地址。该地址用于指代本机(即客户端与服务端在同一台机器上通信)。
    • 这种地址通常用于测试本地服务或程序。
  3. &server_addr.sin_addr

    • server_addr 是一个 struct sockaddr_in 类型的变量,代表服务端的地址信息。
    • sin_addrstruct 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_addrin_addr 转换回人类可读的字符串。

总结

  • inet_pton() 用于将 IPv4 地址(如 "127.0.0.1")从 文本格式 转换为 二进制格式,并将其存储在 server_addr.sin_addr 中,准备进行套接字连接。
  • AF_INET 表示使用 IPv4 地址sin_addrstruct sockaddr_in 结构体中的字段,专门用于存储 IP 地址的二进制形式。
  • 这个过程确保了程序能正确地使用网络地址进行通信。

http://www.ppmy.cn/server/153382.html

相关文章

一、后端到摄像头(监控摄像头IOT)

前言&#xff1a; 开发流程从 后端到摄像头 打通是第一步&#xff0c;那么我们可以着手设计 后端实现 的具体步骤&#xff0c;确保能够稳定地接收和处理来自摄像头的视频流&#xff0c;并提供后续的功能扩展&#xff0c;如视频流转发、存储和控制。 1. 后端系统架构设计 在开始…

科技赋能医疗挂号:SSM 医院预约挂号系统的 Vue 卓越设计与达成

3系统分析 3.1可行性分析 通过对本医院预约挂号系统实行的目的初步调查和分析&#xff0c;提出可行性方案并对其一一进行论证。我们在这里主要从技术可行性、经济可行性、操作可行性等方面进行分析。 3.1.1技术可行性 本医院预约挂号系统采用SSM框架&#xff0c;JAVA作为开发语…

Android简洁缩放Matrix实现图像马赛克,Kotlin

Android简洁缩放Matrix实现图像马赛克&#xff0c;Kotlin 原理&#xff0c;通过Matrix把一个原图缩小到原先的1/n&#xff0c;然后再把缩小后的小图放大n倍&#xff0c;自然就是马赛克效果&#xff08;相当于是放大后像素“糊”成一片了&#xff09;。 import android.content.…

React State(状态)

React State(状态) 引言 在React的世界里&#xff0c;状态&#xff08;State&#xff09;是一个核心概念&#xff0c;它允许我们创建动态和交互式的用户界面。状态是React组件内部数据的存储机制&#xff0c;当状态发生变化时&#xff0c;React会自动重新渲染组件&#xff0c…

RabbitMQ概述

目录 RabbitMQ概述 前言 MQ MQ的作用 为什么选择RabbitMQ RabbitMQ的介绍 RabbitMQ概述 前言 Rabbit, 兔⼦的意思 互联⽹⾏业很多公司, 都喜欢⽤动物命名产品, 或者作为公司的logo, 吉祥物. ⽐如: 腾讯的企鹅, 京东的狗, 美团的袋⿏, 携程的海豚,阿⾥就更多了, 蚂蚁, ⻜…

解决需要用到1.x版本的tensorflow环境的问题

实在不行,组个tensorflow的服务器吧,方便! 是tensorflow环境下运行的,因此需要配置tensorflow环境。 首先在linux服务器上安装anoconda,可以直接下载.sh安装包,然后上传到服务器,使用bash Anxxx.sh,执行安装命令 安装好之后创建一个新的虚拟环境,注意Python版本的选…

vue3 Proxy替换vue2 defineProperty的原因

在 Vue 3 中&#xff0c;响应式系统选择使用 Proxy 代替 Vue 2 中的 Object.defineProperty&#xff0c;主要是因为 Proxy 提供了更强大、更灵活的能力&#xff0c;可以解决 Vue 2 中使用 Object.defineProperty 的一些局限性和性能问题。 以下是详细的原因和对比&#xff1a;…

uniapp 基于xgplayer(西瓜视频) + renderjs开发,实现APP视频播放

背景&#xff1a;在uniapp中因原生video组件功能有限&#xff0c;选择引入xgplayer库来展示视频播放等功能。并且APP端无法操作dom&#xff0c;所以使用了renderjs。 其他的不多说&#xff0c;主要列举一下renderjs中需要注意的点&#xff1a; 1、使用&#xff1a;在标签后&…