通过PuTTY与TCP服务器通信

huangapple go评论88阅读模式
英文:

Communicating with a TCP server via PuTTY

问题

如何通过在PuTTY客户端中键入“exit”来关闭连接?这是我的代码:

package main

import (
	"bufio"
	"fmt"
	"net"
)

func main() {

	// 监听请求
	datastream, err := net.Listen("tcp", ":8080")

	if err != nil {
		fmt.Println(err)
		return
	}
	defer datastream.Close()

	// 接受请求
	for {
		connessione, err := datastream.Accept()
		if err != nil {
			fmt.Println(err)
			return
		}

		go handle(connessione)
	}
}

// 连接处理 > 线程
func handle(connessione net.Conn) {

	fmt.Println("输入“exit”退出")

	for {
		data, err := bufio.NewReader(connessione).ReadString('\n')

		if err != nil {
			fmt.Print(err)
			return
		}
		fmt.Println(data)

	}

}

希望这可以帮助到你!

英文:

How do I close the connection by typing the word "exit" from the PuTTY client? This is my code:

package main

import (
	"bufio"
	"fmt"
	"net"
)

func main() {

	//Ascolta richiesta
	datastream, err := net.Listen("tcp", ":8080")

	if err != nil {
		fmt.Println(err)
		return
	}
	defer datastream.Close()

	//Accetta richiesta
	for {
		connessione, err := datastream.Accept()
		if err != nil {
			fmt.Println(err)
			return
		}

		go handle(connessione)
	}
}

//Connessione Handle > Thread
func handle(connessione net.Conn) {

	fmt.Println("Scrivere exit per uscire")

	for {
		data, err := bufio.NewReader(connessione).ReadString('\n')

		

		if err != nil {
			fmt.Print(err)
			return
		}
		fmt.Println(data)

	}

}

答案1

得分: 0

如果我没记错的话,PuTTY能够使用多种协议访问服务器,其中最常用的是SSH和Telnet。

大多数情况下,人们使用PuTTY来访问SSH服务器,并且在SSH会话中键入"exit"来终止它具有相对复杂的语义:大多数情况下,服务器运行一个命令行shell,SSH协议在该shell和SSH客户端之间传输数据,因此当用户在客户端(例如PuTTY)中键入"exit"时,客户端将其发送到SSH服务器,然后服务器将其发送到shell;shell解释该命令,终止自身,SSH服务器检测到并关闭其会话。
总之,PuTTY本身不以任何方式处理键入"exit";它只是将该字符串发送到远程端。

你没有告诉我们,但从你的代码来看,你编写了一个普通的TCP服务器(我是说,不是SSH服务器),因此看起来PuTTY使用Telnet访问你的服务器。
在这种情况下,键入"exit"到PuTTY将直接将该行发送到你的Go服务器,因此要终止你的服务器,你必须解释发送给它的内容,一旦它接收到一行"exit",它必须终止处理循环并关闭连接。

根据你的代码判断,应该是这样的:

func handle(conn net.Conn) {
    defer conn.Close()

    fmt.Println("Scrivere exit per uscire")

    for {
        data, err := bufio.NewReader(conn).ReadString('\n') 

        if err != nil {
            fmt.Print(err)
            return
        }

        fmt.Println(data)
        if data == "exit" {
            return
        }
    }
}

在这里,我们:

  1. 添加了一个defer语句来关闭连接 - 这样每次从连接的"处理程序"退出时都会发生这种情况。
    不包括这个在你原来的代码中是一个错误,会导致服务器最终资源耗尽。
  2. 添加了一个if语句,分析从客户端发送的文本行,并在该行为"exit"时终止处理程序。

在评论线程之后更新

问题

有一个问题使我最初提供的修复方法无法使用:

  • bufio.Reader.ReadString(delim byte)的文档如下:

    ReadString读取输入中第一个出现的delim,返回包含数据和分隔符的字符串。

    由于代码要求ReadString读取到\n,当客户端发送"exit"后跟换行符(ASCII LF,0x0a)时,ReadString会返回"exit"加上LF字符。

  • PuTTY显然使用的Telnet协议,它使用CRLF序列(0x0b,0x0a)终止客户端发送的每一行,所以if语句在我的示例中接收到的是"exit\r\n"。

实际上,如果OP在调试输出语句中使用类似fmt.Printf("%q\n", data)这样的东西,这两个事实都很容易看到 - 因为%q格式化动词会使任何不可打印的字符在输出中"可见"。

解决方案

首先,原始代码有一个我在第一次阅读时没有注意到的错误:在每次循环迭代中创建了一个新的bufio.Reader实例 - 丢弃了之前的实例(如果有的话)。但是,该类型明确记录了它有权限从源读取器缓冲任意数量的数据 - 也就是说,它能够从读取器中消耗比它将返回给调用任何类型的读取方法的客户端更多的数据。
这意味着当一个bufio.Reader被丢弃时,它自上次读取调用以来缓冲的数据也被丢弃了。
这个错误很严重,需要修复。

其次,我们需要决定如何处理换行符终止字符。
有多种选择:

  • 精确指定服务器支持的换行格式,并实现对该规范的支持。

    例如,我们可以假设我们的协议中的每一行必须以单个LF终止。
    在这种情况下,我们可以将代码(几乎)保持不变,但将客户端提交的字符串与"exit\n"进行比较。

    在这种情况下,通过Telnet协议与服务器通信将无法正常工作,因为Telnet客户端会以CRLF序列终止用户提交的行。

  • 对协议要求宽松,允许LF和CRLF两种方式。

    要支持这一点,代码应该以某种方式处理客户端行可能以任一方式终止的可能性。
    例如,可以在比较语句之前修剪输入行。

我认为最简单的方法是选择第二个选项,并使用bufio.Scanner类型:

func handle(conn net.Conn) {
  defer conn.Close()

  scanner = bufio.NewScanner(conn)
  for scanner.Scan() {
    data := scanner.Text()

    fmt.Printf("%q\n", data)

    if data == "exit" {
      return
    }
  }

  if err := scanner.Err(); err != nil {
    fmt.Println("error:", err)
  }
}
英文:

If memory serves me right, PuTTY is able to use several protocols to access servers, with SSH and Telnet being used the most.

Most of the time people use PuTTY to access SSH servers, and typing "exit" in an SSH session to terminate it has relatively complex semantics: most of the time, the server runs a command-line shell, the SSH protocol is carrying data between that shell and the SSH client, so when the user types "exit" in the client (PuTTY, for instance), the client sends that to the SSH-server and it sends that to the shell; the shell interprets that command, terminates itsef, the SSH server detects that and shuts down its session.
To recap, PuTTY does not in any way handles typing "exit" into it by itself; it merely sends that string to the remote side.

You did not tell us, but form your code, it looks like you have written a plain TCP server (I mean, not an SSH server) and hence looks like PuTTY uses Telnet to access your server.
In this case, typing "exit" into the PuTTY will make it send that line to your Go server, directly, so to terminate your server you have to interpret what is being sent to it, and once it receives a line "exit", it has to terminate the processing loop and close the connection.

Judging from your code, it should be something like

func handle(conn net.Conn) {
    defer conn.Close()

    fmt.Println("Scrivere exit per uscire")

    for {
        data, err := bufio.NewReader(conn).ReadString('\n') 

        if err != nil {
            fmt.Print(err)
            return
        }

        fmt.Println(data)
        if data == "exit" {
            return
        }
    }
}

Here, we

  1. Added a deferred statement which closes the connection — so that this happens each time we exit from the connection "handler".
    Not including that was a bug in your original code which would lead to eventual recource exhaustion on the server.
  2. Added an if statement which analyzes the line of text sent from the client and terminates the handler once that line is "exit".

Update after the comment thread.

The problem

There is a twist which renders the fix I have initially offerred unusable:

  • bufio.Reader.ReadString(delim byte) is documented as follows

    > ReadString reads until the first occurrence of delim in the input, returning a string containing the data up to and including the delimiter.

    Since the code asked ReadString to read until \n, when the client sends "exit" followed by a newline character (ASCII LF, 0x0a), ReadString returns precisely "exit" plus that LF character in a single piece.

  • The Telnet protocol which PuTTY is apparently using, terminates each line the client sends with a CRLF sequence, 0x0b, 0x0a, and that's what gets sent to the server, so the if statement in my example is receiving "exit\r\n".

Actually, both facts could have been easily seen if the OP were using something like fmt.Printf("%q\n", data) in their debug output statement — as the %q formatting verb makes any non-printable character "visible" in the output.

The solutions

First, the original code has a bug I haven't spotted on the first read: a new instance of a bufio.Reader is created on each loop iteration—throwing away the previous one, if any. But this type is explicitly documented as having permission to buffer any amount of data from the source reader—that is, it's able to consume from the reader more data than it is going to return to the client which made a call to any of the type's reading methods.
This means when a bufio.Reader is discarded, the data it has buffered since the last reading call is thrown away, too.
This bug is grave, and is needed to be fixed.

Second, we need to decide what to do with the newline termination character(s).
There are multiple choices:

  • Specify precisely which newline format the server supports in its protocol and then implement support for exactly that specification.

    For instance, we can postulate each line in our wire protocol must be terminated by exactly a single LF.
    In this case we might leave the code (almost) as is, but compare the string submitted by the client with "exit\n".

    In this case, talking to the server via the Telnet protocol will not work as a Telnet client terminates the lines submitted by the user with CRLF sequences.

  • Be lax about the protocol and allow both LFs and CRLFs.

    To support this, the code should somehow deal with the possibility for the client's lines to be terminated either way.
    This could be done, for instance, by trimming the input line before using it in the comparison statement.

I would say the simplest approach is to go with the second option and also use the bufio.Scanner type:

func handle(conn net.Conn) {
  defer conn.Close()

  scanner = bufio.NewScanner(conn)
  for scanner.Scan() {
    data := scanner.Text()

    fmt.Printf("%q\n", data)

    if data == "exit" {
      return
    }
  }

  if err := scanner.Err(); err != nil {
    fmt.Println("error:", err)
  }
}

huangapple
  • 本文由 发表于 2021年11月18日 16:57:28
  • 转载请务必保留本文链接:https://go.coder-hub.com/70017013.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定