2024年7月

Objective-C (OC) 中使用 Core Data 是iOS应用开发中管理模型层对象的一种有效工具。Core Data 使用 ORM (对象关系映射) 技术来抽象化和管理数据。这不仅可以节省时间,还能减少编程错误。以下是使用 Core Data 的详细介绍,包括示例代码,以及深入底层的一些分析。

基本概念

  1. 持久化容器 (
    NSPersistentContainer
    ):

    iOS 10 引入的,封装了 Core Data 栈的设置,包括托管对象模型 (
    NSManagedObjectModel
    ),持久化存储协调器 (
    NSPersistentStoreCoordinator
    ),和上下文 (
    NSManagedObjectContext
    )。

  2. 托管对象模型 (
    NSManagedObjectModel
    ):

    描述应用的数据模型,包括实体(Entity)和这些实体之间的关系。

  3. 持久化存储协调器 (
    NSPersistentStoreCoordinator
    ):

    负责协调托管对象上下文和持久化存储。

  4. 上下文 (
    NSManagedObjectContext
    ):

    用于在内存中管理对象。执行创建、读取、更新、删除操作时,这些更改暂时只发生在上下文中,直到保存更改到持久层。

使用示例

以下是一个简单的使用 Core Data 创建和查询对象的示例:

步骤 1: 配置数据模型

首先,通过 Xcode 的 Data Model Editor 创建数据模型文件(
.xcdatamodeld
)。假设定义了一个
Person
实体,有
name

age
两个属性。

步骤 2: 设置持久化容器

在 AppDelegate 中设置持久化容器:

#import <CoreData/CoreData.h>

@interface AppDelegate : UIResponder <UIApplicationDelegate>

@property (readonly, strong) NSPersistentContainer *persistentContainer;

- (void)saveContext;

@end

@implementation AppDelegate

@synthesize persistentContainer = _persistentContainer;

// 懒加载 persistentContainer
- (NSPersistentContainer *)persistentContainer {
    // 如果容器已经被初始化了,直接返回
    if (_persistentContainer != nil) {
        return _persistentContainer;
    }
    
    // 使用名为 MyModel 的模型文件创建容器
    _persistentContainer = [[NSPersistentContainer alloc] initWithName:@"MyModel"];
    [_persistentContainer loadPersistentStoresWithCompletionHandler:^(NSPersistentStoreDescription *storeDescription, NSError *error) {
        if (error != nil) {
            // 错误处理,实际应用中应该替换为更合适的错误处理
            NSLog(@"Unresolved error %@, %@", error, error.userInfo);
            abort();
        }
    }];
    return _persistentContainer;
}
@end

步骤 3: 使用 Core Data 新增和查询

在合适的地方(如 ViewController)进行数据的新增和查询:

#import "AppDelegate.h"
#import <CoreData/CoreData.h>

- (void)insertNewPersonWithName:(NSString *)name age:(int)age {
    AppDelegate *appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
    NSManagedObjectContext *context = appDelegate.persistentContainer.viewContext;
    
    // 创建新的 Person 实体对象
    NSManagedObject *newPerson = [NSEntityDescription insertNewObjectForEntityForName:@"Person" inManagedObjectContext:context];
    [newPerson setValue:name forKey:@"name"];
    [newPerson setValue:@(age) forKey:@"age"];
    
    NSError *error = nil;
    // 保存到持久层
    if (![context save:&error]) {
        NSLog(@"保存失败: %@, %@", error, error.userInfo);
    }
}

- (NSArray *)fetchPersons {
    AppDelegate *appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
    NSManagedObjectContext *context = appDelegate.persistentContainer.viewContext;
    
    NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"Person"];
    
    NSError *error = nil;
    NSArray *results = [context executeFetchRequest:fetchRequest error:&error];
    if (!results) {
        NSLog(@"查询失败: %@, %@", error, error.userInfo);
    }
    return results;
}

深入分析

Core Data 的底层使用了 SQLite 作为默认的持久化方式(尽管你可以选择内存或者自定义解决方案),但开发者无需直接与数据库交互,所有的操作都是通过上述的对象和 API 完成。Core Data 框架负责转换这些操作为 SQLite 命令并执行。

Core Data 性能优化

  • 批量请求:
    iOS 8 引入了批量删除和更新,这样可以在不加载数据到内存的情况下直接在持久层执行操作,极大提升效率。

  • 预获取:
    对于频繁访问的关联对象,可以使用预获取来减少查询次数。

  • 轻量级迁移:
    对于数据模型的更改,通过轻量级迁移避免手动处理数据结构变动。

封装

对于Core Data的使用,进行二次封装可以提高代码的复用性,让外部调用变得更加简洁。我们可以创建一个单例类
CoreDataManager
来管理Core Data的常见操作,比如增删改查。

首先,你需要确保你的数据模型(.xcdatamodeld文件)已经设置好,举个例子,这里假设我们有一个
Person
的Entity,它有两个属性:
name
(String类型)和
age
(Int16类型)。

步骤 1: 创建Core Data管理类

#import <Foundation/Foundation.h>
#import <CoreData/CoreData.h>

@interface CoreDataManager : NSObject

@property (readonly, strong) NSPersistentContainer *persistentContainer;
+ (instancetype)sharedManager;
- (void)saveContext;
- (void)insertPersonWithName:(NSString *)name age:(NSNumber *)age completion:(void(^)(BOOL success, NSError *error))completion;
- (void)fetchAllPersons:(void(^)(NSArray *persons, NSError *error))completion;

@end

@implementation CoreDataManager

+ (instancetype)sharedManager {
    static CoreDataManager *sharedManager = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sharedManager = [[self alloc] init];
    });
    return sharedManager;
}

- (NSPersistentContainer *)persistentContainer {
    @synchronized (self) {
        if (_persistentContainer == nil) {
            _persistentContainer = [[NSPersistentContainer alloc] initWithName:@"YourModelName"];
            [_persistentContainer loadPersistentStoresWithCompletionHandler:^(NSPersistentStoreDescription *storeDescription, NSError *error) {
                if (error != nil) {
                    NSLog(@"Unresolved error %@, %@", error, error.userInfo);
                    abort();
                }
            }];
        }
    }
    return _persistentContainer;
}

- (void)saveContext {
    NSManagedObjectContext *context = self.persistentContainer.viewContext;
    NSError *error = nil;
    if ([context hasChanges] && ![context save:&error]) {
        NSLog(@"Unresolved error %@, %@", error, error.userInfo);
        abort();
    }
}

- (void)insertPersonWithName:(NSString *)name age:(NSNumber *)age completion:(void(^)(BOOL success, NSError *error))completion {
    NSManagedObjectContext *context = self.persistentContainer.viewContext;
    NSManagedObject *newPerson = [NSEntityDescription insertNewObjectForEntityForName:@"Person" inManagedObjectContext:context];
    [newPerson setValue:name forKey:@"name"];
    [newPerson setValue:age forKey:@"age"];
    
    NSError *error = nil;
    if (![context save:&error]) {
        NSLog(@"Error saving context: %@, %@", error, error.userInfo);
        completion(NO, error);
    } else {
        completion(YES, nil);
    }
}

- (void)fetchAllPersons:(void(^)(NSArray *persons, NSError *error))completion {
    NSManagedObjectContext *context = self.persistentContainer.viewContext;
    NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"Person"];
    
    NSError *error = nil;
    NSArray *results = [context executeFetchRequest:fetchRequest error:&error];
    if (error) {
        NSLog(@"Failed to fetch persons: %@, %@", error, error.userInfo);
        completion(nil, error);
    } else {
        completion(results, nil);
    }
}

@end

使用封装的CoreDataManager

这里展示如何使用
CoreDataManager
进行数据操作:

// 插入新的Person对象
[[CoreDataManager sharedManager] insertPersonWithName:@"John Doe" age:@25 completion:^(BOOL success, NSError *error) {
    if (success) {
        NSLog(@"Person added successfully");
    } else {
        NSLog(@"Failed to add person: %@", error.localizedDescription);
    }
}];

// 获取所有的Person对象
[[CoreDataManager sharedManager] fetchAllPersons:^(NSArray * _Nonnull persons, NSError * _Nonnull error) {
    if (error) {
        NSLog(@"Failed to fetch persons: %@", error.localizedDescription);
    } else {
        for (NSManagedObject *person in persons) {
            NSString *name = [person valueForKey:@"name"];
            NSNumber *age = [person valueForKey:@"age"];
            NSLog(@"Fetched person: %@, age: %@", name, age);
        }
    }
}];

通过上面的封装,我们只需调用简单的方法就可以完成对
Person
对象的增删改查操作,而不用关心Core Data的具体实现细节。这大大提高了代码的可读性和可维护性。

总结

Core Data 是一个功能强大的框架,通过封装复杂的底层细节,使得数据管理变得更加简单。高效地使用 Core Data 必须理解其背后的原理,并遵循最佳实践来设计应用。

rsync

作用:

实现文件的备份

备份位置可以是当前主机,也可以是远程主机

备份过程可以是完全备份,也可以是增量备份

功能:

1)类似于cp的复制功能

将本地主机的一个文件复制到另一个位置下

2)将本地主机的文件推送到远程主机: 也可以是从远程主机拉取文件到本地

使用模式:

shell模式:

就是本地复制

远程shell模式:

可以利用ssh来实现数据的加密传输到远程主机

服务器模式:

rsync工作在守护进程模式下

列表模式:

类似ls

确保各主机时间一致

ntp

补充:

实现文件实时同步

rsync+inotify

rsync+ sersync

rsync: 只负责传递文件到远程主机

inotify/sersync: 将发生了改变的文件找出来

rsync:

模式1:
local(本地模式)

格式:rsync [选项] 源位置... [目的位置]
 选项:
    -p: 复制文件过程中,保持文件属性不变
    -v: 显示复制过程信息
    -a:使用归档模式 (如果复制目录必须使用此选项)
    -z:在传输过程中,以压缩方式进行传输
​

模式2:
远程shell模式

格式:
   Pull: rsync [选项...] [USER@]HOST:源地址... [目的地址]
   Push: rsync [选项...] 源地址... [USER@]HOST:目的地址 
 
 push:  
# rsync -avz -e "ssh" 1.txt root@192.168.1.102:/var/robot/video
sending incremental file list
1.txt
​
sent 91 bytes  received 34 bytes  14.71 bytes/sec
total size is 20  speedup is 0.16
​
pull
# rsync -avz -e "ssh" root@192.168.1.102:/var/robot/video/1.txt /var/robot
receiving incremental file list
1.txt
​
sent 30 bytes  received 84 bytes  32.57 bytes/sec
total size is 20  speedup is 0.18
​

补充:

说明:
rsync: 在传递文件的时候,会首先对比源和目的下的文件的特征码,只有在特征码不同的时候,才会进行传递。

重点说明:

工作中通常都是用rsync+ssh密钥认证方式,目的是为了用免密登录。

# rsync -avz -e ssh root@192.168.1.102:/var/robot/video/ /var/robot/
receiving incremental file list
./
2.txt
​
sent 33 bytes  received 102 bytes  90.00 bytes/sec
total size is 20  speedup is 0.15
# echo "123123" >> 1.txt
# rsync -avz -e ssh root@192.168.1.102:/var/robot/video/ /var/robot/
receiving incremental file list
1.txt
​
sent 36 bytes  received 112 bytes  98.67 bytes/sec
total size is 20  speedup is 0.14

模式3
:守护进程模式 daemon

案例:
将web1,web2上的数据备份到backup上

准备:

关闭:

selinux

关闭防火墙

配置时间同步

关闭:selinux
#临时关闭
setenforce 0 
#永久关闭
sudo sed -i 's/^SELINUX=enforcing/SELINUX=disabled/' /etc/sysconfig/selinux
#关闭防火墙
systemctl disable --now firewalld.service
#配置时间同步
ntpdate ntp1.aliyun.com 
//也可以修改配置文件
vim /etc/ntp.conf

第一步:
配置backup上的rsync,让其工作在守护进程模式(配置服务器端)

1)修改配置文件 (若不存在创建)

   #touch /etc/rsyncd.conf

内容如下:

uid = rsync
gid = rsync
use chroot = yes
max connections = 4
timeout = 100
pid file = /var/lock/rsync.pid
lock file = /var/lock/rsync.lock
log file = /var/log/rsync.log
​
[mod]
path = /var/robot/video
read only = false
hosts allow = 192.168.1.0/24
auth users = vuser
secrets file = /var/robot/video/rsync.passwd
list = false

2)创建相关目录

mkdir -p /var/robot/video

3)创建运行rsync的系统用户

groupadd -r rsync
useradd -r -s /sbin/nologin -g rsync rsync

4)启动rsync

~# rsync --daemon

补充:

rsync默认配置文件/etc/rsyncd.conf

如果想使用其他位置配置文件,可以--config=/path/to/confiFile

rsync --daemon --config=/home/agc.conf

5)检查rsync的端口 (默认873)

~# lsof -i :873
COMMAND   PID USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
rsync   26152 root    5u  IPv4 110834      0t0  TCP *:rsync (LISTEN)
rsync   26152 root    6u  IPv6 110835      0t0  TCP *:rsync (LISTEN)

6)创建虚拟用户文件,以及相关的虚拟用户

~# mkdir -p /var/robot/video
~# touch rsync.passwd
~# chmod 600 rsync.passwd (该文件的权限必须是600)
~# chown rsync.rsync rsync.passwd
~#chown -R rsync:rsync /var/robot/video
~#chmod 750 /var/robot/video
~# vim rsync.passwd
   vuser:123123

补充:

虚拟用户就是这个我文件中的记录

一个用户占一行,冒号前是用户名,冒号后是用户密码

第二部:
配置客户端

1) 创建密码文件(权限要求为600)

echo "123123" > /etc/rsync.password
chmod 600 /etc/rsync.password

2)用rsync来传递数据到服务器的端模块mod中

使用格式:

     rsync [options] [user@]host::modulename /path
​
     rsync [options] /path/ [user@]host::modulename 
​
rsync -avz /var/robot/video vuser@host::mod --password-file=/etc/rsync.password
 

说明:

--password-file= 系统可以自动从该位置下指定的文件中读取密码

总结:

服务器端的配置

1,创建配置文件

2,创建密码文件,修改权限为600

3,创建系统用户

4,创建模块对应的目录,修改目录的属主,属组为系统用户

5,启动damon模式

客户端的配置

1,创建虚拟用户的密码文件,权限为600

2,向模块传递文件或者从目录拉取文件

如果出现错误

第一步; 检查日志

第二部: 检查selinux,iptables是否启动

第三步:检查虚拟用户的文件名称是否正确,权限是否正确

补充:

--delete:让客户端和服务器端的文件完全一致

--exclude:在进行文件传送的时候,排除指定的文件

排除实现方式(客户端)

方式一:
排除一个文件

--exclude=file

方式二:
排除多个文件

--exclude={file1,file2}

方式3:
通配符的方式

排除实现方式(服务端)

在配置文件中添加一个关键字 exclude:这里指定要排除的文件列表,列表中的内容以空格为分隔符

例子:

exclude = a.txt b.txt c.txt

完全同步原理

在文件的传输过程中

发送方有的,会直接传输到接收方

发送方没有的,但是接收方有的文件,则会删除

rsyncd.conf的基本构成

全局参数

......

[模块1]

模块参数

. . . . . .

[模块2]

模块参数

. . . . . .

全局参数

pid file: 指定rsync进程的pid文件的路径和名称

lock file: 指定rsync进程的所文件的路径和名称

log file: rsync的日志文件路径和名称

uid:指定rsync进程以什么身份在后台运行,(必须是系统用户)

gid:指定用户组

模块参数 (可以写在全局部分,如果写在全局部分,则对所有的模块都生效)

path:指定备份目录的路径

use chroot: 是否将用户锁定在家目录中

max connections:指定可以进行同时连接的用户的最大数量

read only:true|false

write only:true|false

list=true|false:设置是否可以显示全部的模块列表

auth users:指定访问模块需要使用的用户名,这里的是虚拟用户(不是存在/etc/password)

secrets file: 指定保存虚拟用户名和密码的数据库文件

hosts allow:指定可以访问模块或者rsync服务器端的ip地址

hosts deny:黑名单

timeout:指定空闲超时时间

补充:

两个参数都没有的时候,那么所有用户都可以任意访问

只有allow,那么仅仅允许白名单的用户可以访问模块

只有deny,那么仅仅是黑名单的用户禁止访问模块

两个参数都存在,优先检查白名单

如果匹配成功,则允许访问

如果匹配失败,就去检查黑名单,如果匹配成功则禁止访问

如果都没匹配成功,则允许访问呢

文件实时同步

rsync+inotify

rsync+sersync

inotify

软件

功能:

可以监控指定目录下的文件

当文件发生了改变,则会触发事件,这样就可以输出触发事件的文件的信息

监控事件

创建

删除

修改

移动

epel

安装位置

客户端

应用程序:

/usr/bin/inotifywait: 真真实现我呢见监控程序

/usr/bin/inotifywatch: 数据统计

inotify+rsync

inotifywait

选项

-r: 递归,对目录中的子目录中的文件做监控

-q: 仅仅打印少量信息(仅仅打印监控的事件)

-m:一直处于监控状态(默认是在前台进行监控)

-d:守护进程的方式来运行(运行在后台)

-o file:将监控到的时间输出到文件中(默认是输出到标准输出)

-s:将错误信息输出到系统日志(默认是将错误·信息输出到标准输出)

--excludei:忽略文件的大小写

-e
<event>
: 指定要监控的事件

access: 访问事件

modify:编辑事件

attrib:修改文件属性事件(修改文件的源数据)

close_write: 当前文件从写模式关闭的时候,会触发该事件

close_nowrote: 当文件从只读模式下关闭的时候,会触发该事件

close:无论以什么方式打开文件,再关闭文件的时候,都会触发该事件

open:当文件被打开的时候,会触发该事件

moved——to:当一个文件被移动到该目录,就会触发该事件

moved_from:当一个文件,从监控目录下移走的时候,触发该事件

moved_self: 在监控目录下执行移动操作,就会触发该事件

move:只要发生了文件的移动,就会触发该事件

create:创建文件的事件

delete:删除文件的事件

-- timefmt
<fmt>
: 指定输出发生这个事件的时间点的显示格式

--format
<ftm>
: 指定的当发生事件以后所输出的信息,以及输出的个数

%f:记录发生事件的文件名

%w:记录发生事件的文件所在的目录的绝对路径

%e:记录发生事件的名称(如果有多个事件多个事件用空格分隔)

%xe:记录发生事件的名称(如果有多个事件,多个事件用X分隔)

%T:输出发生事件的时间(时间的格式由 --timefmt
<fmt>
)

实例:

@web1 ~]#inotifywait -mrq --timefmt "%F%T" --format "%T %w%f %e" -e create,delete,modify /var/robot/video/
@web1 video]# touch 1.txt
web1 ~]# inotifywait -mrq --timefmt "%F%T" --format "%T %w%f %e" -e create,delete,modify /var/robot/video/
2024-07-1915:11:58 /var/robot/video/1.txt CREATE

实时监控脚本如下:

#!/bin/bash
prog="inotifywait"
events="create,delete,modify,attrib"
iopt="-mrq"
​
lpath="/var/robot/video"
​
rhost="192.168.1.10"
user="vuser"
secfile="/etc/rsync.passwd"
ropt="-az"
modName="mod"
​
​
$prog $iopt --format "%w%f" -e $events $lpath | while read line
do
  rsync $ropt $line $user@$rhost::$modName --password-file=$secfile
done

主要:

在实际使用的时候,需要首先进行一次完全备份(rsync)

然后在执行脚本进行实时监控

最近Mem0横空出世,官方称之为PA的记忆层,The memory layer for Personalized AI,有好事者还称这个是RAG的替代者,Mem0究竟为何物,背后的原理是什么,我们今天来一探究竟。

Mem0 介绍

开源地址:
https://github.com/mem0ai/mem0

官方介绍为:

Mem0 provides a smart, self-improving memory layer for Large Language Models, enabling personalized AI experiences across applications.

关键点,是为LLM提供的
智能的,可自我改进
的记忆层,从而可以实现在各种应用中提供
更加个性化的和连贯一致
的用户体验。

主要特点

  • 多层次记忆:支持用户级、会话级和AI代理级的记忆保留。
  • 自适应个性化:根据用户交互不断改进,提供精准个性化记忆。
  • 开发者友好API:简单易用,易于集成。
  • 跨平台一致性:保持不同设备上的行为一致性。
  • 托管服务:简化部署和维护。

快速开始

安装:通过pip安装mem0ai。

pip install mem0ai

基本用法:

import os
from mem0 import Memory

# 依赖LLM提取记忆,所以需要open ai
os.environ["OPENAI_API_KEY"] = "xxx"

# 吃石化 Mem0
m = Memory()

# 通过add方法,存储非结构化的记忆,metadata提供schema定义
result = m.add("I am working on improving my tennis skills. Suggest some online courses.", user_id="alice", metadata={"category": "hobbies"})
print(result)
# Created memory: Improving her tennis skills. Looking for online suggestions.

# Retrieve memories
all_memories = m.get_all()
print(all_memories)

# 搜索记忆  Search memories
related_memories = m.search(query="What are Alice's hobbies?", user_id="alice")
print(related_memories)

# 更新记忆 Update a memory
result = m.update(memory_id="m1", data="Likes to play tennis on weekends")
print(result)

# Get memory history
history = m.history(memory_id="m1")
print(history)

上述的示例代码展示了如何添加记忆、检索记忆、搜索、更新和获取记忆历史。

注意代码里的
metadata
, 这里相当于定义了一个schema,让LLM从非结构化数据里提取相关的记忆信息。

原理分析

透过上面的示例代码,我们先来猜测下mem0的原理:

  • 通过LLM+制定的metadata,抽取记忆信息,这里雷士知识图谱抽取,重点是定制一个合适的prompt来抽取有效信息
  • 相关记忆信息通过向量化存储,因此可以支持记忆信息检索
  • 记忆支持更新,例如demo里的hobbies更新

我们下载代码一探究竟。

记忆管理

def add(
        self,
        data,
        user_id=None,
        agent_id=None,
        run_id=None,
        metadata=None,
        filters=None,
        prompt=None,
    ):
        """
        Create a new memory.

        Args:
            data (str): Data to store in the memory.
            user_id (str, optional): ID of the user creating the memory. Defaults to None.
            agent_id (str, optional): ID of the agent creating the memory. Defaults to None.
            run_id (str, optional): ID of the run creating the memory. Defaults to None.
            metadata (dict, optional): Metadata to store with the memory. Defaults to None.
            filters (dict, optional): Filters to apply to the search. Defaults to None.

        Returns:
            str: ID of the created memory.
        """
        if metadata is None:
            metadata = {}
        embeddings = self.embedding_model.embed(data)

        filters = filters or {}
        if user_id:
            filters["user_id"] = metadata["user_id"] = user_id
        if agent_id:
            filters["agent_id"] = metadata["agent_id"] = agent_id
        if run_id:
            filters["run_id"] = metadata["run_id"] = run_id

        if not prompt:
            prompt = MEMORY_DEDUCTION_PROMPT.format(user_input=data, metadata=metadata)
        extracted_memories = self.llm.generate_response(
            messages=[
                {
                    "role": "system",
                    "content": "You are an expert at deducing facts, preferences and memories from unstructured text.",
                },
                {"role": "user", "content": prompt},
            ]
        )
        existing_memories = self.vector_store.search(
            name=self.collection_name,
            query=embeddings,
            limit=5,
            filters=filters,
        )
        existing_memories = [
            MemoryItem(
                id=mem.id,
                score=mem.score,
                metadata=mem.payload,
                text=mem.payload["data"],
            )
            for mem in existing_memories
        ]
        serialized_existing_memories = [
            item.model_dump(include={"id", "text", "score"})
            for item in existing_memories
        ]
        logging.info(f"Total existing memories: {len(existing_memories)}")
        messages = get_update_memory_messages(
            serialized_existing_memories, extracted_memories
        )
        # Add tools for noop, add, update, delete memory.
        tools = [ADD_MEMORY_TOOL, UPDATE_MEMORY_TOOL, DELETE_MEMORY_TOOL]
        response = self.llm.generate_response(messages=messages, tools=tools)
        tool_calls = response["tool_calls"]

        response = []
        if tool_calls:
            # Create a new memory
            available_functions = {
                "add_memory": self._create_memory_tool,
                "update_memory": self._update_memory_tool,
                "delete_memory": self._delete_memory_tool,
            }
            for tool_call in tool_calls:
                function_name = tool_call["name"]
                function_to_call = available_functions[function_name]
                function_args = tool_call["arguments"]
                logging.info(
                    f"[openai_func] func: {function_name}, args: {function_args}"
                )

                # Pass metadata to the function if it requires it
                if function_name in ["add_memory", "update_memory"]:
                    function_args["metadata"] = metadata

                function_result = function_to_call(**function_args)
                # Fetch the memory_id from the response
                response.append(
                    {
                        "id": function_result,
                        "event": function_name.replace("_memory", ""),
                        "data": function_args.get("data"),
                    }
                )
                capture_event(
                    "mem0.add.function_call",
                    self,
                    {"memory_id": function_result, "function_name": function_name},
                )
        capture_event("mem0.add", self)
        return response

这里的逻辑比较简单

  • 参数的判断、处理
  • 通过 MEMORY_DEDUCTION_PROMPT 结合用户的data,抽取记忆,得到extracted_memories
  • 然后通过data查询相关的existing_memories
  • 然后将extracted_memories、existing_memories 拼接到一起,交予大模型,让大模型调用合适的tool来更新记忆,tools :
    [ADD_MEMORY_TOOL, UPDATE_MEMORY_TOOL, DELETE_MEMORY_TOOL]
  • 根据function call的结果,调用tool_calls更新记忆

本质上全部委托给大模型,通过prompt做了一定的约束。

相关prompt设计

ps,我们来看下相关度的prompt设计。

MEMORY_DEDUCTION_PROMPT = """
Deduce the facts, preferences, and memories from the provided text.
Just return the facts, preferences, and memories in bullet points:
Natural language text: {user_input}
User/Agent details: {metadata}

Constraint for deducing facts, preferences, and memories:
- The facts, preferences, and memories should be concise and informative.
- Don't start by "The person likes Pizza". Instead, start with "Likes Pizza".
- Don't remember the user/agent details provided. Only remember the facts, preferences, and memories.

Deduced facts, preferences, and memories:


从提供的文本中推断出事实、偏好和记忆。
仅以项目符号形式返回事实、偏好和记忆:
自然语言文本:{用户输入}
用户/代理详细信息:{元数据}

推断事实、偏好和记忆的约束:
- 事实、偏好和记忆应简洁且信息丰富。
- 不要以“此人喜欢披萨”开头。而是以“喜欢披萨”开头。
- 不要记住提供的用户/代理详细信息。只记住事实、偏好和记忆。

推断出的事实、偏好和记忆

再来看更新记忆的prompt:

You are an expert at merging, updating, and organizing memories. When provided with existing memories and new information, your task is to merge and update the memory list to reflect the most accurate and current information. You are also provided with the matching score for each existing memory to the new information. Make sure to leverage this information to make informed decisions about which memories to update or merge.

Guidelines:
- Eliminate duplicate memories and merge related memories to ensure a concise and updated list.
- If a memory is directly contradicted by new information, critically evaluate both pieces of information:
    - If the new memory provides a more recent or accurate update, replace the old memory with new one.
    - If the new memory seems inaccurate or less detailed, retain the original and discard the old one.
- Maintain a consistent and clear style throughout all memories, ensuring each entry is concise yet informative.
- If the new memory is a variation or extension of an existing memory, update the existing memory to reflect the new information.

Here are the details of the task:
- Existing Memories:
{existing_memories}

- New Memory: {memory}


您是合并、更新和组织记忆的专家。当您获得现有记忆和新信息时,您的任务是合并和更新记忆列表,以反映最准确和最新的信息。您还会获得每个现有记忆与新信息的匹配分数。请务必利用这些信息,就更新或合并哪些记忆做出明智的决策。

指南:
- 消除重复记忆并合并相关记忆,以确保列表简洁且是最新的。
- 如果新信息与某一记忆直接矛盾,请仔细评估这两部分信息:
    - 如果新记忆提供了更新或更准确的更新内容,用新记忆替换旧记忆。
    - 如果新记忆似乎不准确或不够详细,则保留原始记忆并丢弃新记忆。
- 在所有记忆中保持一致、清晰的风格,确保每个条目都简洁且内容丰富。
- 如果新记忆是现有记忆的变体或扩展,请更新现有记忆以反映新信息。

以下是任务的详细信息:
- 现有记忆:
{现有记忆}

- 新记忆:{记忆} 

Mem0 点评

Mem0 是RAG的杀手?

  • NO, Mem0 是RAG的助手,可以帮助提供更个性化的内容。

Mem0 有什么用处?

  • Mem0可以显著提升个性化AI的能力。通过记住用户的偏好等用户画像信息,AI产品就可以提供更加个性化服务,有较好的想象空间。传统的用户画像依赖于产研去设计schema,只能挖掘存储设计好的一些特征,而
    Mem0通过大模型,可以提供schame base和大模型自己挖掘的记忆,提供了一条更通用的方案
  • PA产品、泛娱乐、教育等各个领域,都可以发挥出作用

Mem0 有什么不足?

  • mem0当前未看到提供一些通用的schema,提供graph base的管理支持
  • 用户的memory应该区分短中长期记忆,mem0当前是未区分的,需要有解决方案
  • 自定义的记忆管理规则
  • 支持结合RAG 和用户对话历史,初始化记忆
  • 完全依赖LLM,成本较大

我们也可以看下mem0的roadmap,有规划提供一些自定义规则支持:

  • Integration with various LLM providers
  • Support for LLM frameworks
  • Integration with AI Agents frameworks
  • Customizable memory creation/update rules
  • Hosted platform support

前言

数字化时代,网络编程已成为软件开发中不可或缺的一环,尤其对于 .NET 开发者而言,掌握 C# 中的网络编程技巧是迈向更高层次的必经之路。无论是构建高性能的 Web 应用,还是实现复杂的分布式系统,网络编程都是支撑这一切的基石。

本篇主要为 .NET 开发者提供一份全面而精炼的 C# 网络编程入门,从基础知识到高级话题,逐一剖析,帮助你建立起扎实的网络编程功底,让你在网络世界的编码之旅中游刃有余。

一、HTTP 请求

HTTP(Hypertext Transfer Protocol)是互联网上应用最为广泛的一种网络协议,主要用于从万维网服务器传输超文本到本地浏览器的传输协议。

在C#中,处理HTTP请求有多种方式,从传统的
System.Net
命名空间到现代的
HttpClient
类,每种方法都有其适用场景。

1、使用
HttpClient
发送HTTP请求

HttpClient
是C#中推荐用于发送HTTP请求的类,它提供了异步的API,可以更好地处理长时间运行的操作,避免阻塞UI线程。

以下是一个简单的GET请求示例:

usingSystem;usingSystem.Net.Http;usingSystem.Threading.Tasks;public classHttpClientExample
{
public static asyncTask Main()
{
using var client = newHttpClient();var response = await client.GetAsync("https://api.example.com/data");if(response.IsSuccessStatusCode)
{
var content = awaitresponse.Content.ReadAsStringAsync();
Console.WriteLine(content);
}
else{
Console.WriteLine($
"Failed to retrieve data: {response.StatusCode}");
}
}
}

2、使用
WebClient
发送HTTP请求

尽管
WebClient
类仍然存在于.NET Framework中,但在.NET Core和后续版本中,它已被标记为过时,推荐使用
HttpClient

不过,对于简单的同步请求,
WebClient
仍然可以使用:

usingSystem;usingSystem.IO;usingSystem.Net;classWebClientExample
{
public static voidMain()
{
using (var client = newWebClient())
{
try{string result = client.DownloadString("https://api.example.com/info");
Console.WriteLine(result);
}
catch(Exception ex)
{
Console.WriteLine($
"Error: {ex.Message}");
}
}
}
}

3、使用
HttpRequestMessage

HttpMessageHandler

对于更复杂的HTTP请求,如需要自定义请求头或处理认证,可以使用
HttpRequestMessage

HttpMessageHandler

这种方式提供了更多的灵活性和控制:

usingSystem;usingSystem.Net.Http;usingSystem.Threading.Tasks;classHttpRequestMessageExample
{
public static asyncTask Main()
{
using var client = newHttpClient();var request = new HttpRequestMessage(HttpMethod.Get, "https://api.example.com/info");
request.Headers.Add(
"Authorization", "Bearer your-access-token");var response = awaitclient.SendAsync(request);if(response.IsSuccessStatusCode)
{
var content = awaitresponse.Content.ReadAsStringAsync();
Console.WriteLine(content);
}
else{
Console.WriteLine($
"Failed to retrieve data: {response.StatusCode}");
}
}
}

4、注意事项

  • 安全性和性能:
    使用
    HttpClient
    时,确保在一个应用程序的生命周期内重用同一个实例,而不是每次请求都创建新的实例。
  • 错误处理:
    总是对HTTP请求的结果进行检查,处理可能发生的异常和非成功的HTTP状态码。
  • 超时和取消:
    使用
    HttpClient
    时,可以通过
    CancellationToken
    来控制请求的超时和取消。

通过掌握这些知识点,能够在C#中有效地处理各种HTTP请求,从简单的GET请求到复杂的POST请求,包括身份验证和错误处理。

二、WebSocket 通信

WebSocket是一种在单个TCP连接上进行全双工通信的协议,它提供了比传统HTTP请求/响应模型更低的延迟和更高的效率,非常适合实时数据流、聊天应用、在线游戏等场景。在C#中,无论是服务器端还是客户端,都可以使用WebSocket进行通信。

1、客户端使用 WebSocket

在C#中,你可以使用
System.Net.WebSockets
命名空间下的
ClientWebSocket
类来创建WebSocket客户端。下面是一个简单的示例,展示了如何连接到WebSocket服务器并发送和接收消息:

usingSystem;usingSystem.IO.Pipelines;usingSystem.Net.WebSockets;usingSystem.Text;usingSystem.Threading;usingSystem.Threading.Tasks;/// <summary>
///WebSocket客户端类,用于与WebSocket服务器建立连接和通信。/// </summary>
public classWebSocketClient
{
/// <summary> ///客户端WebSocket实例。/// </summary> private readonly ClientWebSocket _webSocket = newClientWebSocket();/// <summary> ///用于取消操作的CancellationTokenSource。/// </summary> private readonly CancellationTokenSource _cancellationTokenSource = newCancellationTokenSource();/// <summary> ///连接到指定的WebSocket服务器。/// </summary> /// <param name="uri">WebSocket服务器的URI。</param> public async Task Connect(stringuri)
{
//使用提供的URI连接到WebSocket服务器 await _webSocket.ConnectAsync(newUri(uri), _cancellationTokenSource.Token);
}
/// <summary> ///向WebSocket服务器发送消息。/// </summary> /// <param name="message">要发送的消息字符串。</param> public async Task SendMessage(stringmessage)
{
//将消息转换为UTF8编码的字节 byte[] buffer =Encoding.UTF8.GetBytes(message);//创建ArraySegment,封装要发送的字节缓冲区 ArraySegment<byte> segment = new ArraySegment<byte>(buffer);//发送消息到WebSocket服务器 await _webSocket.SendAsync(segment, WebSocketMessageType.Text, true, _cancellationTokenSource.Token);
}
/// <summary> ///接收WebSocket服务器发送的消息。/// </summary> /// <param name="onMessageReceived">接收到消息时调用的回调函数。</param> public async Task ReceiveMessage(Action<string>onMessageReceived)
{
//当WebSocket连接处于打开状态时,持续接收消息 while (_webSocket.State ==WebSocketState.Open)
{
var buffer = new byte[1024];//接收来自WebSocket服务器的数据 var result = await _webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), _cancellationTokenSource.Token);//如果接收到的类型为关闭,则关闭连接 if (result.MessageType ==WebSocketMessageType.Close)
{
await _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", CancellationToken.None);break;
}
//将接收到的字节转换为字符串,并通过回调函数处理 var receivedMessage = Encoding.UTF8.GetString(buffer, 0, result.Count);
onMessageReceived(receivedMessage);
}
}
/// <summary> ///断开与WebSocket服务器的连接。/// </summary> public asyncTask Disconnect()
{
//取消接收和发送操作 _cancellationTokenSource.Cancel();//关闭WebSocket连接 await _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", CancellationToken.None);
}
}

2、服务器端使用 WebSocket

在服务器端,可以使用ASP.NET Core中的
Microsoft.AspNetCore.WebSockets
来支持WebSocket。

下面是一个简单的WebSocket服务端点配置示例:

usingMicrosoft.AspNetCore.Builder;usingMicrosoft.AspNetCore.Hosting;usingMicrosoft.AspNetCore.Http;usingMicrosoft.Extensions.DependencyInjection;usingSystem;usingSystem.Threading.Tasks;public classStartup
{
/// <summary> ///配置服务容器。/// </summary> /// <param name="services">服务集合。</param> public voidConfigureServices(IServiceCollection services)
{
//添加控制器服务 services.AddControllers();
}
/// <summary> ///配置应用管道。/// </summary> /// <param name="app">应用构建器。</param> /// <param name="env">主机环境。</param> public voidConfigure(IApplicationBuilder app, IWebHostEnvironment env)
{
//在开发环境中启用异常页面 if(env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
//启用路由 app.UseRouting();//启用WebSocket中间件 app.UseWebSockets();//配置端点处理器 app.UseEndpoints(endpoints =>{//映射默认的GET请求处理器 endpoints.MapGet("/", async context =>{await context.Response.WriteAsync("Hello World!");
});
//映射WebSocket请求处理器 endpoints.Map("/ws", async context =>{//检查当前请求是否为WebSocket请求 if(context.WebSockets.IsWebSocketRequest)
{
//接受WebSocket连接 using var webSocket = awaitcontext.WebSockets.AcceptWebSocketAsync();//持续监听WebSocket消息 while (true)
{
//准备接收缓冲区 var buffer = new byte[1024 * 4];//接收WebSocket消息 var result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);//如果收到的类型为关闭消息,则关闭连接 if (result.MessageType ==WebSocketMessageType.Close)
{
await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", CancellationToken.None);break;
}
//解码接收到的消息 var message = Encoding.UTF8.GetString(buffer, 0, result.Count);
Console.WriteLine($
"Received: {message}");//回复消息给客户端 awaitwebSocket.SendAsync(new ArraySegment<byte>(Encoding.UTF8.GetBytes($"Echo: {message}")),
result.MessageType,
result.EndOfMessage,
CancellationToken.None);
}
}
else{//如果不是WebSocket请求,则返回400错误 context.Response.StatusCode = 400;
}
});
});
}
}

在上面的服务器端代码中,首先启用了WebSocket中间件,然后映射了一个
/ws
端点来处理WebSocket连接。

当收到连接请求时,我们接受连接并进入循环,监听客户端发送的消息,然后简单地回传一个回显消息。

3、说明

WebSocket为C#开发者提供了强大的实时通信能力,无论是构建复杂的实时数据流应用还是简单的聊天室,WebSocket都是一个值得考虑的选择。通过掌握客户端和服务器端的实现细节,可以充分利用WebSocket的优势,创建高性能和低延迟的实时应用。

三、 Socket 编程

Socket编程是计算机网络通信中的基础概念,它提供了在不同计算机之间发送和接收数据的能力。

在C#中,Socket编程主要通过
System.Net.Sockets
命名空间下的
Socket
类来实现。Socket可以用于创建TCP/IP和UDP两种主要类型的网络连接,分别对应于流式套接字(Stream Sockets)和数据报套接字(Datagram Sockets)。

1、Socket 基础

Socket地址族:指定网络协议的类型,如
AddressFamily.InterNetwork
用于IPv4。

Socket类型:
SocketType.Stream
用于TCP,
SocketType.Dgram
用于UDP。

Socket协议:
ProtocolType.Tcp

ProtocolType.Udp
,分别用于TCP和UDP。

2、TCP Socket 客户端

TCP Socket客户端通常用于建立持久的连接,并通过流的方式发送和接收数据。

以下是一个简单的TCP客户端示例:

usingSystem;usingSystem.Net;usingSystem.Net.Sockets;usingSystem.Text;public classTcpClientExample
{
public static voidMain()
{
try{//创建一个新的Socket实例 using (Socket socket = newSocket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp))
{
//连接到服务器 IPAddress ipAddress = IPAddress.Parse("127.0.0.1");
IPEndPoint remoteEP
= new IPEndPoint(ipAddress, 11000);
socket.Connect(remoteEP);
//发送数据 string message = "Hello Server!";byte[] data =Encoding.ASCII.GetBytes(message);
socket.Send(data);
//接收服务器响应 data = new byte[1024];int bytes =socket.Receive(data);
Console.WriteLine(
"Received: {0}", Encoding.ASCII.GetString(data, 0, bytes));
}
}
catch(Exception e)
{
Console.WriteLine(
"Error: {0}", e.ToString());
}
}
}

3、TCP Socket 服务器

TCP Socket服务器负责监听客户端的连接请求,并处理来自客户端的数据。

以下是一个简单的TCP服务器示例:

usingSystem;usingSystem.Net;usingSystem.Net.Sockets;usingSystem.Text;public classTcpServerExample
{
public static voidMain()
{
try{//创建一个新的Socket实例 using (Socket listener = newSocket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp))
{
//绑定到本地端口 IPAddress ipAddress =IPAddress.Any;
IPEndPoint localEP
= new IPEndPoint(ipAddress, 11000);
listener.Bind(localEP);
//监听连接 listener.Listen(10);//接受客户端连接 Console.WriteLine("Waiting for a connection...");
Socket handler
=listener.Accept();//接收数据 byte[] data = new byte[1024];int bytes =handler.Receive(data);
Console.WriteLine(
"Text received: {0}", Encoding.ASCII.GetString(data, 0, bytes));//发送响应 string response = "Hello Client!";byte[] responseData =Encoding.ASCII.GetBytes(response);
handler.Send(responseData);
}
}
catch(Exception e)
{
Console.WriteLine(
"Error: {0}", e.ToString());
}
}
}

4、UDP Socket

UDP Socket用于无连接的、不可靠的网络通信,通常用于实时数据传输,如视频流或游戏。

以下是一个简单的UDP客户端和服务器示例:

UDP客户端

usingSystem;usingSystem.Net;usingSystem.Net.Sockets;usingSystem.Text;public classUdpClientExample
{
public static voidMain()
{
try{//创建一个新的Socket实例 using (UdpClient client = newUdpClient())
{
//发送数据 string message = "Hello UDP Server!";byte[] data =Encoding.ASCII.GetBytes(message);
IPEndPoint server
= new IPEndPoint(IPAddress.Parse("127.0.0.1"), 11000);
client.Send(data, data.Length, server);
//接收服务器响应 data = client.Receive(refserver);
Console.WriteLine(
"Received: {0}", Encoding.ASCII.GetString(data));
}
}
catch(Exception e)
{
Console.WriteLine(
"Error: {0}", e.ToString());
}
}
}

UDP服务器

usingSystem;usingSystem.Net;usingSystem.Net.Sockets;usingSystem.Text;public classUdpServerExample
{
public static voidMain()
{
try{//创建一个新的Socket实例 using (UdpClient listener = new UdpClient(11000))
{
//接收数据 IPEndPoint client = new IPEndPoint(IPAddress.Any, 0);byte[] data = listener.Receive(refclient);
Console.WriteLine(
"Text received: {0}", Encoding.ASCII.GetString(data));//发送响应 string response = "Hello UDP Client!";byte[] responseData =Encoding.ASCII.GetBytes(response);
listener.Send(responseData, responseData.Length, client);
}
}
catch(Exception e)
{
Console.WriteLine(
"Error: {0}", e.ToString());
}
}
}

以上示例展示了如何使用C#中的
Socket
类来实现TCP和UDP的客户端与服务器通信。

在实际应用中,可能还需要处理并发连接、错误处理和资源管理等问题。

此外,对于TCP通信,考虑到性能和资源使用,通常建议使用异步编程模型。

四、C# 网络安全

C# 中进行网络编程时,网络安全是一个至关重要的方面,涉及数据传输的保密性、完整性和可用性。以下是一些关键的网络安全知识点,它们对于构建安全的网络应用程序至关重要:

1、SSL/TLS 加密

在C#中使用
HttpClient
时,可以通过
HttpClientHandler
类来配置SSL/TLS相关的选项,确保HTTPS请求的安全性。

下面是一个示例,演示了如何使用
HttpClientHandler
来配置SSL/TLS设置:

usingSystem;usingSystem.Net.Http;usingSystem.Net.Http.Headers;usingSystem.Security.Cryptography.X509Certificates;usingSystem.Threading.Tasks;classProgram
{
static asyncTask Main()
{
//创建 HttpClientHandler 实例 var handler = newHttpClientHandler();//配置 SSL/TLS 设置//设置检查服务器证书的委托 handler.ServerCertificateCustomValidationCallback =HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;//设置是否自动重定向 handler.AllowAutoRedirect = true;//设置代理//handler.UseProxy = true;//handler.Proxy = new WebProxy("http://proxy.example.com:8080");//创建 HttpClient 实例 using var httpClient = newHttpClient(handler);//设置请求头部 httpClient.DefaultRequestHeaders.Accept.Clear();
httpClient.DefaultRequestHeaders.Accept.Add(
new MediaTypeWithQualityHeaderValue("application/json"));//发送 HTTPS 请求 var response = await httpClient.GetAsync("https://api.example.com/data");//检查响应状态 if(response.IsSuccessStatusCode)
{
var content = awaitresponse.Content.ReadAsStringAsync();
Console.WriteLine(content);
}
else{
Console.WriteLine($
"Failed to retrieve data: {response.StatusCode}");
}
}
}

解释

  • ServerCertificateCustomValidationCallback
    :此属性允许你指定一个委托,用来验证服务器的SSL证书。在这个示例中,我们使用了
    HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
    ,它会接受任何证书,这在测试环境中可能有用,但强烈建议在生产环境中使用更严格的证书验证逻辑。
  • AllowAutoRedirect
    :此属性控制是否允许
    HttpClient
    自动处理重定向。默认情况下,它是开启的。
  • UseProxy

    Proxy
    :如果需要通过代理服务器发送请求,可以配置这两个属性。
  • DefaultRequestHeaders
    :用于设置请求的默认头部,如
    Accept
    ,以指定期望的响应格式。

注意事项:

  • 实际应用中,不建议使用
    DangerousAcceptAnyServerCertificateValidator
    ,因为它绕过了正常的证书验证,可能使应用程序暴露于中间人攻击。在生产环境中,应该实现自己的证书验证逻辑,确保只接受有效和可信的证书。
  • 此外,如果应用程序需要处理特定的SSL/TLS协议版本或加密套件,也可以通过
    SslProtocols
    属性进一步定制
    HttpClientHandler
    的SSL/TLS设置。
  • 例如,可以将其设置为
    SslProtocols.Tls12

    SslProtocols.Tls13
    ,以限制使用的协议版本。

2、密码安全存储

在C#中安全地存储密码是一个至关重要的实践,尤其是当涉及到用户账户和敏感信息时。为了保护密码不被泄露或破解,应避免以明文形式存储密码,而是采用加密或哈希的方式。

以下是一些推荐的实践:

  • 使用哈希函数

使用安全的哈希函数,如SHA-256或SHA-512,可以将密码转换为一个固定长度的摘要。但是,简单的哈希容易受到彩虹表攻击,因此需要加入盐值(salt)。

示例代码:

usingSystem;usingSystem.Security.Cryptography;usingSystem.Text;public static classPasswordHasher
{
public static string HashPassword(string password, byte[] salt)
{
using (var sha256 =SHA256.Create())
{
var passwordSalted = Encoding.UTF8.GetBytes(password +Encoding.UTF8.GetString(salt));var hash =sha256.ComputeHash(passwordSalted);returnConvert.ToBase64String(hash);
}
}
public static byte[] GenerateSalt()
{
using (var rng = newRNGCryptoServiceProvider())
{
var salt = new byte[32];
rng.GetBytes(salt);
returnsalt;
}
}
}
//使用示例 byte[] salt =PasswordHasher.GenerateSalt();string hashedPassword = PasswordHasher.HashPassword("password123", salt);
  • 使用加盐哈希

在哈希密码之前,先将随机生成的盐值与密码结合。这可以防止彩虹表攻击和暴力破解。

  • 使用慢速哈希函数

使用像PBKDF2、bcrypt、scrypt或Argon2这样的慢速哈希函数,可以显著增加破解难度,因为它们设计时考虑了防止暴力破解。

示例代码:

usingSystem;usingSystem.Linq;usingSystem.Security.Cryptography;usingSystem.Text;public static classPasswordHasher
{
public static string HashPasswordUsingBcrypt(stringpassword)
{
using (var bcrypt = new Rfc2898DeriveBytes(password, 16, 10000)) //16 bytes of salt, 10000 iterations {return Convert.ToBase64String(bcrypt.GetBytes(24)); //24 bytes of hash }
}
}
//使用示例 string hashedPassword = PasswordHasher.HashPasswordUsingBcrypt("password123");
  • 存储哈希和盐值

在数据库中,除了存储哈希后的密码,还应存储用于该密码的盐值,以便在验证时使用相同的盐值重新计算哈希。

  • 验证密码

在用户登录时,从数据库中检索哈希和盐值,使用相同的哈希函数和盐值对输入的密码进行哈希,然后与存储的哈希值进行比较。

示例代码:

public static bool VerifyPassword(string inputPassword, string storedHash, byte[] storedSalt)
{
string hashOfInput =PasswordHasher.HashPassword(inputPassword, storedSalt);return hashOfInput ==storedHash;
}
  • 不要存储密码重置问题的答案

密码重置问题的答案应该像密码一样被安全地处理,避免以明文形式存储。

ASP.NET Core提供了内置的密码哈希和验证方法,使用这些框架通常比手动实现更安全。总之,安全地存储密码涉及到使用强哈希算法、加盐、适当的迭代次数和存储机制。同时,保持对最新安全实践的关注,并定期更新代码以应对新的威胁。

3、防止SQL注入

使用参数化查询或ORM工具等,防止SQL注入攻击。

string query = "SELECT * FROM SystemUser WHERE Username = @username";
SqlCommand command
= newSqlCommand(query, connection);
command.Parameters.AddWithValue(
"@username", inputUsername);

4、防止跨站脚本攻击(XSS)

对用户输入进行合适的编码和验证,防止恶意脚本注入。

string userContent = "<script>alert('XSS');</script>";string encodedContent = HttpUtility.HtmlEncode(userContent);

5、防止跨站请求伪造(CSRF)

ASP.NET MVC可以使用Anti-Forgery Token等机制来防止CSRF攻击。

@Html.AntiForgeryToken()

6、身份验证和授权

使用更高级的身份验证机制,如JWT(JSON Web Token),并在应用中实施合适的授权策略。

[Authorize]publicActionResult SecureAction()
{
//安全操作 }

7、判断文件安全

在C#中,判断一个文件是否"安全"可以从多个角度考量,这通常涉及到文件的来源、内容、权限以及是否包含潜在的恶意代码等。

下面我会介绍几种可能的方法来检查文件的安全性:

  • 检查文件的来源

确保文件是从可信的源下载或获取的。在Web应用程序中,可以使用
Content-Disposition
响应头来检查文件是否作为附件提供,以及文件名是否符合预期。

  • 验证文件的类型和扩展名

通过检查文件的扩展名或MIME类型来确定文件类型是否符合预期,例如,如果期望图片文件,那么只接受
.jpg
,
.png
等扩展名。

private bool IsFileSafeByExtension(stringfilePath)
{
string[] allowedExtensions = { ".jpg", ".png", ".gif"};string extension =Path.GetExtension(filePath).ToLower();returnallowedExtensions.Contains(extension);
}
  • 检查文件的内容

使用文件签名或魔法数字来验证文件的实际类型与声明的类型是否一致,防止扩展名欺骗。

private bool IsFileSafeByContent(stringfilePath)
{
byte[] magicNumbers =File.ReadAllBytes(filePath);if (magicNumbers.Length >= 2 && magicNumbers[0] == 0xFF && magicNumbers[1] == 0xD8) //JPEG {return true;
}
//Add checks for other formats... return false;
}
  • 扫描病毒和恶意软件

使用反病毒软件或在线API来检查文件是否含有病毒或恶意软件。

private bool IsFileSafeFromVirus(stringfilePath)
{
//Example: Using an antivirus API to scan the file.//This is just a placeholder and you should replace it with actual antivirus software integration. returnAntivirusApi.Scan(filePath);
}
  • 检查文件权限

确保文件具有正确的权限,以防止未经授权的访问。

private bool IsFileSafeByPermissions(stringfilePath)
{
var fileInfo = newFileInfo(filePath);var security =fileInfo.GetAccessControl();//Check permissions here... return true; //Placeholder logic }
  • 文件大小检查

限制文件的大小,避免消耗过多的磁盘空间或内存。

private bool IsFileSafeBySize(string filePath, longmaxSizeInBytes)
{
var fileInfo = newFileInfo(filePath);return fileInfo.Length <=maxSizeInBytes;
}
  • 内容安全策略(CSP)

在Web应用中,使用CSP来限制加载的资源类型和来源,防止XSS等攻击。

  • 综合检查函数示例
private bool IsFileSafe(stringfilePath)
{
return IsFileSafeByExtension(filePath) &&IsFileSafeByContent(filePath)&&IsFileSafeFromVirus(filePath)&&IsFileSafeByPermissions(filePath)&&IsFileSafeBySize(filePath,1024 * 1024); //Limit to 1MB }

请注意,上述代码片段仅作为示例,实际应用中可能需要调整和补充具体的实现细节,例如引入实际的病毒扫描库或API,以及更复杂的权限和内容检查逻辑。

安全检查是多层面的,需要结合具体的应用场景和需求进行综合考量。

8、安全的Cookie处理

Cookies是Web开发中用于存储用户信息的一种常用机制,它们可以在客户端浏览器中保存小量的数据,以便服务器可以跟踪用户的偏好设置、登录状态等信息。然而,如果Cookie处理不当,可能会引发严重的安全问题,如数据泄露、会话劫持(Session Hijacking)和跨站脚本攻击(XSS)。因此,确保Cookie的安全处理至关重要。

以下是处理Cookie时应当遵循的一些最佳实践:

  • 使用HTTPS:
    传输Cookie时,务必使用HTTPS加密连接。HTTPS可以防止中间人攻击(Man-in-the-Middle Attack),保护Cookie数据免受窃听。
  • 设置HttpOnly标志:
    将Cookie标记为HttpOnly可以阻止JavaScript脚本访问Cookie,从而降低跨站脚本攻击(XSS)的风险。
  • 设置Secure标志:
    当Cookie被标记为Secure时,它们只会在HTTPS连接下发送,确保数据在传输过程中的安全性。
  • 限制Cookie的有效路径和域:
    通过设置Cookie的Path和Domain属性,可以控制哪些页面可以访问特定的Cookie,减少攻击面。
  • 使用SameSite属性:
    SameSite属性可以控制Cookie是否随跨站点请求发送,减少跨站请求伪造(CSRF)攻击的可能性。可以选择Strict、Lax或None三种模式之一。
  • 设置合理的过期时间:
    为Cookie设定一个适当的过期时间,可以避免永久性Cookie带来的安全风险,同时也便于清理不再需要的用户信息。
  • 定期审查和更新Cookie策略:
    定期检查Cookie的使用情况,确保所有Cookie设置符合最新的安全标准和隐私法规。

通过遵循这些最佳实践,可以大大增强应用程序的安全性,保护用户数据免受恶意攻击。在Web开发中,安全的Cookie处理不仅是技术要求,也是对用户隐私和数据安全的责任体现。

usingSystem;usingSystem.Web;public classCookieHandler : IHttpHandler
{
public voidProcessRequest(HttpContext context)
{
//创建一个新的Cookie对象 HttpCookie cookie = new HttpCookie("UserSession");//设置Cookie值 cookie.Value = "123456"; //假设这是用户的唯一标识符//设置Cookie的过期时间 cookie.Expires = DateTime.Now.AddDays(1); //设置Cookie在一天后过期//设置HttpOnly属性以增加安全性 cookie.HttpOnly = true;//如果你的网站支持HTTPS,设置Secure属性 if(context.Request.IsSecureConnection)
cookie.Secure
= true;//添加Cookie到响应中 context.Response.AppendCookie(cookie);
}
public boolIsReusable
{
get { return false; }
}
}

在.NET Core或.NET 6+中,使用不同的API来处理Cookie,例如
Microsoft.AspNetCore.Http
命名空间下的
IResponseCookies
接口。

五、总结

通过文章的全面介绍 C# 网络编程,相信对这一块内容有了了解和理解。从简单的 HTTP 请求到复杂的套接字通信,从异步编程模型到安全协议的应用,每一步都为我们构建现代网络应用奠定了坚实的基。在实际项目中,根据需求深入学习和实践这些知识点,将有助于提升.NET开发者在网络编程领域的能力。持续学习和实践是成为优秀 .NET 开发者的不二法门。

如果觉得这篇文章对你有用,欢迎加入微信公众号 [
DotNet技术匠
] 社区,与其他热爱技术的同行交流心得,共同成长。

介绍:
检验和(checksum),在数据处理和数据通信领域中,用于校验目的地一组数据项的和。它通常是以十六进制为数制表示的形式。如果校验和的数值超过十六进制的FF,也就是255. 就要求其补码作为校验和。通常用来在通信中,尤其是远距离通信中保证数据的完整性和准确性。(此引用了检验和的百度百科)

用处:
在计算机网络中的网络层、运输层的IP协议、TCP协议、UDP协议等高层协议,在报文中存在一个校验和的字段,这个字段的作用是:验证报文在传输过程中是否被改动,如果接收方检测到检验和有差错,则TCP段会被直接丢弃。如下图在IP报文中占4字节。

原理:
把要发送的数据看成 16 比特的二进制整数序列,并计算他们的和。若数据字节长度为奇数,则在数据尾部补一个字节的 0 以凑成偶数。

例子:
16 位效验和计算,下图表明一个小的字符串的 16 位效验和的计算。为了计算效验和,发送计算机把每对字符当成 16 位整数处理并计算效验和。如果效验和大于 16 位,那么把进位一起加到最后的效验和中。

主要思想为:
1.打开文件,用一个字符串存下里面内容
2.首先判断字符串长度是否为奇数,若为奇数,后面补一个“0”
3.将字符串里面的字符进行读取,并转化成ASCII码对应。每次读取两个字符,也就是每两个为一组。
4.由于字符要化为十六进制后拼接,那么两位里面的前一位将乘于16的平方(256)
5.每一组的十进制数相加
6.先将相加得到的化为2进制存放在int里面,再进行处理,int型里面有32位,我们分为前16位和后16位,对于溢出的在进行处理

而这是现在网上大多数的做法,我现在只是为了更直观,在4、5、6步进行修改
4.由于字符要化为十六进制后拼接,那么两位里面的前一位将乘于16的平方(256),转化成16进制
5.每一组的16进制数相加
6.相加时,超过了0xFFFF(16位最大值),则进行溢出处理。

c++代码

#include <vector>
#include <sstream>
#include <iomanip>
#include <fstream>

using namespace std;

string to_hex(int value) {
    stringstream ss;
    ss << hex << value;
    return ss.str();
}

string add_hex(string hex1, string hex2) {
    int num1, num2;
    stringstream ss;
    ss << hex << hex1;
    ss >> num1;
    ss.clear();
    ss << hex << hex2;
    ss >> num2;
    int sum = num1 + num2;
    if (sum > 0xFFFF) {
        sum = (sum & 0xFFFF) + 1;  // Add carry to the sum
    }
    stringstream result;
    result << hex << setw(4) << setfill('0') << sum;
    return result.str();
}

string check(string filename) {
    ifstream file(filename);
    if (!file.is_open()) {
        cout << "无法打开文件" << endl;
        exit(0);
    }
    string chuang;
    getline(file, chuang);
    if (chuang.size() % 2 == 1) {
        chuang += "0";
    }
    cout << "得到字符串为 " << chuang << endl;

    vector<int> num;
    for (int i = 0; i < chuang.size(); i += 2) {
        int value = 0;
        value = (int(chuang[i]) * 256) + int(chuang[i + 1]); // Convert 2 characters to int
        num.push_back(value);
    }

    string hexchuang;
    string sum = "0000"; // Initialize sum as "0000"
    for (int i = 0; i < num.size(); i++) {
        int value = num[i];
        sum = add_hex(sum, to_hex(value));
        hexchuang += to_hex(value);
        cout << "转化为16进制后为 " << to_hex(value) << endl;
    }

    cout << "转化为16进制后为 " << hexchuang << endl;
    return sum;
}

int main() {
    string command; // 从命令行输入的命令
    string filename; // 输入的文件名

    cout << "请输入要执行的命令和处理的文件名" << endl;
    cin >> command >> filename;
    //这样运行check_sum D:/infile.txt

    while (true) {
        if (command.compare("check_sum") != 0) {
            cout << "输入命令无效,请重新输入" << endl;
        }
        else {

            cout << "最终的16位校验和: " << check(filename) << endl;
            break;
        }
    }

    return 0;
}