第一节 理解网络服务基础:CS结构,创建一个TCP连接和其管理器

举报
码乐 发表于 2024/02/06 08:21:48 2024/02/06
【摘要】 基本服务结构使用CS结构的网络服务。 这里即是client - Server。 1 基础网络io服务,双倍回显定义服务的退出标记和启动接口信息 const ( CloseMessage = 'Q' Ports = ":8910" )同时我们定义约定指令,如果为非约定指令,我们不执行工作,返回nil TouchChar = map[string]bool{...}...

基本服务结构

使用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

下一节我们将正式进入现代流行的服务提供方式。

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。