我有一个可视化 C# 应用程序,它通过 WebSocket 连接到使用 Heroku 部署的远程 Node.js 服务器。
服务器使用 npm WebSocket 模块“ws”来创建一个 WebSocket-Server。
C# 客户端应用程序使用此 GitHub 存储库中的 WebSocketSharp 库:https ://github.com/sta/websocket-sharp创建连接到服务器的 WebSocket-Client。
这是必要的节点服务器 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);
这是节点服务器 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 连接到服务器大约一分钟后,客户端抛出 WebSocketException 并显示消息“无法从流中读取帧的标头”。
快速的谷歌搜索将我带到这个 GitHub 问题https://github.com/sta/websocket-sharp/issues/202。从上述链接:
“我们通过在 Ext.cs 中修改 ReadBytesAsync(this Stream stream, int length, Action completed, Action error) 设法完全修复它。我们注意到即使没有连接,'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 上的远程副本时才会发生这种情况。