feat: 添加更多示例

main
NorthLan 3 years ago
parent 357ba1be05
commit bb8918e785

@ -1 +1,284 @@
package client
import (
"git.noahlan.cn/northlan/ngs/internal/codec"
"git.noahlan.cn/northlan/ngs/internal/log"
"git.noahlan.cn/northlan/ngs/internal/message"
"git.noahlan.cn/northlan/ngs/internal/packet"
"google.golang.org/protobuf/proto"
"net"
"sync"
)
var (
hsd []byte // handshake data
had []byte // handshake ack data
)
func init() {
var err error
hsd, err = codec.Encode(packet.Handshake, nil)
if err != nil {
panic(err)
}
had, err = codec.Encode(packet.HandshakeAck, nil)
if err != nil {
panic(err)
}
}
type (
// Callback represents the callback type which will be called
// when the correspond events is occurred.
Callback func(data interface{})
// Client is a tiny Ngs client
Client struct {
conn net.Conn // low-level connection
codec *codec.Decoder // decoder
die chan struct{} // connector close channel
chSend chan []byte // send queue
mid uint64 // message id
// events handler
muEvents sync.RWMutex
events map[string]Callback
// response handler
muResponses sync.RWMutex
responses map[uint64]Callback
connectedCallback func() // connected callback
}
)
// NewClient create a new Client
func NewClient() *Client {
return &Client{
die: make(chan struct{}),
codec: codec.NewDecoder(),
chSend: make(chan []byte, 64),
mid: 1,
events: map[string]Callback{},
responses: map[uint64]Callback{},
}
}
// Start connect to the server and send/recv between the c/s
func (c *Client) Start(addr string) error {
conn, err := net.Dial("tcp", addr)
if err != nil {
return err
}
c.conn = conn
go c.write()
// send handshake packet
c.send(hsd)
// read and process network message
go c.read()
return nil
}
// OnConnected set the callback which will be called when the client connected to the server
func (c *Client) OnConnected(callback func()) {
c.connectedCallback = callback
}
// Request send a request to server and register a callback for the response
func (c *Client) Request(route string, v proto.Message, callback Callback) error {
data, err := serialize(v)
if err != nil {
return err
}
msg := &message.Message{
Type: message.Request,
Route: route,
ID: c.mid,
Data: data,
}
c.setResponseHandler(c.mid, callback)
if err := c.sendMessage(msg); err != nil {
c.setResponseHandler(c.mid, nil)
return err
}
return nil
}
// Notify send a notification to server
func (c *Client) Notify(route string, v proto.Message) error {
data, err := serialize(v)
if err != nil {
return err
}
msg := &message.Message{
Type: message.Notify,
Route: route,
Data: data,
}
return c.sendMessage(msg)
}
// On add the callback for the event
func (c *Client) On(event string, callback Callback) {
c.muEvents.Lock()
defer c.muEvents.Unlock()
c.events[event] = callback
}
// Close the connection, and shutdown the benchmark
func (c *Client) Close() {
c.conn.Close()
close(c.die)
}
func (c *Client) eventHandler(event string) (Callback, bool) {
c.muEvents.RLock()
defer c.muEvents.RUnlock()
cb, ok := c.events[event]
return cb, ok
}
func (c *Client) responseHandler(mid uint64) (Callback, bool) {
c.muResponses.RLock()
defer c.muResponses.RUnlock()
cb, ok := c.responses[mid]
return cb, ok
}
func (c *Client) setResponseHandler(mid uint64, cb Callback) {
c.muResponses.Lock()
defer c.muResponses.Unlock()
if cb == nil {
delete(c.responses, mid)
} else {
c.responses[mid] = cb
}
}
func (c *Client) sendMessage(msg *message.Message) error {
data, err := msg.Encode()
if err != nil {
return err
}
//log.Printf("%+v",msg)
payload, err := codec.Encode(packet.Data, data)
if err != nil {
return err
}
c.mid++
c.send(payload)
return nil
}
func (c *Client) write() {
defer close(c.chSend)
for {
select {
case data := <-c.chSend:
if _, err := c.conn.Write(data); err != nil {
log.Println(err.Error())
c.Close()
}
case <-c.die:
return
}
}
}
func (c *Client) send(data []byte) {
c.chSend <- data
}
func (c *Client) read() {
buf := make([]byte, 2048)
for {
n, err := c.conn.Read(buf)
if err != nil {
log.Println(err.Error())
c.Close()
return
}
packets, err := c.codec.Decode(buf[:n])
if err != nil {
log.Println(err.Error())
c.Close()
return
}
for i := range packets {
p := packets[i]
c.processPacket(p)
}
}
}
func (c *Client) processPacket(p *packet.Packet) {
switch p.Type {
case packet.Handshake:
c.send(had)
c.connectedCallback()
case packet.Data:
msg, err := message.Decode(p.Data)
if err != nil {
log.Println(err.Error())
return
}
c.processMessage(msg)
case packet.Kick:
c.Close()
}
}
func (c *Client) processMessage(msg *message.Message) {
switch msg.Type {
case message.Push:
cb, ok := c.eventHandler(msg.Route)
if !ok {
log.Println("event handler not found", msg.Route)
return
}
cb(msg.Data)
case message.Response:
cb, ok := c.responseHandler(msg.ID)
if !ok {
log.Println("response handler not found", msg.ID)
return
}
cb(msg.Data)
c.setResponseHandler(msg.ID, nil)
}
}
func serialize(v proto.Message) ([]byte, error) {
data, err := proto.Marshal(v)
if err != nil {
return nil, err
}
return data, nil
}

@ -1 +1,102 @@
package client
import (
"git.noahlan.cn/northlan/ngs"
"git.noahlan.cn/northlan/ngs/benchmark/testdata"
"git.noahlan.cn/northlan/ngs/component"
"git.noahlan.cn/northlan/ngs/serialize/protobuf"
"git.noahlan.cn/northlan/ngs/session"
"log"
"os"
"os/signal"
"sync/atomic"
"syscall"
"testing"
"time"
)
const (
addr = "127.0.0.1:13250" // local address
conc = 1000 // concurrent client count
)
//
type TestHandler struct {
component.Base
metrics int32
group *ngs.Group
}
func (h *TestHandler) AfterInit() {
ticker := time.NewTicker(time.Second)
// metrics output ticker
go func() {
for range ticker.C {
println("QPS", atomic.LoadInt32(&h.metrics))
atomic.StoreInt32(&h.metrics, 0)
}
}()
}
func NewTestHandler() *TestHandler {
return &TestHandler{
group: ngs.NewGroup("handler"),
}
}
func (h *TestHandler) Ping(s *session.Session, data *testdata.Ping) error {
atomic.AddInt32(&h.metrics, 1)
return s.Push("pong", &testdata.Pong{Content: data.Content})
}
func server() {
components := &component.Components{}
components.Register(NewTestHandler())
ngs.Listen(addr,
ngs.WithDebugMode(),
ngs.WithSerializer(protobuf.NewSerializer()),
ngs.WithComponents(components),
)
}
func client() {
c := NewClient()
chReady := make(chan struct{})
c.OnConnected(func() {
chReady <- struct{}{}
})
if err := c.Start(addr); err != nil {
panic(err)
}
c.On("pong", func(data interface{}) {})
<-chReady
for /*i := 0; i < 1; i++*/ {
c.Notify("TestHandler.Ping", &testdata.Ping{})
time.Sleep(1 * time.Second)
}
}
func TestIO(t *testing.T) {
go server()
// wait server startup
time.Sleep(1 * time.Second)
for i := 0; i < conc; i++ {
go client()
}
log.SetFlags(log.LstdFlags | log.Llongfile)
sg := make(chan os.Signal)
signal.Notify(sg, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGKILL)
<-sg
t.Log("exit")
}

@ -1,8 +1,4 @@
# Nano cluster example
## About this example
# Cluster example
## How to run the example?

@ -1 +1,58 @@
package chat
import (
"fmt"
"git.noahlan.cn/northlan/ngs"
"git.noahlan.cn/northlan/ngs/component"
"git.noahlan.cn/northlan/ngs/examples/cluster/protocol"
"git.noahlan.cn/northlan/ngs/internal/log"
"git.noahlan.cn/northlan/ngs/session"
)
type RoomService struct {
component.Base
group *ngs.Group
}
func newRoomService() *RoomService {
return &RoomService{
group: ngs.NewGroup("all-users"),
}
}
func (rs *RoomService) JoinRoom(s *session.Session, msg *protocol.JoinRoomRequest) error {
if err := s.Bind(msg.MasterUid); err != nil {
return err
}
broadcast := &protocol.NewUserBroadcast{
Content: fmt.Sprintf("User user join: %v", msg.Nickname),
}
if err := rs.group.Broadcast("onNewUser", broadcast); err != nil {
return err
}
return rs.group.Add(s)
}
type SyncMessage struct {
Name string `json:"name"`
Content string `json:"content"`
}
func (rs *RoomService) SyncMessage(s *session.Session, msg *SyncMessage) error {
// Send an RPC to master server to stats
if err := s.RPC("TopicService.Stats", &protocol.MasterStats{Uid: s.UID()}); err != nil {
return err
}
// Sync message to all members in this room
return rs.group.Broadcast("onMessage", msg)
}
func (rs *RoomService) userDisconnected(s *session.Session) {
if err := rs.group.Leave(s); err != nil {
log.Println("Remove user from group failed", s.UID(), err)
return
}
log.Println("User session disconnected", s.UID())
}

@ -1 +1,21 @@
package chat
import (
"git.noahlan.cn/northlan/ngs/component"
"git.noahlan.cn/northlan/ngs/session"
)
var (
// Services is All services in chat server
Services = &component.Components{}
roomService = newRoomService()
)
func init() {
Services.Register(roomService)
}
func OnSessionClosed(s *session.Session) {
roomService.userDisconnected(s)
}

@ -1 +1,44 @@
package gate
import (
"errors"
"fmt"
"git.noahlan.cn/northlan/ngs/component"
"git.noahlan.cn/northlan/ngs/examples/cluster/protocol"
"git.noahlan.cn/northlan/ngs/session"
)
type BindService struct {
component.Base
nextGateUid int64
}
func newBindService() *BindService {
return &BindService{}
}
type (
LoginRequest struct {
Nickname string `json:"nickname"`
}
LoginResponse struct {
Code int `json:"code"`
}
)
func (bs *BindService) Login(s *session.Session, msg *LoginRequest) error {
bs.nextGateUid++
uid := bs.nextGateUid
request := &protocol.NewUserRequest{
Nickname: msg.Nickname,
GateUid: uid,
}
if err := s.RPC("TopicService.NewUser", request); err != nil {
return err
}
return s.Response(&LoginResponse{})
}
func (bs *BindService) BindChatServer(s *session.Session, msg []byte) error {
return errors.New(fmt.Sprintf("not implement"))
}

@ -1 +1,14 @@
package gate
import "git.noahlan.cn/northlan/ngs/component"
var (
// Services is All services in gate server
Services = &component.Components{}
bindService = newBindService()
)
func init() {
Services.Register(bindService)
}

@ -1 +1,179 @@
package cluster
package main
import (
"errors"
"git.noahlan.cn/northlan/ngs"
"git.noahlan.cn/northlan/ngs/examples/cluster/chat"
"git.noahlan.cn/northlan/ngs/examples/cluster/gate"
"git.noahlan.cn/northlan/ngs/examples/cluster/master"
"git.noahlan.cn/northlan/ngs/internal/log"
"git.noahlan.cn/northlan/ngs/serialize/json"
"git.noahlan.cn/northlan/ngs/session"
"github.com/urfave/cli"
"net/http"
"os"
"path/filepath"
"runtime"
)
func main() {
app := cli.NewApp()
app.Name = "ClusterDemo"
app.Author = "NorthLan"
app.Email = "6995syu@163.com"
app.Description = "cluster demo"
app.Commands = []cli.Command{
{
Name: "master",
Flags: []cli.Flag{
cli.StringFlag{
Name: "listen,l",
Usage: "--l --listen :8888 Master service listen address",
Value: "127.0.0.1:34567",
},
},
Action: runMaster,
},
{
Name: "gate",
Flags: []cli.Flag{
cli.StringFlag{
Name: "master",
Usage: "master server address",
Value: "127.0.0.1:34567",
},
cli.StringFlag{
Name: "listen,l",
Usage: "Gate service listen address",
Value: "",
},
cli.StringFlag{
Name: "gate-address",
Usage: "Client connect address",
Value: "",
},
},
Action: runGate,
},
{
Name: "chat",
Flags: []cli.Flag{
cli.StringFlag{
Name: "master",
Usage: "master server address",
Value: "127.0.0.1:34567",
},
cli.StringFlag{
Name: "listen,l",
Usage: "Chat service listen address",
Value: "",
},
},
Action: runChat,
},
}
//log.SetFlags(log.LstdFlags | log.Lshortfile)
if err := app.Run(os.Args); err != nil {
log.Fatalf("Startup server error %+v", err)
}
}
func runMaster(args *cli.Context) error {
listen := args.String("listen")
if listen == "" {
return errors.New("master listen address cannot empty")
}
webDir := filepath.Join(srcPath(), "master", "web")
log.Println("Ngs master server web content directory", webDir)
log.Println("Ngs master listen address", listen)
log.Println("Open http://127.0.0.1:12345/web/ in browser")
http.Handle("/web/", http.StripPrefix("/web/", http.FileServer(http.Dir(webDir))))
go func() {
if err := http.ListenAndServe(":12345", nil); err != nil {
panic(err)
}
}()
// Register session closed callback
session.Lifetime.OnClosed(master.OnSessionClosed)
// Startup Ngs server with the specified listen address
ngs.Listen(listen,
ngs.WithMaster(),
ngs.WithComponents(master.Services),
ngs.WithSerializer(json.NewSerializer()),
ngs.WithDebugMode(),
)
return nil
}
func srcPath() string {
_, file, _, _ := runtime.Caller(0)
return filepath.Dir(file)
}
func runGate(args *cli.Context) error {
listen := args.String("listen")
if listen == "" {
return errors.New("gate listen address cannot empty")
}
masterAddr := args.String("master")
if masterAddr == "" {
return errors.New("master address cannot empty")
}
gateAddr := args.String("gate-address")
if gateAddr == "" {
return errors.New("gate address cannot empty")
}
log.Println("Current server listen address", listen)
log.Println("Current gate server address", gateAddr)
log.Println("Remote master server address", masterAddr)
// Startup Ngs server with the specified listen address
ngs.Listen(listen,
ngs.WithAdvertiseAddr(masterAddr),
ngs.WithClientAddr(gateAddr),
ngs.WithComponents(gate.Services),
ngs.WithSerializer(json.NewSerializer()),
ngs.WithIsWebsocket(true),
ngs.WithWSPath("/ngs"),
ngs.WithCheckOriginFunc(func(_ *http.Request) bool { return true }),
ngs.WithDebugMode(),
)
return nil
}
func runChat(args *cli.Context) error {
listen := args.String("listen")
if listen == "" {
return errors.New("chat listen address cannot empty")
}
masterAddr := args.String("master")
if listen == "" {
return errors.New("master address cannot empty")
}
log.Println("Current chat server listen address", listen)
log.Println("Remote master server address", masterAddr)
// Register session closed callback
session.Lifetime.OnClosed(chat.OnSessionClosed)
// Startup Ngs server with the specified listen address
ngs.Listen(listen,
ngs.WithAdvertiseAddr(masterAddr),
ngs.WithComponents(chat.Services),
ngs.WithSerializer(json.NewSerializer()),
ngs.WithDebugMode(),
)
return nil
}

@ -1 +1,23 @@
package main
package master
import (
"git.noahlan.cn/northlan/ngs/component"
"git.noahlan.cn/northlan/ngs/session"
)
var (
// Services is All services in master server
Services = &component.Components{}
// topicService Topic service
topicService = newTopicService()
// ... other services
)
func init() {
Services.Register(topicService)
}
func OnSessionClosed(s *session.Session) {
topicService.userDisconnected(s)
}

@ -1 +1,90 @@
package master
import (
"errors"
"fmt"
"git.noahlan.cn/northlan/ngs/component"
"git.noahlan.cn/northlan/ngs/examples/cluster/protocol"
"git.noahlan.cn/northlan/ngs/internal/log"
"git.noahlan.cn/northlan/ngs/session"
"strings"
)
type User struct {
session *session.Session
nickname string
gateId int64
masterId int64
balance int64
message int
}
type TopicService struct {
component.Base
nextUid int64
users map[int64]*User
}
func newTopicService() *TopicService {
return &TopicService{
users: map[int64]*User{},
}
}
type ExistsMembersResponse struct {
Members string `json:"members"`
}
func (ts *TopicService) NewUser(s *session.Session, msg *protocol.NewUserRequest) error {
ts.nextUid++
uid := ts.nextUid
if err := s.Bind(uid); err != nil {
return err
}
var members []string
for _, u := range ts.users {
members = append(members, u.nickname)
}
err := s.Push("onMembers", &ExistsMembersResponse{Members: strings.Join(members, ",")})
if err != nil {
return err
}
user := &User{
session: s,
nickname: msg.Nickname,
gateId: msg.GateUid,
masterId: uid,
balance: 1000,
}
ts.users[uid] = user
chat := &protocol.JoinRoomRequest{
Nickname: msg.Nickname,
GateUid: msg.GateUid,
MasterUid: uid,
}
return s.RPC("RoomService.JoinRoom", chat)
}
type UserBalanceResponse struct {
CurrentBalance int64 `json:"currentBalance"`
}
func (ts *TopicService) Stats(s *session.Session, msg *protocol.MasterStats) error {
// It's OK to use map without lock because of this service running in main thread
user, found := ts.users[msg.Uid]
if !found {
return errors.New(fmt.Sprintf("User not found: %v", msg.Uid))
}
user.message++
user.balance--
return s.Push("onBalance", &UserBalanceResponse{user.balance})
}
func (ts *TopicService) userDisconnected(s *session.Session) {
uid := s.UID()
delete(ts.users, uid)
log.Println("User session disconnected", s.UID())
}

@ -71,7 +71,7 @@
// gate address
var gateHost = "127.0.0.1";
var gatePort = 34590;
starx.init({host: gateHost, port: gatePort, path: '/nano'}, function () {
starx.init({host: gateHost, port: gatePort, path: '/ngs'}, function () {
console.log("initialized");
starx.on("onNewUser", onNewUser);
starx.on("onMembers", onMembers);

@ -153,7 +153,7 @@ func main() {
ngs.WithIsWebsocket(true),
ngs.WithPipeline(pip),
ngs.WithCheckOriginFunc(func(_ *http.Request) bool { return true }),
ngs.WithWSPath("/nano"),
ngs.WithWSPath("/ngs"),
ngs.WithDebugMode(),
ngs.WithSerializer(json.NewSerializer()), // override default serializer
ngs.WithComponents(components),

@ -58,7 +58,7 @@
v.messages.push({name:'system', content: "members: "+data.members});
};
starx.init({host: '127.0.0.1', port: 3250, path: '/nano'}, function () {
starx.init({host: '127.0.0.1', port: 3250, path: '/ngs'}, function () {
console.log("initialized");
starx.on("onNewUser", onNewUser);
starx.on("onMembers", onMembers);

@ -9,7 +9,11 @@ require (
)
require (
github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
github.com/urfave/cli v1.22.5 // indirect
golang.org/x/net v0.0.0-20220418201149-a630d4f3e7a2 // indirect
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad // indirect
golang.org/x/text v0.3.7 // indirect

@ -11,6 +11,9 @@ github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XP
github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.1 h1:r/myEWzV9lfsM1tFLgDyu0atFtJ1fXn261LKYj/3DxU=
github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
@ -49,9 +52,16 @@ github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/urfave/cli v1.22.5 h1:lNq9sAHXK2qfdI8W+GRItjCEkI+2oR4d+MEHy1CKXoU=
github.com/urfave/cli v1.22.5/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=

Loading…
Cancel
Save