这几天每天都在与Excel打交道，重复着相同工作表的拆分、数据导出，今天分享一下这几天工作中常用的VBA脚本，让我从重复劳动中彻底解放，现在每天都能准时摸鱼！ 一键拆分多个工作表保存 使用场景：一个包含多个工作表的工作簿，要求每个表单独保存为文件时 Sub SplitWorkbookToCurrentFolder() Dim ws As Worksheet Dim newWorkbook As Workbook Dim savePath As String Dim fileName As String Dim originalWorkbook As Workbook Set originalWorkbook = ThisWorkbook ' 获取当前工作簿所在的文件夹路径 If originalWorkbook.Path <> "" Then savePath = originalWorkbook.Path & "\" Else ' 如果工作簿未保存，使用桌面路径 savePath = CreateObject("WScript.Shell").SpecialFolders("Desktop") & "\" MsgBox "当前工作簿尚未保存，文件将保存到桌面！", vbInformation End If Application.ScreenUpdating = False Application.DisplayAlerts = False On Error GoTo ErrorHandler For Each ws In originalWorkbook.Worksheets ' 复制工作表到新工作簿 ws.Copy Set newWorkbook = ActiveWorkbook ' 生成文件名：工作表名称 + 比对结果 fileName = savePath & ws.Name & "_比对结果.xlsx" ' 处理文件名重复的情况 If Dir(fileName) <> "" Then Dim counter As Integer counter = 1 Do While Dir(savePath & ws.Name & "_比对结果(" & counter & ").xlsx") <> "" counter = counter + 1 Loop fileName = savePath & ws.Name & "_比对结果(" & counter & ").xlsx" End If ' 保存文件 newWorkbook.SaveAs fileName, FileFormat:=xlOpenXMLWorkbook ' 关闭新工作簿 newWorkbook.Close SaveChanges:=False ' 释放对象 Set newWorkbook = Nothing Next ws Application.ScreenUpdating = True Application.DisplayAlerts = True MsgBox "所有工作表已成功保存为独立文件！" & vbNewLine & _ "保存路径：" & savePath, vbInformation, "完成" Exit Sub ErrorHandler: Application.ScreenUpdating = True Application.DisplayAlerts = True MsgBox "保存过程中出现错误：" & Err.Description, vbCritical, "错误" End Sub 表格一键导出为图片 需要将工作薄里各个工作表里的表格数据以图片形式保存 Sub ExportAllSheetsToImages() Dim ws As Worksheet Dim rng As Range Dim cht As ChartObject Dim savePath As String Dim fileName As String Dim lastRow As Long ' 设置保存路径为当前文件夹 savePath = ThisWorkbook.Path & "\" If savePath = "\" Then MsgBox "请先保存工作簿！", vbExclamation Exit Sub End If Application.ScreenUpdating = False Application.CutCopyMode = False For Each ws In ThisWorkbook.Worksheets If ws.Visible = xlSheetVisible Then ' 动态确定数据范围（从B1开始） With ws lastRow = .Cells(.Rows.Count, "B").End(xlUp).Row If lastRow < 1 Then lastRow = 1 Set rng = .Range("B1:H" & lastRow) End With ' 复制区域为图片 rng.CopyPicture Appearance:=xlScreen, Format:=xlPicture ' 创建图表对象 Set cht = ws.ChartObjects.Add(Left:=rng.Left, Top:=rng.Top, _ Width:=rng.Width, Height:=rng.Height) cht.Activate With cht.Chart ' 关键设置：透明背景 .ChartArea.Format.Fill.Visible = msoFalse .ChartArea.Format.Line.Visible = msoFalse .PlotArea.Format.Fill.Visible = msoFalse .PlotArea.Format.Line.Visible = msoFalse ' 粘贴图片 .Paste ' 生成文件名 fileName = savePath & CleanFileName(ws.Name) & ".png" ' 导出图片 .Export Filename:=fileName, FilterName:="PNG" End With ' 删除图表对象 cht.Delete End If Next ws Application.ScreenUpdating = True MsgBox "所有工作表已成功导出为图片！" & vbNewLine & "保存路径：" & savePath, vbInformation End Sub ' 清理文件名中的非法字符 Function CleanFileName(str As String) As String Dim illegalChars As String illegalChars = "\/:*?""<>|" Dim i As Integer For i = 1 To Len(illegalChars) str = Replace(str, Mid(illegalChars, i, 1), "") Next i CleanFileName = str End Function 使用指南 第一步：启用开发工具 文件 → 选项 → 自定义功能区 勾选“开发工具” → 点击确定 第二步：插入VBA代码 按 Alt + F11 打开VBA编辑器 右键项目 → 插入 → 模块 粘贴提供的代码 第三步：运行脚本 回到Excel界面 开发工具 → 宏 选择对应宏名 → 运行 温馨提示：首次使用建议备份文件，熟练后即可大胆使用！愿每一个努力的你，都能被温柔以待，准时下班！
将 iPhone 上的文件传输到 Windows 电脑有多种方法，各有优劣。我采用的是共享方式，顺便来水一篇文章，言归正传，主要是不想再去下载各种软件安装，然后卸载，不如从根本解决问题。 前提条件 iPhone（iOS 13及以上） Windows电脑 确保两者在同一局域网下，不懂百度 操作步骤 1. 在 Windows 下共享文件夹 首先选择一个用来接收文件的文件夹，这里我是新建的一个文件夹名为My_iPhone 其次是右击属性，点击共享栏，选择共享 然后就可以看到设置栏，这里我添加了一个Everyone的权限级别设置为 “读取/写入”（这一步可以不用），然后点击底部的 “共享” 按钮。 共享完成期间可能会弹出专用公用网络，哪里亮了点哪里就行。 共享完成后可以在共享栏里看到共享的网络路径，格式类似为\\YOUR-PC-NAME\iPhoneFiles。 另外需要确认一下自己电脑开机登录用户名和密码，后续在iPhone端需要。 最后就是找到本机当前 IP 地址，连接 WIFI 后一般都是 DHCP 给的随机地址，得自己改成静态 IP 地址，偷懒就自行属性里面找到自己当前 IP 地址，我敲的命令感觉快一点。 2. 在 iPhone 上连接该文件夹 再提醒一遍是在同一局域网下，比如有的时候都连接了WIFI，但不在同一网络下，这是因为目前很多路由器安装时都设置了两个频段（2.4 or 5），还需注意。 如已在同一个网络下，只需打开 iPhone 上的 “文件” App，点击点击右上角的 “…” 三个点图标，然后选择 “连接服务器”。 在 “服务器” 地址栏中，输入正确的网络路径，即在Windows上最后确认的 IP 地址（例如：smb://192.168.1.32），smb 是一个网络协议名，它能被用于 Web 连接和客户端与服务器之间的信息沟通，常用于在计算机间共享文件、打印机、串口等，电脑上的网上邻居就是靠它实现。 输入完成后点击连接会出现连接身份选项，为了安全起见，采用注册用户，然后输入 Windows 电脑开机时的用户名和密码，没有就自己设一个即可，点击连接，成功就会弹到此窗口，可以看到创建的文件夹已被共享。 3. 文件传输 到这里基本就已经成功了，现在只需选择相册中要像传输的文件即可，步骤就不详细赘述了，大概就是选择要上传的文件后选择 分享 -> 存储到“文件”，然后选择这个被共享文件夹（没有就点击浏览->共享->选择文件夹）。 个人感觉速度还是很快的，比某云要便宜，比某盘要实在，比某app要...
最近沉迷抖音，无意看到点灯大师的点灯操作，发现了一种有趣的LED控制技术——查理复用(Charlieplexing)。通过这种技术，可以用n个GPIO引脚控制n×(n-1)个LED。本文将详细记录我的学习过程、实现方案以及遇到的问题和解决方法。 查理复用原理 这里不赘述，就想更新一下😎 这里已经足够详细，简单来讲即将LED反并联连接。这里更加详细。 在本文中我们将利用STM32的GPIO引脚三态特性（输出高、输出低、高阻输入）来控制多个LED的技术。其核心思想是： 每个LED连接在两个不同的GPIO引脚之间 通过控制引脚的电平状态决定LED是否导通 任何时候只允许一个电流路径存在 硬件设计 电路原理图 以下电路连接方式以及真值表： 连接方式 Num GPIO LED1 PA0(+) → PA1(-) LED2 PA1(+) → PA0(-) LED3 PA0(+) → PA2(-) LED4 PA2(+) → PA0(-) LED5 PA1(+) → PA2(-) LED6 PA2(+) → PA1(-) 真值表 驱动代码部分 已模块封装，便于扩展更多LED。代码需在最后一行留空，否则Keil编译会有警告。 CharlieLED.h #ifndef __CHARLIELED_H #define __CHARLIELED_H #include "stm32f10x.h" // Device header typedef enum { LED_1, // PA0 + PA1 LED_2, // PA1 + PA0 LED_3, // PA0 + PA2 LED_4, // PA2 + PA0 LED_5, // PA1 + PA2 LED_6 // PA2 + PA1 } CharlieLED; void Set_CharlieLED(CharlieLED led, FunctionalState state); #endif CharlieLED.c #include "CharlieLED.h" // FunctionalState 是一个枚举类型，用于表示功能状态的开启或关闭 void Set_CharlieLED(CharlieLED led, FunctionalState state) { static const struct { uint16_t anode_pin; uint16_t cathode_pin; } led_map[6] = { {GPIO_Pin_0, GPIO_Pin_1}, // LED1 {GPIO_Pin_1, GPIO_Pin_0}, // LED2 {GPIO_Pin_0, GPIO_Pin_2}, // LED3 {GPIO_Pin_2, GPIO_Pin_0}, // LED4 {GPIO_Pin_1, GPIO_Pin_2}, // LED5 {GPIO_Pin_2, GPIO_Pin_1} // LED6 }; // 初始化 1. 先全部设为高阻态（安全隔离） RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; // 默认保持彻底高阻态 GPIO_Init(GPIOA, &GPIO_InitStructure); // 判断是否点亮 if (state == ENABLE) { // 2. 配置阳极（推挽输出高） GPIO_SetBits(GPIOA, led_map[led].anode_pin); // 高电平 GPIO_InitStructure.GPIO_Pin = led_map[led].anode_pin; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); // 3. 配置阴极（推挽输出低） GPIO_ResetBits(GPIOA, led_map[led].cathode_pin); // 低电平 GPIO_InitStructure.GPIO_Pin = led_map[led].cathode_pin; GPIO_Init(GPIOA, &GPIO_InitStructure); } } 学习笔记 1️⃣. 为何不使用开漏输出而是推挽输出？ 推挽输出首选是因为它内置了完整的驱动能力，高电平时通过PMOS强上拉，低电平时通过NMOS强下拉，无需外接元件。而开漏输出在高电平时依赖外部上拉电阻，会导致驱动能力不足、电路复杂化，且在多LED系统中可能引发电流路径冲突。 在驱动实现中，推挽输出相比开漏输出能直接提供高/低电平驱动能力，无需外接上拉电阻，可双向电流快速切换LED状态，降低功耗并提高响应速度，同时简化电路设计（省去上拉网络）。直白来讲，推挽输出对IO有绝对的控制权。 2️⃣. 为什么必须用高阻态？ 在查理复用中，高阻态是确保LED精确控制的关键。高阻态可以彻底断开引脚，防止非目标LED因漏电微亮，通过GPIO_Mode_IN_FLOATING实现真正电气隔离，确保每次只有一条确定的电流路径。 3️⃣. 切换机制是怎样的？ 首先将所有引脚默认设为高阻态（电气隔离），然后配置目标阳极为推挽输出高，最后设置阴极为推挽输出低。 4️⃣. 如何进行扩展，软件应该怎样实现？ 可以知道该原理为n个GPIO可控制n×(n-1)个LED，沿用二维数组存储引脚组合。然后需先建立引脚映射表，然后加以扩展，如动态扫描（定时器中断控制）、PWM 调光（呼吸灯、亮度渐变）、矩阵键盘（输入检测）以及多路 ADC 模拟信号采集等。查理复用的核心思想是 “分时复用 + 三态控制”，只要灵活运用，可以用极少的 GPIO 实现复杂的交互功能！ 5️⃣. 多路 ADC 模拟信号采集具体方式？ 利用GPIO的三态特性（高电平、低电平、高阻态）分时切换多个模拟开关，将不同传感器轮流连接到同一路ADC上。 例如，3个GPIO通过组合控制可管理6个传感器——当GPIO1输出高、GPIO2输出低、GPIO3设为高阻时，选通传感器1；切换其他组合即可选通其他传感器。每次仅一个传感器被连接，ADC分时读取各信号。其关键点在于： 🟢通过GPIO状态组合动态控制模拟开关； 🔵严格分时避免信号冲突； 🟣切换后需短暂延时确保信号稳定。虽以最少引脚实现多路采集，但需注意抗干扰和时序控制。 一句话总结：通过动态切换GPIO输入/输出状态或控制模拟开关，实现多路模拟信号的分时采集。
QString是 Qt 编程中常用的一个类，用于存储和操作字符串。 问题描述 在学习中因为将Qt默认编码格式给改成了GBK格式，导致在程序再次使用中文时出现乱码。如果把编码格式改回UTF-8，又会导致之前写的注释变为乱码。 解决方法 官方手册的解释如下，大概的意思是说QString存储字符串采用的是Unicode码，每个字符是一个16位的QChar，而不是8位的char。因此，QString能够很好地处理中文字符，一个汉字在QString中算作是一个字符。对于超过65535的Unicode编码，QString则使用两个连续的QChar字符表示。 在构造函数中说用8位字符串str初始化的字符串。使用fromUtf8()函数将给定的const char指针转换为Unicode。所以可以理解为将char*传给QString，要确保编码格式为UTF-8，否则QString在转到Unicode时会出错。 到这里明白了出错原因，因为也是学习于是找找与UTF-8相关的，看到有QString::fromUtf8，于是就以为成功了，但并没有解决该问题，于是继续翻看文档。 发现在我的代码中，"第%1行第%2列" 是一个普通的C++字符串字面量，它包含的是ASCII 字符和一些中文字符，可以理解为没有从UTF-8编码的字节数组开始，而是直接从字符串字面量开始，于是代码改为以下，成功解决乱码。 void Widget::oncursorPositionChanged() { // 获取光标位置 QTextCursor cursor = ui->textEdit->textCursor(); ui->lblPosition->setText(QString::fromLocal8Bit("第%1行第%2列").arg(cursor.blockNumber() + 1).arg(cursor.columnNumber() + 1)); } 下图是来自文心一言的对比：
IPC（Inter-Process Communication）即进程间通信，是计算机科学中的一个重要概念，是指两个不同进程之间数据交换的过程。 Socket 编程原本是为了网络服务的，之前已介绍。后来逐渐发展成一种进程间通信的方式：Unix Domain Socket IPC。它允许在同一台主机上运行的进程之间进行高效的数据传输，无需经过网络协议栈，因此具有低延迟和高性能的特点。通过文件系统中的特殊文件（通常是一个套接字文件），进程可以通过套接字（socket）来进行通信，实现双向的数据传输。 Socket 编程 Socket 编程是网络通信中的一项基础而强大的技术，它允许两台计算机（或同一台计算机上的两个进程）之间进行双向通信。Socket 可以看作是网络通信中的一个端点，通过它可以发送或接收数据。在 TCP/IP 协议族中， Socke...2024-09-013 IPC通信的原理与机制 IPC 通信的原理是通过特定的机制，使得不同进程之间能够相互传递信息、协作完成任务。这些机制主要包括共享内存、消息传递、管道以及套接字等。这些机制主要包括以下几种： 1. 共享内存： 原理：通过将某一块内存区域映射到多个进程的地址空间中，实现不同进程之间的数据共享。 优点：可以高效地传递大量数据。 缺点：需要解决进程间的同步和互斥问题，以保证数据的正确性。如果多个进程同时访问共享内存区域，可能会导致数据冲突或不一致。 2. 消息传递： 原理：通过发送和接收消息的方式进行进程间通信。发送方将消息发送到一个消息队列或者直接发送给特定的进程，接收方则从消息队列中读取消息或者通过特定的接收机制接收消息。 优点：可以实现异步通信和进程间的解耦。发送方和接收方可以独立地运行，不需要知道对方的存在和状态。 缺点：需要解决消息的传递顺序和消息的可靠性问题。如果消息丢失或顺序错乱，可能会导致通信失败或数据不一致。 3. 管道： 原理：一种半双工的通信方式，用于连接一个读进程和一个写进程。管道可以使得两个进程能够单向地传递数据，其中一个进程充当写端，将数据写入管道，另一个进程充当读端，从管道中读取数据。 优点：适用于父子进程或者具有亲缘关系的进程之间的通信。 缺点：不适用于无关进程间的通信。同时，管道只能进行单向通信，且通信双方的进程必须同时存在。 4. 套接字： 原理：一种在不同主机间进行网络通信的机制，它可以在应用层与传输层之间建立连接，并通过网络传输数据。套接字可以通过TCP或UDP协议进行通信，可以在同一台主机上的不同进程间或者不同主机间进行通信。 优点：具有灵活性和可扩展性。套接字可以支持多种通信协议和数据传输方式，适用于各种复杂的网络通信场景。 缺点：需要注意网络传输的可靠性和安全性。网络传输过程中可能会出现数据丢失、延迟等问题，同时还需要考虑网络安全和身份验证等问题。 套接字实现 使用AF_UNIX通信域，可声明一个使用本地文件的 socket，其他进程可绑定该文件完成进程间通信。 socket_ipc.c： #include <stdlib.h> #include <stdio.h> #include <stddef.h> #include <sys/socket.h> #include <sys/un.h> #include <sys/stat.h> #include <errno.h> #include <string.h> #include <unistd.h> #define SOCKET_PATH "unix_domain.socket" #define SERVER_MODE 1 #define CLIENT_MODE 2 #define BUF_LEN 1024 #define OUTTEXT ("请输入要发送的信息：") #define OUTSIZE sizeof(OUTTEXT) static struct sockaddr_un socket_addr; static char *buf; void handle_error(char *err_msg) { perror(err_msg); unlink(SOCKET_PATH); // 释放 exit(-1); } void server_mode(int sockfd) { struct sockaddr_un client_addr; int client_fd, msg_len; /**** 服务端接收消息，收到 EOF 结束信息 ****/ // 1. 绑定 if (bind(sockfd, (struct sockaddr *)&socket_addr, sizeof(socket_addr)) < 0) { handle_error("bind"); } // 2. 挂起监听 if (listen(sockfd, 128) < 0) { handle_error("listen"); } // 3. 获取连接 socklen_t client_addr_len = sizeof(client_addr); if ((client_fd = accept(sockfd, (struct sockaddr *)&client_addr, &client_addr_len)) < 0) { handle_error("accept"); } printf("接收到客户端的连接\n"); do { memset(buf, 0, BUF_LEN); msg_len = recv(client_fd, buf, BUF_LEN, 0); if (strncmp(buf, "EOF", 3)) { printf("接收到客户端数据：%s\n", buf); strcpy(buf, "ojbk\n\0"); } else { printf("接收到结算信息，停止接收数据\n"); } // 回复信息 or 结束信息 send(client_fd, buf, strlen(buf), 0); } while (strncmp(buf, "EOF", 3)); unlink(SOCKET_PATH); } void client_mode(int sockfd) { int msg_len; /**** 客户端发送消息，发送 EOF 结束信息 ****/ // 连接服务端 if (connect(sockfd, (struct sockaddr *)&socket_addr, sizeof(socket_addr)) < 0) { handle_error("connect"); } do { write(STDOUT_FILENO, OUTTEXT, OUTSIZE); msg_len = read(STDIN_FILENO, buf, BUF_LEN); if (send(sockfd, buf, BUF_LEN, 0) < 0) { handle_error("send"); } // 清空缓冲区 memset(buf, 0, BUF_LEN); recv(sockfd, buf, BUF_LEN, 0); printf("接收到服务器回复的消息：%s", buf); } while (strncmp(buf, "EOF", 3)); if (shutdown(sockfd, SHUT_WR) < 0) { handle_error("shutdown"); } } int main(int argc, char const *argv[]) { int fd = 0, mode = 0; // 设计结构 // 通过main方法传参的方式 启动不同的客户端和服务端 // 缺省：如果不填写参数 or 填写的参数默认是server if (argc == 1 || strncmp(argv[1], "server", 6) == 0) { mode = SERVER_MODE; } else if (strncmp(argv[1], "client", 6) == 0) { mode = CLIENT_MODE; } else { perror("参数错误"); exit(EXIT_FAILURE); } // 创建socket_ipc通讯 memset(&socket_addr, 0, sizeof(socket_addr)); buf = malloc(BUF_LEN); socket_addr.sun_family = AF_UNIX; strcpy(socket_addr.sun_path, SOCKET_PATH); fd = socket(AF_UNIX, SOCK_STREAM, 0); if (fd < 0) { handle_error("socket"); } switch (mode) { case SERVER_MODE: server_mode(fd); break; case CLIENT_MODE: client_mode(fd); break; default: break; } close(fd); free(buf); return 0; } 测试结果 IPC 通信的应用场景 操作系统：在操作系统中，每个进程都有自己独立的地址空间和资源，它们不能直接访问其他进程的内存空间。然而，在实际的应用中，进程之间需要进行数据交换、共享资源以及协作工作。为了实现这些功能，操作系统提供了IPC机制，允许进程之间进行通信和数据传输。 多线程编程：在多线程编程中，线程之间需要进行数据共享和同步，可以使用基于内存的IPC机制（如共享内存）来实现。 分布式系统：在分布式系统中，不同节点之间需要进行数据交换和通信，可以使用基于消息的IPC机制（如消息队列、套接字）来实现。 注意事项 同步与互斥：对于共享内存等基于内存的IPC机制，需要处理并发访问和同步的问题，避免数据的冲突和竞争。可以使用信号量、互斥锁等同步机制来保证数据的一致性和正确性。 消息传递的可靠性：对于消息传递等基于消息的IPC机制，需要保证消息的传递顺序和可靠性。可以使用消息队列的持久化存储、确认机制等来提高消息的可靠性。 安全性：在使用IPC通信时，需要注意安全性问题。例如，避免敏感数据的泄露、防止恶意进程的攻击等。可以使用加密、身份验证等安全措施来保护通信过程和数据的安全性。
UDP（用户数据报协议）是一种无连接的协议，相比于TCP（传输控制协议），它不需要建立连接、维护连接状态以及进行错误检查和重传，因此通常用于对实时性要求较高、但对数据完整性要求不高的场景，如视频流、在线游戏等。 本文将通过 UDP 展示如何发送和接收数据，适用于简单的应用场景。对于更复杂的需求，可能需要进一步扩展和优化代码。之前已实现 TCP 的接发数据。 Socket 编程 Socket 编程是网络通信中的一项基础而强大的技术，它允许两台计算机（或同一台计算机上的两个进程）之间进行双向通信。Socket 可以看作是网络通信中的一个端点，通过它可以发送或接收数据。在 TCP/IP 协议族中， Socke...2024-09-013 使用 Socket 建立多个 TCP 连接 使用Socket来建立多个TCP连接是一种常见的网络通信方式，在客户端和服务器之间进行数据交换。需要注意的是，服务器在处理多个客户端时，应该为每个客户端连接创建一个新的线程或进程，或者使用异步IO来避免阻塞。 2024-09-241 UDP 通讯流程 UDP通讯也使用socket，但是接收和发送的函数与 TCP 不一样。由于 UDP 不存在握手这一步骤，所以在绑定地址之后，服务端不需要 listen，客户端也不需要 connect，服务端同样不需要 accept。只要服务端绑定以后，就可以相互发消息了，由于没有握手过程，两端都不能确定对方是否收到消息，这也是 UDP 协议不如 TCP 协议可靠的地方。 编程基本步骤 服务端流程： 创建套接字，选择套接字类型（如 SOCK_DGRAM 表示数据报套接字） 绑定地址和端口 接收数据 处理数据（可选） 发送响应（可选）：如果需要，使用 sendto()发送响应数据 关闭套接字 客户端流程： 创建套接字 指定服务器地址和端口 发送数据，使用 sendto() 函数发送数据 接收响应（可选）：可以使用 recvfrom() 函数接收数据 关闭套接字 函数介绍 UDP 协议接收和发送数据不再使用 send 和 recv 方法，这两个方法一般用于 TCP 通信，UDP 通信使用 sendto 和 recvfrom方法。 需要包含的头文件： #include <sys/types.h> #include <sys/socket.h> 1. sendto ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen); /** * @brief 向指定地址发送缓冲区中的数据（一般用于UDP模式） * * @param sockfd 套接字文件描述符 * @param buf 缓冲区指针 * @param len 缓冲区大小 * @param flags 通信标签，详细减send方法说明 * @param dest_addr 目标地址。如果用于连接模式，该参数会被忽略 * @param addrlen 目标地址长度 * @return ssize_t 发送的消息大小。发送失败返回-1 */ 2. recvfrom ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen); /** * @brief 将接收到的消息放入缓冲区 buf 中。 * * @param sockfd 套接字文件描述符 * @param buf 缓冲区指针 * @param len 缓冲区大小 * @param flags 通信标签，详见recv方法说明 * @param src_addr 可以填NULL，如果 src_addr 不是 NULL，并且底层协议提供了消 息的源地址，则该源地址将被放置在 src_addr 指向的缓冲区中。 * @param addrlen 如果src_addr不为NULL，它应初始化为与 src_addr 关联的缓冲 区的大小。返回时，addrlen 被更新为包含实际源地址的大小。如果提供的缓冲区太小， 则返回的地址将被截断；在这种情况下，addrlen 将返回一个大于调用时提供的值。 * @return ssize_t 实际收到消息的大小。如果接收失败，返回-1 */ 示例程序 udp_server.c： #include <sys/socket.h> #include <sys/types.h> #include <netinet/in.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <arpa/inet.h> #define BUFSIZE (1024 * sizeof(char)) #define PORT_S 6666 #define handle_error(cmd, result) \ if (result < 0) \ { \ perror(cmd); \ return -1; \ } int main(int argc, char const *argv[]) { // 使用UDP协议完成客户端和服务端的通讯 // 收到 EOF 作为关闭的信号 int sockfd, temp_result, client_fd; struct sockaddr_in server_addr, client_addr; char *buf = malloc(BUFSIZE); // 清空 memset(&server_addr, 0, sizeof(server_addr)); memset(&client_addr, 0, sizeof(client_addr)); // 声明IPV4通信协议 server_addr.sin_family = AF_INET; // 我们需要绑定0.0.0.0地址，转换成网络字节序后完成设置 server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 端口随便用一个，但是不要用特权端口 server_addr.sin_port = htons(PORT_S); /******** 网络编程 UDP 编程流程 ********/ // 1. socket 创建 sockfd = socket(AF_INET, SOCK_DGRAM, 0); // 数据报 handle_error("socket", sockfd); // 2. 服务端绑定地址 socklen_t server_len = sizeof(server_addr); socklen_t client_len = sizeof(client_addr); temp_result = bind(sockfd, (struct sockaddr *)&server_addr, server_len); handle_error("bind", temp_result); // 3. 直接就可以收发数据 do { // 接收数据存到缓冲 memset(buf, 0, BUFSIZE); // 清空 // 接收数据 temp_result = recvfrom(sockfd, buf, BUFSIZE, 0, (struct sockaddr *)&client_addr, &client_len); handle_error("recvfrom", temp_result); if (strncmp(buf, "EOF", 3) != 0) { printf("接收到来自客户端(%s:%d)信息：%s\n",inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), buf); strcpy(buf, "OK\n"); } else { printf("收到结束信息，准备关闭\n"); } // 回复数据 temp_result = sendto(sockfd, buf, 4, 0, (struct sockaddr *)&client_addr, client_len); handle_error("sendto", temp_result); } while (strncmp(buf, "EOF", 3) != 0); free(buf); return 0; } udp_client.c #include <sys/socket.h> #include <sys/types.h> #include <netinet/in.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <arpa/inet.h> #include <unistd.h> #define BUFSIZE (1024 * sizeof(char)) #define PORT_S 6666 #define OUTTEXT ("请输入要发送的信息：") #define OUTSIZE sizeof(OUTTEXT) #define handle_error(cmd, result) \ if (result < 0) \ { \ perror(cmd); \ return -1; \ } int main(int argc, char const *argv[]) { // 使用UDP协议完成客户端和服务端的通讯 // 收到 EOF 作为关闭的信号 int sockfd, temp_result, client_fd; struct sockaddr_in server_addr, client_addr; char *buf = malloc(BUFSIZE); // 清空 memset(&server_addr, 0, sizeof(server_addr)); memset(&client_addr, 0, sizeof(client_addr)); // 声明IPV4通信协议 server_addr.sin_family = AF_INET; // 我们需要绑定0.0.0.0地址，转换成网络字节序后完成设置 server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 端口随便用一个，但是不要用特权端口 server_addr.sin_port = htons(PORT_S); /******** 网络编程 UDP 编程流程 ********/ // 1. socket 创建 sockfd = socket(AF_INET, SOCK_DGRAM, 0); // 数据报 handle_error("socket", sockfd); // 2. 客户端不需要绑定地址 socklen_t server_len = sizeof(server_addr); socklen_t client_len = sizeof(client_addr); // 3. 直接就可以收发数据 do { write(STDOUT_FILENO, OUTTEXT, OUTSIZE); // 从控制台读取数据 int buf_len = read(STDIN_FILENO, buf, BUFSIZE - 1); // 字符串最后为 '\0' temp_result = sendto(sockfd, buf, buf_len, 0, (struct sockaddr *)&server_addr, server_len); handle_error("sendto", temp_result); // 接收数据之前需要清空缓存 memset(buf, 0, BUFSIZE); //recvfrom(sockfd, buf, BUFSIZE, 0, (struct sockaddr *)&server_addr, server_len); temp_result = recvfrom(sockfd, buf, BUFSIZE, 0, NULL, NULL); // 地址指针 ==> 之前存在数据，可以为空 handle_error("recvfrom", temp_result); // 处理接收到的数据 if (strncmp(buf, "EOF", 3) != 0) { printf("收到服务端(%s:%d)返回数据：%s\n", inet_ntoa(server_addr.sin_addr), ntohs(server_addr.sin_port), buf); } } while (strncmp(buf, "EOF", 3) != 0); free(buf); return 0; } 运行结果 注意事项 1. 无连接性 UDP 是一种无连接的协议，这意味着在通信开始之前不需要建立连接。因此，UDP 通信通常被称为“不可靠的”或“尽最大努力的”传输，因为它不提供确认机制、重传机制或流量控制等功能。 2. 数据报大小限制 UDP 数据报有一个最大长度限制（通常为 65535 字节），这取决于网络环境和配置。如果发送的数据超过这个限制，它将被拆分成多个数据报进行传输。然而，接收端并不保证这些拆分的数据报会按顺序到达或全部到达。 3. 缓冲区管理 UDP 没有严格的发送缓冲区管理。当调用 sendto() 函数时，内核会直接将数据发送到网络层进行传输。然而接收端有一个接收缓冲区来存储到达的数据报。如果接收缓冲区已满，则新的数据报将被丢弃。 4. 应用场景 由于 UDP 的实时性和高效性特点，它通常用于那些对实时性要求较高但对数据完整性要求不高的应用场景，如视频流、在线游戏、实时通信等。 UDP 通信流程是一个相对简单且高效的过程，它适用于那些对实时性要求较高但对数据完整性要求不高的应用场景。然而，由于UDP的无连接性和不可靠性特点，在使用时需要特别注意数据报的大小限制和缓冲区管理等问题。
使用Socket来建立多个TCP连接是一种常见的网络通信方式，在客户端和服务器之间进行数据交换。需要注意的是，服务器在处理多个客户端时，应该为每个客户端连接创建一个新的线程或进程，或者使用异步IO来避免阻塞。 本文将以多线程和多进程两种方式实现，可以运行多个客户端实例来测试服务器是否能处理多个TCP连接。可以简单地在命令行中多次运行客户端脚本，或者写一个循环来多次创建和关闭客户端连接。 在C语言中，实现一个基本的 TCP 服务器和客户端涉及使用 socket 编程接口，这通常包括 socket(), bind(), listen(), accept(), send(), recv() 和 close() 等函数，之前有所介绍。Socket 编程 Socket 编程是网络通信中的一项基础而强大的技术，它允许两台计算机（或同一台计算机上的两个进程）之间进行双向通信。Socket 可以看作是网络通信中的一个端点，通过它可以发送或接收数据。在 TCP/IP 协议族中， Socke...2024-09-013 示例程序 以多线程和多进程两种方式实现，支持多个客户端连接服务器。需要注意的是服务器将监听一个特定的端口，并接受来自客户端的连接。一旦连接建立，它将接收来自客户端的消息，并将其回显给客户端，而客户端的端口绑定时自动分配的，当然也可以修改程序成一个指定端口。 基于多线程支持多个 TCP 连接 server.c #include <sys/socket.h> #include <sys/types.h> #include <netinet/in.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <arpa/inet.h> #include <unistd.h> #include <pthread.h> #define PORT 6666 #define BUFFSIZE 1024 #define TYPESIZE (sizeof(char) * BUFFSIZE) #define handle_error(cmd, result) \ if(result < 0) \ { \ perror(cmd); \ return -1; \ } void * read_from_client_then_write(void * arg) { // 读取客户端发送过来的数据 回复收到 int client_fd = *(int *)arg; char * read_buf = NULL; char * write_buf = NULL; ssize_t count = 0, send_count = 0; // 初始化 read_buf = malloc(sizeof(char) * BUFFSIZE); write_buf = malloc(sizeof(char) * BUFFSIZE); if (!read_buf) { printf("服务端读缓存创建异常，断开连接\n"); close(client_fd); perror("read_buf"); return NULL; } if (!write_buf) { printf("服务端写缓存创建异常，断开连接\n"); close(client_fd); perror("read_buf"); return NULL; } while (count = recv(client_fd, read_buf, BUFFSIZE, 0)) { if (count < 0) { perror("recv"); } // 接收数据打印到控制台 printf("从_%d_客户端接收到数据:%s", client_fd, read_buf); // 把收到的消息写到写缓存 strcpy(write_buf, "收到\n"); send_count = send(client_fd, write_buf, BUFFSIZE, 0); if (send_count < 0) { perror("send"); } } // 当客户端输入 ctrl+D 退出循环 // shutdown(client_fd, SHUT_RD); close(client_fd); free(read_buf); free(write_buf); } int main(int argc, char const *argv[]) { struct sockaddr_in server_addr, client_addr; int sock_fd, tmp_res; // 清空 memset(&server_addr, 0, sizeof(server_addr)); memset(&client_addr, 0, sizeof(client_addr)); // 填写服务端类型 server_addr.sin_family = AF_INET; // ipv4 // 填写IP地址 0.0.0.0 server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // inet_pton(AF_INET, "0.0.0.0", &server_addr.sin_addr); // 填写端口号 server_addr.sin_port = htons(PORT); /******** 网络编程 socket 流程 ********/ // 1. 创建socket sock_fd = socket(AF_INET, SOCK_STREAM, 0); handle_error("socket", sock_fd); // 2. 绑定地址 tmp_res = bind(sock_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)); handle_error("bind", tmp_res); // 3. 进入监听状态 tmp_res = listen(sock_fd, 128); // 等待队列长度，可以随便填 handle_error("listen", tmp_res); // 4. 获取客户端的连接 ： 需要能够接收多个连接 socklen_t cliaddr_len = sizeof(client_addr); while (1) { int client_fd = accept(sock_fd, (struct sockaddr *)&client_addr, &cliaddr_len); // 不同的进程访问全局变量而导致的竞态资源的问题 // 全局只创建了一个线程号实例，每次创建一个线程都会覆盖掉之前的线程号 pthread_t pid_read_write; handle_error("accept", client_fd); printf("与客户端 %s:%d --> 建立连接_%d_\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), client_fd); // 和每一个客户端使用一个线程交互 把客户端发送的信息打印到控制台 回复收到 if (pthread_create(&pid_read_write, NULL, read_from_client_then_write, (void *)&client_fd)) { perror("pthread_create"); } // 需要等待线程结束 但是不能挂起等待，否则就无法创建下一个连接 pthread_detach(pid_read_write); } printf("释放资源\n"); close(sock_fd); return 0; } client.c #include <sys/socket.h> #include <sys/types.h> #include <netinet/in.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <arpa/inet.h> #include <unistd.h> #include <pthread.h> #define ADDRESS_S ("127.0.0.1") #define ADDRESS_C ("192.168.100.137") #define PORT_S 6666 #define PORT_C 8888 #define BUFFSIZE 1024 #define TYPESIZE (sizeof(char) * BUFFSIZE) #define handle_error(cmd, result) \ if(result < 0) \ { \ perror(cmd); \ return -1; \ } void * read_from_server(void * arg) { // 使用recv接收客户端发送的数据 打印到控制台 char * read_buf = NULL; int client_fd = *(int *)arg; // 得到文件描述符 read_buf = malloc(TYPESIZE); ssize_t count = 0; if (!read_buf) { perror("malloc_server_read_buf"); return NULL; } // 接收数据 // 只要能接受到数据 正常使用 一直挂起 while (count = recv(client_fd, read_buf, BUFFSIZE, 0)) { if (count < 0) { perror("recv"); } fputs(read_buf, stdout); } printf("服务端请求关闭\n"); free(read_buf); return NULL; } void * write_to_server (void * arg) { // 接收控制台输入的信息 写出去 char * write_buf = NULL; int client_fd = *(int *)arg; // 得到文件描述符 write_buf = malloc(TYPESIZE); ssize_t count = 0; if (!write_buf) { perror("malloc_server_write_buf"); return NULL; } while (fgets(write_buf, BUFFSIZE, stdin) != NULL) { // 发送数据 count = send(client_fd, write_buf, BUFFSIZE, 0); if (count < 0) { perror("send"); } } printf("接收到控制台的关闭请求, 不再写入... \n关闭连接\n"); // 可以具体到关闭某一端 shutdown(client_fd, SHUT_WR); free(write_buf); return NULL; } int main(int argc, char const *argv[]) { struct sockaddr_in server_addr, client_addr; int sock_fd, tmp_res; // 清空 memset(&server_addr, 0, sizeof(server_addr)); memset(&client_addr, 0, sizeof(client_addr)); // 填写服务端类型 server_addr.sin_family = AF_INET; // ipv4 // 填写IP地址 inet_pton(AF_INET, ADDRESS_S, &server_addr.sin_addr); // 填写端口号 server_addr.sin_port = htons(PORT_S); /******** 网络编程 socket 流程 ********/ // 1. 创建socket，只有一个文件描述符 sock_fd = socket(AF_INET, SOCK_STREAM, 0); handle_error("socket", sock_fd); // 2. 绑定 (自动分配) // 客户端可以不绑定，系统会自动分配一个空闲的端口号给客户端 // 3. 客户端主动连接服务端 tmp_res = connect(sock_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)); handle_error("connect", tmp_res); printf("连接上服务端_%s:%d\n", inet_ntoa(server_addr.sin_addr), ntohs(server_addr.sin_port)); // 创建子线程用于收消息 pthread_t pid_read, pid_write; pthread_create(&pid_read, NULL, read_from_server, (void *)&sock_fd); // 创建子线程用于发消息 pthread_create(&pid_write, NULL, write_to_server, (void *)&sock_fd); // 阻塞主线程 pthread_join(pid_read, NULL); pthread_join(pid_write, NULL); // 释放资源 printf("释放资源\n"); close(sock_fd); return 0; } makefile server:server.c - $(CC) -o $@ $^ client:client.c - $(CC) -o $@ $^ 运行结果 基于多进程支持多个 TCP 连接 客户端程序和Makefile文件与上述一致，无需更改，唯一变动的是服务端程序，需要注意的是如何避免僵尸进程，新建进程后关闭不会使用到的资源。 server.c #include <sys/socket.h> #include <sys/types.h> #include <sys/wait.h> #include <netinet/in.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <arpa/inet.h> #include <pthread.h> #include <unistd.h> #include <signal.h> #define PORT 6666 #define BUFFSIZE 1024 #define TYPESIZE (sizeof(char) * BUFFSIZE) #define handle_error(cmd, result) \ if(result < 0) \ { \ perror(cmd); \ return -1; \ } void * read_from_client_then_write(void * arg) { // 读取客户端发送过来的数据 回复收到 int client_fd = *(int *)arg; char * read_buf = NULL; char * write_buf = NULL; ssize_t count = 0, send_count = 0; // 初始化 read_buf = malloc(sizeof(char) * BUFFSIZE); write_buf = malloc(sizeof(char) * BUFFSIZE); if (!read_buf) { printf("服务端读缓存创建异常，断开连接\n"); close(client_fd); perror("read_buf"); return NULL; } if (!write_buf) { printf("服务端写缓存创建异常，断开连接\n"); close(client_fd); perror("read_buf"); return NULL; } while (count = recv(client_fd, read_buf, BUFFSIZE, 0)) { if (count < 0) { perror("recv"); } // 接收数据打印到控制台 printf("从_%d_客户端接收到数据:%s", client_fd, read_buf); // 把收到的消息写到写缓存 strcpy(write_buf, "收到\n"); send_count = send(client_fd, write_buf, BUFFSIZE, 0); if (send_count < 0) { perror("send"); } } // 当客户端输入 ctrl+D 退出循环 // shutdown(client_fd, SHUT_RD); close(client_fd); free(read_buf); free(write_buf); } /** * 关闭回收所有的子进程，避免僵尸进程 */ void zombie_dealer(int sig) { pid_t pid; int status; // 一个SIGCHLD可能对应多个子进程的退出 // 使用while循环回收所有退出的子进程，避免僵尸进程的出现 while ((pid = waitpid(-1, &status, WNOHANG)) > 0) { if (WIFEXITED(status)) { printf("子进程: %d 以 %d 状态正常退出，已被回收\n", pid, WEXITSTATUS(status)); } else if (WIFSIGNALED(status)) { printf("子进程: %d 被 %d 信号杀死，已被回收\n", pid, WTERMSIG(status)); } else { printf("子进程: %d 因其它原因退出，已被回收\n", pid); } } } int main(int argc, char const *argv[]) { struct sockaddr_in server_addr, client_addr; int sock_fd, tmp_res; // 注册信号处理函数 回收子进程的资源 SIGCLD signal(SIGCLD, zombie_dealer); // 清空 memset(&server_addr, 0, sizeof(server_addr)); memset(&client_addr, 0, sizeof(client_addr)); // 填写服务端类型 server_addr.sin_family = AF_INET; // ipv4 // 填写IP地址 0.0.0.0 server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // inet_pton(AF_INET, "0.0.0.0", &server_addr.sin_addr); // 填写端口号 server_addr.sin_port = htons(PORT); /******** 网络编程 socket 流程 ********/ // 1. 创建socket sock_fd = socket(AF_INET, SOCK_STREAM, 0); handle_error("socket", sock_fd); // 2. 绑定地址 tmp_res = bind(sock_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)); handle_error("bind", tmp_res); // 3. 进入监听状态 tmp_res = listen(sock_fd, 128); // 等待队列长度，可以随便填 handle_error("listen", tmp_res); // 4. 获取客户端的连接 ： 需要能够接收多个连接 socklen_t cliaddr_len = sizeof(client_addr); while (1) { int client_fd = accept(sock_fd, (struct sockaddr *)&client_addr, &cliaddr_len); // 不同的进程访问全局变量而导致的竞态资源的问题 // 全局只创建了一个线程号实例，每次创建一个线程都会覆盖掉之前的线程号 pthread_t pid_read_write; handle_error("accept", client_fd); // 创建单独的子进程和连接的客户端交互 pid_t pid = fork(); if (pid < 0) { perror("fork"); } else if (pid == 0) { // 子进程 // 关闭不会使用的sock_fd close(sock_fd); printf("与客户端 %s:%d --> 建立连接_%d_\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), client_fd); // 从客户端读取数据 并回复 read_from_client_then_write((void *)&client_fd); // 关闭 client_fd close(client_fd); exit(EXIT_SUCCESS); } else { // 父进程 // 关闭掉不使用的client_fd close(client_fd); } } printf("释放资源\n"); close(sock_fd); return 0; } 运行结果 可以看到两个连接所创建的 client_fd 文件描述符都是4，是因为每次创建子进程后，父进程就会释放 client_fd，对应的文件描述符也就被释放，新的连接被创建时则会从可用的最小文件描述符开始占用，因此新的 client_fd 也是4。虽然 client_fd 是一样的，但对应的底层文件描述却是完全不同的。
Socket 编程是网络通信中的一项基础而强大的技术，它允许两台计算机（或同一台计算机上的两个进程）之间进行双向通信。Socket 可以看作是网络通信中的一个端点，通过它可以发送或接收数据。在 TCP/IP 协议族中， Socket 扮演着核心角色，用于实现不同主机之间的进程间通信。 简单来说就是一种编程接口，允许在不同主机上的进程（运行中的程序）通过网络进行数据交换。 Socket 的类型 Socket 主要分为两种类型： 流式 Socket（SOCK_STREAM）：基于 TCP 协议，提供面向连接的、可靠的数据传输服务。 数据传输的顺序与发送的顺序相同，数据不重复，且接收方必须读取了发送方发送的所有数据后才能继续接收后续数据。 数据报 Socket（SOCK_DGRAM）：基于 UDP 协议，提供无连接的、不可靠的数据传输服务。 数据报是独立的，保留数据边界，发送方和接收方之间没有明确的连接，且发送方和接收方都可能不按顺序接收数据。 套接字的组成 一个套接字主要由以下三个属性组成： 网络地址：通常是 IP 地址，用于标识网络上的设备。 端口号：用于标识设备上的特定应用或进程。端口号是一个16位的数字，范围从0到65535。 协议：如 TCP（传输控制协议）或 UDP（用户数据报协议），定义了数据传输的规则和格式。 Socket 编程的基本步骤 Socket 编程的基本步骤大致相同，无论使用的是哪种 Socket 类型（TCP或UDP），但具体细节（如连接管理）会有所不同。以下是一个简化的 TCP Socket 编程步骤： 服务器端 创建Socket：使用 socket() 函数创建一个新的 Socket。 绑定Socket：使用 bind() 函数将Socket与特定的 IP 地址和端口号绑定。 监听连接：使用 listen() 函数使Socket进入被动监听状态，等待客户端的连接请求。 接受连接：使用 accept()函数接受客户端的连接请求，该函数会返回一个新的 Socket，用于与客户端进行通信。 数据收发：使用 read() / write() 或 recv() / send() 函数与客户端进行数据的发送和接收。 关闭连接：数据传输完成后，使用 close() 函数关闭Socket连接。 客户端 创建Socket：使用 socket() 函数创建一个新的 Socket。 连接服务器：使用 connect() 函数与服务器建立连接。 数据收发：使用 read() / write() 或 recv() / send() 函数与服务器进行数据的发送和接收。 关闭连接：数据传输完成后，使用 close() 函数关闭 Socket 连接。 注意事项 在进行 Socket 编程时，需要注意处理网络异常和错误，例如连接失败、数据接收不完整等。 对于 TCP 连接，要注意处理好连接的建立和断开，避免资源泄露。 对于 UDP 连接，虽然不需要建立连接，但也要注意数据包的完整性和顺序性。 在进行跨网络通信时，需要注意防火墙和网络策略的设置，确保数据能够顺利传输。 示例程序 为方便了解连接建立过程，该仅支持同时建立单个 TCP 连接，实现双方可以互相读写。对于相关函数参数并未做详细解释，如需请自行查看相关文档。目前还是很多都在采用 AF_INET（IPv4），使用时需要注意的是网络字节序和主机字节序转化。 server.c： #include <sys/socket.h> #include <sys/types.h> #include <netinet/in.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <arpa/inet.h> #include <unistd.h> #include <pthread.h> #define PORT 6666 #define BUFFSIZE 1024 #define TYPESIZE (sizeof(char) * BUFFSIZE) #define handle_error(cmd, result) \ if(result < 0) \ { \ perror(cmd); \ return -1; \ } void * read_from_client(void * arg) { // 使用recv接收客户端发送的数据 打印到控制台 char * read_buf = NULL; int client_fd = *(int *)arg; // 得到文件描述符 read_buf = malloc(TYPESIZE); ssize_t count = 0; if (!read_buf) { perror("malloc_server_read_buf"); return NULL; } // 接收数据 // 只要能接受到数据 正常使用 一直挂起 while (count = recv(client_fd, read_buf, BUFFSIZE, 0)) { if (count < 0) { perror("recv"); } fputs(read_buf, stdout); } printf("客户端请求关闭\n"); free(read_buf); return NULL; } void * write_to_client(void * arg) { // 接收控制台输入的信息 写出去 char * write_buf = NULL; int client_fd = *(int *)arg; // 得到文件描述符 write_buf = malloc(TYPESIZE); ssize_t count = 0; if (!write_buf) { perror("malloc_server_write_buf"); return NULL; } while (fgets(write_buf, BUFFSIZE, stdin) != NULL) { // 发送数据 count = send(client_fd, write_buf, BUFFSIZE, 0); if (count < 0) { perror("send"); } } printf("接收到控制台的关闭请求, 不再写入... \n关闭连接\n"); // 可以具体到关闭某一端 shutdown(client_fd, SHUT_WR); free(write_buf); return NULL; } int main(int argc, char const *argv[]) { struct sockaddr_in server_addr, client_addr; int sock_fd, tmp_res, client_fd; // 清空 memset(&server_addr, 0, sizeof(server_addr)); memset(&client_addr, 0, sizeof(client_addr)); // 填写服务端类型 server_addr.sin_family = AF_INET; // ipv4 // 填写IP地址 0.0.0.0 server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // inet_pton(AF_INET, "0.0.0.0", &server_addr.sin_addr); // 填写端口号 server_addr.sin_port = htons(PORT); /******** 网络编程 socket 流程 ********/ // 1. 创建socket sock_fd = socket(AF_INET, SOCK_STREAM, 0); handle_error("socket", sock_fd); // 2. 绑定地址 tmp_res = bind(sock_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)); handle_error("bind", tmp_res); // 3. 进入监听状态 tmp_res = listen(sock_fd, 128); // 等待队列长度，可以随便填 handle_error("listen", tmp_res); // 4. 获取客户端的连接 socklen_t client_addr_len = sizeof(client_addr); // 返回的文件描述符才是能够和客户端收发消息的文件描述符 // 如果调用accept之后没有客户端连接，这里会挂起等待 client_fd = accept(sock_fd, (struct sockaddr *)&client_addr, &client_addr_len); handle_error("accept", client_fd); printf("与客户端%s:%d建立连接 --> 文件描述符是 %d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), client_fd); // 创建子线程用于收消息 pthread_t pid_read, pid_write; pthread_create(&pid_read, NULL, read_from_client, (void *)&client_fd); // 创建子线程用于发消息 pthread_create(&pid_write, NULL, write_to_client, (void *)&client_fd); // 阻塞主线程 pthread_join(pid_read, NULL); pthread_join(pid_write, NULL); // 释放资源 printf("释放资源\n"); close(client_fd); close(sock_fd); return 0; } client.c： #include <sys/socket.h> #include <sys/types.h> #include <netinet/in.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <arpa/inet.h> #include <unistd.h> #include <pthread.h> #define ADDRESS_S ("127.0.0.1") #define ADDRESS_C ("192.168.100.137") #define PORT_S 6666 #define PORT_C 8888 #define BUFFSIZE 1024 #define TYPESIZE (sizeof(char) * BUFFSIZE) #define handle_error(cmd, result) \ if(result < 0) \ { \ perror(cmd); \ return -1; \ } void * read_from_server(void * arg) { // 使用recv接收客户端发送的数据 打印到控制台 char * read_buf = NULL; int client_fd = *(int *)arg; // 得到文件描述符 read_buf = malloc(TYPESIZE); ssize_t count = 0; if (!read_buf) { perror("malloc_server_read_buf"); return NULL; } // 接收数据 // 只要能接受到数据 正常使用 一直挂起 while (count = recv(client_fd, read_buf, BUFFSIZE, 0)) { if (count < 0) { perror("recv"); } fputs(read_buf, stdout); } printf("服务端请求关闭\n"); free(read_buf); return NULL; } void * write_to_server (void * arg) { // 接收控制台输入的信息 写出去 char * write_buf = NULL; int client_fd = *(int *)arg; // 得到文件描述符 write_buf = malloc(TYPESIZE); ssize_t count = 0; if (!write_buf) { perror("malloc_server_write_buf"); return NULL; } while (fgets(write_buf, BUFFSIZE, stdin) != NULL) { // 发送数据 count = send(client_fd, write_buf, BUFFSIZE, 0); if (count < 0) { perror("send"); } } printf("接收到控制台的关闭请求, 不再写入... \n关闭连接\n"); // 可以具体到关闭某一端 shutdown(client_fd, SHUT_WR); free(write_buf); return NULL; } int main(int argc, char const *argv[]) { struct sockaddr_in server_addr, client_addr; int sock_fd, tmp_res; // 清空 memset(&server_addr, 0, sizeof(server_addr)); memset(&client_addr, 0, sizeof(client_addr)); // 填写客户端类型 client_addr.sin_family = AF_INET; // ipv4 // 填写IP地址 0.0.0.0 inet_pton(AF_INET, ADDRESS_C, &client_addr.sin_addr); // 填写端口号 client_addr.sin_port = htons(PORT_C); // 填写服务端类型 server_addr.sin_family = AF_INET; // ipv4 // 填写IP地址 inet_pton(AF_INET, ADDRESS_S, &server_addr.sin_addr); // 填写端口号 server_addr.sin_port = htons(PORT_S); /******** 网络编程 socket 流程 ********/ // 1. 创建socket，只有一个文件描述符 sock_fd = socket(AF_INET, SOCK_STREAM, 0); handle_error("socket", sock_fd); // 2. 绑定 tmp_res = bind(sock_fd, (struct sockaddr *)&client_addr, sizeof(client_addr)); handle_error("bind", tmp_res); // 3. 客户端主动连接服务端 tmp_res = connect(sock_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)); handle_error("connect", tmp_res); printf("连接上服务端_%s:%d\n", inet_ntoa(server_addr.sin_addr), ntohs(server_addr.sin_port)); // 创建子线程用于收消息 pthread_t pid_read, pid_write; pthread_create(&pid_read, NULL, read_from_server, (void *)&sock_fd); // 创建子线程用于发消息 pthread_create(&pid_write, NULL, write_to_server, (void *)&sock_fd); // 阻塞主线程 pthread_join(pid_read, NULL); pthread_join(pid_write, NULL); // 释放资源 printf("释放资源\n"); close(sock_fd); return 0; } 在上述例程中，将客户端绑定到了 192.168.100.137 的 8888 端口，192.168.100.137 实际上是本机 IP，此处等价于 localhost 或 127.0.0.1。此外，通常服务端不需要绑定到具体的 IP 和端口，如果不绑定，启动后会操作系统会随机为客户端分配本机的某个端口。这里将客户端绑定至指定的 IP 和端口，主要是为了在分析时便于区分客户端和服务端，实际的客户端程序完全可以省去这一步。 后续 这里是学习中遇到的或想到的问题，以下都是个人总结。 2024-09-24 补充：进程I/O和网络编程的缓冲区。 ① 进程的I/O缓冲区 进程的缓冲区机制是一种中间存储技术，用于在数据处理和传输中暂存数据。对于提高计算效率、优化资源利用、以及确保数据在处理和传输过程中的稳定性和可靠性至关重要。 进程的I/O缓冲区用于文件读写或通过标准输入输出与终端交互。分为输入缓冲区和输出缓冲区。 （1）输入缓冲区存储从文件或其他外部源接收的数据，直到进程准备好处理它。 （2）输出缓冲区暂存要发送到外部设备或系统的数据，直到能够执行写操作。 ② 网络编程中的缓冲区 网络编程中，尤其是使用socket通信时，内核会在内核空间维护网络缓冲区，它也分为输入输出缓冲区。与I/O缓冲区不同，网络缓冲区的管理是由内核来完成的，用户无法干预。当然，用户可以在应用程序中通过在堆空间动态分配内存的方式构建额外的缓冲区。 （1）数据接收 当网络数据到达网络接口（如以太网卡）时，数据首先被操作系统的网络驱动接收。操作系统会将这些数据存储在内核空间的接收缓冲区中。应用程序之后通过读取系统调用（如recv或read），从内核空间的缓冲区中获取这些数据。 （2）数据发送 当应用程序想要发送数据时，它通过写入系统调用（如send或write）将数据传递给内核。内核将这些数据存放在内核空间的发送缓冲区中，然后根据网络协议的需要适时将数据发送到网络上。 ③ 进程的缓冲模式 设置缓冲模式（C语言可以通过调用setvbuf()函数来设置文件流的缓冲模式） 行缓冲（Line Buffering） 全缓冲（Fully Buffering） 无缓冲（No Buffering） 2024-10-16 补充：进程守护与 I/O 多路复用。 进程守护和 I/O 多路复用是计算机科学中的两个重要概念，它们各自在不同的领域发挥着关键作用。以下是对这两个概念的详细解释： 一、进程守护 定义：守护进程（Daemon Process）是在后台运行的一种特殊类型的进程，通常不与任何终端关联。 特点：独立于用户会话，通常在系统启动时启动，并持续运行以执行一些特定的任务或服务。为了提供系统服务、执行定期任务、监听网络请求等目的而设计。 实现步骤（以创建守护进程为例）： 调用 umask 设置文件创建掩码，以确保守护进程创建的文件具有适当的权限。 关闭不再需要的文件描述符，防止它们在后续的操作中干扰。 执行守护进程的实际工作，这通常是一个简单的无限循环，可以根据实际需求来添加守护进程的具体逻辑。 （可选）设置信号处理函数，以确保守护进程在收到特定信号时能够执行适当的操作。 二、I/O多路复用 定义：I/O多路复用（I/O Multiplexing）是一种计算机编程技术，它允许程序能够同时监视多个输入和输出（I/O）流，而不需要为每个流创建一个独立的线程。 原理：使用一组系统调用同时监听多个文件描述符的 I/O 状态，包括可读、可写、异常等，从而实现高效的I/O操作管理。 常见方法： select：早期的一种多路复用方法，允许程序监视多个文件描述符（例如套接字）是否就绪，从而进行读写操作。但每次调用 select 都需要把 fd_set 集合从用户态拷贝到内核态，开销较大，且对被监控的 fd_set 集合大小有限制（通常为 1024）。 poll：类似于 select，但在某些方面更优越，它克服了 select 的一些限制，如没有最大数量限制（但数量过大后性能会下降）。 epoll（Linux 特有）：高效的多路复用技术，通过维护一个事件表来跟踪文件描述符的状态变化，可以更好地处理大量的 I/O 连接。 kqueue（BSD/Mac 特有）：类似于 epoll，高性能的多路复用机制。 优点：提高了程序的效率和响应能力，能够在单个线程中管理多个 I/O 操作。 三、进程守护与 I/O 多路复用的关系 虽然进程守护和 I/O 多路复用是两个不同的概念，但它们在某些应用场景中可以结合使用。例如，一个守护进程可能需要同时处理多个网络连接或文件 I/O 操作，这时就可以利用 I/O 多路复用技术来提高效率和响应能力。通过 I/O 多路复用，守护进程可以在单个线程中同时监视多个 I/O 流，当有事件发生时进行响应，从而避免了为每个 I/O 流创建一个独立线程的开销。 综上所述，进程守护和 I/O 多路复用都是计算机科学中的重要概念，它们在不同的领域发挥着关键作用。在实际应用中，可以根据具体需求将它们结合使用，以实现更高效、更可靠的程序设计和开发。 —由文心大模型3.5生成
线程同步的主要目的是确保多个线程在访问共享资源时能够按照一定的顺序进行，避免因为多个线程同时操作同一资源而导致的数据混乱或错误，以下解释 Linux 中线程同步的竞态条件和锁。 竞态条件和锁 当多个线程并发访问和修改同一个共享资源（如全局变量）时，如果没有适当的同步措施，就会遇到线程同步问题。这种情况下，程序最终的结果依赖于线程执行的具体时序，导致了竞态条件。 竞态条件（race condition）是一种特定的线程同步问题，指的是两个或者以上进程或者线程并发执行时，其最终的结果依赖于进程或者线程执行的精确时序。它会导致程序的行为和输出超出预期，因为共享资源的最终状态取决于线程执行的顺序和时机。为了确保程序执行结果的正确性和预期一致，需要通过适当的线程同步机制来避免竞态条件。 如何避免竞态条件 避免多线程写入一个地址。 给资源加锁，使同一时间操作特定资源的线程只有一个。 常见的锁机制 锁主要用于互斥，即在同一时间只允许一个执行单元（进程或线程）访问共享资源。包括上面的互斥锁在内，常见的锁机制共有三种： （1）互斥锁（Mutex）：保证同一时刻只有一个线程可以执行临界区的代码。 （2）读写锁（Reader/Writer Locks）：允许多个读者同时读共享数据，但写者的访问是互斥的。 （3）自旋锁（Spinlocks）：在获取锁之前，线程在循环中忙等待，适用于锁持有时间非常短的场景，一般是Linux内核使用。 后续 这里是学习中遇到的或想到的问题，以下都是个人总结。 2024-08-31 补充：关于读写锁中出现的写饥饿问题。 ① 问题描述 读写锁的写饥饿问题（Writer Starvation）是指在使用读写锁时，写线程可能无限期地等待获取写锁，因为读线程持续地获取读锁而不断地推迟写线程的执行。这种情况通常在读操作远多于写操作时出现。 ② 解决方案 Linux 提供了可以修改的属性 pthread_rwlockattr_t，默认情况下，属性中指定的策略为“读优先”，当写操作阻塞时，读线程依然可以获得读锁，从而在读操作并发较高时导致写饥饿问题。我们可以尝试将策略更改为“写优先”，当写操作阻塞时，读线程无法获取锁，避免了写线程持有锁的时间持续延长，使得写线程获取锁的等待时间显著降低，从而避免写饥饿问题。 ③ 个人总结 没有妥善管理读写操作的优先级，导致写者无限期等待资源。
线程处理是计算机编程中的一个重要概念，它涉及到多线程编程的各个方面，包括线程的创建、调度、同步、互斥、通信以及异常处理等。 线程的作用 线程处理在软件开发中扮演着重要角色，主要体现在以下几个方面： 1. 提高应用程序的响应能力：通过多线程处理，可以将耗时的操作放在后台线程中执行，从而避免阻塞主线程，使得应用程序能够更快地响应用户的操作。 2. 提高程序的吞吐量：在多处理器或多核系统上，多线程处理可以使得多个线程并行执行，从而提高程序的执行效率，减少程序的总体执行时间。 3. 实现复杂的并发操作：在需要处理多个并发任务时，如网络通信、文件操作等，多线程处理提供了一种有效的解决方案。 Linux 线程简介 Linux 中的线程是指轻量级的执行单元，相比于进程，具有以下特点： （1）进程（Process）是正在执行的程序的实例。每个进程都有自己的地址空间、代码段、数据段和打开的文件描述符等资源。线程（Thread）是进程内的一个执行单元，它共享相同的地址空间和其他资源，包括文件描述符、信号处理等，但每个线程都有自己的栈空间。 （2）由于共享地址空间和数据段，同一进程的多线程之间进行数据交换比进程间通信方便很多，但也由此带来线程同步问题。 （3）同一进程的多线程共享大部分资源，除了每个线程独立的栈空间。这代表线程的创建、销毁、切换要比进程的创建、销毁、切换的资源消耗小很多，所以多线程比多进程更适合高并发。 线程控制 线程创建 pthread_create #include <pthread.h> /** * 创建一个新线程 * * pthread_t *thread: 指向线程标识符的指针,线程创建成功时,用于存储新创建线程的线程标识符 * const pthread_attr_t *attr: pthead_attr_t结构体,这个参数可以用来设置线程的属性, * 如优先级、栈大小等。如果不需要定制线程属性,可以传入 NULL,此时线程将采用默认属性。 * void *(*start_routine)(void *): 一个指向函数的指针,它定义了新线程开始执行时的入口点。 * 这个函数必须接受一个 void * 类型的参数,并返回 void * 类型的结果 * void *arg: start_routine 函数的参数,可以是一个指向任意类型数据的指针 * return: int 线程创建结果 * 成功 0 * 失败 非0 */ int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg); 每个线程都有一个唯一的标识符（即线程ID），这个标识符是通过 pthread_t 类型的变量来表示的，当pthread_create 成功创建一个线程时，它会将新线程的标识符存储在 thread 参数指向的位置。 pthread_t定义在头文件 <pthreadtypes.h> 中，实际上是 long 类型（long 和 long int 是相同类型的不同写法）的别名。 typedef unsigned long int pthread_t; 新线程执行函数的声明为 void *(*start_routine)(void *)，其入参和返回值都是 void * 指针。我们可以将传递给线程函数的参数包装为结构体，并将其指针作为入参，再在函数内部处理；同理我们可以在线程函数内部将要返回的状态码和返回值包装为结构体，并将指针作为返回值 return。 线程终止 线程终止有以下几种方法： 线程函数执行return语句； 线程函数内部调用 pthread_exit 函数； 其他线程调用 pthread_cancel 函数。 线程终止函数为 pthread_exit： #include <pthread.h> /** * 结束关闭调用该方法的线程，并返回一个内存指针用于存放结果 * void *retval: 要返回给其它线程的数据 */ void pthread_exit(void *retval); 当某个线程调用pthread_exit方法后，该线程会被关闭（相当于return）。线程可以通过retval向其它线程传递信息，retval指向的区域不可以放在线程函数的栈内。其他线程（例如主线程）如果需要获得这个返回值，需要调用pthread_join方法。 #include <pthread.h> /** * 等待指定线程结束，获取目标线程的返回值，并在目标线程结束后回收它的资源 * * pthread_t thread: 指定线程ID * void **retval: 这是一个可选参数，用于接收线程结束后传递的返回值。 * 如果非空，pthread_join 会在成功时将线程的 exit status 复制到 *retval 所指向的内存位置。 * 如果线程没有显式地通过 pthread_exit 提供返回值，则该参数将被设为 NULL 或忽略 * return: int 成功 0 * 失败 1 */ int pthread_join(pthread_t thread, void **retval); 其它终止方式参数说明 #include <pthread.h> /** * @brief 将线程标记为detached状态。POSIX线程终止后，如果没有调用pthread_detach或pthread_join，其资源会继续占用内存，类似于僵尸进程的未回收状态。默认情况下创建线程后，它处于可join状态，此时可以调用pthread_join等待线程终止并回收资源。但是如果主线程不需要等待线程终止，可以将其标记为detached状态，这意味着线程终止后，其资源会自动被系统回收。 * * @param thread 线程ID * @return int 成功返回0，失败返回错误码 */ int pthread_detach(pthread_t thread); /** * @brief 向目标线程发送取消请求。目标线程是否和何时响应取决于它的取消状态和类型 * 取消状态（Cancelability State）：可以是enabled（默认）或disabled。如果取消状态为禁用，则取消请求会被挂起，直至线程启用取消功能。如果取消状态为启用，则线程的取消类型决定它何时取消。 * 取消类型（Cancelability Type）：可以是asynchronous（异步）或deferred（被推迟，默认值）。 * asynchronous：意味着线程可能在任何时候被取消（通常立即被取消，但系统并不保证这一点） * deferred：被推迟意味着取消请求会被挂起，直至被取消的线程执行取消点（cancellation point）函数时才会真正执行线程的取消操作。 * 取消点函数：是在POSIX线程库中专门设计用于检查和处理取消请求的函数。当被取消的线程执行这些函数时，如果线程的取消状态是enabled且类型是deferred，则它会立即响应取消请求并终止执行。man 7 pthreads可以看到取消点函数列表。 * * @param thread 目标线程，即被取消的线程 * @return int 成功返回0，失败返回非零的错误码 * 需要注意的是，取消操作和pthread_cancel函数的调用是异步的，这个函数的返回值只能告诉调用者取消请求是否成功发送。当线程被成功取消后，通过pthread_join和线程关联将会获得PTHREAD_CANCELED作为返回信息，这是判断取消是否完成的唯一方式 */ int pthread_cancel(pthread_t thread); /** * @brief 设置调用线程的取消状态 * PTHREAD_CANCEL_ENABLE：启用取消功能 * PTHREAD_CANCEL_DISABLE：禁用取消功能 * * @param state 目标状态 * @param oldstate 指针，用于返回历史状态 * @return int 成功返回0，失败返回非零错误码 */ int pthread_setcancelstate(int state, int *oldstate); #include <pthread.h> /** * @brief 设置调用线程的取消类型 * PTHREAD_CANCEL_DEFERRED：设置取消类型为推迟 * PTHREAD_CANCEL_ASYNCHRONOUS：设置取消类型为异步 * * @param type 目标类型 * @param oldtype 指针，用于接收历史类型 * @return int 成功返回0，失败返回非零错误码 */ int pthread_setcanceltype(int type, int *oldtype); 测试代码 #include <pthread.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <math.h> #include <string.h> // 定义结构体接收线程结果 typedef struct Result { char *p; int len; } Result; // 如果一个进程的多个线程同时使用到标准的输入输出流，会造成冲突的问题 // 导致只能有一个线程使用到标准到标准的输入输出流。 // 1 号线程 void * one_thread(void * arg) { // 初始化 Result 结构体 Result * result = malloc(sizeof(Result)); // 解析传递的参数 char code = *((char *)arg); // 声明存放读取消息的字符串 char *ans = malloc(101); while (1) { fgets(ans, 100, stdin); if (ans[0] == code) { // 收到回复的消息 free(ans); printf("1 号线程结束\n"); char * one_ans = strdup("1 号线程已被关闭"); result->p = one_ans; result->len = strlen(one_ans); // 结束线程 返回结果 pthread_exit((void *)result); } else { printf("1 号线程正在等待\n"); } } } // 2 号线程 void * two_thread(void * arg) { // 初始化 Result 结构体 Result * result = malloc(sizeof(Result)); // 解析传递的参数 char code = *((char *)arg); // 声明存放读取消息的字符串 char *ans = malloc(101); while (1) { fgets(ans, 100, stdin); if (ans[0] == code) { // 收到回复的消息 free(ans); printf("2 号线程结束\n"); char * one_ans = strdup("2 号线程已被关闭"); result->p = one_ans; result->len = strlen(one_ans); // 结束线程 返回结果 pthread_exit((void *)result); } else { printf("2 号线程正在等待\n"); } } } int main(int argc, char const *argv[]) { // 创建两个线程 pthread_t pid_one; pthread_t pid_two; char one_code = 'o'; char two_code = 't'; Result * one_result = NULL; Result * two_result = NULL; // 创建线程 1 pthread_create(&pid_one, NULL, one_thread, &one_code); // 创建线程 2 pthread_create(&pid_two, NULL, two_thread, &two_code); // 获取线程 1 的结果 pthread_join(pid_one, (void **)&one_result); printf("1 号线程：%s\n，统计：%d\n", one_result->p, one_result->len); // 释放内存 free(one_result->p); free(one_result); // 获取线程 2 的结果 pthread_join(pid_two, (void **)&two_result); printf("2 号线程：%s\n，统计：%d\n", two_result->p, two_result->len); free(two_result->p); free(two_result); return 0; } 后续 这里是学习中遇到的或想到的问题，以下都是个人总结。 2024-08-31 问题：上述代码中命令行输入的消息不以o或t开头，输出信息的线程是不确定的。 解决：由于没有合理控制线程的并发访问，而导致引发竞态条件。