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

186 阅读3分钟

网络服务

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


结语

本节基于TCP的服务帮助我们理解根本的服务逻辑。 输入,指令校验,错误处理,输出管理,等等。 在没有前后端分离时,client是一个 terminal 控制台。很远古。 若有兴趣可以查看,该服务 完整源码。

   https://github.com/hahamx/examples/tree/main/tcps/1_handler_conn
   

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