Go语言实现简单聊天室(附源码)

幸运草 2020年4月20日21:22:56函数代码评论阅读模式

Go语言语法简练,而且性能上表现卓越。今天找出了一个自己闲暇之时做的一个简单的聊天室源码。虽然功能很简单,但是包结构非常清晰,对于初学Go语言的朋友还是有些帮助的。代码对于接口(interface)的使用也具有代表性。

首先来看一下需求。

  • 为了简单,我们先完成一个单聊天服务功能
  • 用户可以连接到服务器
  • 用户可以设定自己的用户名
  • 用户可以向服务器发送消息,同时服务器会向其他用户广播该消息

当然,只有用户连接到服务器的时候,才可以收到消息。

我们简单分析需求以后,可以将我们的项目分为三个模块,一个是通信协议,用来描述客户端和服务器端通信编码方式;还有一个是服务器端,用来接收客户的信息并且向其他客户端广播这些信息;最后是客户端,客户端是用户连接服务器,发送消息,并且我们要考虑为用户提供一个图形化界面。

下面我们就按照通信协议、服务器端、客户端三块来分别描述如何实现。

2

 通信协议

在本例中,我们选择使用TCP方式进行连接,并且选择传输的时候直接通过字符串类型进行传递。本书后面还会介绍http和rpc的使用,不过本例需求较为简单,而且我们也希望处理通信时更为灵活,所以选择tcp通信方式。

本书的此案例放在chatserver项目下,本小节的所有源码都放在项目的protocol包下。

根据前面的需求,我们可以把客户端与服务器的交互分为三种类型的命令:

  • 发送命令(SEND):客户端发送一个聊天信息
  • 名字命令(NAME):客户端发送自己的名字
  • 信息命令(MESS):服务器向客户端广播信息

所有命令在信息传递时都以不同的命令区分符(上文括号中的英文编码)开始,并且以n结束。

比如,我们发送一个Hello给服务器,那么TCP连接传递的具体字符串就是 "SEND Hello/n"。在服务器接收到字符串以后,再以"MESS Hello/n"广播给其他客户端。

在具体实现上,我们的三种消息类型就分别使用结构体进行定义。

我们具体看一下我们代码中的定义:

chatserver/protocol/command.go

1. package protocol

2.

3. import "errors"

4.

5. var (

6.     UnknownCommand = errors.New("Unknow command")

7. )

8.

9. type SendCmd struct{

10.    Message string

11.}

12.

13.type NameCmd struct{

14.    Name string

15.}

16.

17.type MessCmd struct{

18.    Name string

19.    Message string

20.}

有了这三个命令对应的struct,我们需要实现一个reader,用来从tcp socket中读取字符串。再实现一个writer,用于通过tcp socket写字符串。go语言提供了接口io.Reader和io.Writer用于数据流的读写,我们可以在程序当中实现这两个接口,让我们的程序可以通过这两个接口读写TCP。

io.Reader和io.Writer是两个高度抽象的接口,所以我们具体的业务可以自己封装然后再调用标准的实现。io.Reader只有一个Read方法,而io.Writer只有一个write方法。

我们先来实现一下Writer接口:

chatserver/protocol/writer.go

1. package protocol

2.

3. import (

4.     "fmt"

5.     "io"

6. )

7.

8. type Writer struct {

9.     writer io.Writer

10.}

11.

12.func NewWriter(writer io.Writer) *Writer  {

13.    return &Writer{

14.        writer:writer,

15.    }

16.}

17.

18.func (w *Writer) writeString(msg string) error {

19.    _,err := w.writer.Write([]byte(msg))

20.

21.    return err

22.}

23.

24.func (w *Writer) Write(command interface{}) error{

25.    var err error

26.

27.    switch v := command.(type) {

28.    case SendCmd:

29.        err = w.writeString(fmt.Sprintf("SEND %vn",v.Message))

30.    case MessCmd:

31.        err = w.writeString(fmt.Sprintf("MESSAGE %V %vn",v.Name, v.Message))

32.    case NameCmd:

33.        err = w.writeString(fmt.Sprintf("NAME %vn",v.Name))

34.    default:

35.        err = UnknownCommand

36.    }

37.    return err

38.}

protocol包的内容我们就介绍完了,截止目前,我们的项目结构应该是这样的:

--chatserver

----protocol

------command.go

------reader.go

------writer.go

同时对于后续我们信息传递时,具体的信息格式我们也再次梳理一下:

客户端向服务器发一个消息“Hello“,这时具体的传输字符串是这样的:

SENDHellon

一定要注意SEND都是大写,而且后面带有一个空格,同样的,如果客户端要给自己设定名字为“Scott“,则信息传输时的格式为:

NAMEScottn

好了,通信协议我们就介绍这么多,通信协议是为服务器端和客户端的通信准备的,那么接下来我们就基于通信协议完成服务器端和客户端。

3

 服务器端

本小节我们实现聊天服务器的服务器端。

本小节的代码都在chatserver/server包内。

当我们开始这个包的代码开发的时候,我们还是先来定义接口。定义server的接口,将服务器端该有的方法都定义好,这样有利于我们后面代码逻辑的具体实现和思路梳理,代码如下:

chatserver/server/server.go

1. package server

2.

3. type Server interface {

4.     Listen(address string) error

5.     Broadcast(command interface{}) error

6.     Start()

7.     Close()

8. }

我们定义了四个方法,Listen方法用于监听信息的写入;Broadcast方法则用于将收到的信息发送给其他用户;Start和Close方法用于启动和关闭服务器。

接下来我们就来完成服务器端的具体实现,我们要实现接口中的四个方法。同时我们还要注意,因为Broadcast需要向连接在服务器上的客户端广播收到的信息,所以我们应该有个struct来保存所有的客户端。同时,其实我们也需要对于服务器端的信息定义struct。具体的实现如下:

chatserver/server/tcp_server.go

1. package server

2.

3. import (

4.     "errors"

5.     "io"

6.     "log"

7.     "net"

8.     "sync"

9.     "github.com/ScottAI/chatserver/protocol"

10.)

11.

12.type client struct {

13.    conn net.Conn

14.    name string

15.    writer *protocol.Writer

16.}

17.

18.type TcpServer struct {

19.    listener net.Listener

20.    clients []*client

21.    mutex *sync.Mutex

22.}

23.

24.var (

25.    UnknownClient = errors.New("Unknown client")

26.)

27.

28.func NewServer() *TcpServer  {

29.    return &TcpServer{

30.        mutex:&sync.Mutex{},

31.    }

32.}

33.

34.func (s *TcpServer) Listen(address string) error{

35.    l,err := net.Listen("tcp",address)

36.

37.    if err == nil{

38.        s.listener = l

39.    }

40.

41.    log.Printf("Listening on %v",address)

42.

43.    return err

44.}

45.

46.func (s *TcpServer) Close(){

47.    s.listener.Close()

48.}

49.

50.func (s *TcpServer) Start(){

51.    for{

52.        conn,err := s.listener.Accept()

53.

54.        if err != nil{

55.            log.Print(err)

56.        }else{

57.            client := s.accept(conn)

58.            go s.serve(client)

59.        }

60.    }

61.}

62.

63.func (s *TcpServer) Broadcast(command interface{}) error {

64.    for _,client := range s.clients {

65.        client.writer.Write(command)

66.    }

67.    return nil

68.}

69.

70.func (s *TcpServer) Send(name string,command interface{}) error {

71.    for _,client := range s.clients{

72.        if client.name == name{

73.            return client.writer.Write(command)

74.        }

75.    }

76.    return UnknownClient

77.}

78.

79.func (s *TcpServer) accept(conn net.Conn) *client  {

80.    log.Printf("Accepting connection from %v,total clients:%v",conn.RemoteAddr().String(),len(s.clients)+1)

81.

82.    s.mutex.Lock()

83.    defer s.mutex.Unlock()

84.

85.    client := &client{

86.        conn:conn,

87.        writer:protocol.NewWriter(conn),

88.    }

89.

90.    s.clients = append(s.clients,client)

91.    return client

92.}

93.

94.func (s *TcpServer) remove(client *client)  {

95.    s.mutex.Lock()

96.    defer s.mutex.Unlock()

97.

98.    for i,check := range s.clients{

99.        if check == client {

100.            s.clients = append(s.clients[:i],s.clients[i+1:]...)

101.        }

102.    }

103.    log.Printf("Closing connection from %v",client.conn.RemoteAddr().String())

104.    client.conn.Close()

105.}

106.

107.func (s *TcpServer) serve(client *client)  {

108.    cmdReader := protocol.NewReader(client.conn)

109.

110.    defer s.remove(client)

111.

112.    for {

113.        cmd,err := cmdReader.Read()

114.        if err != nil && err != io.EOF {

115.            log.Printf("Read error: %v",err)

116.        }

117.

118.        if cmd != nil {

119.            switch v := cmd.(type) {

120.            case protocol.SendCmd:

121.                go s.Broadcast(protocol.MessCmd{

122.                    Message: v.Message,

123.                    Name : client.name,

124.                })

125.            case protocol.NameCmd:

126.                client.name = v.Name

127.            }

128.        }

129.

130.        if err == io.EOF {

131.            break

132.        }

133.    }

134.}

特别声明:以上文章内容仅代表作者本人观点,不代表变化吧观点或立场。如有关于作品内容、版权或其它问题请于作品发表后的30日内与变化吧联系。

  • 赞助本站
  • 微信扫一扫
  • weinxin
  • 加入Q群
  • QQ扫一扫
  • weinxin
幸运草
Go语言中的常量 函数代码

Go语言中的常量

1 概述 常量,一经定义不可更改的量。功能角度看,当出现不需要被更改的数据时,应该使用常量进行存储,例如圆周率。从语法的角度看,使用常量可以保证数据,在整个运行期间内,不会被更改。例如当前处理器的架构...
Go语言的接口 函数代码

Go语言的接口

Go语言-接口 在Go语言中,一个接口类型总是代表着某一种类型(即所有实现它的类型)的行为。一个接口类型的声明通常会包含关键字type、类型名称、关键字interface以及由花括号包裹的若干方法声明...
Go语言支持的正则语法 函数代码

Go语言支持的正则语法

1 字符 语法 说明 . 任意字符,在单行模式(s标志)下,也可以匹配换行 字符类 否定字符类 d Perl 字符类 D 否定 Perl 字符类 ASCII 字符类 否定 ASCII 字符类 pN U...
Go语言的包管理 函数代码

Go语言的包管理

1 概述 Go 语言的源码复用建立在包(package)基础之上。包通过 package, import, GOPATH 操作完成。 2 main包 Go 语言的入口 main() 函数所在的包(pa...

发表评论