C# WebSockets-Sharp WebSocketSharpException: «Заголовок фрейма не может быть прочитан из потока» - PullRequest
0 голосов
/ 11 января 2020

У меня есть визуальное приложение C#, которое подключается через WebSocket к моему удаленному Node.js серверу, который развернут с помощью Heroku.

Сервер использует npm модуль WebSocket "ws" для создания WebSocket-сервера.

Клиентское приложение C# использует библиотеку WebSocketSharp из этого репозитория GitHub: https://github.com/sta/websocket-sharp для создания WebSocket-Client, который подключается к серверу.

Вот необходимый сервер Node Server. js код:

require('dotenv').config()
var express = require('express');
const API = require('./api_handler').api;

const PORT = process.env.PORT || 5000; 
const HOSTNAME = process.env.HOST || '127.0.0.1';

const app = express();

app.use(express.urlencoded({ extended: true }));
app.use(express.json());

app.post('/', function(req, res){
    api.Auth(req.body,res);
});

app.get('/', function(req, res){
    res.send(':)');
});

const httpServer = app.listen(PORT, function () {
    console.log('Server running at http://' + HOSTNAME + ':' + PORT + '/');
});

const api = new API(httpServer);

Вот сервер Node Server ApiHandler. js код

class API_Handler{
  constructor(httpServer){
        const Database = require('./database').db;
        const { Server : wsServer} = require('ws');

        this.db = new Database();
        this.wss = new wsServer({ server: httpServer });

        this.actions = {write : 'write', read : 'read', authenticate : 'authenticate'};
        this.operations = {insert : 'insert', update : 'update', delete : 'delete', login : 'login', logout : 'logout'};
        this.template = {action : 'action', operation : 'operation',categories : 'categories',category : 'category',IDs : 'ids',fields : 'fields',data : 'data',userid : 'userid',password : 'password',Token : 'Token',Logged : 'logged'}; 
        this.status = {ok : 'ok', error : 'error'};     

        this.CLIENT_TOKENS = [];
        this.wsClients = new Object();
        this.ClientIDs = new Object();

        this.wss.on('connection', (client,req) => { 
            var cookie = this.CookieParseJSON(req.headers.cookie)
            if(this.CLIENT_TOKENS.includes(cookie.Token)){          
                this.CLIENT_TOKENS.splice(this.CLIENT_TOKENS.indexOf(cookie.Token), 1); 
                this.wsClients[cookie.Token] = client;
                client.on('message', (msg) => { 
                  this.handle(JSON.parse(msg),client); 
                });
                client.on('close', () => {
                  console.log('Client disconnected');
                });
                console.log('Client connected');
                this.InitializeClient(client);
            }
            else{
                console.log('Unauthorized Client connected');
                client.close();
            }
        });

  }

    async Auth(req,res){
        if(this.template.password in req){
            var tmp = JSON.parse(JSON.stringify(req));
            tmp.password = (Array.from({length:req.password.length}).map(x=>'*')).join('');
            console.log(tmp);
        }else{console.log(req);}
        var result;     
        if(this.template.action in req){
            if (req.action === this.actions.authenticate){
                if(this.template.operation in req){
                    if(req.operation === this.operations.login){                        
                        if(this.template.userid in req && this.template.password in req ){  
                            result = await this.executeLogin(req);
                        }else{result = this.missingAuthCredentialsResult();}
                    }else{result = this.invalidAuthOperationResult();}
                }   else{result = this.noAuthOperationResult();}
            }else if(req.operation === this.operations.read || req.operation === this.operations.write || req.operation === this.operations.logout){
                result = this.UnAuthedActionResult();
            }   else{result = this.invalidActionResult();}
        }else{result = this.noActionResult();}
        res.json(result);
    }

  async handle(req,client){
        console.log(req);
        var result;

        if(this.template.Token in req){
            if(req.Token in this.wsClients){
                if(this.template.action in req){
                    if(req.action === this.actions.authenticate){
                        if(this.template.operation in req){
                            if(req.operation === this.operations.logout){               
                                await this.executeLogout(req);
                                client.close();return;
                            }else{result = this.invalidAuthOperationResult();}
                        }   else{result = this.noAuthOperationResult();}
                    }if (req.action === this.actions.read){
                        if(this.template.categories in req){    
                            if(this.db.validateCategories(req.categories)){

                                result = await this.executeRead(req);
                                client.send(JSON.stringify(result));return;

                            }else{result = this.invalidCategoriesResult();}
                        }else{result = this.noCategoriesResult()}
                    }else if (req.action === this.actions.write){       
                        if(this.template.category in req){
                            if(this.db.validateCategory(req.category) && this.db.isWritableCategory(req.category)){
                                if(this.template.operation in req){
                                    if(req.operation === this.operations.insert){
                                        if(this.template.data in req){

                                            await this.executeInsert(req);
                                            return;

                                        }else{result = this.noDataResult()}
                                    }else if(req.operation === this.operations.update){
                                        if(this.db.isUpdatableCategory(req.category)){
                                            if(this.template.IDs in req){
                                                if(this.template.fields in req && Array.isArray(req.fields) && req.fields.length > 0){
                                                    if(this.template.data in req){

                                                        await this.executeUpdate(req);
                                                        return;

                                                    }else{result = this.noDataResult()}
                                                }else{result = this.noFieldsResult()}
                                            }else{result = this.noIDsResult()}
                                        }else{result = this.invalidCategoryResult();}
                                    }else if(req.operation === this.operations.delete){
                                        if(this.template.IDs in req){

                                            await this.executeDelete(req);
                                            return;

                                        }else{result = this.noIDsResult()}
                                    }else{result = this.invalidOperationResult();}
                                }else{result = this.noOperationResult();}
                            }else{result = this.invalidCategoryResult();}
                        }else{result = this.noCategoryResult();}
                    }else{result = this.invalidActionResult();}
                }else{result = this.noActionResult();}
            }else{result = this.invalidTokenResult();}
        }else{result = this.noTokenResult();}

        client.send(JSON.stringify(result));
        client.close();
  }

    async executeLogin(req){ 
        if(await this.db.authenticate(req.userid,req.password)){    //successfully logged in
            console.log("Auth Passed");
            var res = new Object();
            var token = this.hex();
            res[this.template.Token] = token;
            res[this.template.Logged] = true;
            this.CLIENT_TOKENS.push(token);
            this.ClientIDs[token] = req.userid;
            return new Promise((resolve,reject) =>{resolve ({ status : this.status.ok, message : this.messages.success.loggedIn, result: res});});  
        }else{
            console.log("Auth Failed");
            var res = new Object();
            res[this.template.Logged] = false;
            return new Promise((resolve,reject) =>{resolve ({ status : this.status.ok, message : this.messages.error.loggedIn, result: res});});    
        }
    }
    async executeLogout(req){ 
        this.wsClients[req.Token].close();
        delete this.wsClients[req.Token];
        delete this.ClientIDs[req.Token];
    }
    async executeRead(req){ 
        req.categories = this.removeDuplicates(req.categories);

        var res = new Object();
        var promises = [];
        for(var i = 0; i < req.categories.length; i++){ promises[i] = this.db.select(req.categories[i]); }

        await Promise.all(promises).then( (results) => {
            for(var i = 0; i < results.length; i++){
                res[req.categories[i]] = (results[i].command === 'SELECT')?{count: results[i].rowCount, values: results[i].rows} : this.messages.error.selectCategory;
        }});

        return new Promise((resolve,reject) =>{ resolve ({ status : this.status.ok, message : this.messages.success.read, result: res});});     
    }
    async executeInsert(req){
        for(var i = 0; i < req.data.length; i++){
            var dbResponse = await this.db.insert(req.category,req.data[i],this.ClientIDs[req.Token]);          
        }
        this.UpdateClientData(req,(req.category === this.db.tables.Transactions || req.category === this.db.tables.ItemTypes));
    }
    async executeUpdate(req){
        for(var i = 0; i < req.ids.length; i++){
            var dbResponse = await this.db.update(req.category,req.ids[i],req.fields,req.data[i],this.ClientIDs[req.Token]);
        }
        this.UpdateClientData(req);
    }
    async executeDelete(req){
        req.ids = this.removeDuplicates(req.ids);
        for(var i = 0; i < req.ids.length; i++){var dbResponse = await this.db.delete(req.category,req.ids[i],this.ClientIDs[req.Token]);}
        this.UpdateClientData(req);
    }

    async InitializeClient(client){
        var read = await this.ReadInit();
        client.send(JSON.stringify(read));
    }

    async ReadInit(){
        return this.executeRead({categories : this.db.tableNames});     
    }

    async UpdateClientData(req, updateSender = false){
        var cats = [req.category];
        if(req.category === this.db.tables.ItemListings){cats.push(this.db.tables.Transactions);}
        var read = await this.ReadAll(cats);
        var readString = JSON.stringify(read);
        this.wss.clients.forEach((client) => {
            if(!updateSender || client !== this.wsClients[req.Token]){
                client.send(readString);
                console.log("REFRESH: Client updated, columns: " + cats);
            }else{
                console.log("REFRESH: Sender-Client was skipped");
            }
        });
    };

    async ReadAll(cats){
        return this.executeRead({categories : cats});   
    }

    removeDuplicates(array){return [...new Set(array)]; }

    hex(){
        return this.randHex(16);
    }

    randHex(len) {
        var maxlen = 8;
        var min =   Math.pow(16,Math.min(len,maxlen)-1);
        var max = Math.pow(16,Math.min(len,maxlen)) - 1;
        var n   = Math.floor( Math.random() * (max-min+1) ) + min;
        var r   = n.toString(16);
        while ( r.length < len ) { r = r + this.randHex( len - maxlen ); }
        return r;
    }

    CookieParseJSON(cookieStr){
        var sep = cookieStr.indexOf('=');
        var key = cookieStr.substr(0,sep);
        var value = cookieStr.substr(sep+1,cookieStr.length - sep -1);
        var obj = new Object();
        obj[key] = value;
        return obj;
    }
}

module.exports.api = API_Handler;

Вот необходимый C# Код клиентского приложения

public void init_Socket() {
    wsSocket = new WebSocket(Socket_URL);
    wsSocket.OnOpen += (sender, e) => {
        Console.WriteLine("Connected to server at "+Socket_URL);
    };
    wsSocket.OnClose += (sender, e) => {
        setToken(NO_TOKEN);
        Console.WriteLine("Disconnected from server! - " + e.Code.ToString());
    };
    wsSocket.OnMessage += (sender, e) => {
        if (wsSocket.ReadyState == WebSocketState.Open && e.IsText) {
            Console.WriteLine("Server Message: " + e.Data);
            ReadHandler handler = new ReadHandler(null);
            handler.handle(e.Data);
        }
    };
}

public void setToken(string token) {
    if(wsSocket != null) {
        wsSocket.SetCookie(new Cookie(Props.Token, token));
    }
}

public void SocketConnect() {
    wsSocket.Connect();
}

private void SendSocketMessage(APIRequest req, APIResponseHandler handler) {
    Console.WriteLine("SOCKET MESSAGE: " + req.ToString());

    if (wsSocket.IsAlive && wsSocket.ReadyState == WebSocketState.Open) {
        wsSocket.SendAsync(req.ToString(), new Action<bool>((bool completed) => {
            Console.WriteLine("SENDING completed!");
        })
    );
    } else{ Console.WriteLine("Socket must be alive in order to send a message.");} 
 }

Так как он работает, когда клиент обновляет информацию на сервере, он отправляет обновленная информация на сервер и сервер затем отправляет эту обновленную информацию всем своим клиентам, кроме отправителя.

Когда клиенты получают обновленную информацию с сервера, они обновляют свои локальные копии в соответствии с данными, полученными с сервера. .

Проблема в том, что после того, как клиент был подключен к серверу через WebSocket в течение примерно минуты, клиент выбрасывает WebSocketExce. с сообщением «Заголовок фрейма не может быть прочитан из потока».

Быстрый поиск в Google приводит меня к этой проблеме GitHub https://github.com/sta/websocket-sharp/issues/202. Из вышеупомянутой ссылки:

"Нам удалось полностью исправить это, изменив ReadBytesAsyn c (поток Stream, длина int, действие завершено, ошибка действия) в Ext.cs. Мы заметили, что 'NetworkStream .EndRead 'может возвращать ноль (0) байтов, даже если соединение не закрывается. При близком чтении документации EndRead говорится только, что нулевое число байт возвращается, когда соединение закрыто. Иначе не всегда верно, получение 0 байтов делает не всегда означает, что соединение закрывается, кажется. Довольно часто возвращается ноль байт, даже когда соединение не закрывается. Происходит время от времени с Chrome, и, кажется, происходит чаще с Firefox. Поэтому замена: if (nread == 0 || nread == length) на: if(nread == length) устраняет проблему. Он гарантирует, что когда кто-то ожидает, например, двух байт кадра, он действительно возвращает два байта назад. ".

Но при проверке кода в библиотека WebSocket-Sharp, указанное выше исправление уже применено к библиотеке (сообщение было м 2016, так что это, вероятно, было исправлено).

Код из библиотеки в 2020 году:

public static class Ext{
 //Other functions and properties of Ext goes here

 internal static void ReadBytesAsync (this Stream stream,int length,Action<byte[]>
                      completed,Action<Exception> error){
   var buff = new byte[length];
   var offset = 0;
   var retry = 0;

   AsyncCallback callback = null;
     callback =
       ar => {
         try {
            var nread = stream.EndRead (ar);
            if (nread <= 0) {
                if (retry < _retry) {
                retry++;
                stream.BeginRead (buff, offset, length, callback, null);
                return;
              }

            if (completed != null)
                completed(buff.SubArray(0, offset));

                return;
            }

            if (nread == length) {
                if (completed != null)
                  completed (buff);

              return;
            }

            retry = 0;

            offset += nread;
            length -= nread;

            stream.BeginRead (buff, offset, length, callback, null);
          }
          catch (Exception ex) {
            if (error != null) {
                error(ex);
                }
            }
        };

      try {
        stream.BeginRead (buff, offset, length, callback, null);
      }
      catch (Exception ex) {
        if (error != null)
          error (ex);
      }
    }
}

Еще одна странная вещь заключается в том, что когда я запускаю сервер локально и подключаюсь к нему, эта проблема никогда происходит. Это происходит только при подключении к удаленной копии, развернутой в Heroku.

...