缘起
最近阅读<
本系列笔记拟采用golang练习之
案例需求(聊天服务器)
- 用户可以连接到服务器。
- 用户可以设定自己的用户名。
- 用户可以向服务器发送消息,同时服务器也会向其他用户广播该消息。
目标
- 定义通信协议, 包括信令定义, 编解码实现
- 实现聊天客户端(时间有限, 后续实现聊天服务端并测试)
设计
- IMsg: 定义消息接口, 以及相关消息的实现. 为方便任意消息内容的解码, 消息传输时, 采用base64转码
- IMsgDecoder: 定义消息解码器及其实现
- IChatClient: 定义聊天客户端接口
- tChatClient: 聊天客户端, 实现IChatClient接口
- IChatServer: 尚未完成
- tChatServer: 尚未完成
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 {
Dial(address string) error
Send(msg IMsg)
RecvHandler(handler RecvFunc)
Close()
}
type RecvFunc func(msg IMsg)
tChatClient.go
聊天客户端, 实现IChatClient接口
package chat_server
import (
"bufio"
"net"
"sync/atomic"
)
type tChatClient struct {
conn net.Conn
closeFlag int32
closeChan chan bool
sendChan chan IMsg
name string
sendLogs []IMsg
recvLogs []IMsg
recvHandler RecvFunc
}
func DialChatClient(address string) (error, IChatClient) {
it := &tChatClient{
conn: nil,
closeFlag: 0,
closeChan: make(chan bool),
sendChan: make(chan IMsg),
name: "anonymous",
sendLogs: []IMsg{},
recvLogs: []IMsg{},
}
e := it.Dial(address)
if e != nil {
return e, nil
}
return nil, it
}
func (me *tChatClient) Dial(address string) error {
c, e := net.Dial("tcp", address)
if e != nil {
return e
}
me.conn = c
go me.beginWrite()
go me.beginRead()
return nil
}
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.isNotClosed() {
me.sendChan <- msg
}
}
func (me *tChatClient) RecvHandler(handler RecvFunc) {
if me.isNotClosed() {
me.recvHandler = handler
}
}
func (me *tChatClient) Close() {
if me.isNotClosed() {
me.closeConn()
}
}
func (me *tChatClient) beginWrite() {
writer := bufio.NewWriter(me.conn)
newline := '\n'
for {
select {
case <- me.closeChan:
_ = me.conn.Close()
me.closeFlag = 2
return
case msg := <- me.sendChan:
_,e := writer.WriteString(msg.Encode())
if e != nil {
me.closeConn()
} else {
_,e = writer.WriteRune(newline)
if e != nil {
me.closeConn()
}
}
}
}
}
func (me *tChatClient) beginRead() {
reader := bufio.NewReader(me.conn)
for me.isNotClosed() {
line, _, err := reader.ReadLine()
if err != nil {
break
}
ok, msg := MsgBuilder.Build(string(line))
if ok {
fn := me.recvHandler
if fn != nil {
fn(msg)
}
}
}
}
func (me *tChatClient) closeConn() {
if atomic.CompareAndSwapInt32(&me.closeFlag, 0, 1) {
me.closeChan <- true
}
}
(未完待续)