如何写出让人一看就懂的邮件—剧情化

前文提到我们通过区分及物动词不及物动词、使用“串联”和“并联”梳理时间轴与使用”governing“制作思维抽屉等方法使得我们的邮件更有条理。今天我们再来介绍三种方法让我们的邮件具有剧情化。

剧情化让我们写的内容生动形象,能够吸引读者眼球。

制作勾起对方兴趣的诱饵,学会使用”引导文“吸引对方

开头表达什么内容需要根据读者的心理及所处环境进行判断。下面列举一些常见的技巧。
– 如果对方期待的是”回答“,以开头引导文就问题进行回答形式,更能让对方对你的答案产生兴趣,如:面试官询问你是否有过XX经验,你觉得机会来了,大讲特讲你在XX经验相关内容,面试官其实只想知道你的答案”Yes”或”No”,脾气好的面试官可能会对谈话内容进行引导,脾气不好的可能就会被出现粗暴打断的尴尬场景。
– 迎合对方的”期待“。此时就需要讲对方爱听的了。
– ”结论第一“不是万能的。之前讨论过,我们需要把我们描述的内容进行总结概括,但也有些例外场景:
– 如果对方不了解事情的来龙去脉,那么我们就不应该把结论放在前面,需要让其了解事情的背景。
– 如果你处理”说话人主场“(不会被不耐烦的打断)情况,也可进行解释说明。

通过”拆分MECE”(Mutually Exclusive Collectively Exhaustive)来制造高潮

通过”拆分MECE”使得我们说明的内容不重复、不遗漏,但也不必执着于MECE,应该着手想要表达重要的信息,确认是否信息是否有必要传达。另外尽量使用”相对MECE“,如:统计一周写代码时间,正常是周一至周五上班,那么周未即可不必列举并统计。

使用专有名词引起共鸣

  • 我们尽量使用专有名词能让案例更生动。
  • 使用专有名词将听重变成”主人公“。
  • 写出”对症下药“的内容。

如何写出让人一看就懂的邮件

我们每天要处理大量的电子邮件,它是每个人工作不可或缺的工具。在快节奏的工作里,大家都不愿意浪费时间与精力,所以我们的目的是为了让对方立刻理解我们表达的内容。我们经常遇见无法抓住重点、难以理解的邮件。反之当我们发送通知、汇报工作、沟通项目时,有没有想过你表达的信息是否被阅读人有效吸收?
我们只需要在我们的邮件里适当使用项目符号(•Bullet)来描述想要表达的内容,既可化繁为简,又可增强想表达的内容。

逻辑性

使用项目符号需要注意的是我们不应只是罗列内容,而是让每个排列都有意义,将想要传递的内容作为主干,补充内容做为分支。下面我们就来看一下如何通过三个方法来让我们的内容有层次感。

分组

我们要做的将相近的内容整理在一起,可以是主次分组,将主干(主要)内容和补充(次要)内容总结到一个大分组里;也可分开不及物动词与及物动词,将描述对象的状态或现象(静)的内容与行为(动)内容分离开来。因为两者接收的印象不一样。让人轻松理解你想表达的内容。

主次分组很简单,不必赘述。为了排除语意上的歧义,我们在陈述时应该区分开不及物动词与及物动词:
– 如果想表达某个瞬间静止画面,“那个时刻状态”那么每个句子都应该使用不及物动词。使用不及物动词来表达状态也能够让动作发生的因果关系变得模糊,从而模糊责任。
– 杯子坏了。
– 一只笔在这里。
– 如果想表达某个瞬间动态画面,“某人对某事产生了影响的行为”那么每个句子都应该使用及物动词来表达。如果想表达行为及背后的原因就用及物动词。
– 我摔坏了杯子。
– 我放了一只笔在这里。

用“串联”与“并联”来梳理时间轴

大家都学过电路的串联与并联,文字的串并联有什么区别呢?其实主要是在时间轴运用不同,即有无时间流动。
– “串联”,有时间流动。
– “并联”,无时间流动。

使用总述点明主旨

大家在增强逻辑的同时不妨在开头就将问题的数量或对策数量写出来,这样可以在读者头脑中创造存放下文表达内容的内容抽屉,理解重点。最经典的就是乔布斯的“Today I want to tell you three stories from my life”与”One more thing”。

iOS 11 electra越狱安装Clutch

需要工具

越狱

连接手机,使用Cydia Impactor 安装electra,此时需要输入Apple ID 帐户用户名与密码,在手机上启动electra点击越狱。

安装Clutch

下载Clutch,配置环境

killall Xcode
cp /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/SDKSettings.plist ~/
/usr/libexec/PlistBuddy -c "Set :DefaultProperties:CODE_SIGNING_REQUIRED NO" /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/SDKSettings.plist
/usr/libexec/PlistBuddy -c "Set :DefaultProperties:AD_HOC_CODE_SIGNING_ALLOWED YES" /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/SDKSettings.plist

编译,不带签名

xcodebuild -arch arm64 clean build CODE_SIGN_IDENTITY=""

传到设备

scp build/Clutch root@192.168.1.142:

增加Clutch.entitlements文件

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>platform-application</key>
    <true/>
    <key>get-task-allow</key>
    <true/>
    <key>task_for_pid-allow</key>
    <true/>
    <key>com.apple.backboardd.debugapplications</key>
    <true/>
    <key>com.apple.springboard.debugapplications</key>
    <true/>
    <key>run-unsigned-code</key>
    <true/>
    <key>com.apple.private.librarian.can-get-application-info</key>
    <true/>
    <key>com.apple.private.mobileinstall.allowedSPI</key>
    <array>
        <string>Lookup</string>
        <string>CopyInstalledAppsForLaunchServices</string>
    </array>
</dict>
</plist>

签名

jtool --sign Clutch --ent Clutch.entitlements --inplace

确认,放入bin目录

jtool --ent Clutch
jtool --sig Clutch
mv Clutch /bootstrap/usr/local/bin/Clutch

centos openvpn实现共享上网

问题描述

在同一局域网内有三个机器,只有一台能上网,没有双网卡,试过使用socks5进行代理,可是没能成功,最后尝试openvpn方案,下面介绍下openvpn配置,记录备忘。

##安装服务端

直接使用yum install openvpn -y 进行安装。

生成证书

openvpn需要使用证书进行校验,客户端与服务端都需要生成对应的证书,在网上找了个脚本Easy-RSA 2.2.2(没有使用最新的版本)。

解压后使用source ./vars导入相关环境变量。

生成ca证书:

./build-ca

生成服务端证书:

# server 为name
./build-key-server server 

生成客户端证书:

# client7 为name
./build-key client7 

生成生成迪菲·赫尔曼交换密钥,openvpn有用到:

./build-dh

服务端配置

使用vim server.conf修改配置文件:

# 端口
port 1194 
# 证书正常名称即可

# 设置服务器为代理路由器,否则客户端流量不走服务器,这点至关重要
push "redirect-gateway def1 bypass-dhcp" 
#设置客户端DNS,否则客户端Ping不通域名
;push "route 192.168.10.0 255.255.255.0" 
;push "route 192.168.20.0 255.255.255.0"

使用vi /etc/sysctl.conf开启路由转发:

net.ipv4.ip_forward = 1

重启内核使其生效:sysctl -p

启动

openvpn server.conf

客户端安装

由于客户端不能上网,所以下载好安装包,传过去:

scp rpm/*.rpm root@192.168.1.7:

进行安装:

rpm -ivh pkcs11-helper-1.11-3.el7.x86_64.rpm;
rpm -ivh lz4-r131-1.el7.x86_64.rpm;
rpm -ivh openvpn-2.4.4-1.el7.x86_64.rpm;

使用vim client.conf修改客户端配置文件 :

# 配置服务端ip与port
remote 192.168.1.8 1194 
# 这句注释掉,不需要tls
tls-auth ta.key 1 
# 修改成自己的
cert client7.crt 
key client7.key 

把证书传过去:

scp ca.crt client7.crt client7.key root@192.168.1.7:/etc/openvpn/client/

启动:

openvpn client.conf

软件开发原则-控制代码行

原则

编写代码单元应该少于15行,方便阅读、理解、测试、重构,超过15行应提取拆分,直至少于15行。

应用

下面我们通过例子来应用此原则。原代码如下:

func initEngine() *gin.Engine {
    router := gin.Default()
    m := melody.New()
    m.Config.MaxMessageSize = 10024
    m.Config.MessageBufferSize = 5120
    Rooms = make(map[string]RoomInfo)
    router.GET("/v", func(c *gin.Context) {
        m.HandleRequest(c.Writer, c.Request)
    })
    m.HandleConnect(func(session *melody.Session) {
        log.Info("[ws] connect:", session)
    })

    m.HandleDisconnect(func(session *melody.Session) {
        log.Info("[ws] disconnect", session)
    })

    m.HandleMessage(func(session *melody.Session, msg []byte) {
        reg, exists := session.Get("reg")
        log.Info("[ws] message:", string(msg), "reg:", reg)
        if string(msg) == WebSocketClientBye {
            if exists {
                sp := (reg).(*WSMessage)
                if removeRoom(sp.RoomID, sp.ClientID) != "" {
                    log.Warnf("Socket removeRoom error:%v", reg)
                }
            }
            log.Info("Socket bye", reg)
            return
        }
        message := &WSMessage{}
        err := json.Unmarshal(msg, message)
        if err != nil {
            log.Error("Json error :", err)
            return
        }
        switch message.CMD {
        case "register":
            log.Debug("register")
            session.Set("reg", message)
            return
        default:
            log.Debug("socket default json:", message)
            // check
            p := (reg).(*WSMessage)
            roomID := p.RoomID
            my := p.ClientID
            m.BroadcastFilter(msg, func(s *melody.Session) bool {
                if reg, exists = s.Get("reg"); exists {
                    sp := (reg).(*WSMessage)
                    if r, ok := Rooms[roomID]; ok {
                        for _, c := range r.Clients {
                            if (sp.ClientID == c.ClientID) && (c.ClientID != my) {
                                log.Info("[ws] <----sent---->", c, my, string(msg))
                                return true
                            }
                        }
                    } else {
                        log.Warnf("[ws] only myself???? roomID:%d,my:%v,msg:%v", roomID, my, string(msg))
                    }
                }
                return false
            })
        }

    })
    return router
}

这是一个初始化并实现WebSocketWeb功能的服务器,省略部分代码,并删除了一些调试信息。注释少的可怜,如果初次到这样的代码一定会觉得头大。在重构过程中发现使用此原则会让代码结构清晰,这也是我们重构的目的。下面简单介绍使用的方法:

  • 提取函数

当函数内容太多的时候一定是把所有功能塞到一起了,应该想办法把相同的功能抽离出新的函数。

原代码:

m.BroadcastFilter(msg, func(s *melody.Session) bool {
                if reg, exists = s.Get("reg"); exists {
                    sp := (reg).(*WSMessage)
                    if r, ok := Rooms[roomID]; ok {
                        for _, c := range r.Clients {
                            if (sp.ClientID == c.ClientID) && (c.ClientID != my) {
                                log.Info("[ws] <----sent---->", c, my, string(msg))
                                return true
                            }
                        }
                    } else {
                        log.Warnf("[ws] only myself???? roomID:%d,my:%v,msg:%v", roomID, my, string(msg))
                    }
                }
                return false
            })

重构代码:

// SendRoomBroadcast 发送房间广播
func (wss *WebSocketServer) SendRoomBroadcast(roomID, my string, msg []byte) {
    wss.Server.BroadcastFilter(msg, func(s *melody.Session) bool {
        if reg, exists := s.Get("reg"); exists {
            sp := (reg).(*WSMessage)
            if r, ok := Rooms[roomID]; ok {
                for _, c := range r.Clients {
                    if (sp.ClientID == c.ClientID) && (c.ClientID != my) {
                        return true
                    }
                }
            } else {
                log.Warnf("[ws] only myself???? roomID:%d,my:%v,msg:%v", roomID, my, string(msg))
            }
        }
        return false
    })
}
  • 封装类

当代码整理差不多后,会发现有很多相同的功能,当相同的功能太多的时候可以考虑封装成类。

// WebSocketServer 结构体
type WebSocketServer struct {
    Server *melody.Melody // websocket
}

// NewWebSocketServer 初始化WebSocketServer
func NewWebSocketServer(router *gin.Engine) *WebSocketServer {
    wss := &WebSocketServer{
        Server: melody.New(),
    }
    // 配置buffer size
    wss.Server.Config.MaxMessageSize = 10024
    wss.Server.Config.MessageBufferSize = 5120
    // 升级为websocket
    router.GET("/v", func(c *gin.Context) {
        wss.Server.HandleRequest(c.Writer, c.Request)
    })
    // 处理连接
    wss.Server.HandleConnect(wss.handleConnect)
    // 处理断开连接
    wss.Server.HandleDisconnect(wss.handleDisconnect)
    // 处理消息
    wss.Server.HandleMessage(wss.handleMessage)
    return wss
}

func (wss *WebSocketServer) handleExit(reg interface{}, exists bool, msg []byte) bool {
    if string(msg) == WebSocketClientBye {
        if exists {
            sp := (reg).(*WSMessage)
            if removeRoom(sp.RoomID, sp.ClientID) != "" {
                log.Warnf("Socket removeRoom error:%v", reg)
            }
        }
        log.Info("Socket bye", reg)
        return true
    }
    return false
}

// SendRoomBroadcast 发送房间广播
func (wss *WebSocketServer) SendRoomBroadcast(roomID, my string, msg []byte) {
    wss.Server.BroadcastFilter(msg, func(s *melody.Session) bool {
        if reg, exists := s.Get("reg"); exists {
            sp := (reg).(*WSMessage)
            if r, ok := Rooms[roomID]; ok {
                for _, c := range r.Clients {
                    if (sp.ClientID == c.ClientID) && (c.ClientID != my) {
                        return true
                    }
                }
            } else {
                log.Warnf("[ws] only myself???? roomID:%d,my:%v,msg:%v", roomID, my, string(msg))
            }
        }
        return false
    })
}

func (wss *WebSocketServer) handleMessage(session *melody.Session, msg []byte) {
    reg, exists := session.Get("reg")
    log.Info("[ws] message:", string(msg), "reg:", reg)
    if wss.handleExit(reg, exists, msg) {
        return
    }
    message := &WSMessage{}
    err := json.Unmarshal(msg, message)
    if err != nil {
        log.Error("Json error :", err)
        return
    }
    switch message.CMD {
    case "register":
        log.Debug("register")
        session.Set("reg", message)
        return
    default:
        log.Debug("socket default json:", message)
        wss.SendRoomBroadcast((reg).(*WSMessage).RoomID, (reg).(*WSMessage).ClientID, msg)
    }
}

func (wss *WebSocketServer) handleConnect(session *melody.Session) {
    log.Info("[ws] connect:", session)
}
func (wss *WebSocketServer) handleDisconnect(session *melody.Session) {
    log.Info("[ws] disconnect", session)
}

总结

这个原则足够简单,只要有耐心就能办到。还是建议大家拿起自己手中的代码体验一下,在重构过程中慢慢体会,不过一定要写测试哦,要不就变成裸奔啦。欢迎拍砖:)

Thrift与golang应用

Apache Thrift是一种流行的远程服务调用框架,它采用接口描述定义并创建服务,它的特定是支持多种语言,可以指定多种传输协议、传输层与服务类型快速创建高效服务。

Thrift

数据类型

基本类型:
bool:布尔值,true 或 false,对应 Java 的 boolean
byte:8 位有符号整数,对应 Java 的 byte
i16:16 位有符号整数,对应 Java 的 short
i32:32 位有符号整数,对应 Java 的 int
i64:64 位有符号整数,对应 Java 的 long
double:64 位浮点数,对应 Java 的 double
string:未知编码文本或二进制字符串,对应 Java 的 String
结构体类型:
struct:定义公共的对象,类似于 C 语言中的结构体定义,在 Java 中是一个 JavaBean
容器类型:
list:对应 Java 的 ArrayList
set:对应 Java 的 HashSet
map:对应 Java 的 HashMap
异常类型:
exception:对应 Java 的 Exception
服务类型:
service:对应服务的类

协议

  • TBinaryProtocol — 二进制编码格式进行数据传输
  • TCompactProtocol — 高效率的、密集的二进制编码格式进行数据传输
  • TJSONProtocol — 使用 JSON 的数据编码协议进行数据传输
  • TSimpleJSONProtocol — 只提供 JSON 只写的协议,适用于通过脚本语言解析

传输层

  • TSocket — 使用阻塞式 I/O 进行传输,是最常见的模式
  • TFramedTransport — 使用非阻塞方式,按块的大小进行传输,类似于 Java 中的 NIO
  • TNonblockingTransport — 使用非阻塞方式,用于构建异步客户端

服务类型

  • TSimpleServer — 单线程服务器端使用标准的阻塞式 I/O
  • TThreadPoolServer — 多线程服务器端使用标准的阻塞式 I/O
  • TNonblockingServer — 多线程服务器端使用非阻塞式 I/O

架构图

定义Thrift文件

Base.thrift文件为API基类描述:

namespace java com.qiknow.proto

/**
 * 请求头信息
 */
struct ReqHeader{
    /**
     * Token
     */
    1: string token
}

/**
 * 用户信息
 */
struct UserInfo{
    /**
     * 头像
     */
    1: string headImgPath
    /**
     * 用户姓名
     */
    2: string name
    /**
     * 昵称
     */
    3: string nickname
    /**
     * 性别(1男,2女,0未知)
     */
    4: i32 gender
    /**
     * 电话
     */
    5: string phoneNo
    /**
     * 用户的标签
     */
    6: list<string> tags
    /**
     * 用户ID
     */
    7: i64 userId
    /**
     * 用户关系。1:其他关系,2:我的关注者,3:关注我的人,4:互相关注,5:好友关系,
     */
    8: i32 relationship
}

Req.thrift为请求描述:

include "Base.thrift"
namespace java com.qiknow.proto

/**
 * 加好友请求参数
 */
struct AddFriendReq{
    1: Base.ReqHeader header
    /**
     * 申请者用户ID
     */
    2: i64 requesterId
    /**
     * 被申请者用户ID
     */
    3: i64 approverId
    /**
     * 加好友备注信息
     */
    4: string comments
}

Resp.thrift为返回描述:

include "Base.thrift"
namespace java com.qiknow.proto

struct AddFriendResp {
    /**
     * 状态码
     */
    1: i32 status
    /**
     * 返回结果描述
     */
    2: string msg
}

UserService.thrift为服务接口描述:

include "Req.thrift"
include "Resp.thrift"

namespace java com.qiknow.proto

/**<pre>
描述:账号服务,包括登录的、登出、获取验证码、修改密码、用户信息等相关功能接口
</pre> */   
service UserService{

    /**<pre>
    描述:申请加好友
    参数:申请加好友参数
    结果:返回申请结果
    认证方式:Token
    </pre> */
    Resp.AddFriendResp AddFriend(1: Req.AddFriendReq req)

}

生成代码

使用thrift生成代码

thrift --gen go -out proto/ idl/UserService.thrift
thrift --gen go -out proto/ idl/Resp.thrift
thrift --gen go -out proto/ idl/Req.thrift
thrift --gen go -out proto/ idl/Base.thrift

编写代码

获取git.apache.org/thrift.git/lib/go/thrift依赖

go get git.apache.org/thrift.git/lib/go/thrift

创建对应的Handler

package handler

import (
    "thrift/proto/req"
    "thrift/proto/resp"
)
type UserHandler struct {
}

func NewUserHandler() *UserHandler {
    return &UserHandler{}
}


// <pre>
// 描述:申请加好友
// 参数:申请加好友参数
// 结果:返回申请结果
// 认证方式:Token
// </pre>
//
// Parameters:
//  - Req
func (p *UserHandler) AddFriend(req *req.AddFriendReq) (r *resp.AddFriendResp, err error) {
    return &resp.AddFriendResp{},nil
}

测试

package handler

import (
    "testing"
    "thrift/proto/req"
    "thrift/proto/resp"
    "thrift/proto/userservice"
    "time"

    "git.apache.org/thrift.git/lib/go/thrift"
    "github.com/stretchr/testify/assert"
)

const SERVER_ADDRESS = "127.0.0.1:56783"

func init() {
    testing
    transportFactory := thrift.NewTTransportFactory()
    protocolFactory := thrift.NewTBinaryProtocolFactoryDefault()
    go runServer(transportFactory, protocolFactory, SERVER_ADDRESS)
    time.Sleep(time.Second)
}

// runServer 启动服务器
func runServer(transportFactory thrift.TTransportFactory, protocolFactory thrift.TProtocolFactory, addr string) error {
    var transport thrift.TServerTransport
    var err error
    transport, err = thrift.NewTServerSocket(addr)
    if err != nil {
        return err
    }
    handler := NewUserHandler()
    processor := userservice.NewUserServiceProcessor(handler)
    server := thrift.NewTSimpleServer4(processor, transport, transportFactory, protocolFactory)
    return server.Serve()
}

func TestServer(t *testing.T) {
    var (
        friendResp *resp.AddFriendResp
    )
    // 建立连接
    var transport, err = thrift.NewTSocket(SERVER_ADDRESS)
    assert.NoError(t, err)
    assert.NotNil(t, transport)
    client := userservice.NewUserServiceClientFactory(transport, thrift.NewTBinaryProtocolFactoryDefault())
    assert.NotNil(t, client)
    err = transport.Open()
    assert.NoError(t, err)
    friendResp, err = client.AddFriend(&req.AddFriendReq{
        RequesterId: 100001,
        ApproverId:  100002,
    })
    assert.NotNil(t, friendResp)
    assert.NoError(t, err)
}

Apache Thrift – 可伸缩的跨语言服务开发框架

WebRTC实践(Android、golang实现视频通信)

项目需要翻了好多关于WebRTC的资料,现在做一个简单的总结。

目标

通过研究WebRTC实现一个基于Android的Demo。

学习

之前没有接触过WebRTC,所以是时候需要恶补了一下,第一想到的是在网上找找相关的书籍,大概翻了下《WebRTC Cookbook》与《WebRTC权威指南》,简单了解了本地媒体、信令、STUN、TURN等知识,找了几个js的例子试了下。

Android编译

通过webrtc.org官网的介绍方法,使用gclient获取了webrtc源代码,用gn生成对应cpu项目,用ninja编译,最后生成并在手机上进行测试,完全没问题。

改造

官方的例子好使,验证了方向没问题就开始吧。使用turnserver搭建STUN与TURN服务器。

首先把代码集成到我们的项目中,只需把例子相关类、资源拷贝到项目,并把org.webrtc:google-webrtc包引入到项目即可。

compile 'org.webrtc:google-webrtc:1.0.+'

由于我们的服务是使用go写的,信令服务器这应该是用不了的,当时也没仔细找这部分代码,直接读代码仿写的。

总结

很快代码全部完成,测试也都通过,皆大欢喜,没想到如此顺利,后来打算拿别人手机试一下,出了问题,原来一直在同一wifi下测试,根本不用考虑穿墙的事。首先考虑到是STUN、TURN服务问题,一个个排查,查看ice发现外网ip可以取到,那么可能就是TURN服务的问题,反复排查不得其解,最后发现TURN要求使用long-term credential认证,也就是说必须得有用户名、密码,最后分享一个测试地址,可以测试STUN与TURN服务是否正常。

WebRTC下的网络连接: STUN, TURN, ICE, TCP

bower

bower与npm类似都是包管理器,不过后者更适合服务器端项目。

使用npm下载安装

npm install -g bower

使用bower进行安装RequireJS:

bower install requirejs --save

我们可以配置自己的依赖文件,只要创建bower.json执行bower install即可自动下载安装依赖,我们也可以使用bower init为我们生成一个bower.json,我们只需简单配置即可,结果如下。

.
├── bower.json
└── bower_components
    └── requirejs
        ├── README.md
        ├── bower.json
        └── require.js

WebRTC简单总结

Web实时通信技术(Web Real-Time Communication,WebRTC)开创性使得浏览器与其他浏览器互动。实现了浏览器中的实时通信功能。

建立WebRTC会话

建立WebRTC会话需要四个主要步骤:

  • 获取本地媒体,使用getUserMedia函数。

  • 在浏览器和对等端之间建立连接,RTCPeerConnection是WebRTC的核心。需要输入包含ICE(Interactive Connectivity Establishment,交互式连接建立技术)打洞通过各种NAT(Network Address Translation,网络地址转换)设备和防火墙时所使用的信息。

  • 将媒体和数据通道联至该连接。建立连接后,可将任意数量本地媒体流关联到对等连接,通过连接发送到远端浏览器,需要注意每次更改媒体时都需要协商。
  • 交换会话描述,从本地或远端发出添加或删除请求时,可请求浏览器生成相应的RTCSessionDescription对象。

  • 1.A请求

  • 2.A返回

  • 3.B请求

  • 4.B返回

  • 5.A决定与B通信,发送会话描述对象(offer,提议)至服务器。

  • 6.服务器将offer发给B

  • 7.B发送会话描述对象(answer,应答)至服务器。

  • 8.A收到服务器传发的answer。

  • 9.A与B打洞

  • 10.协商

  • 11.交换

本地媒体

MediaStreamTrack是WEBRTC中基本媒体单元,MediaStream是MediaStreamTrack对象的集合。

获取本地媒体,可使用applyConstraints设置约束。

 function getMedia() {
  getUserMedia({"audio":true, "video":true},
               gotUserMedia, didntGetUserMedia);
}

function gotUserMedia(stream) {
  myVideoStream = stream;

  // display my local video to me
  attachMediaStream(myVideo, myVideoStream);
}

function didntGetUserMedia() {
  console.log("couldn't get video");
}

信令

在通信中,信令主要作用体现四个方面:

  • 协商媒体功能和设置。
  • 标识和验证会话参与者的身份。
  • 控制媒体会话、指示进度、更改会话和终止会话。
  • 当会话双方同时尝试建立或更改会话时,实施双占分解。

信令方案:

  • WebSocket代理
  • XML http请求
  • SIP
  • Jingle
  • 数据通道

打洞

可以使用turnserver搭建TURN和STUN服务。

  • STUN(Session Traversal Utilities for NAT)

  • TURN(Traversal Using Relay around NAT)

总结

WebRTC易于使用、只需几个步骤即可建立媒体会话,最近项目比较忙,简单总结,与实践结合可以提高对知识的认识。

快节奏、慢生活

​ 这是关于《快节奏,慢生活》阅读笔记,它主要讲述了通过观察电子产品对生活带来的影响,为什么会这样。本书并不给出实际建议,而是希望通过练习、观察、总结、分享讨论,读者自已来发现适合自己的行为准则,改变自己的生活。

一个女子由于走路专注摆弄手机而掉入广场的喷泉的事件比比皆是,这样的事能带来什么反思?电子设备对生活有什么影响?结论是电子设备有时会太过吸引人,但是电子设备的功大于过,我们又需要依赖它。然而我们对电子设备投入越多精力我们就会越是心不在焉,更加容易掉入喷泉里。我们正处于科技洪流,摆在我们面前的选择越多,我们就越难进行选择。

如果我们集中注意力,处于放松姿势并达到情感平衡时,我们在网络的活动效率就会提高,也会以更健康的方式来工作。这个不只适用于网络活动,同时适用于其他社会活动。然而改变的关键就是注意力,我们通过练习提高注意力。

我们可以把工作(上网)当成“手艺”,可以通过练习进行强化和改善。所以多数是以练习的方式让自己找出适合自己的答案。当你被告知一件事时是没有效果的,只要我们自己发现自己的习惯我们就更容易改变

一个熟悉的故事

作者早晨来到咖啡馆准备大干一场,有个好的计划,身体、思想都处理良好状态,打开工作后,同时顺手开始收邮件(担心有遗漏邮件),收完邮件视线停留在上周未完成的任务并读到一条令人不安的消息(一直拖延的任务),心里开始感觉到焦虑,随手打开网页(逃避),查看有没有什么有趣的新闻,之后猛发现自己的拖延行为并责怪自己分散注意力,开始认真阅读并回邮件,读邮件时听到收到邮件声音,立刻分散注意力(无意识)去看邮件标题和作者,然后继续回来回复邮件,接着手机又响了,妻子打来的,接通发现是无关紧要的小事,作者变得有些不耐烦,更加焦虑起来,呼吸急促同时坐姿也变得不端正(有些驼背)。

我们无时无刻的在做选择,下步要关注什么,关注多久,其中有些是有意识控制的,此些是无意识的。在一堆无 意识的情绪下,作者进行了多次切换,无实质损害,但它们也毫无工作时应有的效率及技巧,而且切换是有时间成本的,最后不端正的坐姿和急促的呼吸能够表明当时所承受的压力。

注意力、情感和身体

注意力

什么是注意力?这是一种以清晰且生动的形式,在多个看起来同样重要的物体或思想中选择出一个并专注它的行为。它的对立面就是干扰,一种“迷惑、迷茫、慌张的状态”。

注意力模式

人类的注意力是种调焦机制,是最原始的技能用来追踪猎物与关注周围环境。我们面临的挑战就是如何更有技巧地同时使用这两种模式:

  • 聚焦式注意力(任务专注力)
    • 就像黑暗房间中探索的插电筒光,所照之处清晰。如深度阅读、集中精力听音乐等。
  • 开放式注意力(自我观察力)
    • 这时不仅可以专注单一物体,而且能够接收到周边环境所发生的事,相当于手电筒发出的是散光,可见范围很大,清晰度会下降。

注意力的转换和选择

知道了注意力的两种模式后,我们面临的挑战是:需要牢记当下意图和目标,这样精力不会无目的的游移,同样意味着我们有时需要有技巧地面对干扰。简要的表述就是:我们需要控制自己抗干扰

控制自己

要想控制自己我们需要先了解人类的控制系统。就像计算机一样,人也有自己的控制系统。

  • 从上至下控制系统
    • 由你意识控制的,它能让你自由选择去关注什么。比如:专注左脚的感受,调动你的局部知觉,你能做到的。
  • 从下至上控制系统
    • 是人类为了生存早期进化完全自动化的控制机制,它能扫描周边环境预防潜在威胁,我们不能停止这种警戒机制。比如:人们经常能被路上跑车的轰鸣声吸引,人的注意力会自然的转移到汽车上。
抗干扰

干扰无处不在,我们可以尽量关闭不必要的干扰,但是不能做到排除所有干扰。我们可以在它们产生时注意到它们,并做出有技巧性的决定,是忽略还是反应。换句话说,我们可以通过强化自我意识,增加选择的可能性,并避免无意识的反应。说了这么多抗干扰,那干扰有什么那?

  • 外部干扰
    • 声音、气味、行动及感知周围物质世界。
  • 内部干扰
    • 由思想和身体中产生,比如:由饥饿感到胃痛。

一心多用— 一种注意力训练

注意力是一心多用的重点。人也像计算机一样,同一时间只关注一件事,来回切换,只不过是速度太快,当许多潜在物体都可以被我们关注时,我们就有了选择的能力。

一心多用的缺点是什么?严格说它是没有效率的,连续转换聚焦式注意力需要时间,经济学家把转换注意力花费时间叫作”转换成本“,我们转换越频繁我们在转换上花费的时间就越多,相应的我们的任务或活动的时间就会更少。而且任务之间由于切换引起的衔接问题也是一种成本。

那它的优点?一件事快速转换另一件事的开放式注意力也是有价值的,思想神游让我们会富有创作性。

当我们掌握强化任务注意力、自我意识和做出明智使用注意力的能力—恰好能够帮助我们更成功地进行一心多用并决定何时(以及何时不)进行。

情感

现在的生活节奏越来越快,我们的生活更有挑战性的是:长时间低落情绪的存在。

感情是无价之宝,让我们的生活更有意义,由下至上的警戒系统中产生负面情绪,这种是机制是遗传的,不可或缺的一部分,比如:当我们害怕,为了在紧急时做出反应,从而升高血压心率,快速反应后迅速恢复常态,科学家称为体内平衡。研究表明:当战斗或逃跑成为常态时,身体就不会恢复到基础状态,身体在高度紧张状态下,能够引发一系列问题。

对于这种机制,我们可以抑制或反思,暂停足够长的时间来重新评估,并且我们可能不能关闭它,但我们可以在产生情感时锻炼自制力,做出有意识的选择。

身体

当我们投入工作或上网时会忽略由下至上传回的信号,以至于身体经受病痛折磨,反过来强烈的身体感觉又如同负面情绪一样能够劫持我们的注意力。

我们需要关注身体,否则它会拖累我们。可以通过改变姿势和动作,能改善心情提高注意力质量。比如:散步。

同时我们也需要注意呼吸也占据着特殊的位置,呼吸是自动的,很有少人会关注它。当我们压力过支时我们经常快速浅呼吸,是我们战斗或逃跑的特征,这时呼吸就会剥夺身体需要氧气的权力。

强化任务专注力

我们一起了解了注意力、情感和身体对我们生活的影响。当我们更细心、更放松达到感情平衡时,我们的工作效率会更高。通过自我观察并留心自身的举动,我们可以学习怎样变得更细心、放松并保持情感平衡:关注我们的思想和身体变化,关注何时及什么什么分心,为什么压力过多或感到沮丧,并运用观察到的东西来调整我们的行为。我们该如何强化任务专注力?

正念呼吸

把呼吸当作关注对象,关注吸入和呼出的呼吸,当思想神游时,你只需要将你关注重点重新放在呼吸上。拉回即可。这种训练可以强化聚焦、开放和选择。正念呼吸可以在任何时间进行,有的时候可能由于外界环境无法专注,可以试试在心里数自己的呼吸。呼~1,吸~2……

正念自省(冥想检查)

正念自省让你变得更加注意你的注意力、你的情感、你的呼吸和身体。花几分钟时间依次询问这些问题。

  • 你的呼吸质量如何:浅还是深,快还是慢?你在屏住呼吸吗?
  • 你的身体内部和身体本身发生了什么变化?姿势是什么样的?你能否注意到身体紧绷或者疼痛的部位?你有没有觉得身体的哪些部位是麻木的?
  • 你现在心情是怎样的?觉得开心、沮丧、激动、还是无聊、焦虑?
  • 你的注意力是怎样的?高度集中,还是四散各处,还是处于中间?

这是一门手艺

我们可以把有效的工作(上网)看作为一种”手艺“,一种高技巧性活动集合,就像其他手艺一样,比如:踢球、游泳。这些手艺是有共同点:注意力的价值、平衡的情感状态和一个放松合拍的身体。这也突出了手艺是可以通过练习和训练进行强化、改善的。那么让们一起掌握这些练习吧?