Linux与HTTP中的Cookie和Session

HTTP中的Cookie和Session

本篇介绍

前面几篇已经基本介绍了HTTP协议的大部分内容,但是前面提到了一点「HTTP是无连接、无状态的协议」,那么到底有什么无连接以及什么是无状态。基于这两个问题,随后解释什么是Cookie和Session,以及二者是如何通过代码实现的

本篇的代码部分都是基于上一节的HttpServer

什么是无连接和无状态

HTTP最大的特点就是无连接和无状态

所谓的无连接指的是HTTP请求服务器时不需要HTTP协议自己与服务器建立连接,由于HTTP是基于TCP的,所以整个连接工作交给了TCP,这一点在前面实现HttpServer时也有所体现

所谓的无状态指的是HTTP本身不会保存任何用户信息,举个例子,如果当前网站需要用户登录以进行后续操作,那么默认情况下,一旦这个用户登录成功后切换到其他页面就依旧会出现需要登录的情况

HTTP中的Cookie

基本介绍

既然HTTP是无状态的,那么正常情况下就会出现下面的情况:

在这里插入图片描述

上面图中这种情况的发送就会给用户带来糟糕的交互体验,也会给服务器带来比较大的开销,所以为了解决这个问题,就需要使用Cookie,但是因为HTTP本身是无连接的,所以Cookie本身并不是一种让HTTP从无连接变成有连接的技术,而是利用HTTP请求会携带报头的特点从而让HTTP每次请求时自动携带着Cookie

基于上面的情景,Cookie实际上实现的功能常见的就是保持用户的状态、记录用户偏好等

工作原理

Cookie工作方式与正常写入和读取HTTP请求报头属性比较类似:

  1. 当用户第一次访问网站时,服务器会在响应的HTTP头中设置Set-Cookie字段,用于发送Cookie到用户的浏览器
  2. 浏览器在接收到Cookie后,会将其保存在本地(通常是按照域名进行存储)在之后的请求中,浏览器会自动在HTTP请求头中携带Cookie字段,将之前保存的Cookie信息发送给服务器
  3. 在之后的请求中,浏览器会自动在HTTP请求头中携带Cookie字段,将之前保存的Cookie信息发送给服务器

Cookie的分类

Cookie一般情况下分为两种:

  1. 会话Cookie(Session Cookie):在浏览器关闭时失效
  2. 持久Cookie(Persistent Cookie):带有明确的过期日期或持续时间,可以跨多个浏览器会话存在

如果Cookie是一个持久性的,那么它其实就是浏览器特定目录下的一个文件。但直接查看这些文件可能会看到乱码或无法读取的内容,因为Cookie文件通常以二进制或sqlite格式存储。所以一般想查看Cookie都只能通过浏览器提供的入口进行查看

HttpServer中实现Cookie功能

前置知识

要想在HttpServer中实现给客户端添加Cookie的功能就需要用到一个响应报头属性:Set-Cookie,其值基本形式如下:

Set-Cookie: Cookie-name=Cookie-value

其中,Cookie-name代表的就是需要写入的Cookie的名称,Cookie-value对应的就是Cookie的值

除了上面的基本形式外,Cookie还有一种完整形式:

Set-Cookie: Cookie-name=Cookie-value; expires=Week, Day Month Year Hour:Minutes:Seconds UTC/GMT; path=Request-path; domain=domain-name; secure; Httponly

在上面的完整形式中,每一个字段的含义如下:

字段含义
Cookie-name=Cookie-valueCookie的名称和对应的值,是Set-Cookie中唯一必需的部分
expires指定Cookie的过期时间,格式为标准HTTP日期格式。如果不设置,则为会话Cookie(浏览器关闭即失效),例如expires=Wed, 21 Oct 2025 07:28:00 GMT
path指定Cookie生效的路径范围。例如:path=/admin/表示Cookie只在访问/admin/及其子路径时有效,默认为当前文档路径
domain指定Cookie生效的域名范围。例如:domain=example.com允许Cookie在该域名及其子域名下有效,默认为当前域名(不含子域名)
secure标记Cookie只能通过HTTPS安全连接传输,无值,存在即生效
HttpOnly禁止JavaScript通过document.cookie访问此Cookie,防止XSS攻击窃取Cookie,无值,存在即生效

需要注意的是,如果需要给客户端发送多个Cookie,则不可以直接在同一个Cookie后方拼接,而是需要另起一个Set-Cookie属性

因为Cookie有简单形式和完整形式两种,所以下面先演示简单形式,再扩展到完整形式,但是完整形式中不会演示所有属性,而是针对expirespath这两个属性进行解释

简单形式
写入Cookie

根据Cookie的工作原理第一条,首先需要在客户端第一次请求服务端时向客户端写入一条Cookie值,所以需要在响应报头中添加一条Set-Cookie属性,因为在上一节已经实现了一个登录函数:

void login(HttpRequest &req, HttpResponse &resp)
{LOG(LogLevel::INFO) << "进入登录模块";req.getParamKv();
}

所以本次考虑将添加Set-Cookie属性放到当前函数中,方式很简单,只需要调用HttpResponse类中的insertRespHead函数即可:

!!! note

本次为了方便就不处理是否是第一次添加或者之前添加的Cookie是否存在
void login(HttpRequest &req, HttpResponse &resp)
{   // ...// 添加Cookieresp.insertRespHead("Set-Cookie", "testCookie=newCookie");
}

添加完Cookie之后就需要向响应报头中写入这一条属性,所谓的写入就是进行序列化,所以接下来需要调用HttpResponse类中的serialize方法以及发送对应的响应报头给客户端,但是这一步实际上已经在处理HTTP请求函数handleHttpRequest做过了,所以就不需要再重复做了

完成上面的步骤后,接下来进行测试,需要注意,要让服务器执行login函数就必须请求/login方法,可以考虑直接在浏览器地址栏中请求/login并携带部分参数,此时使用的就是GET请求方式,也可以考虑走完整的步骤,先请求主页,再请求登录页面,通过登录请求/login资源。本次选择前者,但是为了能够看到具体的效果,需要考虑给响应体写入一点内容,确保浏览器可以停止在该页面,所以还需要调用一个设置响应行和响应体的方法,这里在HttpResponse中提供一个setRespLinesetRespBody方法:

=== “设置响应行”

// 设置响应行
void setRespLine(std::string& line)
{_resp_line = line;
}

=== “设置响应体”

void setRespBody(std::string& body)
{_resp_body = body;
}

接着,再在login函数中通过resp调用该方法设置响应行和响应体:

void login(HttpRequest &req, HttpResponse &resp)
{// ...// 设置响应行std::string line = default_http_ver + " " + std::to_string(200) + resp.setStatusCodeDesc(200);resp.setRespLine(line);// 设置响应体std::string body = "<h1>Hello Linux</h1>";resp.setRespBody(body);
}

测试结果如下:

在这里插入图片描述

但是,上面的方法只能实现发送一个Cookie给客户端,因为对于哈希表来说,key相同,新值会覆盖旧值,当前插入的key都是Set-Cookie,所以会出现新的Cookie键值对覆盖旧的Cookie键值对,为了解决这个问题,可以考虑使用一个vector单独保存每一条设置Cookie的字符串

所以,首先需要一个vector成员用于存储设置Cookie的字符串,接着在设置Cookie时,应该以一个完整的字符串存储到vector中,所以实际上需要下面的方法:

void insertRespCookies(const std::string &cookie_str)
{_cookies.push_back(cookie_str);
}

接着,在序列化时直接对每一个设置Cookie的字符串添加换行符:

// 序列化
bool serialize(std::string &out_str)
{// ...// 单独为Cookie添加\r\nstd::for_each(_cookies.begin(), _cookies.end(), [&](std::string s){s += default_sep;out_str += s;});// ...return true;
}

最后,修改设置Cookie的login函数:

void login(HttpRequest &req, HttpResponse &resp)
{// ...// 添加Cookie// resp.insertRespHead("Set-Cookie", "testCookie=newCookie");resp.insertRespCookies("Set-Cookie: testCookie=newCookie");resp.insertRespCookies("Set-Cookie: testCookie1=newCookie1");resp.insertRespCookies("Set-Cookie: testCookie2=newCookie2");// ...
}

编译运行上面的代码即可看到多条Cookie:

在这里插入图片描述

读取Cookie

当前,服务端已经可以向客户端写入Cookie信息,但是光写入还并不够,服务端还需要读取到对应的Cookie并对Cookie进行解析,因为客户端发送的Cookie形式为:

Cookie: testCookie=newCookie

所以需要根据这个字段格式进行解析

在前面实现HttpRequest中已经实现了读取请求报头中的数据函数getPairFromReqHead,所以在该类中存在一个成员keyCookievaluetestCookie=newCookie的键值对

首先根据key找到对应的value,再将value根据拆分出每一个Cookie

需要注意,尽管服务端是分开设置多个Cookie,但是在客户端给服务端发送Cookie时是合并在一个Cookie中,每一个Cookie以;<space>进行分隔,例如:

```
Cookie: testCookie=newCookie; testCookie1=newCookie1
```

接下来的问题就是如何拆分Cookie,拆分Cookie的思路和拆分请求体参数的方式非常类似,而因为要存储每一个Cookie键值对,所以还需要一个哈希表,所以基本结构如下:

// 键值对分隔符
const std::string default_kv_sep = "=";
// Cookie分隔符
const std::string default_cookie_sep = "; ";// HTTP请求
class HttpRequest
{
public:// ...// 获取Cookie键值对bool getCookiePairFromReqHead(){}// ...private:// ...std::unordered_map<std::string, std::string> _cookie_kv; // Cookie键值对// ...
};

接着,根据前面实现从请求体中获取参数键值对的思路实现获取Cookie键值对:

// 获取Cookie键值对
bool getCookiePairFromReqHead()
{// 先提取到Cookieauto keyPos = _kv.find("Cookie");if (keyPos == _kv.end())return false;// 在对应的value中找到Cookie值std::string cookie_value = _kv["Cookie"];auto pos = size_t(-1);while (true){// 找到;auto pos1 = cookie_value.find(default_cookie_sep, pos + 1);if (pos1 == std::string::npos){// 最后一个参数键值对auto pos_t = cookie_value.find(default_kv_sep, pos + 1);if (pos_t == std::string::npos)return false;std::string key = cookie_value.substr(pos + 1, pos_t - pos - 1);std::string value = cookie_value.substr(pos_t + 1);_cookie_kv.insert({key, value});break;}// 找到=auto pos2 = cookie_value.find(default_kv_sep, pos + 1);if (pos2 == std::string::npos)return false;std::string key = cookie_value.substr(pos + 1, pos2 - pos - 1);std::string value = cookie_value.substr(pos2 + 1, pos1 - pos2 - 1);_cookie_kv.insert({key, value});// 修改起始位置pos = pos1 + 1;}return true;
}

在上面的代码中,除了函数开始的逻辑与前面获取POST请求参数的逻辑不一样以外,还有更新pos的逻辑,因为不同的Cookie之间的分隔符是两个字符,而不是一个字符

接下来,为了可以看到拆分后的结果,提供一个打印函数:

void getCookieKv()
{std::for_each(_cookie_kv.begin(), _cookie_kv.end(), [&](std::pair<std::string, std::string> kv){ std::cout << kv.first << ":" << kv.second << std::endl; });
}

有了上面的函数后,接下来就是在指定位置调用该函数:

// 反序列化
bool deserialize(std::string &in_str)
{// ...// 获取CookiegetCookiePairFromReqHead();// 打印获取到的CookiegetCookieKv();// ...return true;
}

编译运行上面的代码,观察结果:

在这里插入图片描述

从上面的结果可以看到,所有设置到客户端的Cookie都可以正常获取

完整形式
介绍

上面已经基本实现了简单形式下Cookie的写入和读取,但是有时简单的Cookie并不能满足实际的需求,所以除了完成简单形式以外,还需要实现完整形式的Cookie

虽然是完整形式,但是客户端最后拿到的Cookie会被浏览器进行分析,并将相关的属性填充到浏览器的相关属性中,而客户端第二次以及后面请求服务器时,携带的Cookie也只是上面简单形式的Cookie

基于上面的原因,在完整形式部分只需要实现形成某些字段的函数即可

本次只是实现完整形式中的expirespath,其他字段不考虑

实现expires

在实现expires字段之前,先了解其格式:

expires=Week, Day Month Year Hour:Minutes:Seconds UTC/GMT;

第一个值为星期,第二个值为年月日,第三个值为时分秒,最后一个代表时区,UTC代表协调世界时,GMT代表格林威治平均时间,对于前面的三个字段来说都可以通过系统调用进行获取,唯独最后一个字段需要手动指定,那么选择UTC还是GMT?推荐UTC,其次再是GMT,原因如下:

UTC全称为Coordinated Universal Time (协调世界时),是现代国际社会使用的主要时间标准:基于高精度的原子钟,不受地球自转不规则性的影响,是国际标准化组织ISO推荐的官方时间标准。是现在全球互联网通信和大多数计算机系统的时间基准,格式精确统一,适合跨系统通信

GMT全称为Greenwich Mean Time (格林威治平均时间):历史上的时间标准,基于英国伦敦格林威治天文台的太阳时,其标准基于地球自转,存在微小的不规则性,在科学和技术领域已逐渐被UTC替代,但在日常交流中仍被广泛使用

在Cookie的expires字段中:推荐使用UTC:更精确,是现代网络协议的标准做法。GMT仍然被广泛支持,大多数浏览器能正确处理。实际上两者时间差异极小(不到1秒),但从标准化和兼容性考虑,优先选择UTC

有了知识基础,接下来就是实现获取时间函数,在前面命名管道与共享内存和日志系统已经实现或者使用了一个获取时间的函数,本次实现的思路和该函数基本一致,但是当时使用的是localtime或者localtime_r函数,这两个函数都会自动包含时区,而实际上在设置expires时不需要设置时区,当客户端获取到时间后会自动根据当前浏览器所处的地区设置时区,所以这里推荐使用另外一个系统调用gmtime或者其可重入版本gmtime_r。下面以gmtime为例,函数原型如下:

struct tm *gmtime(const time_t *timep);

使用方式与localtime一样,此处不再介绍。根据格式Wed, 21 Oct 2025 07:28:00 GMT,首先需要两个额外的函数,根据月份数字获取月份字符串和根据星期数字获取星期字符串,接着再使用这两个函数获取一个完整的expires时间:

=== “根据星期数字获取星期字符串”

std::string getWeekStrFromNumber(int w)
{// 第一个元素最好是星期天,因为gmtime以星期天为第一天const char *weekdays[7] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"};if (w < 0 || w > 12)return std::string();return weekdays[w];
}

=== “根据月份数字获取月份字符串”

std::string getMonthStrFromNumber(int m)
{const char *months[12] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};if (m < 0 || m > 12)return std::string();return months[m];
}

=== “获取expires函数”

 std::string getCookieExpiresTime(int last){// 获取指定的时间之后的时间戳time_t t = time(NULL) + last;struct tm *timer = gmtime(&t);char buffer[1024] = {0};// Wed, 21 Oct 2025 07:28:00 GMT// 注意,涉及到数字的都需要两位,确保一位数可以有前导0snprintf(buffer, sizeof(buffer), "%s, %02d %s %d %02d:%02d:%02d UTC", getWeekStrFromNumber(timer->tm_wday).c_str(),timer->tm_mday,getMonthStrFromNumber(timer->tm_year).c_str(),timer->tm_year + 1900,timer->tm_hour,timer->tm_min,timer->tm_sec);return buffer;}

接着,在login函数中创建一个带有expires的Cookie,过期时间设置为1分钟:

void login(HttpRequest &req, HttpResponse &resp)
{// ...std::string cookie_val = "testCookie=newCookie; expires=" + resp.getCookieExpiresTime(60);resp.insertRespCookies("Set-Cookie: " + cookie_val);// ...
}

编译运行上面的代码,观察结果:

在这里插入图片描述

在控制台打印出实际获取到的时间如下:

在这里插入图片描述

可以看到,这个时间虽然没有时区,但是在浏览器显示时已经会转换成当地时区

实现path

path的实现很简单,只需要在login中设置Cookie时传递path即可:

void login(HttpRequest &req, HttpResponse &resp)
{// ...resp.insertRespCookies("Set-Cookie: testCookie=newCookie; path=/login.html");// ...
}

先设置Cookie:

在这里插入图片描述

此时,testCookie=newCookie一旦被设置,下一层就只会在客户端访问login.html时才会携带该Cookie:

=== “访问主页”

在这里插入图片描述

=== “访问login.html

在这里插入图片描述

Cookie的安全性

在上面的测试中可以发现,不论Cookie是从服务端到客户端,还是客户端到服务端,都是可以在客户端以明文的方式看到的,如果用户输入的是账户名和密码或者其他隐私信息,那么一旦这个Cookie被劫持,就会出现用户信息泄露的问题,这对用户来说是较大的损失,所以Cookie本身是不安全的

HTTP中的Session

基本介绍

上面提到Cookie是不安全的,为了尽可能保证安全这个问题,在现代HTTP中除了使用Cookie外还会使用Session

Session与Cookie不同,Session是存储在服务器端的,当用户第一次请求服务端时,服务端会将用户的信息存储到一个特定的位置,再向客户端写入一个值为sessionID的Cookie,当用户第二次请求服务器时就会携带这个值sessionID的Cookie访问服务器,服务器会根据这个SessionID查找用户的信息,从而实现用户状态的保持

HttpServer中实现Session

要在HttpServer中实现Session,首先可以考虑创建一个Session类,这个类用于存储一个用户的相关信息,相当于之前包含用户信息的Cookie:

class Session
{
public:Session(const std::string &username, const std::string &status):_username(username), _status(status){_create_time = time(nullptr); // 获取时间戳就行了,后面实际需要,就转化就转换一下}~Session(){}
public:std::string _username;std::string _status;uint64_t _create_time;
};

因为服务器获取到的肯定不止一个Session,所以还需要一个类对Session类对象进行管理,此处可以考虑创建一个SessionManager类,因为sessionID是一个随机数,并且一般不能出冲突情况,所以这里考虑使用boost库中获取UUID的函数generator

using session_ptr = std::shared_ptr<Session>;class SessionManager
{
public:SessionManager(){}std::string addSession(session_ptr s){// 使用boost库生成uuid// 创建一个随机数生成器boost::uuids::random_generator generator;// 生成一个随机 UUIDboost::uuids::uuid id = generator();std::string sessionid = boost::uuids::to_string(id);_sessions.insert(std::make_pair(sessionid, s));return sessionid;}session_ptr getSession(const std::string sessionid){if (_sessions.find(sessionid) == _sessions.end())return nullptr;return _sessions[sessionid];}~SessionManager(){}private:std::unordered_map<std::string, session_ptr> _sessions;
};

首先考虑在HttpRequest中创建一个SessionManager类对象指针,并使用SessionManager类对象进行初始化。接着在反序列化时查找出Cookie中是否有sessionid,如果有就根据这个sessionid查找是否已经存在于SessionManager中,如果不存在就创建一个Session对象并插入到SessionManager

但是,在上面创建Session对象时需要使用用户名创建,而这个用户名来自请求参数,如果没有请求参数就不需要创建Session对象,所以需要对前面获取参数的函数进行修改,使其有返回值:

bool getReqParams()
{if (_req_method == "GET"){// 处理GET请求中的参数if (getReqParamsFromReqLine()){_hasArgs = true;return true;}}else if (_req_method == "POST"){// 处理POST请求中的参数if (getReqParamsFromBody()){_hasArgs = true;return true;}}return false;
}

接着,在HttpRequest类中添加一个成员_sessionid,并在deserialize函数中补充逻辑:

bool deserialize(std::string &in_str)
{// ...// 获取参数内容if(getReqParams()) {// 判断制定的sessionid是否存在,不存在则创建if (!_sm->getSession(getSessionIdFromCookiePair())){std::string username = _param_kv["username"];// 根据参数的username创建Session对象,并处于活跃状态std::shared_ptr<Session> s = std::make_shared<Session>(username, "active");// 插入到SessionManager中_sessionid = _sm->addSession(s);}}return true;
}

接着,在用户请求/login时,创建一个Cookie,这个Cookie的值即为sessionid,所以此处还需要在HttpRequest中提供一个获取_sessiondid的函数:

// 获取_sessionid
std::string getSessionId()
{return _sessionid;
}

修改login函数如下:

void login(HttpRequest &req, HttpResponse &resp)
{// ...std::string sessionid = req.getSessionId();std::string cookie_val = "sessionid=" + sessionid;resp.insertRespCookies("Set-Cookie: " + cookie_val);// ...
}

编译上面的代码,观察结果:

在这里插入图片描述

Session的安全性

Session相比Cookie具有明显的安全优势。由于Session将用户敏感数据存储在服务器端,而不是客户端,只通过SessionID标识用户,大大降低了信息泄露风险。客户端仅存储一个不包含实际用户数据的SessionID,即使这个ID被截获,攻击者也无法直接获取用户的实际信息,如密码和个人资料

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/39177.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【Tauri2】001——安装及运行

前言 笔者其实不想写教程&#xff0c;写教程很麻烦。 但是网上关于Tauri2的教程&#xff0c;要么不全&#xff0c;要么是Tauri1的&#xff0c;真的太少了&#xff0c;虽然有官网&#xff0c;还是太少了。 问Ai&#xff0c;也感觉比较离谱&#xff0c;有很多时候&#xff0c;…

【DFS】羌笛何须怨杨柳,春风不度玉门关 - 4. 二叉树中的深搜

本篇博客给大家带来的是二叉树深度优先搜索的解法技巧,在后面的文章中题目会涉及到回溯和剪枝,遇到了一并讲清楚. &#x1f40e;文章专栏: DFS &#x1f680;若有问题 评论区见 ❤ 欢迎大家点赞 评论 收藏 分享 如果你不知道分享给谁,那就分享给薯条. 你们的支持是我不断创作的…

操作系统导论——第13章 抽象:地址空间

一、早期系统 从内存来看&#xff0c;早期的机器并没有提供多少抽象给用户。基本上&#xff0c;机器的物理内存如图13.1所示 操作系统曾经是一组函数&#xff08;实际上是一个库&#xff09;&#xff0c;在内存中&#xff08;在本例中&#xff0c;从物理地址0开始&#xff09;&…

网络爬虫-2:基础与理论

一.同步加载与异步加载 1.1同步加载定义: 页面所有内容一起加载出来,当某一个数据加载有问题,整个页面就不会加载出来(如HiFiNi音乐网站),所以又叫阻塞模式 1.2爬取步骤: 看netword->document 2.1异步加载定义: 数据是分开加载的,当某一份数据有异常时,不影响其他数据…

【Docker系列五】Docker Compose 简介

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

本地安装deepseek大模型,并使用 python 调用

首先进入 ollama 官网 https://ollama.com/点击下载 下载完成后所有都是下一步&#xff0c;就可以 点击搜索 Models &#xff1a; https://ollama.com/search然后点击下载&#xff1a; 选择后复制: ollama run deepseek-r1:32b例如&#xff1a; 让它安装完成后&#xff1…

【CC2530 教程 二】CC2530定时器实现微秒、毫秒、秒延时函数

目录 一、CC2530定时器&#xff1a; 二、CC2530定时器&#xff1a; &#xff08;1&#xff09;定时器1&#xff08;Timer1&#xff09;&#xff1a; &#xff08;2&#xff09;定时器2&#xff08;Timer2&#xff09;&#xff1a; &#xff08;3&#xff09;定时器3和定时…

23种设计模式-创建型模式-工厂方法

文章目录 简介场景问题1. 直接依赖具体实现2. 违反开闭原则3. 条件分支泛滥4. 代码重复风险 解决根本问题完整类图完整代码说明核心优势代码优化静态配置表动态策略 总结 简介 工厂方法是一种创建型设计模式&#xff0c;它提供了在父类中创建对象的接口&#xff0c;但允许子类…

Umi-OCR- OCR 文字识别工具,支持截图、批量图片排版解析

Umi-OCR 是免费开源的离线 OCR 文字识别软件。无需联网&#xff0c;解压即用&#xff0c;支持截图、批量图片、PDF 扫描件的文字识别&#xff0c;能识别数学公式、二维码&#xff0c;可生成双层可搜索 PDF。内置多语言识别库&#xff0c;界面支持多语言切换&#xff0c;提供命令…

【JavaEE】Mybatis基础使用注解 增删改查操作

目录 一、配置日志二、传递参数 #{}三、增(Insert)四、返回主键Options五、删&#xff08;Delete&#xff09;六、改&#xff08;Update&#xff09;七、查&#xff08;Select&#xff09; 一、配置日志 我们加上下面的代码在配置文件中&#xff0c;那么我们在日志中就可以看到…

4.2、网络安全体系与建设内容

目录 网络安全体系架构网络安全组织安全管理网络安全等级保护2.0等保项目流程等保标准变化等保2.0新增内容等保2.0变化智慧城市安全体系应用参考智能交通网络安全体系应用参考 网络安全体系架构 建设网络安全&#xff0c;要体系化&#xff0c;要从一个整体去做考虑&#xff0c…

TCP协议原理

TCP协议原理 本篇介绍 前面已经基本介绍了TCP编程的接口以及基本的步骤&#xff0c;但是并没有其中的原理进行解释。本篇主要聚焦于TCP原理部分&#xff0c;对TCP中重要的内容进行解释 TCP协议报格式 基本示意图如下&#xff1a; 下面针对每一个字段的作用进行简要的概括&a…

go中的文件、目录的操作

1.文件的概念 文件是数据源(保存数据的地方)的一种,比如大家经常使用的word文档,txt文件,excel文件等。文件最主要的作用就是保存数据,它既可以保存一张图片,也可以保存视频,声音等。 文件在程序中以流的形式来操作的。 流:数据在数据源(文件)和程序(内存)之间…

electron js node vscode 调试electron

用npm会下到home里面不知道为什么可能是淘宝源的问题 --------------------------------- 安装cnpm&#xff08;可选&#xff09; sudo npm install -g cnpm --registryhttps://registry.npmmirror.com下下来还没办法直接用 sudo find / -name "cnpm"nano ~/.bashr…

深度解析 BPaaS:架构、原则与研发模式探索

在当今复杂多变的业务环境下&#xff0c;软件开发面临着诸多挑战&#xff0c;如何有效地管理业务复杂性并实现系统的可扩展性成为关键。BPaaS应运而生&#xff0c;它作为一种创新的理念和架构模式&#xff0c;改变着企业研发的方式。本文将深入探讨 BPaaS 是什么&#xff0c;以…

大模型架构记录2 【综述-相关代码】

一 简单聊天机器人搭建 1.1 openai调用 import os from openai import OpenAI from dotenv import load_dotenv, find_dotenvload_dotenv() client OpenAI()# 打印所支持的模型 model_lst client.models.list()for model in model_lst:print (model.id)# 调用API接口 comp…

三个print优雅打印datetime模块的“时间密码”

三个模块&三条print()&#xff0c;玩转python时间的上上下下&#xff0c;优雅打印“时间密码”。 笔记模板由python脚本于2025-03-23 22:50:43创建&#xff0c;本篇笔记适合正确研究时间/日期的coder翻阅。 【学习的细节是欢悦的历程】 博客的核心价值&#xff1a;在于输出…

【Android】VehiclePropertyAccess引起CarService崩溃

VehiclePropertyAccess引起CarService崩溃 VehiclePropertyAccess VehiclePropertyAccess属性&#xff0c;用于定义车辆属性的访问权限。权限包括 读&#xff1a;READ&#xff0c;只可以读取&#xff0c;不能写入。 VehiclePropertyAccess:READ写&#xff1a;WRITE&#xf…

深入理解traceroute命令及其原理

traceroute 是一个网络诊断工具&#xff08;Windows上叫tracert&#xff09;&#xff0c;用于显示数据包从本地主机到远程主机经过的路由&#xff08;跳数&#xff09;。它可以帮助您了解数据包在网络中的传输路径&#xff0c;以及每跳的延迟情况。这对于网络故障排除、分析网络…

Playwright + MCP:用AI对话重新定义浏览器自动化,效率提升300%!

一、引言&#xff1a;自动化测试的“瓶颈”与MCP的革新 传统自动化测试依赖开发者手动编写脚本&#xff0c;不仅耗时且容易因页面动态变化失效。例如&#xff0c;一个简单的登录流程可能需要开发者手动定位元素、处理等待逻辑&#xff0c;甚至反复调试超时问题。而MCP&#xf…