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

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

原则

编写代码单元应该少于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易于使用、只需几个步骤即可建立媒体会话,最近项目比较忙,简单总结,与实践结合可以提高对知识的认识。