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日内与变化吧联系。
- 赞助本站
- 微信扫一扫
-
- 加入Q群
- QQ扫一扫
-
评论