socket.io 客户端在长 Node.js 函数中自动断开连接

Posted

技术标签:

【中文标题】socket.io 客户端在长 Node.js 函数中自动断开连接【英文标题】:socket.io client automatically disconnecting in long Node.js function 【发布时间】:2021-11-18 04:50:35 【问题描述】:

我正在使用 socket.io 将我的应用程序的 swift 客户端与服务器通信。本质上,客户端在打开应用程序时加入一个套接字连接,并且一个作业会立即添加到 Redis 队列中(这是一个需要几秒钟到大约 15 秒的作业)。服务器对作业 ID 的客户端有响应。在处理此作业时,有时客户端会断开连接。这背后似乎没有押韵或原因,因为断开连接的时间完全不一致,而且断开连接也不是发生在函数中的特定点。我想也许我是手动断开与客户端的连接,所以我在客户端每次断开连接之前设置了套接字发射(当这些发射被发射到服务器时,服务器会打印一些告诉我断开连接来自哪里的东西)。这表明断开连接是自动的,因为在结束套接字连接之前客户端从未接收到发射。这是在 Heroku 上运行的。这是我的代码:

//queue initialization
const queue = new Queue('queue', process.env.REDIS_URL)

//client pings this endpoint to get the job id in the queue
app.post('/process', async function(request, response) 
  let job = await queue(request: request.body);
  console.log("Logging job as " + job.id)
  response.json( id: job.id );
);

queue.process(10, async (job) =>  //10 is the max workers per job
    console.log("Started processing")
    const client = await pool.connect()
    let item = job.data.request
    let title = item.title
    let subtitle = item.subtitle
    let id = item.id
    io.to(id).emit("Processing1", ""); //added emissions like these because I thought maybe the socket was timing out, but this didn't help
    console.log("Processing1");

    try 
      await client.query('BEGIN')
        let geoData = await //promise of geocoding endpoint api function
        let lengthOfGeoData = geoData.context.length
        io.to(id).emit("Processing2", "");
        console.log("Processing2");
        var municipality = ""
        var area = ""
        var locality = ""
        var place = ""
        var district = ""
        var region = ""
        var country = ""
        //for loop to go through geoData and set the above values
      if (municipality != "") 
        console.log("Signing in from " + municipality + ", " + area);
       else 
        console.log("Signing in from " + area)
      
      await scrape(municipality, area, id);
      await client.query('COMMIT')
     catch(err) 
      await client.query('ROLLBACK')
      console.log(err)
    
    try 
      await client.query('BEGIN')
      const array = await //a function that queries a Postgres db for some rows, makes json objects out of them, and pushes to the 'array' variable
      var array2 = []
      for (a of array) 
        let difference = getDifference(title, subtitle, a.title, a.subtitle) //math function
        if (difference <= 10) 
          array.push(a)
        
      
      io.to(id).emit("Processing9", "");
      console.log("Processing9");
      await client.query('COMMIT')
     catch(err) 
      await client.query('ROLLBACK')
      console.log("ERROR: Failed arrayHelperFunction")
      console.log(err)
     finally 
      client.release()
      console.log("About to emit this ish to " + id) //should emit to socket here ideally to notify that the processing is done and results can be polled
      io.to(id).emit("finishedLoading", "")
      return array2;
    
);

//when the client polls the queue after it's received the 'done' notifier from the server
app.post('/poll', async function(request, response) 
  console.log("Polling")
  let id = request.body.id
  const results = await queue(id);
  for (r of results.returnvalue) 
    console.log("Sending " + r.title);
  
  response.send(results.returnvalue)
);

//scrape
async function scrape(municipality, area, id) 
  const client = await pool.connect();
  try 
    await client.query('BEGIN')
    var location = ""
    if (municipality != "") 
      location = municipality + ", " + area
     else 
      location = area
    
    let inDatabase = await client.query('SQL statement AS it_does_exist', [params]);
    io.to(id).emit("Processing3", "");
    console.log("Processing3");
    if (inDatabase.rows[0].it_does_exist == false)  
      let query = "book clubs near " + location
      var terminationTime = new Date()
      terminationTime.setHours(terminationTime.getHours() + 4);
      let date = ("0" + terminationTime.getDate()).slice(-2);
      let month = ("0" + (terminationTime.getMonth() + 1)).slice(-2);
      let year = terminationTime.getFullYear();
      let hours = terminationTime.getHours();
      let minutes = terminationTime.getMinutes();
      let seconds = terminationTime.getSeconds();
      let timestamp = year + "-" + month + "-" + date + " " + hours + ":" + minutes + ":" + seconds

      try 
        await client.query(`SQL statement`, [params]);
       catch(err) 
        console.log("FAILURE: scrape() at 1.")
        console.log(err)
      

      var queryLocation = "New York,New York,United States" //default search origination is here
      var queryGLCode = "US"
      io.to(id).emit("Processing4", "");
      console.log("Processing4");
      try 
        await fetch('https://serpapi.com/locations.json?q='+municipality+'&limit=10',  method : "GET" )
          .then(res => res.json())
          .then((json) => 
            for (let index = 0; index < 10; index++) 
              let locationAPIName = json[index].canonical_name
              let locationAPICode = json[index].country_code
              let resultLatitude = json[index].gps[1];
              let resultLongitude = json[index].gps[0];
            
          );
       catch(err) 
        console.log("FAILURE: scrape() at 2.")
        console.log(err)
      
      io.to(id).emit("Processing5", "");
      console.log("Processing5");
      try 
        await Promise.all([
          searchEvents(engine: "google_events", q: query, location: queryLocation, hl: "en", gl: queryGLCode).then(data => async function()
            try 
              await client.query('BEGIN');
              let results = data.events_results
              if (results != null) 
                console.log("first HAD results")
                for (result of results) 
                  var fixedAddress = result.address[0]
                  let address = fixedAddress + ", " + result.address[1]
                      
                  let title = result.title + address

                  var description = result.description

                  let geoData = await geocode(address); //mapbox geocode the address
                  let latitude = Number(geoData.center[0]);
                  let longitude = Number(geoData.center[1]);
                  
                    await client.query(`SQL statement`, [params]);
                  
                
                io.to(id).emit("Processing6", "");
                console.log("Processing6");
               else 
                console.log("first DID NOT have results")
              
              console.log("FIRST BLOCK")
              await client.query('COMMIT');
             catch(err) 
              console.log("Results[0] not found.")
              console.log(err)
              await client.query('ROLLBACK');
            
          ()),

          searchEvents(engine: "google_events", q: query, location: queryLocation, hl: "en", gl: queryGLCode, start: "10").then(data => async function()
            // same as the one above, just with an offset
          ()),

          searchEvents(engine: "google_events", q: query, location: queryLocation, hl: "en", gl: queryGLCode, start: "20").then(data => async function()
            // same as the one above, but with a different offset
          ())
        ])
       catch(err) 
        console.log("FAILURE: scrape() at 3.")
        console.log(err)
      

     else 
      console.log("Location already in the database.")
    
    await client.query('COMMIT')
   catch(err) 
    await client.query('ROLLBACK')
    console.log(err)
   finally 
    client.release()
    return "Resolved";
  


//Client establish socket connection
func establishConnection(_ completion: (() -> Void)? = nil) 
    let socketUrlString: String = appState.server
    self.manager = SocketManager(socketURL: URL(string: socketUrlString)!, config: [.log(false), .reconnects(true), .extraHeaders(["header": "customheader"])])
    self.socket = manager?.defaultSocket
    self.socket?.connect()
    self.socket?.once(clientEvent: .connect, callback:  (data, emitter) in
        if completion != nil
            completion!()
        
    )
  //other socket functions


//Client initial post request
func process() 
    let server = "serverstring" + "process"
    let title = "title"
    let subtitle = "subtitle"
    let package = BookPackage(title: title, subtitle: subtitle, id: mySocketID) //this is after the initial connection
    print("package is \(package)")
            
    guard let url  = URL(string: server) else  return 

    var urlRequest = URLRequest(url: url)
    
    urlRequest.addValue("application/json", forHTTPHeaderField: "Content-Type")
    urlRequest.addValue("application/json", forHTTPHeaderField: "Accept")
    
    urlRequest.httpMethod = "POST"
    
    guard let data = try? JSONEncoder().encode(package) else  return 
            
    urlRequest.httpBody = data

    let task = URLSession.shared.dataTask(with: urlRequest) 
        (data, response, error) in
        if let error = error 
            print(error)
            return
        
        guard let data = data else  return 
        guard let dataString = String(data: data, encoding: String.Encoding.utf8) else  return 
        let jsonData = Data(dataString.utf8)
        var decodedJob: Job? = nil
        do 
            decodedJob = try JSONDecoder().decode(Job.self, from: jsonData) //Job is just a struct in the same form as the json object sent back from the server
         catch 
            print(error.localizedDescription)
        
        DispatchQueue.main.async 
            self.appState.pendingJob = decodedJob
        
    
    // start the task
    task.resume()

此错误唯一一致的部分是用户断开连接之前的日志(旁注:“断开连接的原因”和“断开连接的用户”触发 socket.on('disconnect') 事件:

https://i.stack.imgur.com/7fjuU.png

https://i.stack.imgur.com/z5bmL.png

https://i.stack.imgur.com/aHNt3.png

https://i.stack.imgur.com/64WYI.png

【问题讨论】:

长函数是否阻塞了事件队列? 你能说明客户端是如何发出请求来启动这一切的吗?是来自 javascript 还是浏览器表单帖子? 您是在问题标题中使用“长函数”一词的人。请不要问我它是什么。我在问你。 那么,客户端代码在 iPhone 上运行(因为它看起来像 Swift 代码)?也许 iPhone 声称存在电源管理问题,导致它断开非活动的 socket.io 连接?这看起来可能是客户端问题。我建议搜索与 socket.io 连接断开的 ios 问题,看看你发现的无数点击中是否有任何一个看起来像你的情况。 设置正确的环境变量将自动启用服务器上的调试。对于客户端,您似乎必须设置一个 localStorage 值。这个想法是调试代码已经在 socket.io 中,您只需使用正确的设置启用它。您可能必须使用正确的设置重新启动服务器才能生效。 【参考方案1】:

你应该用 await 阻塞事件循环。客户端每隔一段时间发送一次心跳(使用pingTimeout 定义)。

由于服务器没有收到ping,所以断开连接。

你应该隔离这个过程。找到一种将它与工作/后台进程或异步一起使用的方法,另外在服务器端增加 pingTimeout 可能会对您有所帮助。

【讨论】:

我相信你有迄今为止最正确的答案(这就是我给你赏金的原因),但对我来说这不是解决方案。 @nickcoding2 我明白了,谢谢。但进展如何?你可以在代码执行开始时启动计时器吗?您可以使用 console.time(label);控制台.timeEnd();此外,您可以在每次发送心跳时进行控制台输出。【参考方案2】:

解决你的问题的方法是在启动服务器时修改pingTimeout。

来自Socket.io:

服务器发送ping,如果客户端在pingTimeout ms内没有回复pong,则服务器认为连接已关闭。

同样,如果客户端没有收到来自服务器的 ping 在 pingInterval + pingTimeout ms 内,客户端还认为 连接已关闭。

const io = new Server(httpServer, 
  pingTimeout: 30000
);

【讨论】:

嗨,很遗憾,我已经尝试过这个解决方案,但它不起作用(尝试将 pingTimeout 设置为 50000 毫秒)。运行调试,我可以看到断开连接的原因是“客户端关闭原因传输关闭”,如果这是问题原因将是“ping 超时”。【参考方案3】:

您可以将传输从默认的更改为

 const io = new Server(httpServer, 
  transports: ['polling', 'websocket'],
);

这可能会解决问题,否则您也可以尝试更改upgradeTimeoutpingTimeout

【讨论】:

以上是关于socket.io 客户端在长 Node.js 函数中自动断开连接的主要内容,如果未能解决你的问题,请参考以下文章

Node.js + socket.io 确定每个实例的最大客户端数量

Flask socket.IO 服务器上的 Node.js Socket.io 客户端

Node.js Websockets Socket.IO

如何在 node.js 中接收 socket.io 客户端事件?

我可以通过 node.js 中的 socket.io 将文件上传到服务器吗?

express,用于浏览器客户端和 node.js 客户端的 socket.io 服务器