理解基于tcp的网络服务

简介: 【6月更文挑战第18天】本文构建了一个基于Golang的TCP网络服务,采用C/S架构。服务提供双倍回显功能,类似telnet,监听`:8910`端口。完整源码可在GitHub找到。

1 网络服务

使用CS结构的网络服务。 即client - Server。 基于TCP链接的关键在于读取客户端输入并处理,支持常见字符、数字和退出指令Q。当接收到Q,服务返回确认信息并退出。

本文代码示例包括读取、写入、错误处理和命令解析。服务还包含一个简单的内存键值存储,支持PUT、POP和QUIT命令。

2 基础网络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) 
    }

3 主体处理进程

此服务的关键在于读取数据,并准确处理输入的指令,因此,我们支持常见的英文字符,数字,换行回车等信息

                        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)
    }

4 使用

# 启动

 提示语:
 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

5 包含基础请求处理的网络存单服务

我们将设计一个简单的网络存单,其数据保存在内存的键值对中。

可以存入用户名和金额等等信息(PUT)

    Sers.PutIn(key, value)

或者可以取出用户和所有金额(POP)

   Sers.PopOut(key)

同样地,当我们的服务收到QUIT 退出指令时,将终止连接,退出服务。

    Sers.Service.Close()

我们创建一个扫描器,其Scan方法 返回一个布尔状态,当服务一直存在时,我们可以接受数据并处理他们。

    scer := bufio.NewScanner(conn) 
    for scer.Scan() {...}

6 服务的主体

第一步 我们需要设置一个退出帧,以方便我们

    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)
}

7 错误处理

如果用户输入了错误的无法识别的指令,我们提示他们使用方法。

    `useage:
        PUT name jack
        POP name` 

8 使用该服务

# 服务
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

9 结语

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

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

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

目录
相关文章
|
10月前
|
JSON 中间件 Go
Go 网络编程:HTTP服务与客户端开发
Go 语言的 `net/http` 包功能强大,可快速构建高并发 HTTP 服务。本文从创建简单 HTTP 服务入手,逐步讲解请求与响应对象、URL 参数处理、自定义路由、JSON 接口、静态文件服务、中间件编写及 HTTPS 配置等内容。通过示例代码展示如何使用 `http.HandleFunc`、`http.ServeMux`、`http.Client` 等工具实现常见功能,帮助开发者掌握构建高效 Web 应用的核心技能。
495 61
|
Ubuntu 网络协议 Unix
02理解网络IO:实现服务与客户端通信
网络IO指客户端与服务端通过网络进行数据收发的过程,常见于微信、QQ等应用。本文详解如何用C语言实现一个支持多客户端连接的TCP服务端,涉及socket编程、线程处理及通信流程,并分析“一消息一线程”模式的优缺点。
502 0
|
11月前
|
网络协议 安全 Devops
Infoblox DDI (NIOS) 9.0 - DNS、DHCP 和 IPAM (DDI) 核心网络服务管理
Infoblox DDI (NIOS) 9.0 - DNS、DHCP 和 IPAM (DDI) 核心网络服务管理
473 4
|
网络协议 物联网
VB6网络通信软件上位机开发,TCP网络通信,读写数据并处理,完整源码下载
本文介绍使用VB6开发网络通信上位机客户端程序,涵盖Winsock控件的引入与使用,包括连接服务端、发送数据(如通过`Winsock1.SendData`方法)及接收数据(利用`Winsock1_DataArrival`事件)。代码实现TCP网络通信,可读写并处理16进制数据,适用于自动化和工业控制领域。提供完整源码下载,适合学习VB6网络程序开发。 下载链接:[完整源码](http://xzios.cn:86/WJGL/DownLoadDetial?Id=20)
488 12
|
机器学习/深度学习 人工智能 安全
从攻防演练到AI防护:网络安全服务厂商F5的全方位安全策略
从攻防演练到AI防护:网络安全服务厂商F5的全方位安全策略
328 8
|
安全 网络协议 网络安全
【Azure APIM】APIM服务配置网络之后出现3443端口不通,Management Endpoint不健康状态
如果没有关联的网络安全组,则阻止所有网络流量通过子网和网络接口。
359 30
|
负载均衡 网络协议 算法
不为人知的网络编程(十九):能Ping通,TCP就一定能连接和通信吗?
这网络层就像搭积木一样,上层协议都是基于下层协议搭出来的。不管是ping(用了ICMP协议)还是tcp本质上都是基于网络层IP协议的数据包,而到了物理层,都是二进制01串,都走网卡发出去了。 如果网络环境没发生变化,目的地又一样,那按道理说他们走的网络路径应该是一样的,什么情况下会不同呢? 我们就从路由这个话题聊起吧。
556 4
不为人知的网络编程(十九):能Ping通,TCP就一定能连接和通信吗?
|
云安全 人工智能 安全
公共云网络安全即服务!阿里云稳居市占率第一!
公共云网络安全即服务!阿里云稳居市占率第一!
|
网络协议 测试技术 Linux
Golang 实现轻量、快速的基于 Reactor 模式的非阻塞 TCP 网络库
gev 是一个基于 epoll 和 kqueue 实现的高性能事件循环库,适用于 Linux 和 macOS(Windows 暂不支持)。它支持多核多线程、动态扩容的 Ring Buffer 读写缓冲区、异步读写和 SO_REUSEPORT 端口重用。gev 使用少量 goroutine,监听连接并处理读写事件。性能测试显示其在不同配置下表现优异。安装命令:`go get -u github.com/Allenxuxu/gev`。
339 0
|
网络协议
TCP报文格式全解析:网络小白变高手的必读指南
本文深入解析TCP报文格式,涵盖源端口、目的端口、序号、确认序号、首部长度、标志字段、窗口大小、检验和、紧急指针及选项字段。每个字段的作用和意义详尽说明,帮助理解TCP协议如何确保可靠的数据传输,是互联网通信的基石。通过学习这些内容,读者可以更好地掌握TCP的工作原理及其在网络中的应用。