理解基于tcp的网络服务

本文涉及的产品
Serverless 应用引擎 SAE,800核*时 1600GiB*时
容器镜像服务 ACR,镜像仓库100个 不限时长
云原生网关 MSE Higress,422元/月
简介: 【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

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

目录
相关文章
|
15天前
|
网络协议
Qt中的网络编程(Tcp和Udp)运用详解以及简单示范案例
Tcp和Udp是我们学习网络编程中经常接触到的两个通讯协议,在Qt也被Qt封装成了自己的库供我们调用,对于需要进行网络交互的项目中无疑是很重要的,希望这篇文章可以帮助到大家。 是关于Qt中TCP和UDP的基本使用和特点:
|
4天前
|
网络协议 算法 程序员
提高网络稳定性的关键:TCP滑动窗口与拥塞控制解析
**TCP可靠传输与拥塞控制概要:** 小米讲解TCP如何确保数据可靠性。TCP通过分割数据、编号段、校验和、流量控制(滑动窗口)和拥塞控制(慢开始、拥塞避免、快重传、快恢复)保证数据安全传输。拥塞控制动态调整窗口大小,防止网络过载,提升效率。当连续收到3个相同ACK时执行快重传,快恢复避免剧烈波动。关注“软件求生”获取更多技术内容。
22 4
提高网络稳定性的关键:TCP滑动窗口与拥塞控制解析
|
2天前
|
网络协议 程序员
TCP报文格式全解析:网络小白变高手的必读指南
**TCP报文格式详解摘要** 探索TCP,传输层的关键协议,提供可靠数据传输。报文含源/目的端口(标识应用),32位序号(跟踪字节顺序),确认序号(确认接收),4位首部长度,6位标志(URG, ACK, PSH, RST, SYN, FIN),窗口大小(流量控制),检验和(数据完整性),紧急指针(优先数据)及可变长选项(如MSS, 时间戳)。了解这些字段,能更好地理解TCP连接的建立、管理和数据交换。
19 3
|
7天前
|
网络协议 安全 Shell
`nmap`是一个开源的网络扫描工具,用于发现网络上的设备和服务。Python的`python-nmap`库允许我们在Python脚本中直接使用`nmap`的功能。
`nmap`是一个开源的网络扫描工具,用于发现网络上的设备和服务。Python的`python-nmap`库允许我们在Python脚本中直接使用`nmap`的功能。
|
11天前
|
云安全 安全 网络安全
云端防御:融合云服务与先进网络安全策略
【5月更文挑战第70天】 在数字经济的浪潮中,云计算已成为企业信息化建设的核心动力。然而,伴随其快速发展的是日益严峻的网络安全挑战。本文深入探讨了云服务的基本架构、网络安全的重要性以及信息安全的关键措施,并提出了一个综合框架以增强云环境下的数据安全。通过分析最新的技术趋势和策略,我们旨在为读者提供一套实用的解决方案,以确保在享受云计算带来的便利的同时,有效地防范潜在的网络威胁。
|
8天前
|
网络协议 程序员 定位技术
学习网络的第一步:全面解析OSI与TCP/IP模型
**网络基础知识概览:** 探索网络通信的关键模型——OSI七层模型和TCP/IP五层模型。OSI模型(物理、数据链路、网络、传输、会话、表示、应用层)提供理论框架,而TCP/IP模型(物理、数据链路、网络、传输、应用层)更为实际,合并了会话、表示和应用层。两者帮助理解数据在网络中的传输过程,为网络设计和管理提供理论支持。了解这些模型,如同在复杂的网络世界中持有了地图。
16 2
|
15天前
|
网络协议 网络架构
【网络编程入门】TCP与UDP通信实战:从零构建服务器与客户端对话(附简易源码,新手友好!)
在了解他们之前我们首先要知道网络模型,它分为两种,一种是OSI,一种是TCP/IP,当然他们的模型图是不同的,如下
|
17天前
|
网络协议 Java 网络安全
Java中的网络编程:TCP详解
Java中的网络编程:TCP详解
|
29天前
|
机器学习/深度学习 网络协议 网络性能优化
[计算机网络]深度学习传输层TCP协议
[计算机网络]深度学习传输层TCP协议
26 1
|
13天前
|
缓存 Java 数据库连接
使用Java构建一个高并发的网络服务
使用Java构建一个高并发的网络服务