初学rust,无法在不同的线程中并行读写websocket
Tag rust, 线程, websocket, 读写分离, on by view 328

最近发现一个问题,rust的线程安全机制导致无法实现socket读写分离到两个不同的线程。

先说一下程序的背景,程序是将本地终端pty(cli)拉起,并且将pty的输入输出通过channel对接,并将cli输出的数据经过channel写入到服务端socket,将从服务端socket收取到的数据经另一个channel写入到cli的输入。从而实现远程连接pty。

按照rust的写法,读线程中,在读socket之前需要先锁socket,然后读取,再释放锁;同样,在写线程中,也需要先锁socket,然后写入,再释放锁。这样一来代码应该如下:

连接与初始化代码如下

let (ws_stream, response) =
    connect(Url::parse("wss://ws.postman-echo.com/raw").unwrap()).expect("msg");

println!("Connected to the server");
println!("Response HTTP code: {}", response.status());
println!("Response contains the following headers:");
for (ref header, _value) in response.headers() {
    println!("* {}", header);
}

let socket = Arc::new(Mutex::new(ws_stream));

// init cli
self.pty_start();

let mut me = self.clone();
let skt = socket.clone();
thread::spawn(move || {
    me.watch_socket_read_in(skt);
});
println!("---> watch_socket_read_in");

self.watch_socket_write_out(socket.clone());
println!("---> watch_socket_write_out");

读取方法在一个新起的线程中watch_socket_read_in,如下

fn watch_socket_read_in(&mut self, socket: Arc<Mutex<WebSocket<MaybeTlsStream<TcpStream>>>>) {
    loop {
        let mut skt = socket.lock().unwrap();
        let msg = skt.read_message().unwrap();
        println!("Socket Received: {:?}", msg);
        drop(skt);
        self.tx_in
            .send(msg.clone())
            .expect("send msg into in channel failed");
        println!("send pipe in succ: {:?}", msg);
    }
}

可以看到,不停的从socket读取数据,读取前锁,读取后drop锁。

写入方法在初始化代码所在的主线程中watch_socket_write_out,如下

fn watch_socket_write_out(&mut self, socket: Arc<Mutex<WebSocket<MaybeTlsStream<TcpStream>>>>) {
    let rx = self.rx_out.lock().expect("lock rx out failed");

    for msg in rx.iter() {
        println!("msg from cli -> {:?}", msg);
        let mut skt = socket.lock().unwrap();
        println!("Socket Send    : {:?}", msg);
        skt.write_message(msg).unwrap();
        drop(skt);
    }

    println!("out of socket write out block....")
}

可是,运行的结果却出乎我的意料,运行结果现象是这样的,先是只能够从socket读取到服务端的PING数据,而cli发出的数据经过channel读取出来之后,锁socket,准备发送,但是发现锁socket卡主死锁了,导致无法经socket发送,然后就卡了很久;但是过了一段时间,写socket获取的锁成功了,发了一大堆的数据,然后又轮到读socket卡主,稍后随机的时间后,读socket锁成功,又只能读到PING,如此反复。这种状态的读写,完全不能用,根本实现不了cli与服务端的实时通讯。

分析了一下,应该是socket网络读写是网络通讯,因此读写的锁定socket时长是不确定的且相对耗时算是比较长的,所以导致无法预料是读获取到锁还是写获取到锁,而且这种锁强行将读写串行化了,完全不符合并发读写的要求了。

几经查找,于是采用tokio-tungstenite这个crate替换了tungstenite,因为它可以将WebSocketStream通过split方法分隔为readerwriter,这样一来,读与写就分离开了,在不同的线程中无需对socket加锁。

let (ws_stream, response) =
    connect_async(Url::parse("wss://ws.postman-echo.com/raw").unwrap())
        .await
        .expect("msg");
let (ws_writer, ws_reader) = ws_stream.split();

这样一来,读socketws_reader,写socketws_writer

// socket read
let me = self.clone();
tokio::spawn(async move {
    let mut incoming = ws_reader.map(Result::unwrap);
    while let Some(msg) = incoming.next().await {
        if msg.is_text() {
            println!("Socket Received: {:?}", msg);
            me.tx_in.send(msg).expect("send msg into in channel failed");
        }
    }
});

// socket write
self.watch_socket_write_out(ws_writer).await;
async fn watch_socket_write_out(
    &mut self,
    mut writer: SplitSink<WebSocketStream<MaybeTlsStream<TcpStream>>, Message>,
) {
    let rx = self.rx_out.lock().expect("lock rx out failed");

    for msg in rx.iter() {
        println!("Socket Send    : {:?}", msg.to_text().unwrap());
        writer.send(msg).await.unwrap();
    }

    println!("out of socket write out block....")
}

可以看到,新线程中读socket,主线程中写socketws_readermap方法后,可以在死循环中阻塞调用next()不断的读取socket中的信息。写socket则从channel中读取到数据,按常规的方法send即可。

r56j2avc

接入tokio-tungstenite解决了这个问题,不过它是基于tokio的,tokio是一个协程库,有自己的运行时,用了tokio的程序起协程后,程序会自动启动若干个线程,类比goroutine,它也是有初始的资源消耗的,比如这个程序只需要4个线程,但是使用了tokio的程序,会有10个线程(如上图),内存占用会明显增多。


初学rust,HashMap的clone
Tag rust, clone, hashmap, on by view 262

在rust中有个常用个方法clone,按字面意思就是克隆。这个函数的作用是对对象进行深度拷贝,生成的新对象与原对象相互独立。

很多常用的类型或者容器类型都支持clone,例如rust中的HashMap也支持clone,我们用一段代码实验一下。

#[test]
fn test_hash_map_clone() {
    let xx: Arc<Mutex<HashMap<String, String>>> = Arc::new(Mutex::new(HashMap::new()));
    let mut mp = xx.lock().unwrap();
    mp.insert("hi".to_string(), "hello".to_string());
    println!("origin: {:?}", mp);
    let mut cp = mp.clone();
    cp.insert("k".to_string(), "v".to_string());
    println!("origin: {:?}", mp);
    println!("cp    : {:?}", cp);
}

输出

running 1 test
origin: {"hi": "hello"}
origin: {"hi": "hello"}
cp    : {"hi": "hello", "k": "v"}
test test_hash_map_clone ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out; finished in 0.00s

上面的测试代码运行结果表示,修改克隆后的对象cp,源对象mp不会发生变化。

那么我们自己定义的类型如何才能支持clone呢?使用#[derive(Clone)]这个指令修饰自定义类型,就会自动支持clone,但是要注意,如果自定义类型结构体里,如果有字段类型不支持clone,将无法通过#[derive(Clone)]指令快速支持clone

自定义类型clone测试如下

#[derive(Debug, Clone)]
struct User {
    name: String,
    age: i32,
}

#[test]
fn test_struct_clone() {
    let mut u1 = User {
        name: "rex".to_string(),
        age: 1,
    };
    println!("origin: {:?}", u1);
    let mut ucp = u1.clone();
    ucp.name = "agnes".to_string();
    ucp.age = 2;
    println!("origin: {:?}", u1);
    println!("cp    : {:?}", ucp);
    u1.age = 3;
    println!("origin: {:?}", u1);
    println!("cp    : {:?}", ucp);
}

运行结果

running 1 test
origin: User { name: "rex", age: 1 }
origin: User { name: "rex", age: 1 }
cp    : User { name: "agnes", age: 2 }
origin: User { name: "rex", age: 3 }
cp    : User { name: "agnes", age: 2 }
test test_struct_clone ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out; finished in 0.00s

同样可以看到,修改clone后的对象,源对象不变,修改源对象,clone后的对象也不变。


golang程序并发读写全局变量,导致空指针异常
Tag golang, 数据竞争, 线程安全, on by view 185

最近将一个golang程序由原本的单线程改为双线程处理日志解析,在生产环境节点上运行发现出现了一个空指针异常,异常信息如下:

[E] 2023/02/27 11:53:25 panic.go:838: parse error:runtime error: invalid memory address or nil pointer dereference,line:Feb 27 11:53:25 xxxxx.site nginx: [xxxxxxxxxxxxxxxxxxx] [27/Feb/2023:11:53:25 +0800] [https] [120.232.31.196:443] [xxxxx.map.xx.com] [39.144.41.41:37647] [200] [30.171.153.132:10000] [200] [3339597] [POST /tr?mllc HTTP/1.1] [621] [152] [xxxxxxx.map.xx.com] [Dalvik/2.1.0 (Linux; U; Android 10; HMA-AL00 Build/HUAWEIHMA-AL00)] [-] [0.008] [0.008] [0.004] [0.008] [46795] [311532943564] [1][1:46:0:0:0:0:0] [ECDHE-RSA-AES128-GCM-SHA256] [TLSv1.2] [r][-] [-] [n] [2358837] [-1] [1677470005.398|5|51|126|-1|-1|126|126|130|-1|-1|130|134|134|134|134#200|200|8|152|120.232.31.196|30.171.153.132:10000|0|0] [POST] [/tr] [mllc] [HTTP/1.1] [39.144.41.41] [-] [-] [-] [-] [-] [-] [-] [-] [-] [-] [169.254.213.29:50937] [0]

修改程序,打印出异常栈 x7s4uzea

发现异常发生在代码133行,代码如下 0zhhoxbp

很明显这里不太可能出现空指针,除非运行到这一行的时候sl对象被置为nil,但是我很确定这里不存在其他线程共享sl的情况,也就是不可能被其他线程置为nil,何况,我这里没有任何操作将sl置为nil,百思不得其解。最后发现key1key2这两个变量是全局变量,全局变量在多线程环境下会存在数据竞争问题。原本定义如下

var (
	uriSep *regexp.Regexp = nil

	key1 = ""
	key2 = ""
)

可以看到key1,key2被定义为全局变量,忘记修改了。修改之后,神奇的发现,空指针异常已经不再存在了。

我在发现这个问题之前,在本地开发服务器上尝试重现,但是一直未能重现出来,估计是我本地qps不够高,所以难以复现,生产环境qps是3w到4w左右,这个空指针异常呈现无规律的隔几秒钟出现一次。

golang中,双协程(绑定在双M和双cpu上)中同时读写一个共享变量导致空指针异常,这种情况我还是第一次遇见,以前遇到这种双协程读写一个共享变量的情况都是数据错乱,并没有空指针。据说这种线程安全、数据竞争导致的空指针异常在C++中也是常见的情况。所以,我在想,这里会不会是因为我将2个协程绑核了,所以在双线程绑双核的情况下更容易复现呢。


初学rust,如何在线程中调用成员方法
Tag rust, 线程, 方法, on by view 122

如何在线程中调用成员方法?

普通调用习惯写法

fn watch_receiver(mut self, rx: Receiver<String>) {
    thread::spawn(move || {
        for line in rx.iter() {
            self.push(line);
        }
    });
}

会报错

p.watch_receiver(rx);
    |           ------------------ `p` moved due to this method call
70  |         p.watch_poly();
    |         ^^^^^^^^^^^^^^ value borrowed here after move

这里需要把形参self改为指针&self,然后在方法体中克隆这个指针,就可以在方法中的线程里直接通过这个指针的克隆成员方法。

改为

fn watch_receiver(&self, rx: Receiver<String>) {
    let mut me = self.clone();
    thread::spawn(move || {
        for line in rx.iter() {
            me.push(line);
        }
    });
}

即可通过。

但是要注意,这里的clone,真的是克隆。所以clone前后的变量,即selfme是2个不同的变量,里面的成员也是在不同的内存空间上,修改self中的成员属性,me中对应的成员属性并不会跟着变。所以,如果里面的成员属性需要跟随变化,必须把成员属性定义为指针,这样修改指正所指的值,selfme中成员属性所指的值是相同的。


为go程序的协程绑核
Tag golang, 绑核, on by view 1174

最近在公司的日志处理程序上做性能优化,用到了绑核的情况。背景是这样的,nginx进行http转发,产生日志,然后我们的程序读取日志,用lexer分词器对日志分隔字段,并且对字段进行统计聚合上报,生成监控。日志处理程序最开始是在单个goroutine里进行读取并且解析操作了,但是在核数比较多的大机器上,发现日志生成太快,解析程序处理不过来,在日志rotate的过程中会发生丢失日志的情况。于是针对这个情况进行了优化。

用pprof发现,性能消耗最大的部分是lexer,lexer其实就是个分词器,编译器中常用的技术,逐字符读取每行日志,然后基于状态机状态标记对日志的字段进行分割,中间涉及到的状态也不算太多,主要是双引号(“”)作为定界符提取字符串字段,方括号([])作为定界符提取字符串字段,空字符(空格、\t)和竖线符(|)作为分隔符分隔字段,转义符()对字符串中的字符串定界符(”[])进行转义,总体来说,状态不算复杂,其中也针对lexer优化过尽量减少变量分配和杜绝变量逃逸,lexer实在是已经无可优化了。

于是只好从其他方面下手,首先就是cpu切换的性能损耗。众所周知golang中没有线程的,golang中只有协程(goroutine),而防止cpu切换的性能损耗只有绑核这个方法,具体就是讲指定的核绑定到某个线程上,这样这个线程就会只在这个指定的核上运行,不会被系统切换到其他核上,这样也就不会产生切换的损耗了。但是golang程序中只有goroutine,不能直接操作线程。其实我们是有办法对goroutine进行绑核的。

首先,使用go里面的runtime.LockOSThread()将当前goroutine绑定到它所在的M线程,这样,这个goroutine就不会在M线程之间切换了;然后,我们可以使用cgo,调用pthread_self获取当前协程所在M线程的线程ID,并调用CPU_SET对这个线程ID设置cpuid绑定。具体如下

package affinity

/*
#define _GNU_SOURCE
#include <sched.h>
#include <pthread.h>

int lock_thread(int cpuid) {
  pthread_t tid;
  cpu_set_t cpuset;

  tid = pthread_self();
  CPU_ZERO(&cpuset);
  CPU_SET(cpuid, &cpuset);
  return  pthread_setaffinity_np(tid, sizeof(cpu_set_t), &cpuset);
}

pthread_t current_thread_id() {
  pthread_t tid;

  tid = pthread_self();

  return tid;
}
*/
import "C"

import (
	"fmt"
	"runtime"
)

// SetAffinity 设置CPU绑定
func SetAffinity(cpuID int) (uint64, error) {
	runtime.LockOSThread()
	ret := C.lock_thread(C.int(cpuID))
	tid := uint64(C.ulong(C.current_thread_id()))
	if ret > 0 {
		return 0, fmt.Errorf("set cpu core affinity failed with return code %d", ret)
	}
	return tid, nil
}

这样一来,我们只需要在goroutine中调用SetAffinity就可以将指定的cpuid和当前goroutine进行绑定。这样就实现了goroutine的绑定。

我将日志处理程序改为在主协程中读取文件并且通过channel分发日志行,然后在2个goroutine执行最占cpu的lexer及后续处理,并且在这两个goroutine中绑定cpuid为1,2。

qdx9l7z5

图中可以看到,两个处理日志的goroutine绑定了1,2两个cpu,并且不会切为其他cpu,这两个cpu都在处理日志,所以cpu占用都比较高,相当于把原来一个核处理的任务分担到2个核上了。


观澜版画村春游记
Tag 观澜, 春游, on by view 62

春天已经悄悄到来,南方大城市中很难感受到春的变化。除非出游去远离都市的郊区,本着去看油菜花的目的,上周末去逛了一下深圳边缘的观澜版画村。

勤劳的小蜜蜂🐝 betv6288

油菜花田 kcxao9mw

芦苇 x47lhgk3

这天天气很炎热,人也很多。


M1 Macbook Pro 使用初体验
Tag M1, Macbook, on by view 104

最近体验了一段时间 M1 芯片的 Macbook Pro,我不得不承认,是我当初低估了它。简单的聊一下使用体验。

首先说一下我的日常使用电脑场景,vscode 窗口若干(3到5个窗口),远程开发。chrome 3到4组,每组20-30个tab,微信,QQ,企业微信(消息量4k+),基于electron的ssh终端,Microsoft TODO,Microsoft Remote Desktop,基于Chrome插件的youtube客户端(偶尔划个水)。

我原来的 Macbook Pro 是 Intel i7 的,内存32G,用起来不卡顿,但是风扇能够听到明显的声音,键盘很热,Touchbar位置的外壳烫手。

我现在体验的 Macbook Pro 14寸是M1处理器(8核,4E4P),16G内存。虽然只有16G内存,使用起来一点也不卡顿,并且键盘几近冰凉,Touchbar位置外壳仅有一点点温度。

p9uz43hp

可以看到,温度只有43度,风扇根本没转起来,事实上这台电脑从我拿到手到现在一个多星期风扇几乎一直是没转过。当然,还有一个很明显的地方就是,不插电的情况下续航时间明显比我原来的 Macbook Pro 长多了。

对比一下我 Intel 处理器的 Macbook Pro。

jpttxbxw

注意,这是我使用 Turbo Boost Switcher 强制关闭了 CPU 的 Boost 能力之后的表现,且是开机5分钟后的温度(并没有重度使用),然而多使用一会儿之后,温度很快就到达60度以上了。

总结一下,M1是真香,流畅、凉爽、持久。


2023新年顺德出游
Tag 顺德, 游记, on by view 99

去年因为疫情严重,春节临时取消了回老家过年的计划,改为在深圳过年。这一年的春节也是我第一次在深圳见到群众放烟花,估计是这几年群众过得太压抑了,然后公安部门明显也没有严厉的禁放。仓促的开放疫情管控,打得大家措手不及;但是这似乎也阻止不了大家回归正常生活的愿望。

因为在深圳过年,所以春节期间就决定去顺德逛一逛,这是我老婆那个吃货怂恿的。但是可惜,在顺德也没吃的什么特别的东西。而且,发现到处都是人;尤其是逛清晖园的时候,人特别的多,本来园子还挺漂亮的,但是一眼看去,全是人头。很多人觉得自己已经阳过了,就无所谓了;也不知道阳一次能管多久,希望阳了之后产生的免疫能管得久一点;也就是希望第二次阳不要太快到来,毕竟没有人愿意再经历一次这样的苦难。

清晖园 (人太多了,只好让鱼出镜,祝大家年年有鱼)

yrf6frp7

渔人码头

e55izekr

正月初六深圳采草莓

jujmhymt

采摘园路边的小花花

h74j5oyg


初学rust,使用协程
Tag rust, 协程, on by view 180

最近想使用rust写个tcp透明代理转发服务,这中间涉及到socket监听以及连接处理逻辑中连接后端服务并处理连接后的相关逻辑。

监听端口

fn main() {
    let skt = Socket::new(Domain::IPV4, Type::STREAM, Some(Protocol::TCP)).unwrap();
    let address: SocketAddr = "0.0.0.0:3333".parse().unwrap();
    skt.set_ip_transparent(true).unwrap();
    skt.set_reuse_address(true).unwrap();
    skt.set_reuse_port(true).unwrap();
    skt.bind(&address.into()).unwrap();
    skt.listen(128).unwrap();

    let listener: TcpListener = skt.into();
    println!("Server listening on port 3333");

    for stream in listener.incoming() {
        match stream {
            Ok(stream) => {
                println!("peer addr: {}", stream.peer_addr().unwrap());
                thread::spawn(move || {
                    // connection succeeded
                    handle_client(stream)
                });
            }
            Err(e) => {
                println!("Error: {}", e);
                /* connection failed */
            }
        }
    }
    // close the socket server
    drop(listener);
}

处理客户端连接

fn handle_client(mut stream: TcpStream) {
    let x = get_original_destination_addr(&stream).unwrap();
    println!("target addr: {:?}", x);

    let mut client_stream = get_rs_connect_stream_by_ipport(x.to_string().as_str());
    println!("client stream: {:?}", client_stream);

    let mut data = [0 as u8; 50]; // using 50 byte buffer
    while match stream.read(&mut data) {
        Ok(size) => {
            if size == 0 {
                false
            } else {
                // echo everything!
                println!("len: {:?}", size);
                stream.write(&data[0..size]).unwrap();
                true
            }
        }
        Err(_) => {
            println!(
                "An error occurred, terminating connection with {}",
                stream.peer_addr().unwrap()
            );
            stream.shutdown(Shutdown::Both).unwrap();
            false
        }
    } {}
}

可以看到这里每接受一个连接,就会起一个线程,如果在handle_client里再连接后端的话,就需要针对client_stream再起一个线程在线程中执行死循环,否则死循环会卡主stream这个死循环。这样一来,一个链接就得起2个线程了,那么连接一多,线程就更多了,一个进程能创建的线程数是有限的,因为线程对资源占用相对比较大,这样连接数一多,系统资源就不够用,性能就会很差。

对比到golang中的协程,我们能否在rust中使用协程呢?答案是肯定的。下面简单介绍一下rust中如何使用协程。rust中使用“协程”,我们用到tokio这个包。

对于单个异步,我们可以用async和await就可以了

#[tokio::main]
async fn main() {
    let skt = Socket::new(Domain::IPV4, Type::STREAM, Some(Protocol::TCP)).unwrap();
    let address: SocketAddr = "0.0.0.0:3333".parse().unwrap();
    skt.set_ip_transparent(true).unwrap();
    skt.set_reuse_address(true).unwrap();
    skt.set_reuse_port(true).unwrap();
    skt.bind(&address.into()).unwrap();
    skt.listen(128).unwrap();

    let listener: TcpListener = skt.into();
    println!("Server listening on port 3333");

    for stream in listener.incoming() {
        match stream {
            Ok(stream) => {
                println!("peer addr: {}", stream.peer_addr().unwrap());
                handle_client(stream).await  // 其中 handle_client 改为了 async 异步函数
            }
            Err(e) => {
                println!("Error: {}", e);
                /* connection failed */
            }
        }
    }
    // close the socket server
    drop(listener);
}

其中 handle_client 是 async 异步函数,但是如果我想在handle_client中再起一个异步函数,直接使用async是不行的。我们可以这样处理,使用tokio::spawn就可以了,这里tokio::spawn有点类似golang中的go关键词,调用tokio::spawn的地方可以起一个异步函数。

async fn rs_handle(rs_stream: Arc<TcpStream>, stream: Arc<TcpStream>) {
    let mut client_data = [0 as u8; 50]; // using 50 byte buffer
    while match rs_stream.as_ref().read(&mut client_data) {
        Ok(client_size) => {
            if client_size == 0 {
                false
            } else {
                println!("len: {:?}", client_size);
                stream.as_ref().write(&client_data[0..client_size]).unwrap();
                true
            }
        }
        Err(_) => {
            println!("client error occurred.");
            false
        }
    } {}
}

async fn handle_client(stream: TcpStream) {
    let x = get_original_destination_addr(&stream).unwrap();
    println!("target addr: {:?}", x);

    let stream = Arc::new(stream);
    let rs_stream = Arc::new(get_rs_connect_stream_by_ipport(x.to_string().as_str()));
    println!("rs stream: {:?}", rs_stream);

    let rsh_rs_stream = rs_stream.clone();
    let rsh_stream = stream.clone();
    tokio::spawn(rs_handle(rsh_rs_stream, rsh_stream)); // 这里相当于起一个线程
    println!("rs handle setted.");

    let mut data = [0 as u8; 50]; // using 50 byte buffer
    while match stream.as_ref().read(&mut data) {
        Ok(size) => {
            if size == 0 {
                false
            } else {
                rs_stream.as_ref().write(&data[0..size]).unwrap();
                true
            }
        }
        Err(_) => {
            println!(
                "An error occurred, terminating connection with {}",
                stream.peer_addr().unwrap()
            );
            stream.shutdown(Shutdown::Both).unwrap();
            false
        }
    } {}
}

如上代码,首先需要定义一个异步函数async fn rs_handle(...)然后在调用的地方使用tokio::spawn(rs_handle(...))来调用。就可以实现同等于golang中go关键词的效果,起一个协程。


如何投诉快递公司
Tag 投诉, 快递, on by view 72

快递不派件,丢件。找快递公司,快递公司不理会怎么办?相信很多人都遇到过这种情况。简单讲一下我遇到的情况,我有一个快递14号显示派送中,然后就显示疫情原因派送失败,然后我就想既然是疫情导致的,就等等吧,这一等就等到了月底,都两周多了。于是我打电话给快递总公司,找人工服务,对面说他们会联系一下快递网点公司,然后就没消息了,于是等了一个星期我再打电话给快递总公司,他给我发了一个快递网点公司的电话,让我联系快递网点。我联系了快递网点,对面又说他问一下派件业务员,然后就又是没消息了,于是又等了一个星期。

upcqvz8r

我终于不愿意再等了,没有哪个网点的疫情防控防控半个月的吧。接下来我这样操作

首先,到国家邮政业申诉服务平台(https://sswz.spb.gov.cn/index.html)注册账号并登录。

然后,点我要申诉,填写申诉相关信息。申诉单创建了,如下图

p9qx2eek

当天我给快递总公司去电三次,没能接通,给网点去电两次,没能接通。直到我 12:56 在邮政投诉平台提单投诉,然后,13:00 立马就收到了快递公司的短信,说会有专人跟进处理,14:38 立马快递总公司来电联系我,不出2小时就立马响应我的申诉单了。当天快递公司和网点先后电话联系我5次,最终告诉我快递没下落了,等他查一下监控,然后又说加我微信,如果找不到的话,就给我理赔,加了微信最后告诉我快递找不到了,因为有业务员离职了交接没搞清楚导致的,然后给我按原价赔偿了120块钱。

这快递公司是真的狗,我先后好声好气的联系他们,都如同石沉大海,不给我反馈。快递状态上拿疫情糊弄我。如果不是我了解到邮政投诉,估计这单快递就黄了,损失就是我的了。特此记录一下,有遇到相同情况的人,可以试一下邮政投诉。注意这里的邮政是政府机构,邮件政务管理部门的简称,并不是邮政快递。邮政是一个政务部门,在我国早期叫做邮电,也分政务部门和业务部门,政务部门负责管理,业务部门负责信件和电报业务,后来随着国家发展,邮电被拆分为邮政和电信,电信又发转为三大运营商,所以现在邮政快递是业务部门,邮政是政务部门负责管理包括邮政快递在内的所有快递公司,工信部是三大运营商的政务部门负责管理三大运营商。去政务部门“邮政”去投诉快递公司,一投一个准。快递公司绝对不敢怠慢。