第一节 理解网络服务基础:CS结构,创建一个TCP连接和其管理器
基本服务结构
使用CS结构的网络服务。 这里即是client - Server。
1 基础网络io服务,双倍回显
定义服务的退出标记和启动接口信息
const (
CloseMessage = 'Q'
Ports = ":8910"
)
同时我们定义约定指令,如果为非约定指令,我们不执行工作,返回nil
TouchChar = map[string]bool{...}
func do_jobs() {
if !TouchChar[string(v)] {
return nil
}
...
}
此服务类似于一个telnet拨号,它允许用户登录,并根据服务的提示约定做操作。
在操作系统客户端:
telnet localhost 3010
它将实现一个读取,写入操作的长链接服务
首先实现从tcp连接接受数据的操作,golang 有io 和 bufio库可以做到,其他从控制台获取输入如
var instr string
fmt.Scan(&instr)
scanner.Scan()
net的监听服务,在启动Accept后,将返回一个连接对象,该连接对象有两个基本操作 read,write, close
read 从连接接受消息
write 向连接发送消息,这将在超时到期后自动关闭并返回一个错误 (如果设置了SetDeadline SetReadDeadline)
close 在服务接受到退出指令后,关闭该服务连接。
我们假设网络发送指令到服务器,服务器将执行某个动作,我们将把对方的地址信息一并返回
result := do_jobs(v, conn)
假设我们使用 io 去接受 网络的数据,并且设置退出帧为 Q
CloseMessage = 'Q'
因此在接受到网络消息为 Q时 则退出服务。 并且在退出之前返回一个消息。
if v == CloseMessage {
retMsg := []byte(fmt.Sprintf("%v: %v\n", ErrCloseSent, string(v)))
conn.Write(retMsg)
os.Exit(1)
}
1.1 主体处理进程
此服务的关键在于读取数据,并准确处理输入的指令,因此,我们支持常见的英文字符,数字,换行回车等信息
if len(string(bs[:n])) <= 0 { // 异常处理,用户没有输入字符则进入下一次
continue
} else if string(v) == "\r" || string(v) == "\n" { // 异常处理, 输入回车字符则直接进入下一次
continue
} else if v == CloseMessage { //收到退出指令,服务结束
retMsg := []byte(fmt.Sprintf("%v: %v\n", ErrCloseSent, string(v)))
conn.Write(retMsg)
os.Exit(1)
} else if unicode.IsLetter(rune(v)) { //执行工作
result := do_jobs(v, conn) // 完成工作
_, err = conn.Write(result)
if err != nil {
break
}
} else { //默认只显示信息到服务器
fmt.Printf("Client Say:%v \n", string(v))
}
封装服务函数
在启动服务时指定端口信息,并在结束时执行关闭
func TcpStart() {
cn, err := net.Listen("tcp", Ports)
if err != nil {
panic(err)
}
defer cn.Close()
handler(cn)
}
1.2 使用
# 启动
提示语:
Enter something
# 输入任务 任意字符A 执行结果返回双倍
输入: B
输出: DO: B Addr:[::1]:1080 Result:
BB
# 退出服务
输入: Q
输出: service: close sent: Q
此服务完整源码
https://github.com/hahamx/examples/tree/main/tcps/0_simple_tcp
2 包含基础请求处理的网络存单服务
我们将设计一个简单的网络存单,其数据保存在内存的键值对中。
可以存入用户名和金额等等信息(PUT)
Sers.PutIn(key, value)
或者可以取出用户和所有金额(POP)
Sers.PopOut(key)
同样地,当我们的服务收到QUIT 退出指令时,将终止连接,退出服务。
Sers.Service.Close()
我们创建一个扫描器,其Scan方法 返回一个布尔状态,当服务一直存在时,我们可以接受数据并处理他们。
scer := bufio.NewScanner(conn)
for scer.Scan() {...}
2.1 服务的主体
第一步 我们需要设置一个退出帧,以方便我们
const (
StrMessage = 1 //消息类型 字符
CloseMessage = "QUIT" //退出标记
MaxSize = 2 //最多存几个信息
ports = ":3900" //哪个端口提供服务
)
首先我们需要一个服务管理程序,它帮助我们保存单据和用户信息。 其中
Service 为服务启动程序
Size 为本服务最多能保留多少个。
Running 为服务的状态,启动管理器,根据它判断是否继续执行服务
Fields 为存单的具体信息
type ValueSer struct {
Service net.Listener
Fields map[string]string
Size int
Running bool
}
第二步,我们需要为此做存入,取出,和启动服务的操作提供函数,这将在服务器函数中得到使用。
func NewValues() *ValueSer {
return &ValueSer{Fields: make(map[string]string, MaxSize), Size: MaxSize}
}
func (va *ValueSer) PutIn(mk, mv string) bool {
Lock.Lock()
defer Lock.Unlock()
if len(va.Fields) >= va.Size {
return false
}
va.Fields[mk] = mv
return true
}
func (va *ValueSer) PopOut(mk string) string {
Lock.Lock()
defer Lock.Unlock()
if len(va.Fields) <= 0 {
return ""
}
val := va.Fields[mk]
delete(va.Fields, mk)
return val
}
func (va *ValueSer) Start() net.Listener {
ser, err := net.Listen("tcp", ports)
if err != nil {
log.Fatalln(err)
}
va.Running = true
return ser
}
还有一个小小的需求,使用者的指令如何管理和维护。
当网络中的使用者,传入一整串指令时,我们需要分割它们,并且判断是否属于正常指令,并在指令执行完成后将结果返回。
因此我们在连接处理的函数中将它们集成在一起。
type Infos struct {
Cmd map[string][]string
Result chan string
}
io.WriteString(...)
func handler() { ...
strs := scer.Text()
if strs == "" {
msg := fmt.Sprintf(`useage:
PUT name jack
POP name`)
io.WriteString(cn, msg)
continue
}
fs := strings.Fields(strs)
result := make(chan string)
infos <- Infos{
Cmd: map[string][]string{fs[0]: fs[1:]},
Result: result,
}
io.WriteString(cn, <-result+"\n")
主体服务的指令执行过程
if kk == "POP" {
value := Sers.PopOut(ff[0])
info.Result <- value
} else if kk == "PUT" {
k := ff[0]
v := ff[1]
value := Sers.PutIn(k, v)
info.Result <- fmt.Sprintf("%v", value)
} else if kk == CloseMessage {
Sers.Service.Close()
os.Exit(1)
} else {
info.Result <- "INVALID COMMAND " + kk + "\n"
}
最后,我们启动Tcp服务并根据服务器状态提供任务处理功能。
ser := Sers.Start()
infos := make(chan Infos)
go TcpServer(infos)
for Sers.Running {
cn, err := ser.Accept()
if err != nil {
log.Fatalln(err)
}
go handler(infos, cn)
}
2.2 错误处理
如果用户输入了错误的无法识别的指令,我们提示他们使用方法。
`useage:
PUT name jack
POP name`
2.3 使用该服务
-
服务
window系统需要安装或启用 telnet 请搜索
Mac自带telnet
Linux需要启用
启动服务
telnet localhost 3900
-
示例
case1 输入 存 name 为 jack PUT name jack 返回 true 取 name POP name 返回 jack case2 输入存 money 为 99900 PUT money 99900 返回 true 取 POP money 返回 99900
3 结语
本节基于TCP的服务帮助我们理解根本的服务逻辑。 输入,指令校验,错误处理,输出管理,等等。
在没有前后端分离时,client是一个 terminal 控制台。很远古。
若有兴趣可以查看,该服务 完整源码。
https://github.com/hahamx/examples/tree/main/tcps/1_handler_conn
下一节我们将正式进入现代流行的服务提供方式。
- 点赞
- 收藏
- 关注作者
评论(0)