package nsqd

import (
    "crypto/tls"
    "crypto/x509"
    "encoding/json"
    "errors"
    "fmt"
    "io/ioutil"
    "math/rand"
    "net"
    "os"
    "path"
    "runtime"
    "strings"
    "sync"
    "sync/atomic"
    "time"

    "github.com/nsqio/nsq/internal/clusterinfo"
    "github.com/nsqio/nsq/internal/dirlock"
    "github.com/nsqio/nsq/internal/http_api"
    "github.com/nsqio/nsq/internal/protocol"
    "github.com/nsqio/nsq/internal/statsd"
    "github.com/nsqio/nsq/internal/util"
    "github.com/nsqio/nsq/internal/version"
)

const (
    TLSNotRequired = iota
    TLSRequiredExceptHTTP
    TLSRequired
)

type errStore struct {
    err error
}

type NSQD struct {
    // 64bit atomic vars need to be first for proper alignment on 32bit platforms
    clientIDSequence int64

    sync.RWMutex

    opts atomic.Value

    dl        *dirlock.DirLock
    isLoading int32
    errValue  atomic.Value
    startTime time.Time

    topicMap map[string]*Topic

    lookupPeers atomic.Value

    tcpListener   net.Listener
    httpListener  net.Listener
    httpsListener net.Listener
    tlsConfig     *tls.Config

    poolSize int

    idChan               chan MessageID
    notifyChan           chan interface{}
    optsNotificationChan chan struct{}
    exitChan             chan int
    waitGroup            util.WaitGroupWrapper

    ci *clusterinfo.ClusterInfo
}

func New(opts *Options) *NSQD {
    dataPath := opts.DataPath
    if opts.DataPath == "" {
        cwd, _ := os.Getwd()
        dataPath = cwd
    }

    n := &NSQD{
        startTime:            time.Now(),
        topicMap:             make(map[string]*Topic),
        idChan:               make(chan MessageID, 4096),
        exitChan:             make(chan int),
        notifyChan:           make(chan interface{}),
        optsNotificationChan: make(chan struct{}, 1),
        ci:                   clusterinfo.New(opts.Logger, http_api.NewClient(nil, opts.HTTPClientConnectTimeout, opts.HTTPClientRequestTimeout)),
        dl:                   dirlock.New(dataPath),
    }
    n.swapOpts(opts)
    n.errValue.Store(errStore{})

    err := n.dl.Lock()
    if err != nil {
        n.logf("FATAL: --data-path=%s in use (possibly by another instance of nsqd)", dataPath)
        os.Exit(1)
    }

    if opts.MaxDeflateLevel < 1 || opts.MaxDeflateLevel > 9 {
        n.logf("FATAL: --max-deflate-level must be [1,9]")
        os.Exit(1)
    }

    if opts.ID < 0 || opts.ID >= 1024 {
        n.logf("FATAL: --worker-id must be [0,1024)")
        os.Exit(1)
    }

    if opts.StatsdPrefix != "" {
        var port string
        _, port, err = net.SplitHostPort(opts.HTTPAddress)
        if err != nil {
            n.logf("ERROR: failed to parse HTTP address (%s) - %s", opts.HTTPAddress, err)
            os.Exit(1)
        }
        statsdHostKey := statsd.HostKey(net.JoinHostPort(opts.BroadcastAddress, port))
        prefixWithHost := strings.Replace(opts.StatsdPrefix, "%s", statsdHostKey, -1)
        if prefixWithHost[len(prefixWithHost)-1] != '.' {
            prefixWithHost += "."
        }
        opts.StatsdPrefix = prefixWithHost
    }

    if opts.TLSClientAuthPolicy != "" && opts.TLSRequired == TLSNotRequired {
        opts.TLSRequired = TLSRequired
    }

    tlsConfig, err := buildTLSConfig(opts)
    if err != nil {
        n.logf("FATAL: failed to build TLS config - %s", err)
        os.Exit(1)
    }
    if tlsConfig == nil && opts.TLSRequired != TLSNotRequired {
        n.logf("FATAL: cannot require TLS client connections without TLS key and cert")
        os.Exit(1)
    }
    n.tlsConfig = tlsConfig

    n.logf(version.String("nsqd"))
    n.logf("ID: %d", opts.ID)

    return n
}

func (n *NSQD) logf(f string, args ...interface{}) {
    if n.getOpts().Logger == nil {
        return
    }
    n.getOpts().Logger.Output(2, fmt.Sprintf(f, args...))
}

func (n *NSQD) getOpts() *Options {
    return n.opts.Load().(*Options)
}

func (n *NSQD) swapOpts(opts *Options) {
    n.opts.Store(opts)
}

func (n *NSQD) triggerOptsNotification() {
    select {
    case n.optsNotificationChan <- struct{}{}:
    default:
    }
}

func (n *NSQD) RealTCPAddr() *net.TCPAddr {
    n.RLock()
    defer n.RUnlock()
    return n.tcpListener.Addr().(*net.TCPAddr)
}

func (n *NSQD) RealHTTPAddr() *net.TCPAddr {
    n.RLock()
    defer n.RUnlock()
    return n.httpListener.Addr().(*net.TCPAddr)
}

func (n *NSQD) RealHTTPSAddr() *net.TCPAddr {
    n.RLock()
    defer n.RUnlock()
    return n.httpsListener.Addr().(*net.TCPAddr)
}

func (n *NSQD) SetHealth(err error) {
    n.errValue.Store(errStore{err: err})
}

func (n *NSQD) IsHealthy() bool {
    return n.GetError() == nil
}

func (n *NSQD) GetError() error {
    errValue := n.errValue.Load()
    return errValue.(errStore).err
}

func (n *NSQD) GetHealth() string {
    err := n.GetError()
    if err != nil {
        return fmt.Sprintf("NOK - %s", err)
    }
    return "OK"
}

func (n *NSQD) GetStartTime() time.Time {
    return n.startTime
}

func (n *NSQD) Main() {
    var httpListener net.Listener
    var httpsListener net.Listener

    ctx := &context{n}

    tcpListener, err := net.Listen("tcp", n.getOpts().TCPAddress)
    if err != nil {
        n.logf("FATAL: listen (%s) failed - %s", n.getOpts().TCPAddress, err)
        os.Exit(1)
    }
    n.Lock()
    n.tcpListener = tcpListener
    n.Unlock()
    tcpServer := &tcpServer{ctx: ctx}
    n.waitGroup.Wrap(func() {
        protocol.TCPServer(n.tcpListener, tcpServer, n.getOpts().Logger)
    })

    if n.tlsConfig != nil && n.getOpts().HTTPSAddress != "" {
        httpsListener, err = tls.Listen("tcp", n.getOpts().HTTPSAddress, n.tlsConfig)
        if err != nil {
            n.logf("FATAL: listen (%s) failed - %s", n.getOpts().HTTPSAddress, err)
            os.Exit(1)
        }
        n.Lock()
        n.httpsListener = httpsListener
        n.Unlock()
        httpsServer := newHTTPServer(ctx, true, true)
        n.waitGroup.Wrap(func() {
            http_api.Serve(n.httpsListener, httpsServer, "HTTPS", n.getOpts().Logger)
        })
    }
    httpListener, err = net.Listen("tcp", n.getOpts().HTTPAddress)
    if err != nil {
        n.logf("FATAL: listen (%s) failed - %s", n.getOpts().HTTPAddress, err)
        os.Exit(1)
    }
    n.Lock()
    n.httpListener = httpListener
    n.Unlock()
    httpServer := newHTTPServer(ctx, false, n.getOpts().TLSRequired == TLSRequired)
    n.waitGroup.Wrap(func() {
        http_api.Serve(n.httpListener, httpServer, "HTTP", n.getOpts().Logger)
    })

    n.waitGroup.Wrap(func() { n.queueScanLoop() })
    n.waitGroup.Wrap(func() { n.idPump() })
    n.waitGroup.Wrap(func() { n.lookupLoop() })
    if n.getOpts().StatsdAddress != "" {
        n.waitGroup.Wrap(func() { n.statsdLoop() })
    }
}

type meta struct {
    Topics []struct {
        Name     string `json:"name"`
        Paused   bool   `json:"paused"`
        Channels []struct {
            Name   string `json:"name"`
            Paused bool   `json:"paused"`
        } `json:"channels"`
    } `json:"topics"`
}

func (n *NSQD) LoadMetadata() {
    atomic.StoreInt32(&n.isLoading, 1)
    defer atomic.StoreInt32(&n.isLoading, 0)

    fn := fmt.Sprintf(path.Join(n.getOpts().DataPath, "nsqd.%d.dat"), n.getOpts().ID)
    data, err := ioutil.ReadFile(fn)
    if err != nil {
        if !os.IsNotExist(err) {
            n.logf("ERROR: failed to read channel metadata from %s - %s", fn, err)
        }
        return
    }

    var m meta
    err = json.Unmarshal(data, &m)
    if err != nil {
        n.logf("ERROR: failed to parse metadata - %s", err)
        return
    }

    for _, t := range m.Topics {
        if !protocol.IsValidTopicName(t.Name) {
            n.logf("WARNING: skipping creation of invalid topic %s", t.Name)
            continue
        }
        topic := n.GetTopic(t.Name)
        if t.Paused {
            topic.Pause()
        }

        for _, c := range t.Channels {
            if !protocol.IsValidChannelName(c.Name) {
                n.logf("WARNING: skipping creation of invalid channel %s", c.Name)
                continue
            }
            channel := topic.GetChannel(c.Name)
            if c.Paused {
                channel.Pause()
            }
        }
    }
}

func (n *NSQD) PersistMetadata() error {
    // persist metadata about what topics/channels we have
    // so that upon restart we can get back to the same state
    fileName := fmt.Sprintf(path.Join(n.getOpts().DataPath, "nsqd.%d.dat"), n.getOpts().ID)
    n.logf("NSQ: persisting topic/channel metadata to %s", fileName)

    js := make(map[string]interface{})
    topics := []interface{}{}
    for _, topic := range n.topicMap {
        if topic.ephemeral {
            continue
        }
        topicData := make(map[string]interface{})
        topicData["name"] = topic.name
        topicData["paused"] = topic.IsPaused()
        channels := []interface{}{}
        topic.Lock()
        for _, channel := range topic.channelMap {
            channel.Lock()
            if channel.ephemeral {
                channel.Unlock()
                continue
            }
            channelData := make(map[string]interface{})
            channelData["name"] = channel.name
            channelData["paused"] = channel.IsPaused()
            channels = append(channels, channelData)
            channel.Unlock()
        }
        topic.Unlock()
        topicData["channels"] = channels
        topics = append(topics, topicData)
    }
    js["version"] = version.Binary
    js["topics"] = topics

    data, err := json.Marshal(&js)
    if err != nil {
        return err
    }

    tmpFileName := fmt.Sprintf("%s.%d.tmp", fileName, rand.Int())
    f, err := os.OpenFile(tmpFileName, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
    if err != nil {
        return err
    }

    _, err = f.Write(data)
    if err != nil {
        f.Close()
        return err
    }
    f.Sync()
    f.Close()

    err = atomicRename(tmpFileName, fileName)
    if err != nil {
        return err
    }

    return nil
}

func (n *NSQD) Exit() {
    if n.tcpListener != nil {
        n.tcpListener.Close()
    }

    if n.httpListener != nil {
        n.httpListener.Close()
    }

    if n.httpsListener != nil {
        n.httpsListener.Close()
    }

    n.Lock()
    err := n.PersistMetadata()
    if err != nil {
        n.logf("ERROR: failed to persist metadata - %s", err)
    }
    n.logf("NSQ: closing topics")
    for _, topic := range n.topicMap {
        topic.Close()
    }
    n.Unlock()

    // we want to do this last as it closes the idPump (if closed first it
    // could potentially starve items in process and deadlock)
    close(n.exitChan)
    n.waitGroup.Wait()

    n.dl.Unlock()
}

// GetTopic performs a thread safe operation
// to return a pointer to a Topic object (potentially new)
func (n *NSQD) GetTopic(topicName string) *Topic {
    // most likely, we already have this topic, so try read lock first.
    n.RLock()
    t, ok := n.topicMap[topicName]
    n.RUnlock()
    if ok {
        return t
    }

    n.Lock()

    t, ok = n.topicMap[topicName]
    if ok {
        n.Unlock()
        return t
    }
    deleteCallback := func(t *Topic) {
        n.DeleteExistingTopic(t.name)
    }
    t = NewTopic(topicName, &context{n}, deleteCallback)
    n.topicMap[topicName] = t

    n.logf("TOPIC(%s): created", t.name)

    // release our global nsqd lock, and switch to a more granular topic lock while we init our
    // channels from lookupd. This blocks concurrent PutMessages to this topic.
    t.Lock()
    n.Unlock()

    // if using lookupd, make a blocking call to get the topics, and immediately create them.
    // this makes sure that any message received is buffered to the right channels
    lookupdHTTPAddrs := n.lookupdHTTPAddrs()
    if len(lookupdHTTPAddrs) > 0 {
        channelNames, _ := n.ci.GetLookupdTopicChannels(t.name, lookupdHTTPAddrs)
        for _, channelName := range channelNames {
            if strings.HasSuffix(channelName, "#ephemeral") {
                // we don't want to pre-create ephemeral channels
                // because there isn't a client connected
                continue
            }
            t.getOrCreateChannel(channelName)
        }
    }

    t.Unlock()

    // NOTE: I would prefer for this to only happen in topic.GetChannel() but we're special
    // casing the code above so that we can control the locks such that it is impossible
    // for a message to be written to a (new) topic while we're looking up channels
    // from lookupd...
    //
    // update messagePump state
    select {
    case t.channelUpdateChan <- 1:
    case <-t.exitChan:
    }
    return t
}

// GetExistingTopic gets a topic only if it exists
func (n *NSQD) GetExistingTopic(topicName string) (*Topic, error) {
    n.RLock()
    defer n.RUnlock()
    topic, ok := n.topicMap[topicName]
    if !ok {
        return nil, errors.New("topic does not exist")
    }
    return topic, nil
}

// DeleteExistingTopic removes a topic only if it exists
func (n *NSQD) DeleteExistingTopic(topicName string) error {
    n.RLock()
    topic, ok := n.topicMap[topicName]
    if !ok {
        n.RUnlock()
        return errors.New("topic does not exist")
    }
    n.RUnlock()

    // delete empties all channels and the topic itself before closing
    // (so that we dont leave any messages around)
    //
    // we do this before removing the topic from map below (with no lock)
    // so that any incoming writes will error and not create a new topic
    // to enforce ordering
    topic.Delete()

    n.Lock()
    delete(n.topicMap, topicName)
    n.Unlock()

    return nil
}

func (n *NSQD) idPump() {
    factory := &guidFactory{}
    lastError := time.Unix(0, 0)
    workerID := n.getOpts().ID
    for {
        id, err := factory.NewGUID(workerID)
        if err != nil {
            now := time.Now()
            if now.Sub(lastError) > time.Second {
                // only print the error once/second
                n.logf("ERROR: %s", err)
                lastError = now
            }
            runtime.Gosched()
            continue
        }
        select {
        case n.idChan <- id.Hex():
        case <-n.exitChan:
            goto exit
        }
    }

exit:
    n.logf("ID: closing")
}

func (n *NSQD) Notify(v interface{}) {
    // since the in-memory metadata is incomplete,
    // should not persist metadata while loading it.
    // nsqd will call `PersistMetadata` it after loading
    persist := atomic.LoadInt32(&n.isLoading) == 0
    n.waitGroup.Wrap(func() {
        // by selecting on exitChan we guarantee that
        // we do not block exit, see issue #123
        select {
        case <-n.exitChan:
        case n.notifyChan <- v:
            if !persist {
                return
            }
            n.Lock()
            err := n.PersistMetadata()
            if err != nil {
                n.logf("ERROR: failed to persist metadata - %s", err)
            }
            n.Unlock()
        }
    })
}

// channels returns a flat slice of all channels in all topics
func (n *NSQD) channels() []*Channel {
    var channels []*Channel
    n.RLock()
    for _, t := range n.topicMap {
        t.RLock()
        for _, c := range t.channelMap {
            channels = append(channels, c)
        }
        t.RUnlock()
    }
    n.RUnlock()
    return channels
}

// resizePool adjusts the size of the pool of queueScanWorker goroutines
//
//     1 <= pool <= min(num * 0.25, QueueScanWorkerPoolMax)
//
func (n *NSQD) resizePool(num int, workCh chan *Channel, responseCh chan bool, closeCh chan int) {
    idealPoolSize := int(float64(num) * 0.25)
    if idealPoolSize < 1 {
        idealPoolSize = 1
    } else if idealPoolSize > n.getOpts().QueueScanWorkerPoolMax {
        idealPoolSize = n.getOpts().QueueScanWorkerPoolMax
    }
    for {
        if idealPoolSize == n.poolSize {
            break
        } else if idealPoolSize < n.poolSize {
            // contract
            closeCh <- 1
            n.poolSize--
        } else {
            // expand
            n.waitGroup.Wrap(func() {
                n.queueScanWorker(workCh, responseCh, closeCh)
            })
            n.poolSize++
        }
    }
}

// queueScanWorker receives work (in the form of a channel) from queueScanLoop
// and processes the deferred and in-flight queues
func (n *NSQD) queueScanWorker(workCh chan *Channel, responseCh chan bool, closeCh chan int) {
    for {
        select {
        case c := <-workCh:
            now := time.Now().UnixNano()
            dirty := false
            if c.processInFlightQueue(now) {
                dirty = true
            }
            if c.processDeferredQueue(now) {
                dirty = true
            }
            responseCh <- dirty
        case <-closeCh:
            return
        }
    }
}

// queueScanLoop runs in a single goroutine to process in-flight and deferred
// priority queues. It manages a pool of queueScanWorker (configurable max of
// QueueScanWorkerPoolMax (default: 4)) that process channels concurrently.
//
// It copies Redis's probabilistic expiration algorithm: it wakes up every
// QueueScanInterval (default: 100ms) to select a random QueueScanSelectionCount
// (default: 20) channels from a locally cached list (refreshed every
// QueueScanRefreshInterval (default: 5s)).
//
// If either of the queues had work to do the channel is considered "dirty".
//
// If QueueScanDirtyPercent (default: 25%) of the selected channels were dirty,
// the loop continues without sleep.
func (n *NSQD) queueScanLoop() {
    workCh := make(chan *Channel, n.getOpts().QueueScanSelectionCount)
    responseCh := make(chan bool, n.getOpts().QueueScanSelectionCount)
    closeCh := make(chan int)

    workTicker := time.NewTicker(n.getOpts().QueueScanInterval)
    refreshTicker := time.NewTicker(n.getOpts().QueueScanRefreshInterval)

    channels := n.channels()
    n.resizePool(len(channels), workCh, responseCh, closeCh)

    for {
        select {
        case <-workTicker.C:
            if len(channels) == 0 {
                continue
            }
        case <-refreshTicker.C:
            channels = n.channels()
            n.resizePool(len(channels), workCh, responseCh, closeCh)
            continue
        case <-n.exitChan:
            goto exit
        }

        num := n.getOpts().QueueScanSelectionCount
        if num > len(channels) {
            num = len(channels)
        }

    loop:
        for _, i := range util.UniqRands(num, len(channels)) {
            workCh <- channels[i]
        }

        numDirty := 0
        for i := 0; i < num; i++ {
            if <-responseCh {
                numDirty++
            }
        }

        if float64(numDirty)/float64(num) > n.getOpts().QueueScanDirtyPercent {
            goto loop
        }
    }

exit:
    n.logf("QUEUESCAN: closing")
    close(closeCh)
    workTicker.Stop()
    refreshTicker.Stop()
}

func buildTLSConfig(opts *Options) (*tls.Config, error) {
    var tlsConfig *tls.Config

    if opts.TLSCert == "" && opts.TLSKey == "" {
        return nil, nil
    }

    tlsClientAuthPolicy := tls.VerifyClientCertIfGiven

    cert, err := tls.LoadX509KeyPair(opts.TLSCert, opts.TLSKey)
    if err != nil {
        return nil, err
    }
    switch opts.TLSClientAuthPolicy {
    case "require":
        tlsClientAuthPolicy = tls.RequireAnyClientCert
    case "require-verify":
        tlsClientAuthPolicy = tls.RequireAndVerifyClientCert
    default:
        tlsClientAuthPolicy = tls.NoClientCert
    }

    tlsConfig = &tls.Config{
        Certificates: []tls.Certificate{cert},
        ClientAuth:   tlsClientAuthPolicy,
        MinVersion:   opts.TLSMinVersion,
        MaxVersion:   tls.VersionTLS12, // enable TLS_FALLBACK_SCSV prior to Go 1.5: https://go-review.googlesource.com/#/c/1776/
    }

    if opts.TLSRootCAFile != "" {
        tlsCertPool := x509.NewCertPool()
        caCertFile, err := ioutil.ReadFile(opts.TLSRootCAFile)
        if err != nil {
            return nil, err
        }
        if !tlsCertPool.AppendCertsFromPEM(caCertFile) {
            return nil, errors.New("failed to append certificate to pool")
        }
        tlsConfig.ClientCAs = tlsCertPool
    }

    tlsConfig.BuildNameToCertificate()

    return tlsConfig, nil
}

func (n *NSQD) IsAuthEnabled() bool {
    return len(n.getOpts().AuthHTTPAddresses) != 0
}

nsqd.go的更多相关文章

  1. NSQ源码剖析之nsqd

    NSQ简介 NSQ 是实时的分布式消息处理平台,其设计的目的是用来大规模地处理每天数以十亿计级别的消息.NSQ 具有分布式和去中心化拓扑结构,该结构具有无单点故障.故障容错.高可用性以及能够保证消息的 ...

  2. nsq源码阅读笔记之nsqd(四)——Channel

    与Channel相关的代码主要位于nsqd/channel.go, nsqd/nsqd.go中. Channel与Topic的关系 Channel是消费者订阅特定Topic的一种抽象.对于发往Topi ...

  3. nsq源码阅读笔记之nsqd(二)——Topic

    与Topic相关的代码主要位于nsqd/nsqd.go, nsqd/topic.go中. Topic的获取 Topic通过GetTopic函数获取 GetTopic函数用于获取topic对象,首先先尝 ...

  4. nsq源码阅读笔记之nsqd(三)——diskQueue

    diskQueue是backendQueue接口的一个实现.backendQueue的作用是在实现在内存go channel缓冲区满的情况下对消息的处理的对象. 除了diskQueue外还有dummy ...

  5. nsq源码阅读笔记之nsqd(一)——nsqd的配置解析和初始化

    配置解析 nsqd的主函数位于apps/nsqd.go中的main函数 首先main函数调用nsqFlagset和Parse进行命令行参数集初始化, 然后判断version参数是否存在,若存在,则打印 ...

  6. go语言nsq源码解读二 nsqlookupd、nsqd与nsqadmin

    nsqlookupd: 官方文档解释见:http://bitly.github.io/nsq/components/nsqlookupd.html 用官方话来讲是:nsqlookupd管理拓扑信息,客 ...

  7. (转)go语言nsq源码解读二 nsqlookupd、nsqd与nsqadmin

    转自:http://www.baiyuxiong.com/?p=886 ---------------------------------------------------------------- ...

  8. MVC Code First 自动生成数据库

    1.新建一个MVC项目

  9. 深入NSQ 之旅[转载]

    介绍 NSQ是一个实时的分布式消息平台.它的设计目标是为在多台计算机上运行的松散服务提供一个现代化的基础设施骨架.这篇文章介绍了 基于go语言的NSQ的内部架构,它能够为高吞吐量的网络服务器带来 性能 ...

随机推荐

  1. rails中select不能响应多选的解决办法

    在rails4.2中如果你写如下代码,post的select无法传回多选内容,即使你select设置为多选: <select id='id_size' name='name_size' mult ...

  2. rails中migration数据库后测试不通过的问题

    rails项目中由于后期需求变化,需要在products数据库中增加一个字段来满足多国家商品的分类: rails g migration add_locale_to_products locale:s ...

  3. ruby读取源代码自身的一种方法

    我们知道ruby中如果源代码中一行开头(必须在行的开头)有__END__标示,则表示下面的都是数据行,可以用IO对象DATA来访问这些行.但是如果我们用DATA.rewind一下的话,就可以将文件流指 ...

  4. MQ队列管理器搭建(三)

    MQ集群及网关队列管理器的搭建 描述:     如上图所示,为MQ的集群搭建部署图.CLUSTERA.CLUSTERB分别是两个集群,其中Qm1-Qm3.GateWayA为CLUSTERA集群中的队列 ...

  5. Loader转换器

    一.简介 webpack本身只能处理js模块,Loader可以理解为模块和资源的转换器,它本身是一个函数,接受文件作为参数,返回转换的结果.因此,我们就能通过require来加载任何类型的模块和文件. ...

  6. RunTime运行时在iOS中的应用之UITextField占位符placeholder

    RunTime运行时机制 runtime是一套比较底层的纯C语言API, 属于1个C语言库, 包含了很多底层的C语言API. 在我们平时编写的Objective-C代码中, 程序运行过程时, 其实最终 ...

  7. iframe中 父页面和子页面查找元素的方法

    从父页面中查找iframe子页面中对象的方法:JS: document.getElementById('iframe').contentWindow //查找iframe加载的页面的window对象 ...

  8. Hyper Text Transfer Protocol(超文本传输协议)

    HTTP简介 HTTP协议是Hyper Text Transfer Protocol(超文本传输协议)的缩写,是用于从万维网(WWW:World Wide Web )服务器传输超文本到本地浏览器的传送 ...

  9. Kali Linux Live USB初始化+使用日记

    1.Live USB制作官方guide:Making a Kali Bootable USB Drive:https://docs.kali.org/downloading/kali-linux-li ...

  10. STOMP协议规范

    原文: STOMP Protocol Specification, Version 1.2 摘要 STOMP是一个简单的可互操作的协议, 被用于通过中间服务器在客户端之间进行异步消息传递.它定义了一种 ...