谢顶道人

谢顶道人 查看完整档案

北京编辑河北农业大学  |  兽医 编辑滴滴出行  |  PHP-Go 编辑 t.ti-node.com/ 编辑
编辑

排头兵@滴滴出行
公众号:高性能API社区

个人动态

谢顶道人 赞了问题 · 2019-10-30

socket_read的两个参数:PHP_NORMAL_READ 和 PHP_BINARY_READ

简单地拿php的socket写了个小daemo, 代码如下:

server端:
<?php

$address = "127.0.0.1";
$port = 20461;
//创建socket资源
$sock = socket_create(AF_INET, SOCK_STREAM, SOL_TCP) or die("socket_create() 失败的原因是:" . socket_strerror(socket_last_error()) . "/n");
//阻塞模式
socket_set_block($sock) or die("socket_set_block() 失败的原因是:" . socket_strerror(socket_last_error()) . "/n");
//绑定到socket端口
$result = socket_bind($sock, $address, $port) or die("socket_bind() 失败的原因是:" . socket_strerror(socket_last_error()) . "/n");
//监听端口
$result = socket_listen($sock) or die("socket_listen() 失败的原因是:" . socket_strerror(socket_last_error()) . "/n");
echo "OKnBinding the socket on $address:$port ... ";
echo "OKnNow ready to accept connections.nListening on the socket ... n";
do {

$msgsock = socket_accept($sock) or  die("socket_accept() failed: reason: " . socket_strerror(socket_last_error()) . "/n");
echo "Read client data \n";
$buf = socket_read($msgsock, 8192, PHP_NORMAL_READ);
echo "Received msg: $buf   \n";
//数据传送 向客户端写入返回结果
$msg = "welcome" . PHP_EOL;
socket_write($msgsock, $msg, strlen($msg)) or die("socket_write() failed: reason: " . socket_strerror(socket_last_error()) ."/n");
socket_close($msgsock);

} while (true);
socket_close($sock);

client端:
<?php
global $argv;
$host = "127.0.0.1";
$port = 20461;
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP)or die("Could not create socketn"); // 创建一个Socket
$connection = socket_connect($socket, $host, $port) or die("Could not connet servern"); // 连接
socket_write($socket, $argv[1] . PHP_EOL) or die("Write failedn");
//$buff = socket_read($socket, 1024, PHP_NORMAL_READ);
while ($buff = socket_read($socket, 1024, PHP_NORMAL_READ)) {

echo("Response was:" . $buff . PHP_EOL);

}
socket_close($socket);

我把服务端启动之后,客户端发送数据会报错:
图片描述
但是把客户端的socket_read改成PHP_BINARY_READ就可以了;

关注 5 回答 3

谢顶道人 收藏了问题 · 2019-10-30

socket_read的两个参数:PHP_NORMAL_READ 和 PHP_BINARY_READ

简单地拿php的socket写了个小daemo, 代码如下:

server端:
<?php

$address = "127.0.0.1";
$port = 20461;
//创建socket资源
$sock = socket_create(AF_INET, SOCK_STREAM, SOL_TCP) or die("socket_create() 失败的原因是:" . socket_strerror(socket_last_error()) . "/n");
//阻塞模式
socket_set_block($sock) or die("socket_set_block() 失败的原因是:" . socket_strerror(socket_last_error()) . "/n");
//绑定到socket端口
$result = socket_bind($sock, $address, $port) or die("socket_bind() 失败的原因是:" . socket_strerror(socket_last_error()) . "/n");
//监听端口
$result = socket_listen($sock) or die("socket_listen() 失败的原因是:" . socket_strerror(socket_last_error()) . "/n");
echo "OKnBinding the socket on $address:$port ... ";
echo "OKnNow ready to accept connections.nListening on the socket ... n";
do {

$msgsock = socket_accept($sock) or  die("socket_accept() failed: reason: " . socket_strerror(socket_last_error()) . "/n");
echo "Read client data \n";
$buf = socket_read($msgsock, 8192, PHP_NORMAL_READ);
echo "Received msg: $buf   \n";
//数据传送 向客户端写入返回结果
$msg = "welcome" . PHP_EOL;
socket_write($msgsock, $msg, strlen($msg)) or die("socket_write() failed: reason: " . socket_strerror(socket_last_error()) ."/n");
socket_close($msgsock);

} while (true);
socket_close($sock);

client端:
<?php
global $argv;
$host = "127.0.0.1";
$port = 20461;
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP)or die("Could not create socketn"); // 创建一个Socket
$connection = socket_connect($socket, $host, $port) or die("Could not connet servern"); // 连接
socket_write($socket, $argv[1] . PHP_EOL) or die("Write failedn");
//$buff = socket_read($socket, 1024, PHP_NORMAL_READ);
while ($buff = socket_read($socket, 1024, PHP_NORMAL_READ)) {

echo("Response was:" . $buff . PHP_EOL);

}
socket_close($socket);

我把服务端启动之后,客户端发送数据会报错:
图片描述
但是把客户端的socket_read改成PHP_BINARY_READ就可以了;

谢顶道人 赞了文章 · 2019-10-21

使用 sigprocmask 和 sigpending 在程序正文中捕获和处理信号

最近在尝试使用 epoll写一个类似 libevent 的库。那么,如何像 libevent 一样,在 event loop 里加入对信号事件的观测呢?
我查了一下资料,一个可行的方法,就是使用 sigprocmask() 及其相关功能来实现啦。

但是请注意,这个方法是存在缺陷的,请看官留心。
个人在继续研究之后,暂时是不打算使用此种方法来实现信号事件,而改用另一个方法

Reference

《UNIX 环境高级编程》
sigprocmask , sigpending 和 sigsuspend函数
errno多线程安全
Linux 多线程应用中编写安全的信号处理函数

UNIX 系统主要信号

以下就只列出主要的信号了:

名称说明FreeBSDLinuxmacOSSolaris默认动作
SIGABRT调用了abort()YYYY终止 + core
SIGALRMalarm()产生的YYYY终止
SIGBUS硬件故障YYYY终止 + core
SIGCHLD子进程状态改变YYYY忽略
SIGHUP连接断开YYYY终止
SIGINTCtrl + CYYYY终止
SIGKILL终止;不可捕获YYYY终止
SIGPIPE向关闭的管道写YYYY终止
SIGQUITCtrl + \YYYY终止 + core
SIGSEGV段错误YYYY终止 + core
SIGSTOP停止YYYY暂停进程
SIGTERMkill(1)YYYY终止
SIGUSR1用户自定义1YYYY终止
SIGUSR2用户自定义2YYYY终止
SIGPOLL可轮训的设备发生事件.Y.Y终止
SIGPWR主电源失效,电池电量不足.Y.Y终止或忽略

如果要在 C 里面发送一个信号的话,那么可以用 kill()raise()。其中后者是想当前进程发信号,而前者可以向任意进程发信号。kill()pid 参数可以有以下可能值:

  • pid > 0:发给指定进程
  • pid == 0:发给与当前进程属于同一进程组的所有进程,但需要权限允许
  • pid < 0:发给进程组 ID 等于 (0 - pid) 的所有进程,但需要权限允许
  • pid == -1:发给所有进程,但需要权限允许

信号集操作

    #include <signal.h>

    int sigemptyset(sigset_t *set);
    int sigfillset(sigset_t *set);
    int sigaddset(sigset_t *set, int signum);
    int sigdelset(sigset_t *set, int signum);
    int sigismember(const sigset_t *set, int signum);

上面的几个函数语义都很清楚了,就是在一个集合里面配置多个信号。
除了 sigismenber() 实际上返回的是 BOOL 类型之外,其他的函数均返回 0 代表成功,-1 代表失败。

sigprocmask 和 sigpending

    #include <signal.h>

    int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
    int sigpending(sigset_t *set);

sigprocmask() 返回的是 0 或者 -1 的状态值,而 sigpending() 返回 BOOL
其中 how 可以有以下值:

  • SIG_BLOCK:屏蔽信号(注意,不是“忽略”信号)
  • SIG_UNBLOCK:解屏蔽
  • SIG_SETMASK:将整个表配置设置进去。这适用于 sigprocmask() 恢复阶段。后续有说明

“屏蔽” 信号的含义

sigprocmask()的作用,主要就是屏蔽指定的信号。这个 “屏蔽” 的含义需要说明清楚。
首先我们大致数一下信号在内核里的处理流程吧(不是准确的流程,只是便于说明):

  1. 内核等待信号中断
  2. 信号产生,触发内核中断
  3. 内核将信号存下来,或者说设置信号标志
  4. 内核根据用户空间的配置,处理信号。如果用户空间没有特别配置,则按照默认行为处理
  5. 处理完成后,清除信号标志
  6. 回到 1,继续等待

sigprocmask()所做的 “屏蔽”,其实就是将上述的信号处理流程,卡在了 3 和 4 之间,让内核能够将信号标志设置好,但是却到不了判断并处理的那一步。
换句话说,即便进程调用 signal() 函数,设置了 SIG_IGN 标志,但如果指定的信号被 sigprocmask()屏蔽了的话,内核也不会去判断是否该忽略这个信号,而只是把信号标志卡在那儿,直到调用sigprocmask()执行SIG_UNBLOCK为止,才能让内核继续走到第 4 步。

在程序正文处理信号

这里所说的 “正文”,指的是:
  不在 signal()sigaction() 中指定的 handler 中处理信号事件,而是在普通的程序流程能够中捕捉信号,并且处理信号。

这么做有很多好处:

  • 中断处理函数有很多限制,只能调用某些系统调用,否则可能导致上下文异常。但在正文中就不会有这个问题
  • 中断处理函数和正文之间可以视为两个不同的线程,两者之间的同步比较麻烦
  • 在正文中处理,可以实现类似于 libeventEV_SIGNAL 功能——而这也是笔者正在研究的。

基本软件流程如下:

  1. 使用 signal()sigaction() 将需要捕获的信号设置为 SIG_IGN
  2. 使用 sigprocmask() 屏蔽需要捕获的信号,同时注意将屏蔽之前的信号集保存下来(oset参数)
  3. 进行相应操作(比如 epoll()
  4. 如果发现 errnoEINTR,那么就可以用 sigpending() 获取被屏蔽的信号集,判断需要捕获的信号是否在信号集中
  5. 使用 sigprocmask() 执行一次 SIG_UNBLOCK 操作,让内核清除信号集标志
  6. 回到 2,重新屏蔽信号

缺陷

不过这个流程有一个 bug,就是信号有可能在 4 和 6 之间产生,这样的话,就捕获不到了——这还需要想想怎么处理。

sigaction 函数

这里顺便记一下 sigaction() 吧,POSIX 是建议不要再使用 signal() 了。
简单情况下,只需要使用 struct sigcation 里的 sa_handlersa_mask 就可以替代 signal() 调用了。

    #include <signal.h>

    struct sigaction {
        void     (*sa_handler)(int);
        void     (*sa_sigaction)(int, siginfo_t *, void *);
        sigset_t   sa_mask;
        int        sa_flags;
        void     (*sa_restorer)(void);
    };

    int sigaction(int signum, const struct sigaction *act,
                  struct sigaction *oldact);

errno 的线程安全问题

前文提及 “如果发现 errno 为 EINTR ...”。有同学可能会问了:“errno 是一个全局变量啊,这安全不?”
实际上,errno线程安全的……呃,这个优点,其实笔者自己也是才知道……看了一下 errno 的原理,觉得实在是很厉害啊!

但是,使用 errno 只有一点要注意,就是虽然在程序正文中,errno 是线程安全的,但是在中断处理函数中却并不是这样。其他位置的话,随意。

这里参考的资料是这个还有这个

查看原文

赞 1 收藏 0 评论 0

谢顶道人 收藏了文章 · 2019-09-29

理解 Golang 中 slice 的底层设计

Slice 结构体

golang 中的 slice 数据类型,是利用指针指向某个连续片段的数组。
一个 slice 在 golang 中占用24个 bytes

a = make([]int, 0)
unsafe.Sizeof(a)    // 24

var c int
unsafe.Sizeof(c)    // 8, 一个 int 在 golang 中占用 8 个bytes(本机是64位操作系统)

在 runtime 的 slice.go 中,定义了 slice 的 struct

type slice struct {
    array unsafe.Pointer    // 8 bytes
    len   int                // 8 bytes
    cap   int                // 8 bytes
    // 确认了,slice 的大小 24
}
  • array 是指向真实的数组的 ptr
  • len 是指切片已有元素个数
  • cap 是指当前分配的空间

准备调试

简单准备一段程序,看看 golang 是如何初始化一个切片的

package main

import "fmt"

func main() {
    a := make([]int, 0)
    a = append(a, 2, 3, 4)
    fmt.Println(a)
}

Slice 初始化

使用 dlv 调试,反汇编后:

(dlv) disassemble
TEXT main.main(SB) /Users/such/gomodule/runtime/main.go
main.go:5       0x10b70f0       65488b0c2530000000              mov rcx, qword ptr gs:[0x30]
main.go:5       0x10b70f9       488d4424e8                      lea rax, ptr [rsp-0x18]
main.go:5       0x10b70fe       483b4110                        cmp rax, qword ptr [rcx+0x10]
main.go:5       0x10b7102       0f8637010000                    jbe 0x10b723f      main.go:5       0x10b7108*      4881ec98000000                  sub rsp, 0x98
main.go:5       0x10b710f       4889ac2490000000                mov qword ptr [rsp+0x90], rbp
main.go:5       0x10b7117       488dac2490000000                lea rbp, ptr [rsp+0x90]
main.go:6       0x10b711f       488d051a0e0100                  lea rax, ptr [rip+0x10e1a]
main.go:6       0x10b7126       48890424                        mov qword ptr [rsp], rax
main.go:6       0x10b712a       0f57c0                          xorps xmm0, xmm0
main.go:6       0x10b712d       0f11442408                      movups xmmword ptr [rsp+0x8], xmm0
main.go:6       0x10b7132       e8b99af8ff                      ** call $runtime.makeslice **
main.go:6       0x10b7137       488b442418                      mov rax, qword ptr [rsp+0x18]
main.go:6       0x10b713c       4889442460                      mov qword ptr [rsp+0x60], rax
main.go:6       0x10b7141       0f57c0                          xorps xmm0, xmm0
main.go:6       0x10b7144       0f11442468                      movups xmmword ptr [rsp+0x68], xmm0
...

在一堆指令中,看到 call $runtime.makeslice 的调用应该是初始化 slice

func makeslice(et *_type, len, cap int) unsafe.Pointer {
    mem, overflow := math.MulUintptr(et.size, uintptr(cap))
    if overflow || mem > maxAlloc || len < 0 || len > cap {
        // NOTE: Produce a 'len out of range' error instead of a
        // 'cap out of range' error when someone does make([]T, bignumber).
        // 'cap out of range' is true too, but since the cap is only being
        // supplied implicitly, saying len is clearer.
        // See golang.org/issue/4085.
        mem, overflow := math.MulUintptr(et.size, uintptr(len))
        if overflow || mem > maxAlloc || len < 0 {
            panicmakeslicelen()
        }
        panicmakeslicecap()
    }

    return mallocgc(mem, et, true)
}

makeslice 最后返回真正值存储的数组域的内存地址,函数中 uintptr() 是什么呢?

println(uintptr(0), ^uintptr(0))
// 0    18446744073709551615    为什么按位异或后是这个数?

var c int = 1
println(^c, ^uint64(0))
// -2    18446744073709551615

从这几行代码验证,有符号的1,二进制为:0001,异或后:1110,最高位1是负数,表示-2;
uint64二进制:0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000
异或后:1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111
因为无符号的,转换成10进制,就是 2 ^ 64 - 1 = 18446744073709551615
。所以,其实^uintptr(0) 就是指当前机器(32位,uint32;64位,uint64)的最大值。
我们可以打印下现在的 a

(dlv) p a
[]int len: 1, cap: 0, [0]

Slice 扩容

=>      main.go:7       0x10b7149       eb00                            jmp 0x10b714b
        main.go:7       0x10b714b       488d0dee0d0100                  lea rcx, ptr [rip+0x10dee]
        main.go:7       0x10b7152       48890c24                        mov qword ptr [rsp], rcx
        main.go:7       0x10b7156       4889442408                      mov qword ptr [rsp+0x8], rax
        main.go:7       0x10b715b       0f57c0                          xorps xmm0, xmm0
        main.go:7       0x10b715e       0f11442410                      movups xmmword ptr [rsp+0x10], xmm0
        main.go:7       0x10b7163       48c744242003000000              mov qword ptr [rsp+0x20], 0x3
        main.go:7       0x10b716c       e84f9bf8ff                      call $runtime.growslice
        main.go:7       0x10b7171       488b442428                      mov rax, qword ptr [rsp+0x28]
        main.go:7       0x10b7176       488b4c2430                      mov rcx, qword ptr [rsp+0x30]
        main.go:7       0x10b717b       488b542438                      mov rdx, qword ptr [rsp+0x38]
        main.go:7       0x10b7180       4883c103                        add rcx, 0x3
        main.go:7       0x10b7184       eb00                            jmp 0x10b7186
        main.go:7       0x10b7186       48c70002000000                  mov qword ptr [rax], 0x2
        main.go:7       0x10b718d       48c7400803000000                mov qword ptr [rax+0x8], 0x3
        main.go:7       0x10b7195       48c7401004000000                mov qword ptr [rax+0x10], 0x4
        main.go:7       0x10b719d       4889442460                      mov qword ptr [rsp+0x60], rax
        main.go:7       0x10b71a2       48894c2468                      mov qword ptr [rsp+0x68], rcx
        main.go:7       0x10b71a7       4889542470                      mov qword 
        ...

在对 slice 做 append 的时候,其实是调用了 call runtime.growslice,看看做了什么:

func growslice(et *_type, old slice, cap int) slice {
    if cap < old.cap {
        panic(errorString("growslice: cap out of range"))
    }

    if et.size == 0 {
        // append should not create a slice with nil pointer but non-zero len.
        // We assume that append doesn't need to preserve old.array in this case.
        return slice{unsafe.Pointer(&zerobase), old.len, cap}
    }

    newcap := old.cap
    doublecap := newcap + newcap
    if cap > doublecap {
        newcap = cap
    } else {
        if old.len < 1024 {
            newcap = doublecap
        } else {
            for 0 < newcap && newcap < cap {
                newcap += newcap / 4
            }
            if newcap <= 0 {
                newcap = cap
            }
        }
    }

    var overflow bool
    var lenmem, newlenmem, capmem uintptr
    // Specialize for common values of et.size.
    // For 1 we don't need any division/multiplication.
    // For sys.PtrSize, compiler will optimize division/multiplication into a shift by a constant.
    // For powers of 2, use a variable shift.
    switch {
    case et.size == 1:
        lenmem = uintptr(old.len)
        newlenmem = uintptr(cap)
        capmem = roundupsize(uintptr(newcap))
        overflow = uintptr(newcap) > maxAlloc
        newcap = int(capmem)
    case et.size == sys.PtrSize:
        lenmem = uintptr(old.len) * sys.PtrSize
        newlenmem = uintptr(cap) * sys.PtrSize
        capmem = roundupsize(uintptr(newcap) * sys.PtrSize)
        overflow = uintptr(newcap) > maxAlloc/sys.PtrSize
        newcap = int(capmem / sys.PtrSize)
    case isPowerOfTwo(et.size):
        var shift uintptr
        if sys.PtrSize == 8 {
            // Mask shift for better code generation.
            shift = uintptr(sys.Ctz64(uint64(et.size))) & 63
        } else {
            shift = uintptr(sys.Ctz32(uint32(et.size))) & 31
        }
        lenmem = uintptr(old.len) << shift
        newlenmem = uintptr(cap) << shift
        capmem = roundupsize(uintptr(newcap) << shift)
        overflow = uintptr(newcap) > (maxAlloc >> shift)
        newcap = int(capmem >> shift)
    default:
        lenmem = uintptr(old.len) * et.size
        newlenmem = uintptr(cap) * et.size
        capmem, overflow = math.MulUintptr(et.size, uintptr(newcap))
        capmem = roundupsize(capmem)
        newcap = int(capmem / et.size)
    }

    if overflow || capmem > maxAlloc {
        panic(errorString("growslice: cap out of range"))
    }

    var p unsafe.Pointer
    if et.ptrdata == 0 {
        // 申请内存
        p = mallocgc(capmem, nil, false)
        
        // 清除未使用的地址
        memclrNoHeapPointers(add(p, newlenmem), capmem-newlenmem)
    } else {
        p = mallocgc(capmem, et, true)
        if lenmem > 0 && writeBarrier.enabled {
            bulkBarrierPreWriteSrcOnly(uintptr(p), uintptr(old.array), lenmem)
        }
    }
    // 拷贝大小为 lenmem 个btyes,从old.array到p
    memmove(p, old.array, lenmem)

    return slice{p, old.len, newcap}

具体扩容的策略:

  • 如果要申请的容量(cap)大于 2 倍的原容量(old.cap)或者 原容量 < 1024 ,那么newcap = old.cap + old.cap
  • 否则,计算 newcap += newcap / 4,知道 newcap 不小于要申请的容量,如果溢出,newcap = cap(要申请的容量)

扩容完成后就开始根据 t.size 的大小,重新计算地址,其中新 slice 的 len 为原 slice 的 cap (只有 slice 的 len 超过 cap,才需要扩容)。
接着申请 capmem 大小的内存,从 old.array 拷贝 lenmem 个 bytes (就是原 slice 整个拷贝,lenmem 就是计算的原切片的大小)到 p

a := make([]int, 0)
a = append(a, 1)
println("1 times:", len(a), cap(a))    // 1 times: 1 1

a = append(a, 2, 3)
println("2 times:", len(a), cap(a))    // 2 times: 3 4

a = append(a, 4)
println("3 times:", len(a), cap(a))    // 3 times: 4 4

可以看出:

  1. 如果 append 后的 len 大于 cap 的2倍,即扩大至大于 len 的第一个2的倍数
  2. 如果 append 后的 len 大于 cap 且小于 cap 的两倍,cap扩大至2倍
  3. 如果 append 后的 len 小于 cap,直接追加

Slice污染

使用 slice,也许不知不觉中就会造成一些问题。

a := []int{1, 2, 3, 4, 5}
shadow := a[1:3]
shadow = append(shadow, 100)
fmt.Println(shadow, a)
// [2 3 100] [1 2 3 100 5]

结果很意外,但也是符合逻辑。a 的结构体中 array 是指向数组 [1,2,3,4,5]的内存地址,shadow 是指向其中 [2,3] 的内存地址。在向 shadow 增加后,会直接修改真实的数组,间接影响到指向数组的所有切片。所以可以修改上述代码为:

a := []int{1, 2, 3, 4, 5}
shadow := append([]int{}, a[1:3]...)
shadow = append(shadow, 100)
fmt.Println(shadow, a)
// [2 3 100] [1 2 3 4 5]

如果某个函数的返回值,是上述的这种情况 return a[1:3],还会造成 [1,2,3,4,5] 锁占用的内存无法释放。

黑魔法

知道了 slice 本身是指向真实的数组的指针,在 Golang 中提供了 unsafe 来做指针操作。

a := []int{1, 2, 3, 4, 5}
shadow := a[1:3]
shadowPtr := uintptr(unsafe.Pointer(&shadow[0]))
offset := unsafe.Sizeof(int(0))
fmt.Println(*(*int)(unsafe.Pointer(shadowPtr - offset)))    // 1
fmt.Println(*(*int)(unsafe.Pointer(shadowPtr + 2*offset)))    // 4

shadowPtr 是 a 的第1个下标的位置,一个 int 在64位机器上是8 bytes,向前偏移1个 offset,是 a 的第0个下标 1;向后偏移2个 offset,是 a 的第3个下标 4。

并发安全

slice 是非协程安全的数据类型,如果创建多个 goroutineslice 进行并发读写,会造成丢失。看一段代码

package main

import (
    "fmt"
    "sync"
)

func main () {
    a := make([]int, 0)
    var wg sync.WaitGroup
    for i := 0; i < 10000; i++ {
        wg.Add(1)
        go func(i int) {
            a = append(a, i)
            wg.Done()
        }(i)
    }
    wg.Wait()
    fmt.Println(len(a))
}
// 9403 9876 9985 9491 ...

多次执行,每次得到的结果都不一样,总之一定不会是想要的 10000 个。想要解决这个问题,按照协程安全的编程思想来考虑问题,
可以考虑使用 channel 本身的特性(阻塞)来实现安全的并发读写。

func main() {
    a := make([]int, 0)
    buffer := make(chan int)
    go func() {
        for v := range buffer {
            a = append(a, v)
        }
    }()

    var wg sync.WaitGroup
    for i := 0; i < 10000; i++ {
        wg.Add(1)
        go func(i int) {
            buffer <- i
            wg.Done()
        }(i)
    }
    wg.Wait()
    fmt.Println(len(a))
}
// 10000
查看原文

谢顶道人 收藏了文章 · 2019-08-05

椭圆曲线加密算法原理解析(ECC)

前言

随着计算机性能的提升,市面上的加密技术越来越不安全,1024位的RSA私钥加密已经可以破解,目前有效的手段只是将1024位换成2048位,但随着技术的进步,RSA算法的破解难度会越来越低,因此需要用更安全的加密算法来代替,下面我们来介绍更为安全的ECC公钥加密算法
什么是ECC
ECC是Elliptic Curve Cryptography(椭圆曲线密码学)的缩写,是一种基于椭圆曲线数学的公开密钥加密算法,其本质是利用离散对数问题实现加密。
ECC的主要优势,是在使用更小的密钥的同时,提供更快的性能和更高等级的安全。
什么是椭圆曲线
Wolfram MathWorld(线上数学百科全书,http://mathworld.wolfram.com) 给出了非常精准的定义:
一条椭圆曲线就是一组被 y^2 = x^3 + ax + b 定义的且满足 4a^3 + 27b^2 ≠ 0 的点集。
4a^3 + 27b^2 ≠ 0 这个限定条件是为了保证曲线不包含奇点(在数学中是指曲线上任意一点都存在切线)。
椭圆曲线示例图:

clipboard.png

                   不同的椭圆曲线对应不同的形状(b=1,a从2到-3)

clipboard.png

      左(带锐点):y^2 = x^3
     右(曲线自交):y^2 = x^3 -3x + 2
      都不是有效的椭圆曲线

关于阿贝尔群(abelian group)

阿贝尔群的概念是抽象代数的基本概念之一,是一种代数结构,由一个集合以及一个二元运算所组成。
如果一个集合或者运算是群的话,就必须满足以下条件(+ 表示二元运算):
1、封闭性(closure),如果a和b被包含于群,那么a+b也一定是群的元素;
2、结合律(associativity);
3、存在一个单位元(identity element)0,0与任意元素运算不改变其值的元素,即 a+0 = 0+a = a;
4、每个元素都存在一个逆元(inverse);
5、交换律(commutativity),即 a+b = b+a;

椭圆曲线中的阿贝尔群


我们可以在椭圆曲线上定义一个群:
1、群中的元素就是椭圆曲线上的点;
2、单位元就是无穷处的点0;
3、相反数P,是关于X轴对称的另一边的点;
4、二元运算规则定义如下:取一条直线上的三点(这条直线和椭圆曲线相交的三点),P, Q, R(皆非零),他们的总和等于0,
P+Q+R=0。

clipboard.png
如果P, Q, R在一条直线上的话,他们满足:

                P+(Q+R)=Q+(P+R)=R+(P+Q)=⋯=0。

当P,Q点为同一点时,P=Q,满足:

clipboard.png
这样,我们可以直观的证明:+运算符是符合交换律和结合律的,这是一个阿贝尔群。
因为阿贝尔群满足交换律和结合律,所以点P和点-R的二元运算结果必会在曲线上,即P+P+P的结果必会在曲线上的另一点Q,
以此类推,可以得出得出:

          Q=kP(k个相同的点P进行二元运算(数乘),记做kP)

离散对数问题

前文中有提到离散对数问题,我们熟悉的RSA算法,是基于大数的质因数分解,即对两个质数相乘容易,而将其合数分解很难的这个特点进行加密。
而ECC算法是在有限域Fp定义公式:Q=kP,已知大数k和点P的情况下,很容易求点Q,但是已知的点P、点Q,却很难求得k,这就是经典的离散对数问题,ECC算法正是利用该特点进行加密,点Q为公钥,大数k为私钥,点P为基点,和RSA最大的实际区别,主要是密钥长度

椭圆曲线加密算法原理

描述一条Fp上的椭圆曲线,常用到六个参量:T=(p,a,b,n,x,y)。
(p 、a 、b) 用来确定一条椭圆曲线,p为素数域内点的个数,a和b是其内的两个大数;
x,y为G基点的坐标,也是两个大数;
n为点G基点的阶;
以上六个量就可以描述一条椭圆曲线,有时候我们还会用到h(椭圆曲线上所有点的个数p与n相除的整数部分)。
现在我们描述一个利用椭圆曲线进行加密通信的过程:
1、选定一条椭圆曲线 Ep(a,b) 并取椭圆曲线上一点,作为基点P。
2、选择一个大数k作为私钥,并生成公钥 Q=kP。
3、将 Ep(a,b) 和点Q、P传给用户。
4、用户接到信息后 ,将待传输的明文编码到Ep(a,b)上的一点M,并产生一个随机整数r。
5、公钥加密(密文C是一个点对):
C={rP, M+rQ}
6、私钥解密(M + rQ - k(rP) ,解密结果就是点M),公式如下:

        M + rQ - k(rP) = M + r(kP) - k(rP) = M

7、对点M进行解码就可以得到明文
假设在加密过程中,有一个第三者H,H只能知道椭圆曲线 Ep(a,b)、公钥Q、基点P、密文点C,而通过公钥Q、基点P求私钥k或者通过密文点C、基点P求随机数r都是非常困难的,因此得以保证数据传输的安全。

ECC应用

因为在安全性、加解密性能、网络消耗方面有较大优势,ECC加密算法大有取代RSA成为下一代主流加密算法的趋势。如今ECC应用范围很广,在TLS、区块链(比特币、以太坊等等)、SM2国密算法、证书、银行政府机构等许多方面都有大量应用。

查看原文

谢顶道人 收藏了文章 · 2019-07-21

在 Golang 中使用 Go 关键字和 Channel 实现并行

Go 关键字和 channel 的用法

go 关键字用来创建 goroutine (协程),是实现并发的关键。go 关键字的用法如下:
//go 关键字放在方法调用前新建一个 goroutine 并让他执行方法体
go GetThingDone(param1, param2);

//上例的变种,新建一个匿名方法并执行
go func(param1, param2) {
}(val1, val2)

//直接新建一个 goroutine 并在 goroutine 中执行代码块
go {
    //do someting...
}
因为 goroutine 在多核 cpu 环境下是并行的。如果代码块在多个 goroutine 中执行,我们就实现了代码并行。那么问题来了,怎么拿到并行的结果呢?这就得用 channel 了。
//resultChan 是一个 int 类型的 channel。类似一个信封,里面放的是 int 类型的值。
var resultChan chan int
//将 123 放到这个信封里面,供别人从信封中取用
resultChan <- 123
//从 resultChan 中取值。这个时候 result := 123
result := <- resultChan

使用 go 关键字和 channel 实现非阻塞调用

阻塞的意思是调用方在被调用的代码返回之前必须一直等待,不能处理别的事情。而非阻塞调用则不用等待,调用之后立刻返回。那么返回值如何获取呢?Node.js 使用的是回调的方式,Golang 使用的是 channel。
/**
 * 每次调用方法会新建一个 channel : resultChan,
 * 同时新建一个 goroutine 来发起 http 请求并获取结果。
 * 获取到结果之后 goroutine 会将结果写入到 resultChan。
 */
func UnblockGet(requestUrl string) chan string {
    resultChan := make(chan string)
    go func() {
        request := httplib.Get(requestUrl)
        content, err := request.String()
        if err != nil {
            content = "" + err.Error()
        }
        resultChan <- content
    } ()
    return resultChan
}

由于新建的 goroutine 不会阻塞函数主流程的执行,所以调用 UnblockGet 方法会立刻得到一个 resultChan 返回值。一旦 goroutine 执行完毕拿到结果就会写入到 resultChan 中,这时外部就可以从 resultChan 中获取执行结果。

一个很 low 的并行示例

fmt.Println(time.Now())
resultChan1 := UnblockGet("http://127.0.0.1/test.php?i=1")
resultChan2 := UnblockGet("http://127.0.0.1/test.php?i=2")

fmt.Println(<-resultChan1)
fmt.Println(<-resultChan1)
fmt.Println(time.Now())

上面两个 http 请求是在两个 goroutine 中并行的。总的执行时间小于 两个请求时间和。

这个例子只是为了体现 go 和 channel 的用法,有内存泄漏问题,千万不要在线上这么搞。因为新建的 channel 没有 close。下次写一个更高级一点的。

简单的实现 http multi GET

type RemoteResult struct {
    Url string
    Result string
}

func RemoteGet(requestUrl string, resultChan chan RemoteResult)  {
    request := httplib.NewBeegoRequest(requestUrl, "GET")
    request.SetTimeout(2 * time.Second, 5 * time.Second)
    //request.String()
    content, err := request.String()
    if err != nil {
        content = "" + err.Error()
    }
    resultChan <- RemoteResult{Url:requestUrl, Result:content}
}
func MultiGet(urls []string) []RemoteResult {
    fmt.Println(time.Now())
    resultChan := make(chan RemoteResult, len(urls))
    defer close(resultChan)
    var result []RemoteResult
    //fmt.Println(result)
    for _, url := range urls {
        go RemoteGet(url, resultChan)
    }
    for i:= 0; i < len(urls); i++ {
        res := <-resultChan
        result = append(result, res)
    }
    fmt.Println(time.Now())
    return result
}

func main() {
    urls := []string{
        "http://127.0.0.1/test.php?i=13",
        "http://127.0.0.1/test.php?i=14",
        "http://127.0.0.1/test.php?i=15",
        "http://127.0.0.1/test.php?i=16",
        "http://127.0.0.1/test.php?i=17",
        "http://127.0.0.1/test.php?i=18",
        "http://127.0.0.1/test.php?i=19",
        "http://127.0.0.1/test.php?i=20"    }
    content := MultiGet(urls)
    fmt.Println(content)
}
查看原文

谢顶道人 收藏了文章 · 2019-07-18

给APP接入极光推送 — 后端(PHP)

作者:黄志成(小黄)

作者博客:博客地址

新版本推送的需求变的更加复杂.需要接入一个专业的推送了.之前一直基于APP的IM来实现的.

目前市面上主流的推送平台有 极光、友盟、信鸽等等..

通过调研最后选择了 极光推送平台

什么是推送平台呢?下面通过一张图来举例

image

我们业务服务器请求极光平台提供的接口.请求他们,然后由极光平台将信息推送给我们的用户.

一直提到的推送,可能有些新手朋友还是很懵.什么是推送??

通常HTTP请求中,都是由客户端向服务端请求,然后服务器响应数据.可是有些情况下,需要我们主动向客户端响应数据.

而这个主动响应数据的过程就是推送.

那么是如何实现推送的呢?

简单点说就是建立一条长连接.平常的HTTP都是短连接,响应完数据后就会被关闭.而长连接却一直连接着.既然是一直连接着的,我们就可以找到这条连接,然后主动给他推送消息.

好了.回到正题.下面我们来继续说如何接入极光推送.

先了解几个概念

  • Registration ID
客户端初始化 JPush 成功后,JPush 服务端会分配一个 Registration ID,作为此设备的标识(同一个手机不同 APP 的 Registration ID 是不同的)。开发者可以通过指定具体的 Registration ID 来进行对单一设备的推送。
  • 别名
每个用户只能指定一个别名。 同一个应用程序内,对不同的用户,建议取不同的别名。这样,尽可能根据别名来唯一确定用户。
  • 标签
为安装了应用程序的用户打上标签,其目的主要是方便开发者根据标签,来批量下发 Push 消息。 可为每个用户打多个标签。

咱们在通过一张图来理解之间的关系

image

首先 Registration ID 是唯一的.我们可以给这个Id设置一个别名,也就是一个备注.这个别名可以设置成与我们平台的用户账号相同.这样可以方便我们推送.

当然逐个推送是很麻烦的.我们可以给这些用户打一个 Tag 标签. 比如北京的用户放在北京的Tag标签下.单身的用户放在单身的Tag标签下.我们以Tag来推送,就能达到群发效果.

了解概念后.我们应该清楚这个时候需要把 JPush 注册用户与开发者App 用户绑定起来。

这个绑定有两个基本思路:

  • 把绑定关系保存到 JPush 服务器端
  • 把绑定关系保存到开发者应用服务器中

第一种就是之前说到的别名和标签。由客户端来设置别名.

客户端开发者会调用 setAlias或者setTags API 来设置关系

SDK 把该关系设置保存到 JPush Server 上

在服务器端推送消息时,指定向之前设置过的别名或者标签推送.

第二种就相对麻烦一些.由客户端将Registration ID传递给服务端,然后由服务端来处理对应关系.

我们这里采用的是第一种,通过客户端来设置别名或者标题.

JPush提供四种消息形式:通知,自定义消息,富媒体和本地通知。

这里我们主要介绍通知消息.只要理解了第一种,其他在文档中看看就能很轻松理解.

  • 通知
或者说 Push Notification,即指在手机的通知栏(状态栏)上会显示的一条通知信息。 通知主要用于提示用户的目的,应用于新闻内容、促销活动、产品信息、版本更新提醒、订单状态提醒等多种场景

这里先附上官网文档的地址,下面所说的内容都是基于这个文档的.

文档地址:极光推送

还是先了解几个概念

  • platform:推送平台

JPush 当前支持 Android, iOS, Windows Phone 三个平台的推送。其关键字分别为:"android", "ios", "winphone"。

如果目标平台为 iOS 平台 需要在 options 中通过 apns_production 字段来设定推送环境。True 表示推送生产环境,False 表示要推送开发环境; 如果不指定则为推送生产环境

推送到所有平台:

{ "platform" : "all" }

指定特定推送平台:

{ "platform" : ["android", "ios"] }

通常情况下指定 android 和 ios 就可以了。如果产品有winphone版本的也可以选择推送到所有平台,这里没有什么太多问题.

  • audience:推送目标

推送设备对象,表示一条推送可以被推送到哪些设备列表。确认推送设备对象,JPush 提供了多种方式,比如:别名、标签、注册ID、分群、广播等。

这里具体的参数就请看文档吧.

我们在封装推送方法的使用,需要对Android 和 ios 的配置进行分别设置.下面我会在我封装的方法里进行说明.

首先先去集成sdk.通过Composer就可以了

在项目中的 composer.json 文件中添加 jpush 依赖:

"require": {
    "jpush/jpush": "^3.5"
}

执行 $ php composer.phar install** 或 **$ composer install 进行安装。

也可以直接下载.下载地址去文档中找吧~

composer 安装完后 就可以通过命名空间直接引用了.

下面是我简单封装的一个推送方法.

<?php
/**
 * Created by PhpStorm.
 * User: huangzhicheng
 * Date: 2018/8/28
 * Time: 下午3:12
 */

namespace data\tools;

use data\tools\config\Output;
use JPush\Client as Client;

class JPush
{
    /**
     * 通过别名发送极光推送消息
     * @param $title // 标题
     * @param $content // 内容
     * @param $alias // 别名
     * @param array $params // 扩展字段
     * @param string $ios_badge // ios 角标数
     * @param array $platform // 推送设备
     * @return array|bool
     * @author huangzhicheng 2018年08月29日
     */
    public static function pushMessageByAlias ($title, $content, $alias, $params = [], $ios_badge = '0', $platform = ['ios', 'android'])
    {

        if (!is_array ($alias)) return false;
        $jpush_conf = Output::getJPushKey (); // 获取配置信息 app_key 和 master_secret

        $app_key = $jpush_conf[ 'app_key' ];
        $master_secret = $jpush_conf[ 'master_secret' ];
        try {
            // 初始化
            $client = new Client($app_key, $master_secret);

            $result = $client->push ()
                ->setPlatform ($platform)
                ->addAlias ($alias)
                ->iosNotification (
                    $content, [
                    'sound' => '1',
                    'badge' => (int)$ios_badge,
                    'content-available' => true,
                    'category' => 'jiguang',
                    'extras' => $params,
                ])
                ->androidNotification ($content, [
                    'title' => $title,
                    //'build_id' => 2,
                    'extras' => $params,
                ])
                ->options ([
                    'sendno' => 100,
                    'time_to_live' => 86400,
                    'apns_production' => true, // ios推送证书的选择,True 表示推送生产环境,False 表示要推送开发环境
                    //'big_push_duration' => 10,
                ])
                ->send ();
            return $result;
        } catch (\Exception $e) {
            // 写入错误日志
            // 这里根据自己的业务来定
        }
    }
}

可以根据代码看出来.我使用 iosNotificationandroidNotification 方法进行两个设备的推送设置.

// Ios的通知配置项
->iosNotification (
    $content, // 推送的内容
    [
        'sound' => '1', // 是否有声音
        'badge' => (int)$ios_badge, // 显示的角标数
        'content-available' => true, // 去文档中查看具体用处,一般设置为true或者1
        'category' => 'jiguang', // 这里也去文档中查看吧
        'extras' => $params, // 扩展字段 根据自己业务场景来定.
    ])

安卓的推送也是类似的.更多具体的配置项去文档中查看。

在开发的过程中发现了一个坑.就是我们的业务需求不需要Ios显示角标,根据文档所述,设置badge为0就可以了.但是我们设置后还是没效果.查看网页控制端的API调用记录时,发现传递还是显示角标的参数.

最后跟踪方法,发现是他们SDK的问题.类的路径是 /JPush/PushPayload.php

if (isset($notification['badge']) && (int)$notification['badge']) {
    $ios['badge'] = $notification['badge'];
}

当传递为0的时候就不走这一步,然后默认就是

if (!isset($ios['badge'])) {
    $ios['badge'] = '+1';
}

太坑了,最后修改SDK解决了这个问题.当然在最新版的SDK中官方已经解决了这个问题.我这个包是之前别的项目里的.我直接拿来用的.

最后就调用封装的代码

const PUSH_TYPE = [
        'push_new_info' => '1',
        'push_visitor_alert' => '2'

    ];

const APP_NAME = "****";
    
public static function pushNewInfoNotice ($uids, $title, $url, $txt, $type = '1')
{

    $ext = [
        'push_type' => strval (self::PUSH_TYPE[ 'push_new_info' ]),
        'info_type' => strval ($type),//1-资讯,2-项目
        'title' => empty($title) ? self::APP_NAME : $title,
        'content' => $txt,
        'redirect_url' => $url
    ];

    $res = JPush::pushMessageByAlias ($title, $txt, $uids, $ext);
    return $res;
}

我们只要调用这个方法就能实现推送了.

$ext 就是我们与客户端定义的信息格式.他们会根据push_type来执行不同操作.

最后再补充一点.就是我们开发和生产使用的是一个极光应用.所以不能随便发送广播消息在开发环境中.

那如何区分生产环境和开发环境呢?

下面是官方给出的建议

1.使用相同的 Appkey 和 包名,推送时使用 registrationID、tag、alias 针对性的对测试机进行推送测试

2.如果你一定要测试广播推送,那么在官网新建一个测试应用,Appkey 和包名不一样,专门用作测试

3.如果你还需要包名一样并测试广播推送,那么在官网新建一个极光账号,新建一个测试应用,配相同的包名进行测试。

这篇文章就记录到这,以后有想补充的在更新了.

完成于:2018年09月01日00:22:32

查看原文

谢顶道人 赞了文章 · 2019-07-18

给APP接入极光推送 — 后端(PHP)

作者:黄志成(小黄)

作者博客:博客地址

新版本推送的需求变的更加复杂.需要接入一个专业的推送了.之前一直基于APP的IM来实现的.

目前市面上主流的推送平台有 极光、友盟、信鸽等等..

通过调研最后选择了 极光推送平台

什么是推送平台呢?下面通过一张图来举例

image

我们业务服务器请求极光平台提供的接口.请求他们,然后由极光平台将信息推送给我们的用户.

一直提到的推送,可能有些新手朋友还是很懵.什么是推送??

通常HTTP请求中,都是由客户端向服务端请求,然后服务器响应数据.可是有些情况下,需要我们主动向客户端响应数据.

而这个主动响应数据的过程就是推送.

那么是如何实现推送的呢?

简单点说就是建立一条长连接.平常的HTTP都是短连接,响应完数据后就会被关闭.而长连接却一直连接着.既然是一直连接着的,我们就可以找到这条连接,然后主动给他推送消息.

好了.回到正题.下面我们来继续说如何接入极光推送.

先了解几个概念

  • Registration ID
客户端初始化 JPush 成功后,JPush 服务端会分配一个 Registration ID,作为此设备的标识(同一个手机不同 APP 的 Registration ID 是不同的)。开发者可以通过指定具体的 Registration ID 来进行对单一设备的推送。
  • 别名
每个用户只能指定一个别名。 同一个应用程序内,对不同的用户,建议取不同的别名。这样,尽可能根据别名来唯一确定用户。
  • 标签
为安装了应用程序的用户打上标签,其目的主要是方便开发者根据标签,来批量下发 Push 消息。 可为每个用户打多个标签。

咱们在通过一张图来理解之间的关系

image

首先 Registration ID 是唯一的.我们可以给这个Id设置一个别名,也就是一个备注.这个别名可以设置成与我们平台的用户账号相同.这样可以方便我们推送.

当然逐个推送是很麻烦的.我们可以给这些用户打一个 Tag 标签. 比如北京的用户放在北京的Tag标签下.单身的用户放在单身的Tag标签下.我们以Tag来推送,就能达到群发效果.

了解概念后.我们应该清楚这个时候需要把 JPush 注册用户与开发者App 用户绑定起来。

这个绑定有两个基本思路:

  • 把绑定关系保存到 JPush 服务器端
  • 把绑定关系保存到开发者应用服务器中

第一种就是之前说到的别名和标签。由客户端来设置别名.

客户端开发者会调用 setAlias或者setTags API 来设置关系

SDK 把该关系设置保存到 JPush Server 上

在服务器端推送消息时,指定向之前设置过的别名或者标签推送.

第二种就相对麻烦一些.由客户端将Registration ID传递给服务端,然后由服务端来处理对应关系.

我们这里采用的是第一种,通过客户端来设置别名或者标题.

JPush提供四种消息形式:通知,自定义消息,富媒体和本地通知。

这里我们主要介绍通知消息.只要理解了第一种,其他在文档中看看就能很轻松理解.

  • 通知
或者说 Push Notification,即指在手机的通知栏(状态栏)上会显示的一条通知信息。 通知主要用于提示用户的目的,应用于新闻内容、促销活动、产品信息、版本更新提醒、订单状态提醒等多种场景

这里先附上官网文档的地址,下面所说的内容都是基于这个文档的.

文档地址:极光推送

还是先了解几个概念

  • platform:推送平台

JPush 当前支持 Android, iOS, Windows Phone 三个平台的推送。其关键字分别为:"android", "ios", "winphone"。

如果目标平台为 iOS 平台 需要在 options 中通过 apns_production 字段来设定推送环境。True 表示推送生产环境,False 表示要推送开发环境; 如果不指定则为推送生产环境

推送到所有平台:

{ "platform" : "all" }

指定特定推送平台:

{ "platform" : ["android", "ios"] }

通常情况下指定 android 和 ios 就可以了。如果产品有winphone版本的也可以选择推送到所有平台,这里没有什么太多问题.

  • audience:推送目标

推送设备对象,表示一条推送可以被推送到哪些设备列表。确认推送设备对象,JPush 提供了多种方式,比如:别名、标签、注册ID、分群、广播等。

这里具体的参数就请看文档吧.

我们在封装推送方法的使用,需要对Android 和 ios 的配置进行分别设置.下面我会在我封装的方法里进行说明.

首先先去集成sdk.通过Composer就可以了

在项目中的 composer.json 文件中添加 jpush 依赖:

"require": {
    "jpush/jpush": "^3.5"
}

执行 $ php composer.phar install** 或 **$ composer install 进行安装。

也可以直接下载.下载地址去文档中找吧~

composer 安装完后 就可以通过命名空间直接引用了.

下面是我简单封装的一个推送方法.

<?php
/**
 * Created by PhpStorm.
 * User: huangzhicheng
 * Date: 2018/8/28
 * Time: 下午3:12
 */

namespace data\tools;

use data\tools\config\Output;
use JPush\Client as Client;

class JPush
{
    /**
     * 通过别名发送极光推送消息
     * @param $title // 标题
     * @param $content // 内容
     * @param $alias // 别名
     * @param array $params // 扩展字段
     * @param string $ios_badge // ios 角标数
     * @param array $platform // 推送设备
     * @return array|bool
     * @author huangzhicheng 2018年08月29日
     */
    public static function pushMessageByAlias ($title, $content, $alias, $params = [], $ios_badge = '0', $platform = ['ios', 'android'])
    {

        if (!is_array ($alias)) return false;
        $jpush_conf = Output::getJPushKey (); // 获取配置信息 app_key 和 master_secret

        $app_key = $jpush_conf[ 'app_key' ];
        $master_secret = $jpush_conf[ 'master_secret' ];
        try {
            // 初始化
            $client = new Client($app_key, $master_secret);

            $result = $client->push ()
                ->setPlatform ($platform)
                ->addAlias ($alias)
                ->iosNotification (
                    $content, [
                    'sound' => '1',
                    'badge' => (int)$ios_badge,
                    'content-available' => true,
                    'category' => 'jiguang',
                    'extras' => $params,
                ])
                ->androidNotification ($content, [
                    'title' => $title,
                    //'build_id' => 2,
                    'extras' => $params,
                ])
                ->options ([
                    'sendno' => 100,
                    'time_to_live' => 86400,
                    'apns_production' => true, // ios推送证书的选择,True 表示推送生产环境,False 表示要推送开发环境
                    //'big_push_duration' => 10,
                ])
                ->send ();
            return $result;
        } catch (\Exception $e) {
            // 写入错误日志
            // 这里根据自己的业务来定
        }
    }
}

可以根据代码看出来.我使用 iosNotificationandroidNotification 方法进行两个设备的推送设置.

// Ios的通知配置项
->iosNotification (
    $content, // 推送的内容
    [
        'sound' => '1', // 是否有声音
        'badge' => (int)$ios_badge, // 显示的角标数
        'content-available' => true, // 去文档中查看具体用处,一般设置为true或者1
        'category' => 'jiguang', // 这里也去文档中查看吧
        'extras' => $params, // 扩展字段 根据自己业务场景来定.
    ])

安卓的推送也是类似的.更多具体的配置项去文档中查看。

在开发的过程中发现了一个坑.就是我们的业务需求不需要Ios显示角标,根据文档所述,设置badge为0就可以了.但是我们设置后还是没效果.查看网页控制端的API调用记录时,发现传递还是显示角标的参数.

最后跟踪方法,发现是他们SDK的问题.类的路径是 /JPush/PushPayload.php

if (isset($notification['badge']) && (int)$notification['badge']) {
    $ios['badge'] = $notification['badge'];
}

当传递为0的时候就不走这一步,然后默认就是

if (!isset($ios['badge'])) {
    $ios['badge'] = '+1';
}

太坑了,最后修改SDK解决了这个问题.当然在最新版的SDK中官方已经解决了这个问题.我这个包是之前别的项目里的.我直接拿来用的.

最后就调用封装的代码

const PUSH_TYPE = [
        'push_new_info' => '1',
        'push_visitor_alert' => '2'

    ];

const APP_NAME = "****";
    
public static function pushNewInfoNotice ($uids, $title, $url, $txt, $type = '1')
{

    $ext = [
        'push_type' => strval (self::PUSH_TYPE[ 'push_new_info' ]),
        'info_type' => strval ($type),//1-资讯,2-项目
        'title' => empty($title) ? self::APP_NAME : $title,
        'content' => $txt,
        'redirect_url' => $url
    ];

    $res = JPush::pushMessageByAlias ($title, $txt, $uids, $ext);
    return $res;
}

我们只要调用这个方法就能实现推送了.

$ext 就是我们与客户端定义的信息格式.他们会根据push_type来执行不同操作.

最后再补充一点.就是我们开发和生产使用的是一个极光应用.所以不能随便发送广播消息在开发环境中.

那如何区分生产环境和开发环境呢?

下面是官方给出的建议

1.使用相同的 Appkey 和 包名,推送时使用 registrationID、tag、alias 针对性的对测试机进行推送测试

2.如果你一定要测试广播推送,那么在官网新建一个测试应用,Appkey 和包名不一样,专门用作测试

3.如果你还需要包名一样并测试广播推送,那么在官网新建一个极光账号,新建一个测试应用,配相同的包名进行测试。

这篇文章就记录到这,以后有想补充的在更新了.

完成于:2018年09月01日00:22:32

查看原文

赞 7 收藏 5 评论 0

谢顶道人 发布了文章 · 2019-06-18

永强持续教你加解密:对称篇(二)

永强被吓坏了!因为永强看到了某个微信群有人指出公众号里上篇打酱油附送的那篇文章《震惊!北京一男子竟然用swoole做了这种事!》的内容实在是太low了,这种low文章就不要拿出来发了。

但是给永强留下了面积巨大的心理阴影。他尚未见识过社交网络的恶毒嘴脸。

所以永强本来昨天要发的文章拖到了今天,但是永强实在是怕了,他怕被人喷了被人骂了。虽然我百般鼓励,但他还是心有余悸。尽管我都已经直接告诉他“你那玩意根本就没人看”了,他还是依然不敢发了。然后我不得不摆出PS大法给他做了一张图,他看了看那张图后又收了我6.66元的微信红包,决定继续鼓起勇气发了。

是时候表现一下我的PS精湛技术了!

图片描述

“我们历经千辛万苦,摸打滚爬过数不清的错误,发射了不知道多少枚长征系列,耗费了一代航天人的心血,终于看到了地球与月亮通信的曙光,然后就在五分钟,我们惊讶地发现,原来老王的smartmesh技术早就实现了,甚至连地球文明与外形文明的通信都给出了完美的解决方案…” ——— 尼古拉斯 * 赵永强

众所周知,作为精通各种技术表演的我早就已经不屑于采用ppt的方式吹牛了,一般我都是直接上机操作表演,当然了,程序都是提前写好了的,全是mock的假数据,脚本实现自动化,无论谁来操作都是流畅的,一切都是完美的!

作为一个追求完美的人,我还得继续接着吹上次聊到结尾,好像是遗留了两个问题:

  • ecb、cfb、cbc等这些后缀是什么意思
  • iv向量又是什么意思

鉴于DES和3DES已经属于不建议使用的方法了,所以这次我们直接用AES加密进行装逼表演,比如下面这坨代码,你们复制粘贴走运行一下:

<?php
$ava_methods = openssl_get_cipher_methods();
// 选用aes-128-ecb
$my_method   = 'aes-128-ecb';
if ( !in_array( $my_method, $ava_methods ) ) { 
  exit( '错误的加密方法'.PHP_EOL );
}
// 加密用的密码
$key  = "1234567812345678";
// 加密的内容
$data = "12345678abcdxxoo12345678abcdxxoo";
$enc_data = openssl_encrypt( $data, $my_method, $key, OPENSSL_RAW_DATA );
$hex = bin2hex( $enc_data );
echo $hex.' : '.strlen( $hex ).PHP_EOL;

我这里运行结果是:

c1391e34caf38f8c2a477cbda3772533c1391e34caf38f8c2a477cbda3772533d96aa42b59151a9e9b5925fc9d95adaf : 96

分析一下上面代码:这次我们选用的加密方法是AES-128-ECB,这个128是什么意思?128就是密钥长度的意思:128bit;如果你留心的话,还会注意到有aes-192-ecb和aes-256-ecb,其实就是指加密密钥长度为192bit、256bit,然后是值得注意的一个地方是:

$enc_data = openssl_encrypt( $data, $my_method, $key, OPENSSL_RAW_DATA );
最后一个参数是OPENSSL_RAW_DATA,如果选用这个option的话,经过加密后的数据会是奇怪的二进制数据,无法直接通过文本方式查看,所以要看的话必须先使用bin2hex函数处理一下。

注意了哈,我选的这个密钥1234567812345678是有特殊用意的,这个密钥的长度是16字节也就是128bit,而我们选用的aes加密方法中要求的密钥长度就是128bit,那么我们尝试将密钥增加几位变成:1234567812345678abc,然后其他代码不做任何改动,再次执行加密,结果如下:

c1391e34caf38f8c2a477cbda3772533c1391e34caf38f8c2a477cbda3772533d96aa42b59151a9e9b5925fc9d95adaf : 96

就是说用“1234567812345678”和“1234567812345678abc”加密后的数据都是一样的。看起来如果我们选用128bit密钥长度的话,一旦密钥长度超过128bit后面多余的部分会被直接无视掉~~~

然后我们再尝试将密钥“1234567812345678”缩短一个字节,改成“123456781234567”,其他地方代码不做任何改动,运行一波儿,结果如下:

c202e5b1dc36c3147e50d02df7ab700cc202e5b1dc36c3147e50d02df7ab700cda89b056d926d3fea2e59ffc552b1d98 : 96

这次不行了,已经不一样了~

然后,我们将注意力放到明文和密文上来:

明文:12345678abcdxxoo12345678abcdxxoo
密文:c1391e34caf38f8c2a477cbda3772533c1391e34caf38f8c2a477cbda3772533d96aa42b59151a9e9b5925fc9d95adaf

仔细观察有一个比较屌的地方,我们将密文每隔32个字符长度就分割一下,你们感受一下:

c1391e34caf38f8c2a477cbda3772533
c1391e34caf38f8c2a477cbda3772533
d96aa42b59151a9e9b5925fc9d95adaf

卧槽,竟然有前两段是一样的!???卧槽。。。 。。。

图片描述

仔细看了一把明文12345678abcdxxoo12345678abcdxxoo,分析一下,卧槽!:

12345678abcdxxoo
12345678abcdxxoo

难道说明文“12345678abcdxxoo”被密钥“1234567812345678”加密后后的密文就是“c1391e34caf38f8c2a477cbda3772533”?

时机已然成熟了!是时候继续深入装逼了!为什么会出现这个结果?现在我们开始说“ecb、cfb、cbc等这些后缀是什么意思”。

你若有所思的猜测到:“难道说对称加密的时候,都是将明文先分块,然后再分别对分块加密?”,我欣慰地看着你说:“嗯,是的,肯定是,不然老子往下没法写了,我特么都快编不下去了…”

  • DES和3DES会将明文以64bit(8字节)作为一个单元进行分组;
  • AES则会将明文以128bit(16字节)作为一个单元进行分组;
  • 无论是AES还是DES,当最后一个分组的数据长度不满足分组标准长度的时候,会用某种填充方式进行填充;
  • AES对一个16字节分组加密完毕后,分组大小依然为16字节;

比如说这段明文“12345678abcdxxoo12345678abcdxxoo”,一共是32字节,理论上说就会被先按照16字节分组:“12345678abcdxxoo”是一组,剩下的“12345678abcdxxoo”是另外一组,我们用程序验证一下:

<?php
$ava_methods = openssl_get_cipher_methods();
$my_method   = 'aes-128-ecb';
if ( !in_array( $my_method, $ava_methods ) ) { 
  exit( '错误的加密方法'.PHP_EOL );
}
$key  = "123456781234567";
// 注意!这段明文长度刚好为32字节!
$data = "12345678abcdxxoo12345678abcdxxoo";
echo '明文长度:'.strlen( $data ).PHP_EOL;
$enc_data = openssl_encrypt( $data, $my_method, $key, OPENSSL_RAW_DATA );
echo '密文长度:'.strlen( $enc_data ).PHP_EOL;

注意明文长度我选择刚好为32字节。保存运行一下,至于你们那里是什么结果我不知道,反正我这里是这样的:

图片描述

我日,感觉被打脸了,为毛加密后多出了16字节?

我们将明文从32字节的“12345678abcdxxoo12345678abcdxxoo”修改成33字节的“12345678abcdxxoo12345678abcdxxooa”,这样的话,明文会被分成三个16字节的分组,由于最后一个分组只有一个字节,所以剩余15字节会被填充:

图片描述

似乎印证了我们一个猜测:当最后一个明文分组小于要求分组标准大小时,不会产生新的分组;当最后一个明文分组大于等于要求分组标准大小时,会产生一个新的分组。

我不想填充怎么办?修改一下加密和解密函数的最后那个OPENSSL_NO_PADDING选项即可,你们感受一下:

<?php
$ava_methods = openssl_get_cipher_methods();
// 选用aes-128-ecb
$my_method   = 'aes-128-ecb';
if ( !in_array( $my_method, $ava_methods ) ) {
  exit( '错误的加密方法'.PHP_EOL );
}
// 加密用的密码
$key  = "1234567812345678";
// 加密的内容
$data = "12345678abcdxxoo12345678abcdxxoo";
echo '明文:'.$data.',长度为'.strlen( $data ).PHP_EOL;
$enc_data = openssl_encrypt( $data, $my_method, $key, OPENSSL_NO_PADDING );
echo '密文:'.$enc_data.',长度为'.strlen( $enc_data ).PHP_EOL;
$hex = bin2hex( $enc_data );
echo "密文十六进制:".$hex.',长度为'.strlen( $hex ).PHP_EOL;
$dec_data = openssl_decrypt( $data, $my_method, $key, OPENSSL_NO_PADDING );
$dec_data = openssl_decrypt( $enc_data, 'aes-128-ecb', $key, OPENSSL_NO_PADDING );
echo "解密:".$dec_data.PHP_EOL;

上面代码运行一下,结果如下图:

图片描述

有时候一些同学在做跨语言加解密的时候,基本上都是栽在了填充上。具体表现就是PHP加密后让Java解密,然后发现解密失败;或者Java加密PHP解密结果也是挂了。这个时候首先检查一下PADDING这里,基本上都是这样的问题。

那么说了这么多,总结一下:

AES和DES以及3DES这种加密方式被称为分组密码,分组密码每次只能加密固定长度的明文,所以如果明文很长的话,就需要轮流为每个分组明文进行加密,AES的分组长度是128bit,而DES的分组长度为64bit;如果一旦需要对多个分组进行轮流加密,加入明文被分成了三个明文分组,那么就需要对三个明文进行迭代加密(粗暴理解就是轮流加密),然而会有很多种不同的迭代方式,这种不同的迭代方式专业名词就叫“模式”,这些模式有:ECB、CBC、OFB、CFB、CTR… …

PS:⚠️对明文进行分组的方式是固定的,唯一不同的就是分组长度不一样而已;模式是指对多个明文从第一个开始轮流加密到最后一个的这个过程,是怎么轮流执行的。

图片描述

查看原文

赞 7 收藏 4 评论 1

谢顶道人 发布了文章 · 2019-06-12

永强教你加解密:对称篇(一)

大家好,我是永强,就是老李之前经常给你们说的区块链大神、大学肄业却依然大公司iOS主程一波儿流、只生活在老李口中尚未真实露面的骗钱高手、老王的左膀右臂 ——— 赵永强。我和尼古拉斯赵四之间并没有什么强关联,我只是单方面认识他而已。

之前老李企图让我发表一些关于如何进行高端骗工资骗钱的教程,被我义正言辞地拒绝了:

                   毕竟是毕生绝学,不能就这么轻而易举地教给你们

不过后面有时间我可以给大家出一些关于“如何在公司混日子还能升职加薪”的入门级教程,传男不传女,独家绝技!敬请期待!

图片描述

言归正传,那个一直以来我对加解密技术都是耿耿于怀的,因为很多年前有一次面试中被这东西给坑掉了,虽然我后来就自己对加解密中一些自己不懂的地方请教了对方而且他也没有给我讲清楚…

事情都过去好几年了,本人自我感觉已经一定程度掌握了一些关于加密的高端技术,因为决定出来装一波儿逼,时间比较紧张,我打算赶在2020年农历新年之前把逼装完,你们要注意配合。

我知道老李之前在社区发表了一些关于加密啊、解密啊之类的东西,甚至还扯上了什么DH什么ECDH,又是质数又是椭圆曲线,不过这并不与本系列产生冲突,这并不重要,不要在意这些细节,他那个too young too simple,sometimes naive…

这将是一个大概由四篇左右的文章组成的系列文章,所以在正式开始之前,我不得不强调一点 ——— 如下这几门学科的基础理论知识:

  • 《离散数学》
  • 《微分与积分》
  • 《空间几何》
  • 《概率论》

你并不需要具备。。。

不过你总得知道除了html和css之外的任意一门编程语言。尽管本人精通上到CLanguage下到Perl之类的各种语言,但是本文将采用世界上最好的语言进行一些程序演示,后面老李可能会使用CLanguage和Golang进行其他语言的演示补充。

简单说来呢,加解密技术就是分为两大类:

  • 对称加解密
  • 非对称加解密

其中,常见的对称加解密算法有DES、3DES、AES;而非对称加解密技术比较典型的则是RSA,就是什么公钥私钥证书什么乱七八糟的。

我们先从对称加解密开始,粗暴地说呢,对称加解密就是“加密和解密的时候用同一个密码”,听起来就非常对称,有没有?

用图表达一下就是:

图片描述

最一开始的时候,我朝人民一般都是倾向于使用“天王盖地虎”,“宝塔镇河妖”这种加解密技术;然而,美帝用了一种叫做DES的技术进行对称加解密,这玩意一度成为业界通用的对称加解密技术,银行、五角大楼都爱用这玩意,可惜好景不长、世风日下、世态炎凉,这玩意的破解成本越来越低越来越低~~ 于是,为了续命,就又有一些白胡子老头给DES打补丁,缝缝补补搞出来一个玩意叫做3DES,继续用,又不是不能用…这个顾名思义就行了,别打我,真的:3DES就是用DES处理(注意是处理,我没说是加密)了三次的意思。就目前看来,3DES实际上用的可能也并不是十分广泛了,所以如果大家在选择对称加解密技术的时候,尽量避开DES和3DES就可以了。

呵呵,喜新厌旧的沙雕人类…虽然DES已经没人用了,但毕竟也是辉煌过,我觉得还是得动手表演一波儿。我们知道,在php7里,原来的mcrypt系列加解密已经被放弃掉了,官方建议我们使用openssl系列来进行加解密,所以确保你的PHP环境里安装了openssl标准扩展。

<?php
// 这个函数打印出来openssl支持的所有加密方法以及模式的组合
$arr_ava_methods = openssl_get_cipher_methods();
print_r( $arr_ava_methods );

文件保存成test.php后,执行一把:php test.php | grep des,结果你们感受一下:

图片描述

其中带有ede的,比如des-ede*这样的就表示是3DES。还有这么多奇奇怪怪的后缀是什么含义?回头再说…又不是不能用。

图片描述

筛选一下,我们看des(非des3)有几种带着尾巴的具体方法:

  • des-cbc
  • des-cfb*(注意后面的通配符星号)
  • des-ecb
  • des-ofb

我们先用传统des方法继续进行装逼表演:

<?php
// 我们就选用des-ecb方法进行一次des加密
$ava_methods = openssl_get_cipher_methods();
$my_method   = 'des-ecb';
if ( !in_array( $my_method, $ava_methods ) ) {
  exit( '错误的加密方法'.PHP_EOL );
}
$key  = "123456";
$data = "helloMOTO";
echo "明文:".$data.PHP_EOL;
$enc_data  = openssl_encrypt( $data, $my_method, $key );
echo "密文:".$enc_data.PHP_EOL;
$dec_data  = openssl_decrypt( $enc_data, $my_method, $key );
echo "明文:".$dec_data.PHP_EOL;

保存为test.php执行一把:

图片描述

完美!就像老王的meshbox一样,完美!

简单解析一下:

换个方法继续一下:我们使用openssl_get_cipher_methods()函数获取到可以使用的所有des加密方法,然后简单判断一下我们选用的方法是否在其中;紧接着我们用123456作为密码,helloMOTO作为明文内容,openssl_encrypt()就是加密函数,openssl_decrypt()就是解密函数,具体的函数原型出门左拐查手册,总之一切都是这么的完美!

<?php
// 我们就选用des-ecb方法进行一次des加密
$ava_methods = openssl_get_cipher_methods();
$my_method   = 'des-cbc';
if ( !in_array( $my_method, $ava_methods ) ) {
  exit( '错误的加密方法'.PHP_EOL );
}
$key  = "123456";
$data = "helloMOTO";
echo "明文:".$data.PHP_EOL;
$enc_data  = openssl_encrypt( $data, $my_method, $key );
echo "密文:".$enc_data.PHP_EOL;
$dec_data  = openssl_decrypt( $enc_data, $my_method, $key );
echo "明文:".$dec_data.PHP_EOL;

执行一波儿,结果如下图:

图片描述

并不完美,报错了,一个warning级的错误,虽然并不影响加密和解密,但是毕竟是报错了,错误原文我复制粘贴过来,你们感受下:

PHP Warning: openssl_encrypt(): Using an empty Initialization Vector (iv) is potentially insecure and not recommended in /home/ubuntu/lab/test.php on line 10

大概意思就是:用了一个并不推荐而且不安全的空iv在test.php的第十行。我正在翻译的这句的时候,已经精通英语的老李在旁边跟我说“你这翻译也太硬了,要学会人性化,看好了,一看你这就是没上过全日制大学本科的恶果”:

PHP警告:openssl_encrypt():iv向量最好别是空的,不推荐这么用,而且这样并不安全~

什么是iv向量?先抛开这个问题,我先写一段代码,让他能跑起来:

<?php
$ava_methods = openssl_get_cipher_methods();
$my_method   = 'des-cbc';
if ( !in_array( $my_method, $ava_methods ) ) {
  exit( '错误的加密方法'.PHP_EOL );
}
// 处理iv向量的两行代码
$iv_length = openssl_cipher_iv_length( $my_method );
$iv        = openssl_random_pseudo_bytes( $iv_length );
$key  = "123456";
$data = "helloMOTO";
echo "明文:".$data.PHP_EOL;
$enc_data  = openssl_encrypt( $data, $my_method, $key, 0, $iv );
echo "密文:".$enc_data.PHP_EOL;
$dec_data  = openssl_decrypt( $enc_data, $my_method, $key, 0, $iv );
echo "明文:".$dec_data.PHP_EOL;

注意到8、9、10和15、17两行,均为iv向量做了改动,然后这次代码保存了运行一波儿:

图片描述

完美!就像老王的meshbox一样,完美!

那么,在des加解密中,我们遗留了两个问题:

  • des-cbcdes-cfb等这些后缀是什么意思?
  • iv向量是什么鬼东西?

长按或许有惊喜

图片描述

查看原文

赞 24 收藏 13 评论 3

认证与成就

  • 获得 994 次点赞
  • 获得 13 枚徽章 获得 0 枚金徽章, 获得 4 枚银徽章, 获得 9 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

  • Tiginx

    山寨Nginx项目,学习C语言和Unix环境编程的一个成果验收项目,了解下?

  • TiRPC

    基于JSON文本协议的swoole RPC服务器

注册于 2014-12-11
个人主页被 6.5k 人浏览