2024年7月

远古的一篇博客,内容散落于博文和评论
https://sparxeng.com/blog/software/must-use-net-system-io-ports-serialport

C# 和 .NET Framework 提供了一种快速的应用程序开发,非常适合需要随着硬件设计的发展跟踪不断变化的需求的早期开发。在大多数方面都很理想。但.NET 附带的
System.IO.Ports.SerialPort
类是一个明显的例外。委婉地说,它是由计算机科学家设计的,远远超出了他们的核心能力领域。他们既不了解串行通信的特征,也不了解常见的用例。在发布之前,它也不可能在任何真实场景中进行测试,而不会发现漏洞,这些缺陷会使
System.IO.Ports.SerialPort
(以下简称IOPSP)的可靠通信成为真正的噩梦。(
StackOverflow 上的大量证据证明了这一点
)。

更令人惊讶的是,当底层kernel32.dll API 非常好时,还会发生这种级别的问题(我在使用 .NET 之前使用过 WinAPI)。.NET 工程师不仅没有设计出合理的接口,还选择无视非常成熟的 WinAPI 设计,也没有从二十年的内核团队串行移植经验中吸取教训。

下面列出其可靠和不可靠的成员列表:

  • event DataReceived (100%冗余,也完全不可靠)
  • BytesToRead 属性 (完全不可靠)
  • Read
    ,
    ReadExisting
    ,
    ReadLine
    函数(处理错误完全错误,并且是同步的)
  • PinChanged
    event (顺序完全不能保证)

可以安全使用的列表:

  • 属性:
    BaudRate

    DataBits

    Parity

    StopBits
    ,但仅在打开端口之前。并且仅适用于标准波特率。
  • Handshake
    属性
  • 构造函数、 PortName属性、 Open函数 IsOpen函数、 GetPortNames函数

还有一个没人使用的成员,因为 MSDN 没有给出任何示例,但对你绝对是必不可少的

  • BaseStream

唯一正常工作的串行端口读取方法是通过
BaseStream
访问。

以下示例是接收数据的错误方式:

port.DataReceived += port_DataReceived;

// (later, in DataReceived event)
try {
    byte[] buffer = new byte[port.BytesToRead];
    port.Read(buffer, 0, buffer.Length);
    raiseAppSerialDataEvent(buffer);
}
catch (IOException exc) {
    handleAppSerialError(exc);
}

下面是正确的方法,它与基础 Win32 API 的使用方式相匹配:

byte[] buffer = new byte[blockLimit];
Action kickoffRead = null;
kickoffRead = delegate {
    port.BaseStream.BeginRead(buffer, 0, buffer.Length, delegate (IAsyncResult ar) {
        try {
            int actualLength = port.BaseStream.EndRead(ar);
            byte[] received = new byte[actualLength];
            Buffer.BlockCopy(buffer, 0, received, 0, actualLength);
            raiseAppSerialDataEvent(received);
        }
        catch (IOException exc) {
            handleAppSerialError(exc);
        }
        kickoffRead();
    }, null);
};
kickoffRead();

从 .NET 4.5 开始,可以改为调用
ReadAsync
BaseStream 对象,该对象在内部调用
BeginRead

EndRead

或者就直接调用 Win32 API的方式。

第一个例子的问题在于:

第一个也是最严重的是
DataReceived
在线程池线程上触发,并且可以再次触发,而无需等待上一个事件处理程序返回。因此,它会导致你进入竞争条件,当你去读取缓冲区时,比
BytesToRead
承诺的要少,因为事件处理程序的另一个实例同时读取它们。应用程序程序员可以通过显式同步来克服这个问题。

但同步不会解决实现本身中存在的竞争条件。
BytesToRead
调用
ClearCommError
以获取缓冲区级别,并丢弃其他所有内容。但是
ClearCommError
是对串口寄存器错误标志位的原子交换——你只会看到一次,然后就会清除他们。框架中正在查看这些标志的其他代码以触发
PinChanged

ErrorReceived
事件由于
BytesToRead
(查看并清除寄存器) 会忽略它们,因此事件会丢失。事实上,
ErrorReceived
事件的 MSDN 页面显示“由于操作系统决定是否引发此事件,因此不会报告所有奇偶校验错误。这是一个彻头彻尾的谎言 — 事件丢失发生在
BytesToRead

BytesToWrite
的 getter 函数中。

译者注:这段原因写在作者和游客的讨论中,感兴趣的可以看看原文。如果写过STM32就能理解作者的意思(STM32的有些寄存器在读后就会清除,这样就会导致丢失了一些事件)。作者表示如果你对串口通信的要求很高,(高性能/低时延/错误等)你就应该使用Win32 API或者 p/invoke 或 C++/CLI。(作者列了写商业库说也能用,感兴趣的在原文评论区自行找一下)。如果你对串口通信的要求不高,其实
DataReceived
的方式其实也能用。

一、灰度发布(金丝雀发布)

灰度发布时使用比较平稳的过渡方式升级或者替换产品项目的方法统称

主要作用

  • 及时发现项目问题
  • 尽早获取用户反馈的信息,以改进产品
  • 如果项目产生问题,可以将问题影响控制到最小范围

【1】、基于不同IP进行灰度发布

proxy 代理

web01 开启80端口 开启8001端口

web02 开启80端口 开启8002端口

80端口(老业务)

800x端口(测试业务)

我们要在nginx中创建集群和if判断,将新业务和老业务分开,根据IP去区分,一类IP访问的时候会显示老业务,一类IP访问的时候是新业务1,另一类IP访问的时候是新业务2

# 修改nginx配置文件
	# 创建3个集群
upstream s8001{
        server 192.168.121.171:8001;
    }
    upstream s8002{
        server 192.168.121.172:8002;
    }
    # 下面的是老业务
    upstream default{
        server 192.168.121.171:80;
        server 192.168.121.172:80;
    }
 server {
        listen       80;
        server_name  localhost;
        # set 设置变量的含义,让group=default
        set $group "default";
        # 通过if判断来区分IP,给group赋不同的值
        if ($remote_addr ~ "192.168.121.171"){
            set $group s8001;
        }
        if ($remote_addr ~ "192.168.121.172"){
            set $group s8002;
        }
        #charset koi8-r;

        #access_log  logs/host.access.log  main;

        location / {
            # 根据上面if判断,group得到的不同的值,去访问不同的网站
            proxy_pass http://$group;
            root   html;
            index  index.html index.htm;
        }

配置web01

在web01中创建一个虚拟主机,监听8001端口。

server{
        listen 8001;
        server_name localhost;
        root html8001;
        index index.html;
    }
[root@web01 nginx]# mkdir html8001
[root@web01 nginx]# 
[root@web01 nginx]# echo web01-8001 > html8001/index.html
[root@web01 nginx]# echo web01-80 > html/index.html
[root@web01 nginx]# sbin/nginx

配置web02

在web02中创建一个虚拟主机,监听8002端口。

server{
        listen 8002;
        server_name localhost;
        root html8002;
        index index.html;
[root@web02 nginx]# mkdir html8002
[root@web02 nginx]# echo web02-8002 > html8002/index.html
[root@web02 nginx]# echo web02-80 > html/index.html
[root@web02 nginx]# sbin/nginx 

进行测试

预期结果

当我们使用IP为192.168.121.171去访问proxy时,会看到web01的新业务

当我们使用IP为192.168.121.172去访问proxy时,会看到web02的新业务

当我们使用其他IP去访问proxy时,会以轮询的形式看到web01和web02的老业务

root@proxy[19:30:15]:/usr/local/nginx
$ curl 192.168.121.170
web01-80
root@proxy[19:30:15]:/usr/local/nginx
$ curl 192.168.121.170
web02-80
[root@web01 nginx]# curl 192.168.121.170
web01-8001
[root@web01 nginx]# curl 192.168.121.170
web01-8001
[root@web02 nginx]# curl 192.168.121.170
web02-8002
[root@web02 nginx]# curl 192.168.121.170
web02-8002

【2】、通过用户id测试

对于一个网站来说,它可以有许多的用户去进行登录,但是,每一个用户都有一个对应且唯一的用户ID,我们可以针对用户的ID来进行区分,从而可以进灰度发布

准备一个测试网站

root@proxy[21:41:36]:~
$ ll php-memcached-demo.tar.gz 
-rw-r--r--. 1 root root 158156 Jun 24 19:49 php-memcached-demo.tar.gz
root@proxy[21:41:46]:~
$ cp -r php-memcached-demo/* /usr/local/nginx/html/

修改网页源码(home.php)

如果用户名是以abc开头的,则点击开始跳转到192.168.121.171

如果不是则点击开始跳转到192.168.121.172

以此实现灰度发布

Welcome :  <?php
if(preg_match("/^abc/",$_SESSION['login_user'] ) ){
echo "<a href='http://192.168.121.171'>开始</a>";
}
else
{
echo "<a href='http://192.168.121.172'>开始</a>";
}
?>

二、网站限速

  • 需要共享的文件数量较大较多
  • 服务器自身带宽有限
  • 频繁遭受黑客攻击
  • 业务利益最大化

速度限制

limit_rate 50k; 速度极限就是50k,不管运营商给的带宽是多少。

【1】、全局限速

# 修改nginx的配置文件,在http中写入如下内容,如果不写入server中,则表示全局限速,如果写入某一个server中,则表示只限制某一个虚拟主机
#gzip  on;
limit_rate 100k;
server {
listen       80;
server_name  localhost;
...
}
# 进行测试网速
[root@web01 nginx]# wget http://192.168.121.171/test.img
--2024-06-24 22:22:28--  http://192.168.121.171/test.img
Connecting to 192.168.121.171:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 104857600 (100M) [application/octet-stream]
Saving to: ‘test.img’

test.img                           2%[>                                                         ]   2.44M  99.7KB/s    eta 16m 38s

【2】、局部限速

有全局没有局部,按照全局算

有全局有局部,按照据不算

limit_rate 0k; 0k表示不限速

limit_rate 100k; # 全局限速
    server {
        limit_rate 200k;  # 虚拟主机局部限速
        listen       80;
        server_name  localhost;

        #charset koi8-r;

        #access_log  logs/host.access.log  main;
        location /file_a{
        limit_rate 300k;  # 某一目录限速
        }
        location /file_b{
        limit_rate 0k;  # 0k 表示不限速
        }

【3】、限速bug-突破限制

在上面我们进行了速度限制,但是存在一个bug。

比方说file_a目录下有两个文件需要下载,我在同时下载这两个文件,每一个的下载速度都在300k左右,但是此时我在你的服务器上的下载总速度是600k左右,相当于变相的突破了我们限制的速度

所以我们还需要继续加限制,让其连接数在同一时刻只能是1

# 修改nginx配置文件
limit_conn_zone $binary_remote_addr zone=addr:10m;  # 设置连接限制
    #gzip  on;
    limit_rate 100k;
    server {
        limit_rate 200k;
        listen       80;
        server_name  localhost;

        #charset koi8-r;

        #access_log  logs/host.access.log  main;
        location /file_a{
        limit_rate 300k;
        limit_conn addr 1;  # 启用连接限制
        }

三、防盗链

一个网站中的链接,连接到了另外一个网站中。这就是盗链,盗取了别的网站的信息,放在了自己的网站上

因此我们需要防盗链的配置,不允许别的网站来通过盗链盗取我的数据

具体实现原理:

通过referers参数来实现,referers是一个请求头,它能够标识你是从哪个地址来访问我这个网站的。我们可以设置,只有从自己的IP和空过来的请求才可以看到我的网页,其他都不许看,返回403

 server {
        listen       80;
        server_name  localhost;
        valid_referers none 192.168.121.171; # 只有从空和192.168.121.171来的请求才可以访问
        if ($invalid_referer){
            return 403;  # 其它全报403错误
        }
        charset utf8;

我最近重新拾起了计算机视觉,借助Python的opencv还有face_recognition库写了个简单的图像识别demo,额外定制了一些内容,原本想打包成exe然后发给朋友,不过在这当中遇到了许多小问题,都解决了,记录一下踩过的坑。

1、Pyinstaller打包过程当中出现warning,跟dll文件有关,将"C:\Windows\System32\downlevel"这个文件夹配置到环境变量当中,后面还有出现warning,原本想在下面加缺失的dll,后面实在加不过来,就换路子了,顺便推荐一下一个下载dll的网站
免费下载缺失的 DLL 文件 | DLL‑files.com (dll-files.com)
许多缺失的dll都可以在这里找到并且下载。

2、conda还有pip之间可能存在的冲突。安装了conda之后,我的Python版本变成了3.12,不过Pyinstaller还是之前下载的版本,上网看了一下,可能有兼容问题,随即关闭了conda,然后问题解决了一半。

3、face_recognition的model可能存在的路径问题。

注释是源代码的,我改成了模型存放的具体路径了(face_recognition_models文件夹下面那几个dat文件),因为我跳转到resource_filename想一看究竟的时候发现这个函数是None,索性改了。最后改完就可以打包成功。

好久没更新了,这半年做了很多也学了很多,先是去了杭州的创业公司实习,再到为知名的开源向量数据库infinity提交了pr,然后又去了量化实习,现在在家准备秋招,有时间会多多分享。

思路:

对于插入操作,设插入
\(\{t,p\}\)

  • 若当前
    \(1 \sim t\)
    有空位,那么就放进去。

  • 否则,
    \(1 \sim t\)
    是被塞满了的:


    • 首先容易想到的是找到
      \(1 \sim t\)
      中贡献最小的那个工作,若贡献比
      \(p\)
      还小,可以与之替换掉。

    • 但是假了,考虑这样一种情况:在
      \(1 \sim t\)
      外有一个更小的值,可以跟
      \(1 \sim t\)
      中的某个工作换一个位置,然后再将这个替换过来的工作替换掉,这样无疑是更优的。

    • 考虑如何快速维护这个东西,使用两棵线段树:


      • 第一棵线段树维护所有截止时间在区间
        \([l,r]\)
        的时刻完成的任务的截止时间的最大值。

      • 第二棵线段树维护所有截止时间在区间
        \([l,r]\)
        的时刻完成的任务的贡献的最小值。

    • 我们需要找到经过替换能替换到的最远时刻:



      • \(A_{fi}\)
        表示当前
        \(1 \sim t\)
        中截止时间最晚的时间,
        \(A_{se}\)

        \(1 \sim t\)
        中截止时间最晚的工作完成的时刻。


      • \(B_{fi}\)
        表示当前
        \(1 \sim A_{fi}\)
        中截止时间最晚的时间,
        \(B_{se}\)

        \(1 \sim A_{fi}\)
        中截止时间最晚的工作完成的时刻。

      • 那么若
        \(A_{fi} < B_{fi}\)


        • 说明可以将
          \(A_{se}\)

          \(B_{se}\)
          时刻的工作调换一下。

        • 因为可以使得
          \(1 \sim t\)
          内的工作的最晚截止时刻更长。

      • 然后一直重复交换操作,直到不满足
        \(A_{fi} < B_{fi}\)
        为止。

    • 经过上述的操作,
      \(A_{fi}\)
      达到了最大值;令
      \(C_{fi}\)
      表示当前
      \(1 \sim A_{fi}\)
      中工作贡献的最小值,
      \(C_{se}\)
      表示完成最小贡献的工作所处的时刻。


    • \(C_{se} > t\)
      ,可以将
      \(A_{se}\)

      \(C_{se}\)
      交换一下。

    • 此时的
      \(C_{fi}\)
      就是可以找到替换的最小值,若
      \(p > C_{fi}\)
      ,则可以替换。

对于删除操作:

  • 若删除的工作我们选择完成了,设在
    \(t\)
    时刻完成:


    • 那么容易想到,可以找到截止时间在
      \(t \sim T\)
      中贡献最大且没有完成的工作,顶替上来即可。

    • 但是也假了,考虑这样一种情况,可以将
      \(t\)

      \(1 \sim t-1\)
      时刻的某个工作
      \(t'\)
      交换,使得
      \(t' \sim T\)
      的最大贡献在
      \(t' \sim t\)
      中,则只看
      \(t \sim T\)
      是不优的。

    • 我们需要将
      \(t\)
      换到尽可能前面去,考虑二分:



      • \(1 \sim mid\)
        中截止时间最晚的时间是大于等于
        \(t\)
        的,说明
        \(1 \sim mid\)
        中有一个位置可以与
        \(t\)
        换,令
        \(r=mid-1\)
        ;否则
        \(l=mid+1\)

      • 设当前找到的最靠前的时刻为
        \(t'\)
        ,令
        \(t \gets t'\)
        ,然后再在
        \(1 \sim t\)
        的范围内二分。

      • 重复二分直到找不到
        \(1 \sim t-1\)
        范围内的点与
        \(t\)
        交换,即不存在
        \(t'\)

    • 此时我们得到了最小的
      \(t\)
      ,找
      \(t \sim T\)
      内贡献最大且没有完成的工作顶替即可。

    • 可以使用第三棵线段树:维护所有截止时间在区间
      \([l,r]\)
      的时刻未完成的任务的贡献的最大值。

  • 若并没有完成该工作,则直接在第三棵线段树上将这个点的贡献消除即可。

第三棵线段树需要支持一个撤销操作,因为可能有完全一模一样的工作,需要在叶子节点处使用
multiset
维护最大值。

时间复杂度为
\(O(N \log^3 N)\)

该做法码量和常数较大,谨慎使用。

完整代码:

#include<bits/stdc++.h>
#define Add(x,y) (x+y>=mod)?(x+y-mod):(x+y)
#define lowbit(x) x&(-x)
#define pi pair<ll,ll>
#define pii pair<ll,pair<ll,ll>>
#define iip pair<pair<ll,ll>,ll>
#define ppii pair<pair<ll,ll>,pair<ll,ll>>
#define fi first
#define se second
#define full(l,r,x) for(auto it=l;it!=r;it++) (*it)=x
#define Full(a) memset(a,0,sizeof(a))
#define open(s1,s2) freopen(s1,"r",stdin),freopen(s2,"w",stdout);
using namespace std;
typedef double db;
typedef unsigned long long ull;
typedef long long ll;
bool Begin;
const ll N=1e5+10,INF=1e18;
inline ll read(){
    ll x=0,f=1;
    char c=getchar();
    while(c<'0'||c>'9'){
        if(c=='-')
          f=-1;
        c=getchar();
    }
    while(c>='0'&&c<='9'){
        x=(x<<1)+(x<<3)+(c^48);
        c=getchar();
    }
    return x*f;
}
inline void write(ll x){
	if(x<0){
		putchar('-');
		x=-x;
	}
	if(x>9)
	  write(x/10);
	putchar(x%10+'0');
}
// T1 维护 1 ~ x 时刻中完成的任务的截止时间的最大值
// T2 维护 1 ~ x 时刻中完成的任务的得分的最小值
// T3 维护 1 ~ x 时刻中未完成的任务的得分的最大值 
ll n,q,c,x,y,z,l,r,t,ans;
ll a[N],b[N],X[N],Y[N],Z[N];
map<pi,ll> cnt;
map<iip,ll> F;
class Tree1{
public:
	pi H[N<<2];  //{最大值,位置}
	pi add(pi a,pi b){
		if(a.fi>b.fi)
		  return a;
		return b;
	}
	void pushup(ll k){
		H[k]=add(H[k<<1],H[k<<1|1]);
	}
	void build(ll k,ll l,ll r){
		if(l==r){
			H[k].fi=0;
			H[k].se=l;
			return ;
		}
		ll mid=(l+r)>>1;
		build(k<<1,l,mid);
		build(k<<1|1,mid+1,r);
		pushup(k);
	}
	void update(ll k,ll l,ll r,ll i,ll v){
		if(l==i&&i==r){
			H[k].fi=v;
			return ;
		}
		ll mid=(l+r)>>1;
		if(i<=mid)
		  update(k<<1,l,mid,i,v);
		else
		  update(k<<1|1,mid+1,r,i,v);
		pushup(k);
	}
	void del(ll k,ll l,ll r,ll i){
		if(l==i&&i==r){
			H[k].fi=0;
			return ;
		}
		ll mid=(l+r)>>1;
		if(i<=mid)
		  del(k<<1,l,mid,i);
		else
		  del(k<<1|1,mid+1,r,i);
		pushup(k);
	}
	pi query(ll k,ll l,ll r,ll L,ll R){
		if(L>R)
		  return {-INF,0};
		if(l==L&&R==r)
		  return H[k];
		ll mid=(l+r)>>1;
		if(R<=mid)
		  return query(k<<1,l,mid,L,R);
		else if(L>mid)
		  return query(k<<1|1,mid+1,r,L,R);
		else
		  return add(query(k<<1,l,mid,L,mid),query(k<<1|1,mid+1,r,mid+1,R));
	}
	void Swap(ll x,ll y){
		ll xx=query(1,1,n,x,x).fi;
		ll yy=query(1,1,n,y,y).fi;
		update(1,1,n,x,yy);
		update(1,1,n,y,xx);
	} 
}T1;
class Tree2{
public:
	pi H[N<<2];  //{最小值,位置}
	pi add(pi a,pi b){
		if(a.fi<b.fi)
		  return a;
		return b;
	}
	void pushup(ll k){
		H[k]=add(H[k<<1],H[k<<1|1]);
	}
	void build(ll k,ll l,ll r){
		if(l==r){
			H[k].fi=0;
			H[k].se=l;
			X[l]=Y[l]=Z[l]=0;
			return ;
		}
		ll mid=(l+r)>>1;
		build(k<<1,l,mid);
		build(k<<1|1,mid+1,r);
		pushup(k);
	}
	void update(ll k,ll l,ll r,ll i,ll x,ll y,ll c){
		if(l==i&&i==r){
			H[k].fi=y;
			X[i]=x,Y[i]=y,Z[i]=c;
			F[{{x,y},c}]=i;
			return ;
		}
		ll mid=(l+r)>>1;
		if(i<=mid)
		  update(k<<1,l,mid,i,x,y,c);
		else
		  update(k<<1|1,mid+1,r,i,x,y,c);
		pushup(k);
	}
	void del(ll k,ll l,ll r,ll i){
		if(l==i&&i==r){
			H[k].fi=0;
			F[{{X[i],Y[i]},Z[i]}]=0;
			X[i]=Y[i]=Z[i]=0;
			return ;
		}
		ll mid=(l+r)>>1;
		if(i<=mid)
		  del(k<<1,l,mid,i);
		else
		  del(k<<1|1,mid+1,r,i);
		pushup(k);
	}
	pi query(ll k,ll l,ll r,ll L,ll R){
		if(L>R)
		  return {INF,0}; 
		if(l==L&&R==r)
		  return H[k];
		ll mid=(l+r)>>1;
		if(R<=mid)
		  return query(k<<1,l,mid,L,R);
		else if(L>mid)
		  return query(k<<1|1,mid+1,r,L,R);
		else
		  return add(query(k<<1,l,mid,L,mid),query(k<<1|1,mid+1,r,mid+1,R));
	}
	void Swap(ll x,ll y){
		ll xx1=X[x],yy1=Y[x],cc1=Z[x],xx2=X[y],yy2=Y[y],cc2=Z[y];
		update(1,1,n,x,xx2,yy2,cc2);
		update(1,1,n,y,xx1,yy1,cc1);
	}
}T2;
class Tree3{
public:
	ll id[N];
	multiset<pii> S[N];
	iip H[N<<2]; // {{x,y},c}
	iip add(iip a,iip b){
		if(a.fi.se>b.fi.se)
		  return a;
		return b;
	}
	void pushup(ll k){
		H[k]=add(H[k<<1],H[k<<1|1]);
	}
	void update(ll k,ll l,ll r,ll x,ll y,ll c){
		if(l==x&&x==r){
			S[x].insert({-y,{-x,-c}});
			auto t=(*S[x].begin());
			H[k]={{-t.se.fi,-t.fi},-t.se.se};
			return ;
		}
		ll mid=(l+r)>>1;
		if(x<=mid)
		  update(k<<1,l,mid,x,y,c);
		else
		  update(k<<1|1,mid+1,r,x,y,c);
		pushup(k);
	}
	void del(ll k,ll l,ll r,ll x,ll y,ll c){
		if(l==x&&x==r){
			S[x].erase({-y,{-x,-c}});
			auto t=(*S[x].begin());
			H[k]={{-t.se.fi,-t.fi},-t.se.se};
			return ;
		}
		ll mid=(l+r)>>1;
		if(x<=mid)
		  del(k<<1,l,mid,x,y,c);
		else
		  del(k<<1|1,mid+1,r,x,y,c);
		pushup(k);
	}
	iip query(ll k,ll l,ll r,ll L,ll R){
		if(l==L&&R==r)
		  return H[k];
		ll mid=(l+r)>>1;
		if(R<=mid)
		  return query(k<<1,l,mid,L,R);
		else if(L>mid)
		  return query(k<<1|1,mid+1,r,L,R);
		else
		  return add(query(k<<1,l,mid,L,mid),query(k<<1|1,mid+1,r,mid+1,R));
	}
}T3;
void insert(ll x,ll y){
	c=++cnt[{x,y}];
	pi A,B;
	pi C;
	while(1){
		A=T1.query(1,1,n,1,x);
		B=T1.query(1,1,n,1,A.fi);
		if(A.fi<B.fi){
			T1.Swap(A.se,B.se);
			T2.Swap(A.se,B.se);
		}
		else
		  break;
	}
	A=T1.query(1,1,n,1,x);
	C=T2.query(1,1,n,1,A.fi);
	if(C.se>x){
		T1.Swap(A.se,C.se);
		T2.Swap(A.se,C.se);
	}
	C=T2.query(1,1,n,1,x);
	if(C.fi<y){
		ans+=y-C.fi;
		if(C.fi){
			T3.update(1,1,n,X[C.se],Y[C.se],Z[C.se]);
			T1.del(1,1,n,C.se);
			T2.del(1,1,n,C.se);
		}
		T1.update(1,1,n,C.se,x);
		T2.update(1,1,n,C.se,x,y,c);
	}
	else
	  T3.update(1,1,n,x,y,c);
}
void del(ll x,ll y){
	c=cnt[{x,y}]--;
	if(F[{{x,y},c}]){
		ans-=y;
		while(1){
			z=F[{{x,y},c}];
			l=1,r=z-1,t=-1;
			while(l<=r){
				ll mid=(l+r)>>1;
				if(T1.query(1,1,n,1,mid).fi>=z){
					t=mid;
					r=mid-1;
				}
				else
				  l=mid+1;
			}
			if(t==-1)
			  break;
			T1.Swap(t,z);
			T2.Swap(t,z);
		}
		z=F[{{x,y},c}];
		T1.del(1,1,n,z);
		T2.del(1,1,n,z);
		iip A=T3.query(1,1,n,z,n);
		if(A.se){
			ans+=A.fi.se;
			T1.update(1,1,n,z,A.fi.fi);
			T2.update(1,1,n,z,A.fi.fi,A.fi.se,A.se);
			T3.del(1,1,n,A.fi.fi,A.fi.se,A.se);
		}
	}
	else
	  T3.del(1,1,n,x,y,c);
}
bool End;
/*[ABC363G] Dynamic Scheduling
int main(){
	n=read(),q=read();
	for(int i=1;i<=n;i++)
	  a[i]=read();
	for(int i=1;i<=n;i++)
	  b[i]=read();
	T1.build(1,1,n);
	T2.build(1,1,n);
	for(int i=1;i<=n;i++)
	  insert(a[i],b[i]);
//	write(ans);
//	putchar('\n');
	while(q--){
		x=read();
		del(a[x],b[x]);
		a[x]=read(),b[x]=read();
		insert(a[x],b[x]);
		write(ans);
		putchar('\n');
	}
	cerr<<'\n'<<abs(&Begin-&End)/1048576<<"MB";
	return 0;
}*/
/*P4511 [CTSC2015] 日程管理
int main(){
	n=read(),q=read();
	T1.build(1,1,n);
	T2.build(1,1,n);
	while(q--){
		cin>>op;
		x=read(),y=read();
		if(op[0]=='A')
		  insert(x,y);
		else
		  del(x,y);
		write(ans);
		putchar('\n');
	}
	return 0;
}
*/

做自己感觉有意思的或者能解决自己需求的项目作为入门,我觉得是有帮助的,不会觉得那么无聊。

一个最简单的前后端分离项目应该是怎么样的?

我觉得就是前端有个按钮,点击向后端发送一个get请求,获取到数据后,将数据显示在前端上。

结合最近感兴趣的SemanticKernel,有了做这样的Demo学习的想法,用户点击按钮,返回一句夸人的话。

Vue:

前后端分离的一个很明显的好处就是,你可以使用多个前端使用同一个后端服务,比如我也用Avalona做了一个这样的客户端应用,也可以共用这个后端服务,如下所示:

开始使用.NET 8 Web Api

选择Web Api模板:

image-20240725092622106

其他信息:

image-20240725093117690

这里有几点可以注意:

配置https是什么意思?

配置HTTPS是指在网络服务器上设置和启用安全超文本传输协议(HTTPS)。HTTPS是HTTP的安全版本,它在HTTP协议的基础上添加了SSL/TLS加密层,以确保数据在客户端和服务器之间的传输过程中是加密的,从而保护数据的机密性和完整性。

启用OpenAPI支持是什么意思?

OpenAPI(以前称为Swagger规范)是一种用于描述、生成、消费和可视化RESTful Web服务的规范。它允许开发者定义API的各个方面,包括路径、操作、请求参数、响应和认证方法。通过使用OpenAPI规范,开发者可以更容易地创建、维护和使用API文档,从而提高开发效率和API的可理解性。

启用OpenAPI支持是指在软件项目中集成和配置OpenAPI规范,以便能够生成、使用和展示符合OpenAPI标准的API文档。这意味着项目将能够利用OpenAPI的各种工具和生态系统来简化API的设计、开发、文档编写和测试过程。

不使用顶级语句是什么意思?

在C#中,"不使用顶级语句"(Not using top-level statements)是指在编写代码时不采用C# 9.0引入的顶级语句特性。

使用控制器是什么意思?

控制器是MVC(Model-View-Controller)中的Controller,在Web API开发中,"使用控制器"(Using Controllers)是指采用一种设计模式,其中API的逻辑被组织到称为"控制器"的类中。控制器负责处理HTTP请求、执行相应的业务逻辑,并返回HTTP响应。

为了维护方便与规范化,自己再加上一层Model、一层Services:

image-20240725130842155

现在想一下自己想添加什么服务,想法是使用SemanticKernel接入大语言模型,当我们请求的时候让它返回一句夸人的话。

SemanticKernel现在就知道它是为了让LLM快速集成进我们的应用的就行了。

安装SemanticKernel:

image-20240725131101780

在Services中添加SemanticKernelService:

 public class SemanticKernelService
 {
     private readonly Kernel _kernel;
     public SemanticKernelService()
     {
         var handler = new OpenAIHttpClientHandler();
         var builder = Kernel.CreateBuilder()
        .AddOpenAIChatCompletion(
           modelId: "Qwen/Qwen2-7B-Instruct",
           apiKey: "你的apikey",
           httpClient: new HttpClient(handler));
         _kernel = builder.Build();
     }

     public async Task<string> Praiseyuzai()
     {
         var skPrompt = """                           
                       你是一个夸人的专家,回复一句话夸人。                         
                       你的回复应该是一句话,不要太长,也不要太短。                               
                       """;
         var result = await _kernel.InvokePromptAsync(skPrompt);
         var str = result.ToString();
         return str;
     }

 }

可能很多人看SemanticKernel的介绍会觉得只能用OpenAI的模型,其实只要兼容了OpenAI格式的在线模型都可以的,本地大模型的话也是可以通过实现接口实现接入的,本文选择的平台是硅基流动下的Qwen/Qwen2-7B-Instruct模型,免费使用。

由于不是OpenAI需要将请求转发到硅基流动提供的Api上,需要在模型中添加OpenAIHttpClientHandler类如下所示:

 public class OpenAIHttpClientHandler : HttpClientHandler
 {
     protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
     {
         UriBuilder uriBuilder;
         switch (request.RequestUri?.LocalPath)
         {
             case "/v1/chat/completions":
                 uriBuilder = new UriBuilder(request.RequestUri)
                 {
                     // 这里是你要修改的 URL
                     Scheme = "https",
                     Host = "api.siliconflow.cn",
                     Path = "v1/chat/completions",
                 };
                 request.RequestUri = uriBuilder.Uri;
                 break;
         }

         HttpResponseMessage response = await base.SendAsync(request, cancellationToken);

         return response;
     }

我们与大语言模型聊天,就是在提供一个Prompt,这里我们的Prompt如下:

  var skPrompt = """                           
                       你是一个夸人的专家,回复一句话夸人。                         
                       你的回复应该是一句话,不要太长,也不要太短。                               
                       """;

大语言模型会根据这个Prompt给我们回复。

现在项目结构如下所示:

image-20240725131839064

现在将构造的这个服务,添加到依赖注入容器中:

image-20240725131923106

更规范的做法应该是传入一个接口和一个实现类,本次入门直接传入实现类即可。

现在来看看控制器怎么写?

先看看模板自带的一个控制器:

 [ApiController]
 [Route("[controller]")]
 public class WeatherForecastController : ControllerBase
 {
     private static readonly string[] Summaries = new[]
     {
         "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
     };

     private readonly ILogger<WeatherForecastController> _logger;

     public WeatherForecastController(ILogger<WeatherForecastController> logger)
     {
         _logger = logger;
     }

     [HttpGet(Name = "GetWeatherForecast")]
     public IEnumerable<WeatherForecast> Get()
     {
         return Enumerable.Range(1, 5).Select(index => new WeatherForecast
         {
             Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
             TemperatureC = Random.Shared.Next(-20, 55),
             Summary = Summaries[Random.Shared.Next(Summaries.Length)]
         })
         .ToArray();
     }
 }

模仿它的样子写一个控制器:

[ApiController]
[Route("[controller]")]
public class SemantickernelController : ControllerBase
{
    private readonly ILogger<SemantickernelController> _logger;
    private readonly SemanticKernelService _semanticKernelService;
    
    public SemantickernelController(ILogger<SemantickernelController> logger,SemanticKernelService semanticKernelService)
    {
        _logger = logger;
        _semanticKernelService = semanticKernelService;
    }

    [HttpGet]
    public async Task<string> Get()
    {
        _logger.LogInformation($"执行Praise请求 时间:{DateTime.Now}");
        var str = await _semanticKernelService.Praise();
        return str;
    }
 
}

在构造函数中注入了我们刚刚注册的服务类。

[HttpGet]
public async Task<string> Get()
{
   _logger.LogInformation($"执行Praise请求 时间:{DateTime.Now}");
   var str = await _semanticKernelService.Praise();
   return str;
}

这个的写法其实也不规范,后面可以使用
ActionResult<T>
替代,现在先不用管,能用就行。

现在启动项目,会跳出Swagger UI:

image-20240725132604898

可以在上面调试写的接口,试试刚刚创建的Get请求:

image-20240725132824604

我们刚刚写的

 _logger.LogInformation($"执行Praise请求 时间:{DateTime.Now}");

在调用接口的时候,就可以看到信息输出在控制台上了,如下所示:

image-20240725133048007

到时候为了让我们能够通过局域网访问,在Program中添加:

image-20240725133156224

到时候前端访问还需要解决一下跨域的问题,在Program中添加:

image-20240725133235285

即可。

到这里为止,我们就已经使用.NET 8 Web Api构建了一个简单的只有一个Get请求的后端服务了。

下期分享Vue与Avalonia中的部分。