吴振宇

吴振宇 查看完整档案

北京编辑  |  填写毕业院校no job  |  ceo 编辑 www.kuangjue.com 编辑
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 个人简介什么都没有

个人动态

吴振宇 关注了用户 · 2020-12-02

konohanaruto @narutos

关注 8

吴振宇 收藏了文章 · 2018-03-23

gRPC服务发现&负载均衡

gRPC服务发现&负载均衡

构建高可用、高性能的通信服务,通常采用服务注册与发现、负载均衡和容错处理等机制实现。根据负载均衡实现所在的位置不同,通常可分为以下三种解决方案:

1、集中式LB(Proxy Model)

clipboard.png

在服务消费者和服务提供者之间有一个独立的LB,通常是专门的硬件设备如 F5,或者基于软件如 LVS,HAproxy等实现。LB上有所有服务的地址映射表,通常由运维配置注册,当服务消费方调用某个目标服务时,它向LB发起请求,由LB以某种策略,比如轮询(Round-Robin)做负载均衡后将请求转发到目标服务。LB一般具备健康检查能力,能自动摘除不健康的服务实例。 该方案主要问题:

  1. 单点问题,所有服务调用流量都经过LB,当服务数量和调用量大的时候,LB容易成为瓶颈,且一旦LB发生故障影响整个系统;

  2. 服务消费方、提供方之间增加了一级,有一定性能开销。

2、进程内LB(Balancing-aware Client)

clipboard.png

针对第一个方案的不足,此方案将LB的功能集成到服务消费方进程里,也被称为软负载或者客户端负载方案。服务提供方启动时,首先将服务地址注册到服务注册表,同时定期报心跳到服务注册表以表明服务的存活状态,相当于健康检查,服务消费方要访问某个服务时,它通过内置的LB组件向服务注册表查询,同时缓存并定期刷新目标服务地址列表,然后以某种负载均衡策略选择一个目标服务地址,最后向目标服务发起请求。LB和服务发现能力被分散到每一个服务消费者的进程内部,同时服务消费方和服务提供方之间是直接调用,没有额外开销,性能比较好。该方案主要问题:

  1. 开发成本,该方案将服务调用方集成到客户端的进程里头,如果有多种不同的语言栈,就要配合开发多种不同的客户端,有一定的研发和维护成本;

  2. 另外生产环境中,后续如果要对客户库进行升级,势必要求服务调用方修改代码并重新发布,升级较复杂。

3、独立 LB 进程(External Load Balancing Service)

clipboard.png

该方案是针对第二种方案的不足而提出的一种折中方案,原理和第二种方案基本类似。
不同之处是将LB和服务发现功能从进程内移出来,变成主机上的一个独立进程。主机上的一个或者多个服务要访问目标服务时,他们都通过同一主机上的独立LB进程做服务发现和负载均衡。该方案也是一种分布式方案没有单点问题,一个LB进程挂了只影响该主机上的服务调用方,服务调用方和LB之间是进程内调用性能好,同时该方案还简化了服务调用方,不需要为不同语言开发客户库,LB的升级不需要服务调用方改代码。
该方案主要问题:部署较复杂,环节多,出错调试排查问题不方便。

gRPC服务发现及负载均衡实现

gRPC开源组件官方并未直接提供服务注册与发现的功能实现,但其设计文档已提供实现的思路,并在不同语言的gRPC代码API中已提供了命名解析和负载均衡接口供扩展。

clipboard.png

其基本实现原理:

  1. 服务启动后gRPC客户端向命名服务器发出名称解析请求,名称将解析为一个或多个IP地址,每个IP地址标示它是服务器地址还是负载均衡器地址,以及标示要使用那个客户端负载均衡策略或服务配置。

  2. 客户端实例化负载均衡策略,如果解析返回的地址是负载均衡器地址,则客户端将使用grpclb策略,否则客户端使用服务配置请求的负载均衡策略。

  3. 负载均衡策略为每个服务器地址创建一个子通道(channel)。

  4. 当有rpc请求时,负载均衡策略决定那个子通道即grpc服务器将接收请求,当可用服务器为空时客户端的请求将被阻塞。

根据gRPC官方提供的设计思路,基于进程内LB方案(即第2个案,阿里开源的服务框架 Dubbo 也是采用类似机制),结合分布式一致的组件(如Zookeeper、Consul、Etcd),可找到gRPC服务发现和负载均衡的可行解决方案。接下来以GO语言为例,简单介绍下基于Etcd3的关键代码实现:

1)命名解析实现:resolver.go

package etcdv3

import (
    "errors"
    "fmt"
    "strings"

    etcd3 "github.com/coreos/etcd/clientv3"
    "google.golang.org/grpc/naming"
)

// resolver is the implementaion of grpc.naming.Resolver
type resolver struct {
    serviceName string // service name to resolve
}

// NewResolver return resolver with service name
func NewResolver(serviceName string) *resolver {
    return &resolver{serviceName: serviceName}
}

// Resolve to resolve the service from etcd, target is the dial address of etcd
// target example: "http://127.0.0.1:2379,http://127.0.0.1:12379,http://127.0.0.1:22379"
func (re *resolver) Resolve(target string) (naming.Watcher, error) {
    if re.serviceName == "" {
        return nil, errors.New("grpclb: no service name provided")
    }

    // generate etcd client
    client, err := etcd3.New(etcd3.Config{
        Endpoints: strings.Split(target, ","),
    })
    if err != nil {
        return nil, fmt.Errorf("grpclb: creat etcd3 client failed: %s", err.Error())
    }

    // Return watcher
    return &watcher{re: re, client: *client}, nil
}

2)服务发现实现:watcher.go

package etcdv3

import (
    "fmt"
    etcd3 "github.com/coreos/etcd/clientv3"
    "golang.org/x/net/context"
    "google.golang.org/grpc/naming"
    "github.com/coreos/etcd/mvcc/mvccpb"
)

// watcher is the implementaion of grpc.naming.Watcher
type watcher struct {
    re            *resolver // re: Etcd Resolver
    client        etcd3.Client
    isInitialized bool
}

// Close do nothing
func (w *watcher) Close() {
}

// Next to return the updates
func (w *watcher) Next() ([]*naming.Update, error) {
    // prefix is the etcd prefix/value to watch
    prefix := fmt.Sprintf("/%s/%s/", Prefix, w.re.serviceName)

    // check if is initialized
    if !w.isInitialized {
        // query addresses from etcd
        resp, err := w.client.Get(context.Background(), prefix, etcd3.WithPrefix())
        w.isInitialized = true
        if err == nil {
            addrs := extractAddrs(resp)
            //if not empty, return the updates or watcher new dir
            if l := len(addrs); l != 0 {
                updates := make([]*naming.Update, l)
                for i := range addrs {
                    updates[i] = &naming.Update{Op: naming.Add, Addr: addrs[i]}
                }
                return updates, nil
            }
        }
    }

    // generate etcd Watcher
    rch := w.client.Watch(context.Background(), prefix, etcd3.WithPrefix())
    for wresp := range rch {
        for _, ev := range wresp.Events {
            switch ev.Type {
            case mvccpb.PUT:
                return []*naming.Update{{Op: naming.Add, Addr: string(ev.Kv.Value)}}, nil
            case mvccpb.DELETE:
                return []*naming.Update{{Op: naming.Delete, Addr: string(ev.Kv.Value)}}, nil
            }
        }
    }
    return nil, nil
}

func extractAddrs(resp *etcd3.GetResponse) []string {
    addrs := []string{}

    if resp == nil || resp.Kvs == nil {
        return addrs
    }

    for i := range resp.Kvs {
        if v := resp.Kvs[i].Value; v != nil {
            addrs = append(addrs, string(v))
        }
    }

    return addrs
}

3)服务注册实现:register.go

package etcdv3

import (
    "fmt"
    "log"
    "strings"
    "time"

    etcd3 "github.com/coreos/etcd/clientv3"
    "golang.org/x/net/context"
    "github.com/coreos/etcd/etcdserver/api/v3rpc/rpctypes"
)

// Prefix should start and end with no slash
var Prefix = "etcd3_naming"
var client etcd3.Client
var serviceKey string

var stopSignal = make(chan bool, 1)

// Register
func Register(name string, host string, port int, target string, interval time.Duration, ttl int) error {
    serviceValue := fmt.Sprintf("%s:%d", host, port)
    serviceKey = fmt.Sprintf("/%s/%s/%s", Prefix, name, serviceValue)

    // get endpoints for register dial address
    var err error
    client, err := etcd3.New(etcd3.Config{
        Endpoints: strings.Split(target, ","),
    })
    if err != nil {
        return fmt.Errorf("grpclb: create etcd3 client failed: %v", err)
    }

    go func() {
        // invoke self-register with ticker
        ticker := time.NewTicker(interval)
        for {
            // minimum lease TTL is ttl-second
            resp, _ := client.Grant(context.TODO(), int64(ttl))
            // should get first, if not exist, set it
            _, err := client.Get(context.Background(), serviceKey)
            if err != nil {
                if err == rpctypes.ErrKeyNotFound {
                    if _, err := client.Put(context.TODO(), serviceKey, serviceValue, etcd3.WithLease(resp.ID)); err != nil {
                        log.Printf("grpclb: set service '%s' with ttl to etcd3 failed: %s", name, err.Error())
                    }
                } else {
                    log.Printf("grpclb: service '%s' connect to etcd3 failed: %s", name, err.Error())
                }
            } else {
                // refresh set to true for not notifying the watcher
                if _, err := client.Put(context.Background(), serviceKey, serviceValue, etcd3.WithLease(resp.ID)); err != nil {
                    log.Printf("grpclb: refresh service '%s' with ttl to etcd3 failed: %s", name, err.Error())
                }
            }
            select {
            case <-stopSignal:
                return
            case <-ticker.C:
            }
        }
    }()

    return nil
}

// UnRegister delete registered service from etcd
func UnRegister() error {
    stopSignal <- true
    stopSignal = make(chan bool, 1) // just a hack to avoid multi UnRegister deadlock
    var err error;
    if _, err := client.Delete(context.Background(), serviceKey); err != nil {
        log.Printf("grpclb: deregister '%s' failed: %s", serviceKey, err.Error())
    } else {
        log.Printf("grpclb: deregister '%s' ok.", serviceKey)
    }
    return err
}

4)接口描述文件:helloworld.proto

syntax = "proto3";

option java_multiple_files = true;
option java_package = "com.midea.jr.test.grpc";
option java_outer_classname = "HelloWorldProto";
option objc_class_prefix = "HLW";

package helloworld;

// The greeting service definition.
service Greeter {
    //   Sends a greeting
    rpc SayHello (HelloRequest) returns (HelloReply) {
    }
}

// The request message containing the user's name.
message HelloRequest {
    string name = 1;
}

// The response message containing the greetings
message HelloReply {
    string message = 1;
}

5)实现服务端接口:helloworldserver.go

package main

import (
    "flag"
    "fmt"
    "log"
    "net"
    "os"
    "os/signal"
    "syscall"
    "time"

    "golang.org/x/net/context"
    "google.golang.org/grpc"

    grpclb "com.midea/jr/grpclb/naming/etcd/v3"
    "com.midea/jr/grpclb/example/pb"
)

var (
    serv = flag.String("service", "hello_service", "service name")
    port = flag.Int("port", 50001, "listening port")
    reg = flag.String("reg", "http://127.0.0.1:2379", "register etcd address")
)

func main() {
    flag.Parse()

    lis, err := net.Listen("tcp", fmt.Sprintf("0.0.0.0:%d", *port))
    if err != nil {
        panic(err)
    }

    err = grpclb.Register(*serv, "127.0.0.1", *port, *reg, time.Second*10, 15)
    if err != nil {
        panic(err)
    }

    ch := make(chan os.Signal, 1)
    signal.Notify(ch, syscall.SIGTERM, syscall.SIGINT, syscall.SIGKILL, syscall.SIGHUP, syscall.SIGQUIT)
    go func() {
        s := <-ch
        log.Printf("receive signal '%v'", s)
        grpclb.UnRegister()
        os.Exit(1)
    }()

    log.Printf("starting hello service at %d", *port)
    s := grpc.NewServer()
    pb.RegisterGreeterServer(s, &server{})
    s.Serve(lis)
}

// server is used to implement helloworld.GreeterServer.
type server struct{}

// SayHello implements helloworld.GreeterServer
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
    fmt.Printf("%v: Receive is %s\n", time.Now(), in.Name)
    return &pb.HelloReply{Message: "Hello " + in.Name}, nil
}

6)实现客户端接口:helloworldclient.go

package main

import (
    "flag"
    "fmt"
    "time"

    grpclb "com.midea/jr/grpclb/naming/etcd/v3"
    "com.midea/jr/grpclb/example/pb"
    "golang.org/x/net/context"
    "google.golang.org/grpc"
    "strconv"
)

var (
    serv = flag.String("service", "hello_service", "service name")
    reg = flag.String("reg", "http://127.0.0.1:2379", "register etcd address")
)

func main() {
    flag.Parse()
    r := grpclb.NewResolver(*serv)
    b := grpc.RoundRobin(r)

    ctx, _ := context.WithTimeout(context.Background(), 10*time.Second)
    conn, err := grpc.DialContext(ctx, *reg, grpc.WithInsecure(), grpc.WithBalancer(b))
    if err != nil {
        panic(err)
    }

    ticker := time.NewTicker(1 * time.Second)
    for t := range ticker.C {
        client := pb.NewGreeterClient(conn)
        resp, err := client.SayHello(context.Background(), &pb.HelloRequest{Name: "world " + strconv.Itoa(t.Second())})
        if err == nil {
            fmt.Printf("%v: Reply is %s\n", t, resp.Message)
        }
    }
}

7)运行测试

  1. 运行3个服务端S1、S2、S3,1个客户端C,观察各服务端接收的请求数是否相等?

    clipboard.png

  2. 关闭1个服务端S1,观察请求是否会转移到另外2个服务端?

    clipboard.png

  3. 重新启动S1服务端,观察另外2个服务端请求是否会平均分配到S1?

    clipboard.png

    clipboard.png

  4. 关闭Etcd3服务器,观察客户端与服务端通信是否正常?
    关闭通信仍然正常,但新服务端不会注册进来,服务端掉线了也无法摘除掉。

  5. 重新启动Etcd3服务器,服务端上下线可自动恢复正常。

  6. 关闭所有服务端,客户端请求将被阻塞。

参考:

http://www.grpc.io/docs/
https://github.com/grpc/grpc/blob/master/doc/load-balancing.md
查看原文

吴振宇 赞了文章 · 2018-03-23

gRPC服务发现&负载均衡

gRPC服务发现&负载均衡

构建高可用、高性能的通信服务,通常采用服务注册与发现、负载均衡和容错处理等机制实现。根据负载均衡实现所在的位置不同,通常可分为以下三种解决方案:

1、集中式LB(Proxy Model)

clipboard.png

在服务消费者和服务提供者之间有一个独立的LB,通常是专门的硬件设备如 F5,或者基于软件如 LVS,HAproxy等实现。LB上有所有服务的地址映射表,通常由运维配置注册,当服务消费方调用某个目标服务时,它向LB发起请求,由LB以某种策略,比如轮询(Round-Robin)做负载均衡后将请求转发到目标服务。LB一般具备健康检查能力,能自动摘除不健康的服务实例。 该方案主要问题:

  1. 单点问题,所有服务调用流量都经过LB,当服务数量和调用量大的时候,LB容易成为瓶颈,且一旦LB发生故障影响整个系统;

  2. 服务消费方、提供方之间增加了一级,有一定性能开销。

2、进程内LB(Balancing-aware Client)

clipboard.png

针对第一个方案的不足,此方案将LB的功能集成到服务消费方进程里,也被称为软负载或者客户端负载方案。服务提供方启动时,首先将服务地址注册到服务注册表,同时定期报心跳到服务注册表以表明服务的存活状态,相当于健康检查,服务消费方要访问某个服务时,它通过内置的LB组件向服务注册表查询,同时缓存并定期刷新目标服务地址列表,然后以某种负载均衡策略选择一个目标服务地址,最后向目标服务发起请求。LB和服务发现能力被分散到每一个服务消费者的进程内部,同时服务消费方和服务提供方之间是直接调用,没有额外开销,性能比较好。该方案主要问题:

  1. 开发成本,该方案将服务调用方集成到客户端的进程里头,如果有多种不同的语言栈,就要配合开发多种不同的客户端,有一定的研发和维护成本;

  2. 另外生产环境中,后续如果要对客户库进行升级,势必要求服务调用方修改代码并重新发布,升级较复杂。

3、独立 LB 进程(External Load Balancing Service)

clipboard.png

该方案是针对第二种方案的不足而提出的一种折中方案,原理和第二种方案基本类似。
不同之处是将LB和服务发现功能从进程内移出来,变成主机上的一个独立进程。主机上的一个或者多个服务要访问目标服务时,他们都通过同一主机上的独立LB进程做服务发现和负载均衡。该方案也是一种分布式方案没有单点问题,一个LB进程挂了只影响该主机上的服务调用方,服务调用方和LB之间是进程内调用性能好,同时该方案还简化了服务调用方,不需要为不同语言开发客户库,LB的升级不需要服务调用方改代码。
该方案主要问题:部署较复杂,环节多,出错调试排查问题不方便。

gRPC服务发现及负载均衡实现

gRPC开源组件官方并未直接提供服务注册与发现的功能实现,但其设计文档已提供实现的思路,并在不同语言的gRPC代码API中已提供了命名解析和负载均衡接口供扩展。

clipboard.png

其基本实现原理:

  1. 服务启动后gRPC客户端向命名服务器发出名称解析请求,名称将解析为一个或多个IP地址,每个IP地址标示它是服务器地址还是负载均衡器地址,以及标示要使用那个客户端负载均衡策略或服务配置。

  2. 客户端实例化负载均衡策略,如果解析返回的地址是负载均衡器地址,则客户端将使用grpclb策略,否则客户端使用服务配置请求的负载均衡策略。

  3. 负载均衡策略为每个服务器地址创建一个子通道(channel)。

  4. 当有rpc请求时,负载均衡策略决定那个子通道即grpc服务器将接收请求,当可用服务器为空时客户端的请求将被阻塞。

根据gRPC官方提供的设计思路,基于进程内LB方案(即第2个案,阿里开源的服务框架 Dubbo 也是采用类似机制),结合分布式一致的组件(如Zookeeper、Consul、Etcd),可找到gRPC服务发现和负载均衡的可行解决方案。接下来以GO语言为例,简单介绍下基于Etcd3的关键代码实现:

1)命名解析实现:resolver.go

package etcdv3

import (
    "errors"
    "fmt"
    "strings"

    etcd3 "github.com/coreos/etcd/clientv3"
    "google.golang.org/grpc/naming"
)

// resolver is the implementaion of grpc.naming.Resolver
type resolver struct {
    serviceName string // service name to resolve
}

// NewResolver return resolver with service name
func NewResolver(serviceName string) *resolver {
    return &resolver{serviceName: serviceName}
}

// Resolve to resolve the service from etcd, target is the dial address of etcd
// target example: "http://127.0.0.1:2379,http://127.0.0.1:12379,http://127.0.0.1:22379"
func (re *resolver) Resolve(target string) (naming.Watcher, error) {
    if re.serviceName == "" {
        return nil, errors.New("grpclb: no service name provided")
    }

    // generate etcd client
    client, err := etcd3.New(etcd3.Config{
        Endpoints: strings.Split(target, ","),
    })
    if err != nil {
        return nil, fmt.Errorf("grpclb: creat etcd3 client failed: %s", err.Error())
    }

    // Return watcher
    return &watcher{re: re, client: *client}, nil
}

2)服务发现实现:watcher.go

package etcdv3

import (
    "fmt"
    etcd3 "github.com/coreos/etcd/clientv3"
    "golang.org/x/net/context"
    "google.golang.org/grpc/naming"
    "github.com/coreos/etcd/mvcc/mvccpb"
)

// watcher is the implementaion of grpc.naming.Watcher
type watcher struct {
    re            *resolver // re: Etcd Resolver
    client        etcd3.Client
    isInitialized bool
}

// Close do nothing
func (w *watcher) Close() {
}

// Next to return the updates
func (w *watcher) Next() ([]*naming.Update, error) {
    // prefix is the etcd prefix/value to watch
    prefix := fmt.Sprintf("/%s/%s/", Prefix, w.re.serviceName)

    // check if is initialized
    if !w.isInitialized {
        // query addresses from etcd
        resp, err := w.client.Get(context.Background(), prefix, etcd3.WithPrefix())
        w.isInitialized = true
        if err == nil {
            addrs := extractAddrs(resp)
            //if not empty, return the updates or watcher new dir
            if l := len(addrs); l != 0 {
                updates := make([]*naming.Update, l)
                for i := range addrs {
                    updates[i] = &naming.Update{Op: naming.Add, Addr: addrs[i]}
                }
                return updates, nil
            }
        }
    }

    // generate etcd Watcher
    rch := w.client.Watch(context.Background(), prefix, etcd3.WithPrefix())
    for wresp := range rch {
        for _, ev := range wresp.Events {
            switch ev.Type {
            case mvccpb.PUT:
                return []*naming.Update{{Op: naming.Add, Addr: string(ev.Kv.Value)}}, nil
            case mvccpb.DELETE:
                return []*naming.Update{{Op: naming.Delete, Addr: string(ev.Kv.Value)}}, nil
            }
        }
    }
    return nil, nil
}

func extractAddrs(resp *etcd3.GetResponse) []string {
    addrs := []string{}

    if resp == nil || resp.Kvs == nil {
        return addrs
    }

    for i := range resp.Kvs {
        if v := resp.Kvs[i].Value; v != nil {
            addrs = append(addrs, string(v))
        }
    }

    return addrs
}

3)服务注册实现:register.go

package etcdv3

import (
    "fmt"
    "log"
    "strings"
    "time"

    etcd3 "github.com/coreos/etcd/clientv3"
    "golang.org/x/net/context"
    "github.com/coreos/etcd/etcdserver/api/v3rpc/rpctypes"
)

// Prefix should start and end with no slash
var Prefix = "etcd3_naming"
var client etcd3.Client
var serviceKey string

var stopSignal = make(chan bool, 1)

// Register
func Register(name string, host string, port int, target string, interval time.Duration, ttl int) error {
    serviceValue := fmt.Sprintf("%s:%d", host, port)
    serviceKey = fmt.Sprintf("/%s/%s/%s", Prefix, name, serviceValue)

    // get endpoints for register dial address
    var err error
    client, err := etcd3.New(etcd3.Config{
        Endpoints: strings.Split(target, ","),
    })
    if err != nil {
        return fmt.Errorf("grpclb: create etcd3 client failed: %v", err)
    }

    go func() {
        // invoke self-register with ticker
        ticker := time.NewTicker(interval)
        for {
            // minimum lease TTL is ttl-second
            resp, _ := client.Grant(context.TODO(), int64(ttl))
            // should get first, if not exist, set it
            _, err := client.Get(context.Background(), serviceKey)
            if err != nil {
                if err == rpctypes.ErrKeyNotFound {
                    if _, err := client.Put(context.TODO(), serviceKey, serviceValue, etcd3.WithLease(resp.ID)); err != nil {
                        log.Printf("grpclb: set service '%s' with ttl to etcd3 failed: %s", name, err.Error())
                    }
                } else {
                    log.Printf("grpclb: service '%s' connect to etcd3 failed: %s", name, err.Error())
                }
            } else {
                // refresh set to true for not notifying the watcher
                if _, err := client.Put(context.Background(), serviceKey, serviceValue, etcd3.WithLease(resp.ID)); err != nil {
                    log.Printf("grpclb: refresh service '%s' with ttl to etcd3 failed: %s", name, err.Error())
                }
            }
            select {
            case <-stopSignal:
                return
            case <-ticker.C:
            }
        }
    }()

    return nil
}

// UnRegister delete registered service from etcd
func UnRegister() error {
    stopSignal <- true
    stopSignal = make(chan bool, 1) // just a hack to avoid multi UnRegister deadlock
    var err error;
    if _, err := client.Delete(context.Background(), serviceKey); err != nil {
        log.Printf("grpclb: deregister '%s' failed: %s", serviceKey, err.Error())
    } else {
        log.Printf("grpclb: deregister '%s' ok.", serviceKey)
    }
    return err
}

4)接口描述文件:helloworld.proto

syntax = "proto3";

option java_multiple_files = true;
option java_package = "com.midea.jr.test.grpc";
option java_outer_classname = "HelloWorldProto";
option objc_class_prefix = "HLW";

package helloworld;

// The greeting service definition.
service Greeter {
    //   Sends a greeting
    rpc SayHello (HelloRequest) returns (HelloReply) {
    }
}

// The request message containing the user's name.
message HelloRequest {
    string name = 1;
}

// The response message containing the greetings
message HelloReply {
    string message = 1;
}

5)实现服务端接口:helloworldserver.go

package main

import (
    "flag"
    "fmt"
    "log"
    "net"
    "os"
    "os/signal"
    "syscall"
    "time"

    "golang.org/x/net/context"
    "google.golang.org/grpc"

    grpclb "com.midea/jr/grpclb/naming/etcd/v3"
    "com.midea/jr/grpclb/example/pb"
)

var (
    serv = flag.String("service", "hello_service", "service name")
    port = flag.Int("port", 50001, "listening port")
    reg = flag.String("reg", "http://127.0.0.1:2379", "register etcd address")
)

func main() {
    flag.Parse()

    lis, err := net.Listen("tcp", fmt.Sprintf("0.0.0.0:%d", *port))
    if err != nil {
        panic(err)
    }

    err = grpclb.Register(*serv, "127.0.0.1", *port, *reg, time.Second*10, 15)
    if err != nil {
        panic(err)
    }

    ch := make(chan os.Signal, 1)
    signal.Notify(ch, syscall.SIGTERM, syscall.SIGINT, syscall.SIGKILL, syscall.SIGHUP, syscall.SIGQUIT)
    go func() {
        s := <-ch
        log.Printf("receive signal '%v'", s)
        grpclb.UnRegister()
        os.Exit(1)
    }()

    log.Printf("starting hello service at %d", *port)
    s := grpc.NewServer()
    pb.RegisterGreeterServer(s, &server{})
    s.Serve(lis)
}

// server is used to implement helloworld.GreeterServer.
type server struct{}

// SayHello implements helloworld.GreeterServer
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
    fmt.Printf("%v: Receive is %s\n", time.Now(), in.Name)
    return &pb.HelloReply{Message: "Hello " + in.Name}, nil
}

6)实现客户端接口:helloworldclient.go

package main

import (
    "flag"
    "fmt"
    "time"

    grpclb "com.midea/jr/grpclb/naming/etcd/v3"
    "com.midea/jr/grpclb/example/pb"
    "golang.org/x/net/context"
    "google.golang.org/grpc"
    "strconv"
)

var (
    serv = flag.String("service", "hello_service", "service name")
    reg = flag.String("reg", "http://127.0.0.1:2379", "register etcd address")
)

func main() {
    flag.Parse()
    r := grpclb.NewResolver(*serv)
    b := grpc.RoundRobin(r)

    ctx, _ := context.WithTimeout(context.Background(), 10*time.Second)
    conn, err := grpc.DialContext(ctx, *reg, grpc.WithInsecure(), grpc.WithBalancer(b))
    if err != nil {
        panic(err)
    }

    ticker := time.NewTicker(1 * time.Second)
    for t := range ticker.C {
        client := pb.NewGreeterClient(conn)
        resp, err := client.SayHello(context.Background(), &pb.HelloRequest{Name: "world " + strconv.Itoa(t.Second())})
        if err == nil {
            fmt.Printf("%v: Reply is %s\n", t, resp.Message)
        }
    }
}

7)运行测试

  1. 运行3个服务端S1、S2、S3,1个客户端C,观察各服务端接收的请求数是否相等?

    clipboard.png

  2. 关闭1个服务端S1,观察请求是否会转移到另外2个服务端?

    clipboard.png

  3. 重新启动S1服务端,观察另外2个服务端请求是否会平均分配到S1?

    clipboard.png

    clipboard.png

  4. 关闭Etcd3服务器,观察客户端与服务端通信是否正常?
    关闭通信仍然正常,但新服务端不会注册进来,服务端掉线了也无法摘除掉。

  5. 重新启动Etcd3服务器,服务端上下线可自动恢复正常。

  6. 关闭所有服务端,客户端请求将被阻塞。

参考:

http://www.grpc.io/docs/
https://github.com/grpc/grpc/blob/master/doc/load-balancing.md
查看原文

赞 56 收藏 93 评论 21

吴振宇 赞了问题 · 2018-03-02

解决swoole websocket服务进行mysql断线重连不生效的问题

代码结构大致是这样的:

<?php

class server
{
    private $server;
    private $conn = null;

    public function __construct()
    {
        if (!$this->initDb()) exit("终止启动\n"); //连接数据库
        $this->server = new swoole_websocket_server('111.111.111.111', 1234); //实例化server
        //消息
        $this->server->on('message', function (swoole_websocket_server $server, $frame) {
            //这里使用数据库连接$conn
        });
        //work进程开启
        $this->server->on('workerStart', function (swoole_websocket_server $server, $worker_id) {
            if ($worker_id == 0) {
                // 每10秒检测一次数据库连接
                $server->tick(10 * 1000, function ($timer_id) {
                    if (!$this->conn->ping()) {
                        echo "数据库已断开!正在尝试重新连接...\n";
                        $this->initDb(); //连接数据库
                    }
                });
            }
        });
        $this->server->start();
    }

    // 连接数据库
    private function initDb() {
        $conn = new mysqli('127.0.0.1', 'root','root','test', 3306);
        if ($conn->connect_errno) {
            printf("数据库连接失败: %s\n", $conn->connect_error);
            return false;
        } else {
            $conn->set_charset("utf8");
            echo "连接数据库成功!\n";
            $this->conn = $conn;
            return true;
        }
    }
}
new server();

每十秒检测mysql连接状态,如果断开连接则重新走initDb,this->$conn重新赋值。 然后我手动重启数据库,程序检测到数据库断开之后进行重连,并且连接成功。

但是当接收到消息事件使用$conn时,却还是提示mysql server has gone away,明明已经重新连接了啊。

我猜想,是不是因为swoole server在注册事件时就绑定了所使用到的变量,所以conn虽然重新赋值了,但是并没有生效到swoole server里?

关注 3 回答 2

吴振宇 收藏了文章 · 2018-02-26

Swoole 2.1 正式版发布,协程+通道带来全新的 PHP 编程模式

PHP的异步、并行、高性能网络通信引擎 Swoole 已发布 2.1.0 版本。新版本提供了全新的短名 API,完整支持了协程(Coroutine)+通道(Channel)特性,为 PHP 语言带来了全新的编程模式。Swoole 2.1API借鉴至Go语言,在此向Go语言开发组致敬。

Coroutine

go(function () {
    co::sleep(0.5);
    echo "hello";
});
go("test");
go([$object, "method"]);

Channel

$chan = new chan(128);
$chan->push(1234);
$chan->push(1234.56);
$chan->push("hello world");
$chan->push(["hello world"]);
$chan->push(new stdclass);
$chan->push(fopen("test.txt", "r+"));
while($chan->pop());

Go语言的chan不同,由于PHP是动态语言,所以可以向通道内投递任意类型的变量。

Channel Select

$c1 = new chan(3);
$c2 = new chan(2);
$c3 = new chan(2);
$c4 = new chan(2);

$c3->push(3);
$c3->push(3.1415);

$c4->push(3);
$c4->push(3.1415);

go(function () use ($c1, $c2, $c3, $c4) {
    echo "select\n";
    for ($i = 0; $i < 1; $i++)
    {
        $read_list = [$c1, $c2];
        $write_list = [$c3, $c4];
        // $write_list = null;
        $result = chan::select($read_list, $write_list, 5);
        var_dump($result, $read_list, $write_list);

        foreach($read_list as $ch)
        {
            var_dump($ch->pop());
        }

        foreach($write_list as $ch)
        {
            var_dump($ch->push(666));
        }
        echo "exit\n";
    }
});

go(function () use ($c3, $c4) {
    echo "producer\n";
    co::sleep(1);
    $data = $c3->pop();
    echo "pop[1]\n";
    var_dump($data);
});

go(function () {
    co::sleep(10);
});

go(function () use ($c1, $c2) {

    co::sleep(1);
    $c1->push("resume");
    $c2->push("hello");
});

MySQL Client

go(function () {
    $db = new Co\MySQL();
    $server = array(
        'host' => '127.0.0.1',
        'user' => 'root',
        'password' => 'root',
        'database' => 'test',
    );

    $db->connect($server);

    $result = $db->query('SELECT * FROM userinfo WHERE id = 3');
    var_dump($result);
});

Redis Client

go(function () {
    $redis = new Co\Redis;
    $res = $redis->connect('127.0.0.1', 6379);
    $ret = $redis->set('key', 'value');
    var_dump($redis->get('key'));
});

Http Client

go(function () {
    $http = new Co\Http\Client("www.google.com", 443, true);
    $http->setHeaders(function () {
        
    });
    $ret = $http->get('/');
    var_dump($http->body);
});

Http2 Client

go(function () {
    $http = new Co\Http2\Client("www.google.com", 443, true);
    $req = new co\Http2\Request;
    $req->path = "/index.html";
    $req->headers = [
        'host' => "www.google.com",
        "user-agent" => 'Chrome/49.0.2587.3',
        'accept' => 'text/html,application/xhtml+xml,application/xml',
        'accept-encoding' => 'gzip',
    ];
    $req->cookies = ['name' => 'rango', 'email' => 'rango@swoole.com'];
    $ret = $http->send($req);
    var_dump($http->recv());
});

其他 API

co::sleep(100);
co::fread($fp);
co::fwrite($fp, "hello world");
co::gethostbyname('www.google.com');

服务器端

$server = new Co\Http\Server('127.0.0.1', 9501);

$server->on('Request', function($request, $response) {

    $http = new Co\Http\Client("www.google.com", 443, true);
    $http->setHeaders(function () {
        "X-Power-By" => "Swoole/2.1.0",
    });
    $ret = $http->get('/');
 
    if ($ret) {
        $response->end($http->body);
    }
    else{
        $response->end("recv failed error : {$http->errCode}");
    }
});

$server->start();

Swoole提供了很多Co\ServerCo\WebSocket\ServerCo\Http\ServerCo\Redis\Server4个支持协程的Server类,可以在这些服务器程序中使用协程API

查看原文

吴振宇 发布了文章 · 2018-02-24

Will PHP die in 2018?

事实上,你在2018年死亡的机会远远高于PHP。

看到Quora的回答很有意思,分享给大家,本文基于Quora的回答做一些延伸。

PHP拥有庞大的用户群

  • 27.8 % 网站使用 WordPress [1]
  • 3.3% 网站使用 Joomla
  • 2.2% of 网站使用 Drupal (including some government)
  • 1.2% of 网站使用 Magento

其中共有34.5%[2]的网站是使用流行的PHP框架构建的,大概全球有83.1% [3]的网站是使用php语言构建的

在PHP死之前:

  • Facebook and WordPress 要die掉.
  • Wikipedia 要die掉.
  • Yahoo 要die掉.
  • Tesla’s 的网站要die掉, Cloudflare, Slack 也是.
  • Flickr 要die掉. Tumblr 要die掉. Photobucket 也得die掉.
  • Magento和许多世界级的电子商务解决方案都不得不消亡
  • SourceForge 要die掉.
  • MailChimp(由它管理的数百万商业邮件列表)不得不die掉

所以PHP也 不会轻易死掉。

我们不讨论语言之争,但有很多情况下并不需要强类型语言,解决同样的问题,强类型语言可能需要更多的工作量。有人说强类型不容易出错,你却不知道PHP7也支持指定类型,PHP一直在进步。

PHP一直在积极的维护和升级

PHP已经30年了是的,旧的东西可能很难看,但几乎任何10年前的代码今天都不会符合当前的语言标准,每个语言都有他的缺点。即使有些公司、开发者 喜新厌旧的尝各种新语言,全球仍然有成千上万的PHPer的力量源泉支持,并且有Zend 公司的背书,PHP 必定也会与时俱进的迭代、打磨 ,PHP仍然在积极的维护和升级,PHP7版本,开发组对性能极致要求的理念 ,对其进行了翻天覆地的更新就已经证明了这一点。

让人意外的惊喜,Quora其中一个外国友人高度赞赏 『Swoole将PHP变成一个疯狂的强大的异步工具,用于创建可扩展的后端.』 韩老师十年如一日的维护精神鼓舞着国内PHPer,现在已经影响了全球的使用者,现在swoole更是增加了协程的特性,这种探索精神值得我们学习、称赞。PHP的底层是c语言,有极其强大的扩展机制,透露出无限的可能,其中生态发展需要各位智慧结晶 ,使其愈发强大。

正视自己

在 WEB时代PHP是王者,现在应用终端多方面发展,互联网用户爆发式增长,如今,我们不否认PHP 在有些地方能力的欠缺,比如微服务的构建,常驻内存的服务级系统,密集计算,大数据的生态构建等等等等,这些都是我们努力的方向,我们并不会抱着历史的优越web数据的地位 一直洋洋自得,那样真的会死。Swoft 是目前看到正在努力的微服务构建开源框架,每天都在紧锣密鼓的提交代码,Swoole不就是一个常驻内存的例子?其实各个项目大家都可以参与其中,也可以琢磨另外自己感兴趣的方向,保守和固步自封,抱着自己那点技术生怕别人学走是不对的,分享和探索,积极的交流,风向正气,才是进步的方向。

By the way, PHP2018 不是死,而是崛起!

原文链接:http://kuangjue.com/article/317
相关数据资料引用:

[1] 42 Amazing WordPress Statistics

[2] Compare Top 3 CMS (2017): WordPress vs. Joomla vs. Drupal

[3] Usage Statistics and Market Share of Server-side Programming Languages for Websites, January 2018

查看原文

赞 15 收藏 5 评论 14

吴振宇 赞了文章 · 2018-02-09

Swoole 2.1 正式版发布,协程+通道带来全新的 PHP 编程模式

PHP的异步、并行、高性能网络通信引擎 Swoole 已发布 2.1.0 版本。新版本提供了全新的短名 API,完整支持了协程(Coroutine)+通道(Channel)特性,为 PHP 语言带来了全新的编程模式。Swoole 2.1API借鉴至Go语言,在此向Go语言开发组致敬。

Coroutine

go(function () {
    co::sleep(0.5);
    echo "hello";
});
go("test");
go([$object, "method"]);

Channel

$chan = new chan(128);
$chan->push(1234);
$chan->push(1234.56);
$chan->push("hello world");
$chan->push(["hello world"]);
$chan->push(new stdclass);
$chan->push(fopen("test.txt", "r+"));
while($chan->pop());

Go语言的chan不同,由于PHP是动态语言,所以可以向通道内投递任意类型的变量。

Channel Select

$c1 = new chan(3);
$c2 = new chan(2);
$c3 = new chan(2);
$c4 = new chan(2);

$c3->push(3);
$c3->push(3.1415);

$c4->push(3);
$c4->push(3.1415);

go(function () use ($c1, $c2, $c3, $c4) {
    echo "select\n";
    for ($i = 0; $i < 1; $i++)
    {
        $read_list = [$c1, $c2];
        $write_list = [$c3, $c4];
        // $write_list = null;
        $result = chan::select($read_list, $write_list, 5);
        var_dump($result, $read_list, $write_list);

        foreach($read_list as $ch)
        {
            var_dump($ch->pop());
        }

        foreach($write_list as $ch)
        {
            var_dump($ch->push(666));
        }
        echo "exit\n";
    }
});

go(function () use ($c3, $c4) {
    echo "producer\n";
    co::sleep(1);
    $data = $c3->pop();
    echo "pop[1]\n";
    var_dump($data);
});

go(function () {
    co::sleep(10);
});

go(function () use ($c1, $c2) {

    co::sleep(1);
    $c1->push("resume");
    $c2->push("hello");
});

MySQL Client

go(function () {
    $db = new Co\MySQL();
    $server = array(
        'host' => '127.0.0.1',
        'user' => 'root',
        'password' => 'root',
        'database' => 'test',
    );

    $db->connect($server);

    $result = $db->query('SELECT * FROM userinfo WHERE id = 3');
    var_dump($result);
});

Redis Client

go(function () {
    $redis = new Co\Redis;
    $res = $redis->connect('127.0.0.1', 6379);
    $ret = $redis->set('key', 'value');
    var_dump($redis->get('key'));
});

Http Client

go(function () {
    $http = new Co\Http\Client("www.google.com", 443, true);
    $http->setHeaders(function () {
        
    });
    $ret = $http->get('/');
    var_dump($http->body);
});

Http2 Client

go(function () {
    $http = new Co\Http2\Client("www.google.com", 443, true);
    $req = new co\Http2\Request;
    $req->path = "/index.html";
    $req->headers = [
        'host' => "www.google.com",
        "user-agent" => 'Chrome/49.0.2587.3',
        'accept' => 'text/html,application/xhtml+xml,application/xml',
        'accept-encoding' => 'gzip',
    ];
    $req->cookies = ['name' => 'rango', 'email' => 'rango@swoole.com'];
    $ret = $http->send($req);
    var_dump($http->recv());
});

其他 API

co::sleep(100);
co::fread($fp);
co::fwrite($fp, "hello world");
co::gethostbyname('www.google.com');

服务器端

$server = new Co\Http\Server('127.0.0.1', 9501);

$server->on('Request', function($request, $response) {

    $http = new Co\Http\Client("www.google.com", 443, true);
    $http->setHeaders(function () {
        "X-Power-By" => "Swoole/2.1.0",
    });
    $ret = $http->get('/');
 
    if ($ret) {
        $response->end($http->body);
    }
    else{
        $response->end("recv failed error : {$http->errCode}");
    }
});

$server->start();

Swoole提供了很多Co\ServerCo\WebSocket\ServerCo\Http\ServerCo\Redis\Server4个支持协程的Server类,可以在这些服务器程序中使用协程API

查看原文

赞 88 收藏 71 评论 28

吴振宇 赞了文章 · 2018-01-24

聊聊 2018 年后端技术趋势

今天太忙,少写一点,后面再补充。

异步模式

Go 语言越来越热门,很多大型互联网公司后端正在转向 GO 。Java 圈知名的服务化框架 Dubbo 也宣布转型异步模式。这是一个大趋势,异步模式已经被市场验证和认可。

在 Web 服务器选择上,几年前大部分人就开始选择异步非阻塞的 Nginx,而不是同步阻塞的 Apache。就是因为 Nginx 这样的异步程序,它的适应性更好、并发能力更强。现在在后端业务开发编程方面,技术力量强的团队已经开始将技术栈从同步模式切换为异步了。

同步阻塞模式存在较多缺陷,并发能力弱、适应性差、慢速请求导致服务不可用。如:后台接口中调用第三方 API 的场景,同步模式效果极差。过去那些使用 Java、PHP、C++、Python、Ruby 语言开发的同步阻塞模式框架,用的人越来越少。

Node.js

虽然 Node.js 也很热门,很流行,但仍然很少见到企业将 Node.js 作为公司后端方面的主要编程语言。C++、Java、PHP、Python 语言同样也有一些类似的方案,包括 Swoole-1.0 也是基于类似于 Node.js 的异步回调模式。

本质原因是异步回调的技术方案,以及在它之上所做的一些优化方案,包括 Promise、Future、Yield/Generator、Async/Await 等,改变了程序开发的风格和习惯。如果要使用这些技术,那么工程师需要额外学习这些关键词和函数的使用方法。

使用这些技术方案是无法兼容已有程序的。可以说研发成本巨大,难以平滑过渡。影响了异步回调技术栈的普及。这种编程模式很难让所有人都接受。

协程

而协程模式,兼顾了同步阻塞的可维护性和异步非阻塞的高并发能力。将会成为未来后端开发领域的主流技术方案。

最重要的,协程模式只需要对已有项目代码进行少量调整就可以运行起来,甚至可以完全兼容老项目。只需要框架层进行兼容即可。这使得团队可以平滑过渡。

使用协程模式,开发者完全不需要学习额外的关键词和函数用法。编码风格与同步模式下是完全一致的。

各种协程技术里,GO 是最耀眼的那一个。协程、通道、静态语言、性能、富编译、标准库丰富、生态完整、Google 等,优势太多了。GO 语言,个人认为是目前所有编程语言中,最值得学习的

Swoole 2.x 让 PHP 这门 20多年历史的老牌后台编程语言也能有协程的能力。相比 Go 这样的技术,PHP + Swoole 的技术栈,更适合快速开发、快速迭代、业务驱动的场景。毕竟动态语言比静态语言还是要更加灵活、开发效率更高。而 Go 更适合编写系统级软件、核心业务。

2018 年我重构了 swoole framework 这个很老的项目,底层支持 Swoole 2.x 协程模式。主要原因是车轮公司内部有很多项目基于这个框架,尤其是服务层全部用了 swoole framework。我们希望业务代码一行不改,可以一键切换为协程模式。理论上其他的同步阻塞 PHP 框架,如 Laravel 、Yii ,都可以实现只修改底层兼容 Swoole 2.x 协程,实现项目代码无缝切换协程模式。

使用协程后,整个系统的性能、并发能力、稳定性有了巨大提升。过去,线上经常出现一个慢请求导致整个服务器卡住的问题不存在了。

PHP & Swoole

虽然 Swoole 2.0 只有不到两年的历史,相比 Go 语言 10 年的耕耘,还有很长一段路要走。但相比 GO 这样的静态语言,PHP + Swoole 还是有很多优势的,PHP 更加简单易用,PHP 是动态语言,使用起来更加灵活。

当然,如果是新项目还是推荐使用 Swoft 这个专门为 Swoole 2.x 的框架,它的历史包袱更少,因此稳定性更好。

现在有了 Swoole 2.0 协程,我们对 PHP 的未来仍然充满信心。

查看原文

赞 230 收藏 203 评论 48

吴振宇 关注了标签 · 2018-01-18

php

PHP,是英文超文本预处理语言 Hypertext Preprocessor 的缩写。PHP 是一种开源的通用计算机脚本语言,尤其适用于网络开发并可嵌入HTML 中使用。PHP 的语法借鉴吸收 C语言、Java 和 Perl 等流行计算机语言的特点,易于一般程序员学习。(目前是 Web 开发性价比最高的语言)

关注 91668

吴振宇 关注了标签 · 2018-01-18

java

Java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的 Java 程序设计语言和 Java 平台(即 JavaSE, JavaEE, JavaME)的总称。Java 技术具有卓越的通用性、高效性、平台移植性和安全性。

Java编程语言的风格十分接近 C++ 语言。继承了 C++ 语言面向对象技术的核心,Java舍弃了 C++ 语言中容易引起错误的指針,改以引用取代,同时卸载原 C++ 与原来运算符重载,也卸载多重继承特性,改用接口取代,增加垃圾回收器功能。在 Java SE 1.5 版本中引入了泛型编程、类型安全的枚举、不定长参数和自动装/拆箱特性。太阳微系统对 Java 语言的解释是:“Java编程语言是个简单、面向对象、分布式、解释性、健壮、安全与系统无关、可移植、高性能、多线程和动态的语言”。

版本历史

重要版本号版本代号发布日期
JDK 1.01996 年 1 月 23 日
JDK 1.11997 年 2 月 19 日
J2SE 1.2Playground1998 年 12 月 8 日
J2SE 1.3Kestrel2000 年 5 月 8 日
J2SE 1.4Merlin2002 年 2 月 6 日
J2SE 5.0 (1.5.0)Tiger2004 年 9 月 30 日
Java SE 6Mustang2006 年 11 月 11 日
Java SE 7Dolphin2011 年 7 月 28 日
Java SE 8JSR 3372014 年 3 月 18 日
最新发布的稳定版本:
Java Standard Edition 8 Update 11 (1.8.0_11) - (July 15, 2014)
Java Standard Edition 7 Update 65 (1.7.0_65) - (July 15, 2014)

更详细的版本更新查看 J2SE Code NamesJava version history 维基页面

新手帮助

不知道如何开始写你的第一个 Java 程序?查看 Oracle 的 Java 上手文档

在你遇到问题提问之前,可以先在站内搜索一下关键词,看是否已经存在你想提问的内容。

命名规范

Java 程序应遵循以下的 命名规则,以增加可读性,同时降低偶然误差的概率。遵循这些命名规范,可以让别人更容易理解你的代码。

  • 类型名(类,接口,枚举等)应以大写字母开始,同时大写化后续每个单词的首字母。例如:StringThreadLocaland NullPointerException。这就是著名的帕斯卡命名法。
  • 方法名 应该是驼峰式,即以小写字母开头,同时大写化后续每个单词的首字母。例如:indexOfprintStackTraceinterrupt
  • 字段名 同样是驼峰式,和方法名一样。
  • 常量表达式的名称static final 不可变对象)应该全大写,同时用下划线分隔每个单词。例如:YELLOWDO_NOTHING_ON_CLOSE。这个规范也适用于一个枚举类的值。然而,static final 引用的非不可变对象应该是驼峰式。

Hello World

public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello, World!");
    }
}

编译并调用:

javac -d . HelloWorld.java
java -cp . HelloWorld

Java 的源代码会被编译成可被 Java 命令执行的中间形式(用于 Java 虚拟机的字节代码指令)。

可用的 IDE

学习资源

常见的问题

下面是一些 SegmentFault 上在 Java 方面经常被人问到的问题:

(待补充)

关注 142713

认证与成就

  • 获得 42 次点赞
  • 获得 4 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 4 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

  • php-nsq

    php72扩展开发的 nsq客户端

  • Swoole

    postgresql 协程支持

注册于 2017-04-09
个人主页被 1.2k 人浏览