Golang GO와 마이크로서비스 ChatServer 2

13634 단어 golang

인연


최근 읽기<>(유금량, 2021.1) 본 시리즈 노트는 golang으로 연습할 예정

케이스 요구사항(채팅 서버)

  • 사용자는 서버에 연결할 수 있습니다..
  • 사용자는 자신의 사용자 이름을 설정할 수 있습니다..
  • 사용자는 서버에 메시지를 보낼 수 있고 서버도 다른 사용자에게 이 메시지를 방송할 수 있다..

  • 목표

  • 채팅 서비스 단말기, 포트 감청 지원, 여러 클라이언트의 연결, 메시지 수신, 방송, 끊기, 로그 수집을 실현한다
  • 기존의 채팅 클라이언트를 개조하여 클라이언트와 서비스 측의 통신에 동시에 적합하게 하고 자물쇠가 잠기지 않도록 쓰기 버퍼를 설정한다
  • 여러 클라이언트의 연결을 테스트하고 송수신하고 끊으며 서버 로그를 진단합니다

  • 설계

  • IMsg: 메시지 인터페이스를 정의하고 관련 메시지의 실현.임의의 메시지 내용의 디코딩을 편리하게 하기 위해, 메시지 전송 시,base64 디코딩을 사용합니다
  • IMsgDecoder: 메시지 디코더와 그 실현을 정의합니다
  • IChatClient: 채팅 클라이언트 인터페이스를 정의합니다.이번에는 서버에 맞게 닫기 알림 방법을 추가합니다.
  • tChatClient: 채팅 클라이언트, IChatClient 인터페이스 실현.이번 추가는 닫기 알림, 쓰기 버퍼와 읽기 시간 초과 제어, 쓰기 순환 세부 문제를 해결합니다.
  • IChatServer: 채팅 서버 인터페이스를 정의하여 테스트를 편리하게 하기 위해 로그 수집 방법을 제공합니다
  • tChatServer: 채팅 서버 IChatServer 구현

  • 단원 테스트


    ChatServer_test.go
    package chat_server
    
    import (
        "fmt"
        cs "learning/gooop/chat_server"
        "strings"
        "testing"
        "time"
    )
    
    func Test_ChatServer(t *testing.T) {
        fnAssertTrue := func(b bool, msg string) {
            if !b {
                t.Fatal(msg)
            }
        }
    
        port := 3333
        server := cs.NewChatServer()
        err := server.Open(port)
        if err != nil {
            t.Fatal(err)
        }
    
        clientCount := 3
        address := fmt.Sprintf("localhost:%v", port)
        for i := 0;i < clientCount;i++ {
            err, client := cs.DialChatClient(address)
            if err != nil {
                t.Fatal(err)
            }
    
            id := fmt.Sprintf("c%02d", i)
            client.RecvHandler(func(client cs.IChatClient, msg cs.IMsg) {
                t.Logf("%v recv: %v
    ", id, msg) }) go func() { client.SetName(id) client.Send(&cs.NameMsg{id }) n := 0 for range time.Tick(time.Duration(1) * time.Second) { client.Send(&cs.ChatMsg{id, fmt.Sprintf("msg %02d from %v", n, id) }) n++ if n >= 3 { break } } client.Close() }() } passedSeconds := 0 for range time.Tick(time.Second) { passedSeconds++ t.Logf("%v seconds passed", passedSeconds) if passedSeconds >= 5 { break } } server.Close() logs := server.GetLogs() fnHasLog := func(log string) bool { for _,it := range logs { if strings.Contains(it, log) { return true } } return false } for i := 0;i < clientCount;i++ { msg := fmt.Sprintf("tChatServer.handleIncomingConn, clientCount=%v", i + 1) fnAssertTrue(fnHasLog(msg), "expecting log: " + msg) msg = fmt.Sprintf("tChatServer.handleClientClosed, c%02d", i) fnAssertTrue(fnHasLog(msg), "expecting log: " + msg) } }

    테스트 출력

    $ go test -v ChatServer_test.go 
    === RUN   Test_ChatServer
    tChatServer.handleIncomingConn, clientCount=1
    tChatServer.handleIncomingConn, clientCount=2
    tChatServer.handleIncomingConn, clientCount=3
        ChatServer_test.go:59: 1 seconds passed
        ChatServer_test.go:35: c00 recv: &{c00 msg 00 from c00}
        ChatServer_test.go:35: c02 recv: &{c00 msg 00 from c00}
        ChatServer_test.go:35: c02 recv: &{c01 msg 00 from c01}
        ChatServer_test.go:35: c01 recv: &{c00 msg 00 from c00}
        ChatServer_test.go:35: c01 recv: &{c01 msg 00 from c01}
        ChatServer_test.go:35: c01 recv: &{c02 msg 00 from c02}
        ChatServer_test.go:35: c00 recv: &{c01 msg 00 from c01}
        ChatServer_test.go:35: c00 recv: &{c02 msg 00 from c02}
        ChatServer_test.go:35: c02 recv: &{c02 msg 00 from c02}
        ChatServer_test.go:35: c00 recv: &{c01 msg 01 from c01}
        ChatServer_test.go:35: c01 recv: &{c00 msg 01 from c00}
        ChatServer_test.go:35: c01 recv: &{c02 msg 01 from c02}
        ChatServer_test.go:35: c02 recv: &{c01 msg 01 from c01}
        ChatServer_test.go:35: c02 recv: &{c00 msg 01 from c00}
        ChatServer_test.go:59: 2 seconds passed
        ChatServer_test.go:35: c00 recv: &{c00 msg 01 from c00}
        ChatServer_test.go:35: c02 recv: &{c02 msg 01 from c02}
        ChatServer_test.go:35: c00 recv: &{c02 msg 01 from c02}
    tChatClient.postConnClosed, c00, serverFlag=false
    tChatClient.postConnClosed, c02, serverFlag=false
    tChatClient.postConnClosed, c01, serverFlag=false
    tChatClient.postConnClosed, c02, serverFlag=true
    tChatServer.handleClientClosed, c02
    tChatServer.handleClientClosed, c02, clientCount=2
    tChatClient.postConnClosed, c01, serverFlag=true
    tChatServer.handleClientClosed, c01
    tChatServer.handleClientClosed, c01, clientCount=1
        ChatServer_test.go:59: 3 seconds passed
    tChatClient.postConnClosed, c00, serverFlag=true
    tChatServer.handleClientClosed, c00
    tChatServer.handleClientClosed, c00, clientCount=0
        ChatServer_test.go:59: 4 seconds passed
        ChatServer_test.go:59: 5 seconds passed
    --- PASS: Test_ChatServer (5.00s)
    PASS
    ok      command-line-arguments  5.003s

    IMsg.go


    메시지 인터페이스를 정의하고 관련 메시지의 실현을 정의합니다.임의의 메시지 내용의 디코딩을 편리하게 하기 위해, 메시지 전송 시,base64 디코딩을 사용한다
    package chat_server
    
    import (
        "encoding/base64"
        "fmt"
    )
    
    type IMsg interface {
        Encode() string
    }
    
    type NameMsg struct {
        Name string
    }
    
    func (me *NameMsg) Encode() string {
        return fmt.Sprintf("NAME %s
    ", base64.StdEncoding.EncodeToString([]byte(me.Name))) } type ChatMsg struct { Name string Words string } func (me *ChatMsg) Encode() string { return fmt.Sprintf("CHAT %s %s
    ", base64.StdEncoding.EncodeToString([]byte(me.Name)), base64.StdEncoding.EncodeToString([]byte(me.Words)), ) }

    IMsgDecoder.go


    메시지 디코더 및 그 실현 정의
    package chat_server
    
    import (
        "encoding/base64"
        "strings"
    )
    
    
    type IMsgDecoder interface {
        Decode(line string) (bool, IMsg)
    }
    
    type tMsgDecoder struct {
    }
    
    func (me *tMsgDecoder) Decode(line string) (bool, IMsg) {
        items := strings.Split(line, " ")
        size := len(items)
    
        if items[0] == "NAME" && size == 2 {
            name, err := base64.StdEncoding.DecodeString(items[1])
            if err != nil {
                return false, nil
            }
    
            return true, &NameMsg{
                Name: string(name),
            }
        }
    
        if items[0] == "CHAT" && size == 3 {
            name, err := base64.StdEncoding.DecodeString(items[1])
            if err != nil {
                return false, nil
            }
    
            words, err := base64.StdEncoding.DecodeString(items[2])
            if err != nil {
                return false, nil
            }
    
            return true, &ChatMsg{
                Name: string(name),
                Words: string(words),
            }
        }
    
        return false, nil
    }
    
    
    var MsgDecoder = &tMsgDecoder{}

    IChatClient.go


    채팅 클라이언트 인터페이스를 정의합니다.이번에는 서버에 맞게 닫기 알림 방법을 추가합니다.
    package chat_server
    
    type IChatClient interface {
        GetName() string
        SetName(name string)
    
        Send(msg IMsg)
        RecvHandler(handler ClientRecvFunc)
        CloseHandler(handler ClientCloseFunc)
    
        Close()
    }
    
    type ClientRecvFunc func(client IChatClient, msg IMsg)
    type ClientCloseFunc func(client IChatClient)

    tChatClient.go


    채팅 클라이언트, IChatClient 인터페이스 구현.이번 추가 닫기 알림, 쓰기 버퍼, 읽기 시간 초과 제어, 쓰기 순환 세부 문제 해결.
    package chat_server
    
    import (
        "bufio"
        "fmt"
        "io"
        "net"
        "sync/atomic"
        "time"
    )
    
    type tChatClient struct {
        conn net.Conn
        name string
        openFlag int32
        closeFlag int32
        serverFlag bool
    
        closeChan chan bool
        sendChan chan IMsg
    
        sendLogs []IMsg
        dropLogs []IMsg
        recvLogs []IMsg
        pendingSend int32
    
        recvHandler ClientRecvFunc
        closeHandler ClientCloseFunc
    }
    
    var gMaxPendingSend int32 = 100
    
    func DialChatClient(address string) (error, IChatClient) {
        conn, err := net.Dial("tcp", address)
        if err != nil {
            return err, nil
        }
    
        return nil, openChatClient(conn, false)
    }
    
    func openChatClient(conn net.Conn, serverFlag bool) IChatClient {
        it := &tChatClient{
            conn: conn,
            openFlag: 0,
            closeFlag: 0,
            serverFlag: serverFlag,
    
            closeChan: make(chan bool),
            sendChan: make(chan IMsg, gMaxPendingSend),
    
            name: "anonymous",
            sendLogs: []IMsg{},
            dropLogs: []IMsg{},
            recvLogs: []IMsg{},
        }
        it.open()
        return it
    }
    
    
    func (me *tChatClient) GetName() string {
        return me.name
    }
    
    func (me *tChatClient) SetName(name string) {
        me.name = name
    }
    
    func (me *tChatClient) open(){
        if !atomic.CompareAndSwapInt32(&me.openFlag, 0, 1) {
            return
        }
    
        go me.beginWrite()
        go me.beginRead()
    }
    
    
    func (me *tChatClient) isClosed() bool {
        return me.closeFlag != 0
    }
    
    func (me *tChatClient) isNotClosed() bool {
        return !me.isClosed()
    }
    
    func (me *tChatClient) Send(msg IMsg) {
        if me.isClosed() {
            return
        }
    
        if me.pendingSend < gMaxPendingSend {
            atomic.AddInt32(&me.pendingSend, 1)
            me.sendChan 

    IChatServer.go


    채팅 서버 인터페이스를 정의하여 테스트를 편리하게 하고 로그 수집 방법을 제공합니다
    package chat_server
    
    type IChatServer interface {
        Open(port int) error
        Broadcast(msg IMsg)
        Close()
        GetLogs() []string
    }

    tChatServer.go


    채팅 서버 IChatServer 구현
    package chat_server
    
    import (
        "errors"
        "fmt"
        "net"
        "sync"
        "sync/atomic"
    )
    
    type tChatServer struct {
        openFlag int32
        closeFlag int32
    
        clients []IChatClient
        clientCount int
        clientLock *sync.RWMutex
    
        listener net.Listener
        recvLogs []IMsg
    
        logs []string
    }
    
    func NewChatServer() IChatServer {
        it := &tChatServer{
            openFlag: 0,
            closeFlag: 0,
    
            clients: []IChatClient{},
            clientCount: 0,
            clientLock: new(sync.RWMutex),
    
            listener: nil,
            recvLogs: []IMsg{},
        }
        return it
    }
    
    func (me *tChatServer) Open(port int) error {
        if !atomic.CompareAndSwapInt32(&me.openFlag, 0, 1) {
            return errors.New("server already opened")
        }
    
        listener, err := net.Listen("tcp", fmt.Sprintf(":%v", port))
        if err != nil {
            return err
        }
    
        me.listener = listener
        go me.beginListening()
        return nil
    }
    
    func (me *tChatServer) logf(f string, args... interface{}) {
        msg := fmt.Sprintf(f, args...)
        me.logs = append(me.logs, msg)
        fmt.Println(msg)
    }
    
    func (me *tChatServer) GetLogs() []string {
        return me.logs
    }
    
    func (me *tChatServer) isClosed() bool {
        return me.closeFlag != 0
    }
    
    func (me *tChatServer) isNotClosed() bool {
        return !me.isClosed()
    }
    
    func (me *tChatServer) beginListening() {
        for !me.isClosed() {
            conn, err := me.listener.Accept()
            if err != nil {
                me.Close()
                break
            }
    
            me.handleIncomingConn(conn)
        }
    }
    
    
    func (me *tChatServer) Close() {
        if !atomic.CompareAndSwapInt32(&me.closeFlag, 0, 1) {
            return
        }
    
        _ = me.listener.Close()
        me.closeAllClients()
    }
    
    func (me *tChatServer) closeAllClients() {
        me.clientLock.Lock()
        defer me.clientLock.Unlock()
    
        for i,it := range me.clients {
            if it != nil {
                it.Close()
                me.clients[i] = nil
            }
        }
        me.clientCount = 0
    }
    
    
    func (me *tChatServer) handleIncomingConn(conn net.Conn) {
        // init client
        client := openChatClient(conn, true)
        client.RecvHandler(me.handleClientMsg)
        client.CloseHandler(me.handleClientClosed)
    
        // lock me.clients
        me.clientLock.Lock()
        defer me.clientLock.Unlock()
    
        // append to me.clients
        if len(me.clients) > me.clientCount {
            me.clients[me.clientCount] = client
        } else {
            me.clients = append(me.clients, client)
        }
        me.clientCount++
    
        me.logf("tChatServer.handleIncomingConn, clientCount=%v", me.clientCount)
    }
    
    func (me *tChatServer) handleClientMsg(client IChatClient, msg IMsg) {
        me.recvLogs = append(me.recvLogs, msg)
    
        if nameMsg,ok := msg.(*NameMsg);ok {
            client.SetName(nameMsg.Name)
    
        } else if _, ok := msg.(*ChatMsg);ok {
            me.Broadcast(msg)
        }
    }
    
    func (me *tChatServer) handleClientClosed(client IChatClient) {
        me.logf("tChatServer.handleClientClosed, %s", client.GetName())
    
        me.clientLock.Lock()
        defer me.clientLock.Unlock()
    
        if me.clientCount <= 0 {
            return
        }
    
        lastI := me.clientCount - 1
        for i,it := range me.clients {
            if it == client {
                if i == lastI {
                    me.clients[i] = nil
                } else {
                    me.clients[i], me.clients[lastI] = me.clients[lastI], nil
                }
                me.clientCount--
                break
            }
        }
    
        me.logf("tChatServer.handleClientClosed, %s, clientCount=%v", client.GetName(), me.clientCount)
    }
    
    func (me *tChatServer) Broadcast(msg IMsg) {
        me.clientLock.RLock()
        defer me.clientLock.RUnlock()
    
        for _,it := range me.clients {
            if it != nil {
                it.Send(msg)
            }
        }
    }

    (미완성 미계속)

    좋은 웹페이지 즐겨찾기