3
3
4
4
using System ;
5
5
using System . Buffers ;
6
+ using System . Diagnostics ;
6
7
using System . IO . Pipelines ;
7
8
using System . Text ;
8
9
using System . Threading ;
@@ -26,15 +27,26 @@ public class Http1OutputProducer : IHttpOutputProducer, IHttpOutputAborter, IDis
26
27
private readonly IHttpMinResponseDataRateFeature _minResponseDataRateFeature ;
27
28
private readonly TimingPipeFlusher _flusher ;
28
29
29
- // This locks access to to all of the below fields
30
+ // This locks access to all of the below fields
30
31
private readonly object _contextLock = new object ( ) ;
31
32
32
33
private bool _completed = false ;
33
34
private bool _aborted ;
34
35
private long _unflushedBytes ;
35
-
36
+ private bool _autoChunk ;
36
37
private readonly PipeWriter _pipeWriter ;
37
38
39
+ private const int BeginChunkLengthMax = 5 ;
40
+ private const int EndChunkLength = 2 ;
41
+
42
+ // Chunked responses need to be treated uniquely when using GetMemory + Advance.
43
+ // We need to know the size of the data written to the chunk before calling Advance on the
44
+ // PipeWriter, meaning we internally track how far we have advanced through a current chunk (_advancedBytesForChunk).
45
+ // Once write or flush is called, we modify the _currentChunkMemory to prepend the size of data written
46
+ // and append the end terminator.
47
+ private int _advancedBytesForChunk ;
48
+ private Memory < byte > _currentChunkMemory ;
49
+
38
50
public Http1OutputProducer (
39
51
PipeWriter pipeWriter ,
40
52
string connectionId ,
@@ -58,28 +70,92 @@ public Task WriteDataAsync(ReadOnlySpan<byte> buffer, CancellationToken cancella
58
70
return Task . FromCanceled ( cancellationToken ) ;
59
71
}
60
72
73
+ return WriteAsync ( buffer , cancellationToken ) . AsTask ( ) ;
74
+ }
75
+
76
+ public ValueTask < FlushResult > WriteDataToPipeAsync ( ReadOnlySpan < byte > buffer , CancellationToken cancellationToken = default )
77
+ {
78
+ if ( cancellationToken . IsCancellationRequested )
79
+ {
80
+ return new ValueTask < FlushResult > ( Task . FromCanceled < FlushResult > ( cancellationToken ) ) ;
81
+ }
82
+
61
83
return WriteAsync ( buffer , cancellationToken ) ;
62
84
}
63
85
64
- public Task WriteStreamSuffixAsync ( )
86
+ public ValueTask < FlushResult > WriteStreamSuffixAsync ( )
65
87
{
66
88
return WriteAsync ( _endChunkedResponseBytes . Span ) ;
67
89
}
68
90
69
- public Task FlushAsync ( CancellationToken cancellationToken = default )
91
+ public ValueTask < FlushResult > FlushAsync ( CancellationToken cancellationToken = default )
70
92
{
71
93
return WriteAsync ( Constants . EmptyData , cancellationToken ) ;
72
94
}
73
95
74
- public Task WriteChunkAsync ( ReadOnlySpan < byte > buffer , CancellationToken cancellationToken )
96
+ public Memory < byte > GetMemory ( int sizeHint = 0 )
97
+ {
98
+ if ( _autoChunk )
99
+ {
100
+ return GetChunkedMemory ( sizeHint ) ;
101
+ }
102
+ else
103
+ {
104
+ return _pipeWriter . GetMemory ( sizeHint ) ;
105
+ }
106
+ }
107
+
108
+ public Span < byte > GetSpan ( int sizeHint = 0 )
109
+ {
110
+ if ( _autoChunk )
111
+ {
112
+ return GetChunkedMemory ( sizeHint ) . Span ;
113
+ }
114
+ else
115
+ {
116
+ return _pipeWriter . GetMemory ( sizeHint ) . Span ;
117
+ }
118
+ }
119
+
120
+ public void Advance ( int bytes )
121
+ {
122
+ if ( _autoChunk )
123
+ {
124
+ if ( bytes < 0 )
125
+ {
126
+ throw new ArgumentOutOfRangeException ( nameof ( bytes ) ) ;
127
+ }
128
+
129
+ if ( bytes + _advancedBytesForChunk > _currentChunkMemory . Length - BeginChunkLengthMax - EndChunkLength )
130
+ {
131
+ throw new InvalidOperationException ( "Can't advance past buffer size." ) ;
132
+ }
133
+ _advancedBytesForChunk += bytes ;
134
+ }
135
+ else
136
+ {
137
+ _pipeWriter . Advance ( bytes ) ;
138
+ }
139
+ }
140
+
141
+ public void CancelPendingFlush ( )
142
+ {
143
+ // TODO we may not want to support this.
144
+ _pipeWriter . CancelPendingFlush ( ) ;
145
+ }
146
+
147
+ // This method is for chunked http responses
148
+ public ValueTask < FlushResult > WriteChunkAsync ( ReadOnlySpan < byte > buffer , CancellationToken cancellationToken )
75
149
{
76
150
lock ( _contextLock )
77
151
{
78
152
if ( _completed )
79
153
{
80
- return Task . CompletedTask ;
154
+ return default ;
81
155
}
82
156
157
+ CommitChunkToPipe ( ) ;
158
+
83
159
if ( buffer . Length > 0 )
84
160
{
85
161
var writer = new BufferWriter < PipeWriter > ( _pipeWriter ) ;
@@ -96,7 +172,7 @@ public Task WriteChunkAsync(ReadOnlySpan<byte> buffer, CancellationToken cancell
96
172
return FlushAsync ( cancellationToken ) ;
97
173
}
98
174
99
- public void WriteResponseHeaders ( int statusCode , string reasonPhrase , HttpResponseHeaders responseHeaders )
175
+ public void WriteResponseHeaders ( int statusCode , string reasonPhrase , HttpResponseHeaders responseHeaders , bool autoChunk )
100
176
{
101
177
lock ( _contextLock )
102
178
{
@@ -117,6 +193,7 @@ public void WriteResponseHeaders(int statusCode, string reasonPhrase, HttpRespon
117
193
writer . Commit ( ) ;
118
194
119
195
_unflushedBytes += writer . BytesCommitted ;
196
+ _autoChunk = autoChunk ;
120
197
}
121
198
}
122
199
@@ -139,7 +216,6 @@ public void Abort(ConnectionAbortedException error)
139
216
{
140
217
// Abort can be called after Dispose if there's a flush timeout.
141
218
// It's important to still call _lifetimeFeature.Abort() in this case.
142
-
143
219
lock ( _contextLock )
144
220
{
145
221
if ( _aborted )
@@ -153,20 +229,33 @@ public void Abort(ConnectionAbortedException error)
153
229
}
154
230
}
155
231
156
- public Task Write100ContinueAsync ( )
232
+ public ValueTask < FlushResult > Write100ContinueAsync ( )
157
233
{
158
234
return WriteAsync ( _continueBytes . Span ) ;
159
235
}
160
236
161
- private Task WriteAsync (
237
+ public void Complete ( Exception exception = null )
238
+ {
239
+ // TODO What to do with exception.
240
+ // and how to handle writing to response here.
241
+ }
242
+
243
+ private ValueTask < FlushResult > WriteAsync (
162
244
ReadOnlySpan < byte > buffer ,
163
245
CancellationToken cancellationToken = default )
164
246
{
165
247
lock ( _contextLock )
166
248
{
167
249
if ( _completed )
168
250
{
169
- return Task . CompletedTask ;
251
+ return default ;
252
+ }
253
+
254
+ if ( _autoChunk )
255
+ {
256
+ // If there is data that was chunked before writing (ex someone did GetMemory->Advance->WriteAsync)
257
+ // make sure to write whatever was advanced first
258
+ CommitChunkToPipe ( ) ;
170
259
}
171
260
172
261
var writer = new BufferWriter < PipeWriter > ( _pipeWriter ) ;
@@ -188,5 +277,56 @@ private Task WriteAsync(
188
277
cancellationToken ) ;
189
278
}
190
279
}
280
+
281
+ private Memory < byte > GetChunkedMemory ( int sizeHint )
282
+ {
283
+ // The max size of a chunk will be the size of memory returned from the PipeWriter (today 4096)
284
+ // minus 5 for the max chunked prefix size and minus 2 for the chunked ending, leaving a total of
285
+ // 4089.
286
+
287
+ if ( _currentChunkMemory . Length == 0 )
288
+ {
289
+ // First time calling GetMemory
290
+ _currentChunkMemory = _pipeWriter . GetMemory ( sizeHint ) ;
291
+ }
292
+
293
+ var memoryMaxLength = _currentChunkMemory . Length - BeginChunkLengthMax - EndChunkLength ;
294
+ if ( _advancedBytesForChunk == memoryMaxLength )
295
+ {
296
+ // Chunk is completely written, commit it to the pipe so GetMemory will return a new chunk of memory.
297
+ CommitChunkToPipe ( ) ;
298
+ _currentChunkMemory = _pipeWriter . GetMemory ( sizeHint ) ;
299
+ }
300
+
301
+ var actualMemory = _currentChunkMemory . Slice (
302
+ BeginChunkLengthMax + _advancedBytesForChunk ,
303
+ memoryMaxLength - _advancedBytesForChunk ) ;
304
+
305
+ return actualMemory ;
306
+ }
307
+
308
+ private void CommitChunkToPipe ( )
309
+ {
310
+ var writer = new BufferWriter < PipeWriter > ( _pipeWriter ) ;
311
+
312
+ Debug . Assert ( _advancedBytesForChunk <= _currentChunkMemory . Length ) ;
313
+
314
+ if ( _advancedBytesForChunk > 0 )
315
+ {
316
+ var bytesWritten = writer . WriteBeginChunkBytes ( _advancedBytesForChunk ) ;
317
+ if ( bytesWritten < BeginChunkLengthMax )
318
+ {
319
+ // If the current chunk of memory isn't completely utilized, we need to copy the contents forwards.
320
+ // This occurs if someone uses less than 255 bytes of the current Memory segment.
321
+ // Therefore, we need to copy it forwards by either 1 or 2 bytes (depending on number of bytes)
322
+ _currentChunkMemory . Slice ( BeginChunkLengthMax , _advancedBytesForChunk ) . CopyTo ( _currentChunkMemory . Slice ( bytesWritten ) ) ;
323
+ }
324
+
325
+ writer . Write ( _currentChunkMemory . Slice ( bytesWritten , _advancedBytesForChunk ) . Span ) ;
326
+ writer . WriteEndChunkBytes ( ) ;
327
+ writer . Commit ( ) ;
328
+ _advancedBytesForChunk = 0 ;
329
+ }
330
+ }
191
331
}
192
332
}
0 commit comments