简单Scala TCP服务器中的Socket问题

6

我是Scala的新手,所以问题可能很简单,但我已经花了一些时间尝试解决它。我有一个简单的Scala TCP服务器(没有actors,单线程):

import java.io._
import java.net._

object Application {
  def readSocket(socket: Socket): String = {
    val bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream))
    var request = ""
    var line = ""
    do {
      line = bufferedReader.readLine()
      if (line == null) {
        println("Stream terminated")
        return request
      }
      request += line + "\n"
    } while (line != "")
    request
  }

  def writeSocket(socket: Socket, string: String) {
    val out: PrintWriter = new PrintWriter(new OutputStreamWriter(socket.getOutputStream))
    out.println(string)
    out.flush()
  }

  def main(args: Array[String]) {
    val port = 8000
    val serverSocket = new ServerSocket(port)
    while (true) {
      val socket = serverSocket.accept()
      readSocket(socket)
      writeSocket(socket, "HTTP/1.1 200 OK\r\n\r\nOK")
      socket.close()
    }
  }
}

服务器监听来自 localhost:8000 的请求,并在响应中发送一个带有单词 OK 的 HTTP 响应体。接着我像这样运行 Apache Benchmark:

ab -c 1000 -n 10000 http://localhost:8000/

第一次使用很顺利。第二次启动ab时,它会挂起,并在netstat -a | grep 8000中产生以下输出:

....
tcp        0      0 localhost.localdo:43709 localhost.localdom:8000 FIN_WAIT2  
tcp        0      0 localhost.localdo:43711 localhost.localdom:8000 FIN_WAIT2  
tcp        0      0 localhost.localdo:43717 localhost.localdom:8000 FIN_WAIT2  
tcp        0      0 localhost.localdo:43777 localhost.localdom:8000 FIN_WAIT2  
tcp        0      0 localhost.localdo:43722 localhost.localdom:8000 FIN_WAIT2  
tcp        0      0 localhost.localdo:43725 localhost.localdom:8000 FIN_WAIT2  
tcp6       0      0 [::]:8000               [::]:*                  LISTEN     
tcp6      83      0 localhost.localdom:8000 localhost.localdo:43724 CLOSE_WAIT 
tcp6      83      0 localhost.localdom:8000 localhost.localdo:43786 CLOSE_WAIT 
tcp6       1      0 localhost.localdom:8000 localhost.localdo:43679 CLOSE_WAIT 
tcp6      83      0 localhost.localdom:8000 localhost.localdo:43735 CLOSE_WAIT 
tcp6      83      0 localhost.localdom:8000 localhost.localdo:43757 CLOSE_WAIT 
tcp6      83      0 localhost.localdom:8000 localhost.localdo:43754 CLOSE_WAIT 
tcp6      83      0 localhost.localdom:8000 localhost.localdo:43723 CLOSE_WAIT
....

由于服务器不再提供更多的请求。还有一个细节:使用相同参数的ab脚本在同一台机器上测试简单的Node.js服务器时可以顺利运行。因此,这个问题与我设置为可重用的打开TCP连接的数量无关。

sudo sysctl -w net.ipv4.tcp_tw_recycle=1
sudo sysctl -w net.ipv4.tcp_tw_reuse=1

请问有谁能给我一些线索,告诉我我漏掉了什么?

编辑:已在上述代码中添加了流处理的终止。

    if (line == null) {
      println("Stream terminated")
      return request
    }

CLOSE_WAIT 表示 TCP 正在等待应用程序关闭其套接字。另一个问题是您没有发送正确的 HTTP 行终止符。它们被指定为 \r\n,而不是 \n。 - user207421
我已将代码更新为\r\n,尽管curlab似乎都可以正常使用\n。至于CLOSE_WAIT - 它似乎不是问题的根源。谢谢您的评论。 - nab
CLOSE_WAIT 是一个问题的症状,即应用程序没有关闭套接字。 - user207421
我没有看到你在哪里关闭输入/输出流?在系统执行完成后,这些不应该被关闭吗? - sc_ray
你能找到解决问题的方法吗?我很好奇是什么解决了它? - sc_ray
@sc_ray 我已经发布了答案 - nab
2个回答

2

对于那些今后可能遇到同样问题的人,我在此发布(部分)回答自己的问题。首先,问题的本质并不在源代码中,而是在系统本身中限制了许多连接。问题在于传递给readSocket函数的socket在某些情况下出现了损坏,即无法读取,并且bufferedReader.readLine()在第一次调用时要么返回null,要么无限期挂起。以下两个步骤使代码在某些机器上运行:

  1. Increase the number of concurrent connections to a socket with

    sysctl -w net.core.somaxconn=65535
    
  2. Provide the second parameter to ServerSocket constructor which will explicitly set the length of connection queue:

    val maxQueue = 50000
    val serverSocket = new ServerSocket(port, maxQueue)
    

以上步骤解决了EC2 m1.large实例的问题,但是我在本地机器上仍然遇到问题。更好的方法是使用Akka来处理这种情况:

import akka.actor._
import java.net.InetSocketAddress
import akka.util.ByteString

class TCPServer(port: Int) extends Actor {

  override def preStart {
    IOManager(context.system).listen(new InetSocketAddress(port))
  }

  def receive = {
    case IO.NewClient(server) =>
      server.accept()
    case IO.Read(rHandle, bytes) => {
      val byteString = ByteString("HTTP/1.1 200 OK\r\n\r\nOK")
      rHandle.asSocket.write(byteString)
      rHandle.close()
    }
  }
}

object Application {
  def main(args: Array[String]) {
    val port = 8000
    ActorSystem().actorOf(Props(new TCPServer(port)))
  }
}

0

首先,我建议您在不使用ab的情况下尝试一下。您可以尝试以下操作:

echo "I'm\nHappy\n" | nc -vv localhost 8000

其次,我建议处理流的结束。这是当BufferedReader.readLine()返回null时发生的情况。上面的代码只检查空字符串。在你修复这个问题之后,我建议再试一次。然后使用ab进行测试,在一切看起来都很好的情况下。如果问题仍然存在,请告诉我们。

首先,该代码在浏览器、curl、telnet等中都可以运行。其次,“null”处理不是根本原因。事实上,在我的情况下,它只会在客户端关闭连接时发生,而这并不是问题所在。我已经更新了问题以使其更加清晰。感谢您的建议。 - nab

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接