如何在SignalR WebSockets中并行发起请求?解决图片瓦片加载缓慢问题
Ah, I've run into this exact issue with SignalR and tile-based loading before—let me break down why you're seeing serial processing and how to fix it.
By default, SignalR limits a single client connection to processing one invocation at a time (the default value of MaxConcurrentInvocationsPerClient is set to 1). So when you fire off 100 tile requests over one connection, they queue up and process sequentially, which explains that slow 50-second wait (100 * 0.5s).
Here are the most effective solutions to enable parallel processing:
The simplest fix is to adjust SignalR's server-side configuration to allow multiple concurrent invocations on a single connection. This lets your client send multiple tile requests that the server processes in parallel.
In your server's Startup/Program.cs, configure the Hub options:
builder.Services.AddSignalR(options => { // Allow up to 10 concurrent invocations per client (tune this based on your server capacity) options.MaxConcurrentInvocationsPerClient = 10; });
On the client side, you can now fire off multiple requests without waiting for each to complete. For example (JavaScript client):
const connection = new signalR.HubConnectionBuilder() .withUrl("/tileHub") .build(); // Fire off 10 parallel requests const tileRequests = []; for (let i = 0; i < 10; i++) { const x = /* tile x coordinate */; const y = /* tile y coordinate */; tileRequests.push(connection.invoke("GetTile", x, y)); } // Wait for all to complete and render tiles Promise.all(tileRequests) .then(tiles => { tiles.forEach(tile => renderTile(tile)); }) .catch(err => console.error(err));
If you need even more parallelism (or can't adjust the server config), create multiple independent SignalR connections from the client. Each connection will handle its own queue of requests, so multiple connections mean multiple parallel processing pipelines.
Just be careful not to create too many connections (aim for 5-16, depending on server resources) to avoid overwhelming the server:
// Create 5 separate connections const connections = Array.from({ length: 5 }, () => new signalR.HubConnectionBuilder() .withUrl("/tileHub") .build() ); // Initialize all connections await Promise.all(connections.map(conn => conn.start())); // Distribute tile requests across connections const tileCoordinates = [/* your list of (x,y) tile positions */]; tileCoordinates.forEach((coord, index) => { const connection = connections[index % connections.length]; connection.invoke("GetTile", coord.x, coord.y) .then(tile => renderTile(tile, coord.x, coord.y)) .catch(err => console.error(`Failed to load tile ${coord.x},${coord.y}:`, err)); });
Instead of sending individual requests per tile, send batches of tile coordinates in one invocation. On the server, process all tiles in the batch in parallel using Task.WhenAll, then return all tiles at once. This reduces the number of SignalR messages and is easier on both client and server.
Server-side Hub method:
public async Task<List<byte[]>> GetTilesBatch(List<(int X, int Y)> tileCoords) { // Generate all tiles in parallel var tileTasks = tileCoords.Select(coord => GenerateTileAsync(coord.X, coord.Y)); return await Task.WhenAll(tileTasks); } // Ensure your tile generation is truly async (no blocking calls!) private async Task<byte[]> GenerateTileAsync(int x, int y) { // Simulate 0.5s generation time (replace with your actual JPG generation logic) await Task.Delay(500); // Return the JPG byte array for the tile return GenerateTileImage(x, y); }
Client-side code:
// Group tiles into batches of 20 (adjust based on your server's capacity) const batchSize = 20; const tileBatches = []; const tileCoordinates = [/* your list of (x,y) positions */]; for (let i = 0; i < tileCoordinates.length; i += batchSize) { tileBatches.push(tileCoordinates.slice(i, i + batchSize)); } // Process each batch in parallel Promise.all(tileBatches.map(batch => connection.invoke("GetTilesBatch", batch) .then(tiles => { tiles.forEach((tile, index) => { const coord = batch[index]; renderTile(tile, coord.x, coord.y); }); }) )) .catch(err => console.error("Batch request failed:", err));
- Cache Tiles: Once a tile is generated, cache it on the server (e.g., in memory or Redis) so repeat requests don't reprocess the same tile. This will drastically reduce load over time.
- Throttle Requests: Even with parallelism, don't flood the server with hundreds of requests at once. Use a request queue with concurrency limits (e.g., 20 concurrent requests max) to avoid overwhelming your server's CPU/memory.
- Avoid Blocking Code: Make sure your tile generation logic is truly asynchronous (no blocking calls like
Thread.Sleep). Blocking code will tie up server threads and limit parallelism.
内容的提问来源于stack exchange,提问作者Peter

