diff --git a/5-network/09-resume-upload/article.md b/5-network/09-resume-upload/article.md index 05e83697f8..b4b93c071b 100644 --- a/5-network/09-resume-upload/article.md +++ b/5-network/09-resume-upload/article.md @@ -1,34 +1,36 @@ -# 可恢复的(resumable)文件上传 +# 可恢复的文件上传 使用 `fetch` 方法来上传文件相当容易。 -当文件上传过程中连接丢失,此时如何恢复上传呢?目前还没有针对此问题的内建选项,但是我们有实现它的一些方法。 +连接断开后如何恢复上传?这里没有对此的内建选项,但是我们有实现它的一些方式。 -当我们上传大型文件的时候(如果我们可能需要恢复),我们期待可恢复上传应带有上传进度指示。由于 `fetch` 不允许追踪上传进度,我们将会使用 [XMLHttpRequest](info:xmlhttprequest)。 +对于大文件(如果我们可能需要恢复),可恢复的上传应该带有上传进度提示。由于 `fetch` 不允许跟踪上传进度,我们将会使用 [XMLHttpRequest](info:xmlhttprequest)。 ## 不太实用的进度事件 -要恢复上传,我们需要知道在连接丢失前已经上传了多少。 +要恢复上传,我们需要知道在连接断开前已经上传了多少。 -我们有 `xhr.upload.onprogress` 来追踪上传进程。 +我们有 `xhr.upload.onprogress` 来跟踪上传进度。 -不幸的是,它在这里没什么作用,它在数据发送 **sent** 完成时触发,但是它真的被服务器接收了吗?浏览器并不知道。 +不幸的是,它不会帮助我们在此处恢复上传,因为它会在数据 **被发送** 时触发,但是服务器是否接收到了?浏览器并不知道。 -或许它只是被本地代理缓冲(buffered),或是有可能远程服务器处理进程宕机无法处理它们,亦或是当连接断开时它刚刚从中间丢失,且没有到达服务器。 +或许它是由本地网络代理缓冲的(buffered),或者可能是远程服务器进程刚刚终止而无法处理它们,亦或是它在中间丢失了,并没有到达服务器。 -因此,这个事件只是对于显示一个漂亮的进度条来说很有用。 +这就是为什么此事件仅适用于显示一个好看的进度条。 -要恢复上传,我们需要知道服务器具体接收了多少字节。只有服务器能告诉我们它接收了多少。 +要恢复上传,我们需要 **确切地** 知道服务器接收的字节数。而且只有服务器能告诉我们,因此,我们将发出一个额外的请求。 ## 算法 -1. 首先,我们创建一个独一无二的标识符作为我们上传的文件 id,例如: +1. 首先,创建一个文件 id,以唯一地标识我们要上传的文件: ```js let fileId = file.name + '-' + file.size + '-' + +file.lastModifiedDate; ``` - 这对恢复上传很有用,它能告诉服务器我们我们要恢复的文件是什么。 + 在恢复上传时需要用到它,以告诉服务器我们要恢复的内容。 -2. 发送请求到服务器,获取该文件已经上传了多少字节,就像这样: + 如果名称,或大小,或最后一次修改事件发生了更改,则将有另一个 `fileId`。 + +2. 向服务器发送一个请求,询问它已经有了多少字节,像这样: ```js let response = await fetch('status', { headers: { @@ -36,40 +38,45 @@ } }); - // 服务器已有的字节 + // 服务器已有的字节数 let startByte = +await response.text(); ``` - 这假设服务器通过 `X-File-Id` 头跟踪文件上传。应该在服务端实现。 + 这假设服务器通过 `X-File-Id` header 跟踪文件上传。应该在服务端实现。 + + 如果服务器上尚不存在该文件,则服务器响应应为 `0`。 -3. 然后我们可以使用 `Blob` 方法 `slice` 来自 `startByte` 的要发送的文件: +3. 然后,我们可以使用 `Blob` 和 `slice` 方法来发送从 `startByte` 开始的文件: ```js xhr.open("POST", "upload", true); - // 发送文件 id,这样服务器就能知道要恢复哪个文件 + // 文件 id,以便服务器知道我们要恢复的是哪个文件 xhr.setRequestHeader('X-File-Id', fileId); - // 发送我们正在恢复的字节,因此服务器知道我们正在恢复文件 + + // 发送我们要从哪个字节开始恢复,因此服务器知道我们正在恢复 xhr.setRequestHeader('X-Start-Byte', startByte); xhr.upload.onprogress = (e) => { console.log(`Uploaded ${startByte + e.loaded} of ${startByte + e.total}`); }; - // 文件可以来自于 input.files[0] 或者其他资源 + // 文件可以是来自 input.files[0],或者另一个源 xhr.send(file.slice(startByte)); ``` - 这里我们将服务器的文件 id 作为 `X-File-Id` 发送,此时服务器就知道我们正在上传哪个文件,并且起始字节为 `X-Start-Byte`,因此服务器知道我们并不是从头开始上传,而是恢复文件。 + 这里我们将文件 id 作为 `X-File-Id` 发送给服务器,所以服务器知道我们正在上传哪个文件,并且,我们还将起始字节作为 `X-Start-Byte` 发送给服务器,所以服务器知道我们不是重新上传它,而是恢复其上传。 - 服务器应该检查它的记录,如果这个文件之前上传过且当前上传大小是 `X-Start-Byte`,此时将数据附加到原来文件上。 + 服务器应该检查其记录,如果有一个上传的该文件,并且当前已上传的文件大小恰好是 `X-Start-Byte`,那么就将数据附加到该文件。 -这是用 Node.js 写的客户端和服务端的 demo。 +这是用 Node.js 写的包含客户端和服务端代码的示例。 -在这个网页上,它只有部分能工作,因为 Node.js 位于另一个服务 Nginx 后面,该服务器缓冲上传过程,当完全上传后才传递给 Node.js。 +在本网站上,它只有部分能工作,因为 Node.js 位于另一个服务 Nginx 后面,该服务器缓冲(buffer)上传的内容,当完全上传后才将其传递给 Node.js。 但是你可以下载这些代码,在本地运行以进行完整演示: [codetabs src="upload-resume" height=200] -正如你所见,现代网络方法在功能上和文件管理器非常接近 —— 控制 headers,进度指示,发送文件片段等等。 +正如我们所看到的,现代网络方法在功能上已经与文件管理器非常接近 —— 控制 header,进度指示,发送文件片段等。 + +我们可以实现可恢复的上传等。 diff --git a/5-network/09-resume-upload/upload-resume.view/server.js b/5-network/09-resume-upload/upload-resume.view/server.js index 83ce59f7ab..a6e82a85c1 100644 --- a/5-network/09-resume-upload/upload-resume.view/server.js +++ b/5-network/09-resume-upload/upload-resume.view/server.js @@ -17,14 +17,14 @@ function onUpload(req, res) { res.end(); } - // we'll files "nowhere" + // 我们将“无处”保存文件 let filePath = '/dev/null'; - // could use a real path instead, e.g. + // 可以改用真实路径,例如 // let filePath = path.join('/tmp', fileId); debug("onUpload fileId: ", fileId); - // initialize a new upload + // 初始化一个新上传 if (!uploads[fileId]) uploads[fileId] = {}; let upload = uploads[fileId]; @@ -32,7 +32,7 @@ function onUpload(req, res) { let fileStream; - // if startByte is 0 or not set, create a new file, otherwise check the size and append to existing one + // 如果 startByte 为 0 或者没设置,创建一个新文件,否则检查大小并附加到现有的大小 if (!startByte) { upload.bytesReceived = 0; fileStream = fs.createWriteStream(filePath, { @@ -40,13 +40,13 @@ function onUpload(req, res) { }); debug("New file created: " + filePath); } else { - // we can check on-disk file size as well to be sure + // 我们也可以检查磁盘上的文件大小以确保 if (upload.bytesReceived != startByte) { res.writeHead(400, "Wrong start byte"); res.end(upload.bytesReceived); return; } - // append to existing file + // 附加到现有文件 fileStream = fs.createWriteStream(filePath, { flags: 'a' }); @@ -59,26 +59,26 @@ function onUpload(req, res) { upload.bytesReceived += data.length; }); - // send request body to file + // 将 request body 发送到文件 req.pipe(fileStream); - // when the request is finished, and all its data is written + // 当请求完成,并且其所有数据都以写入完成 fileStream.on('close', function() { if (upload.bytesReceived == req.headers['x-file-size']) { debug("Upload finished"); delete uploads[fileId]; - // can do something else with the uploaded file here + // 可以在这里对上传的文件进行其他操作 res.end("Success " + upload.bytesReceived); } else { - // connection lost, we leave the unfinished file around + // 连接断开,我们将未完成的文件保留在周围 debug("File unfinished, stopped at " + upload.bytesReceived); res.end(); } }); - // in case of I/O error - finish the request + // 如果发生 I/O error —— 完成请求 fileStream.on('error', function(err) { debug("fileStream error"); res.writeHead(500, "File error"); diff --git a/5-network/09-resume-upload/upload-resume.view/uploader.js b/5-network/09-resume-upload/upload-resume.view/uploader.js index 2e53ce4a31..bcc9650ede 100644 --- a/5-network/09-resume-upload/upload-resume.view/uploader.js +++ b/5-network/09-resume-upload/upload-resume.view/uploader.js @@ -4,8 +4,8 @@ class Uploader { this.file = file; this.onProgress = onProgress; - // create fileId that uniquely identifies the file - // we could also add user session identifier (if had one), to make it even more unique + // 创建唯一标识文件的 fileId + // 我们还可以添加用户会话标识符(如果有的话),以使其更具唯一性 this.fileId = file.name + '-' + file.size + '-' + +file.lastModifiedDate; } @@ -31,9 +31,9 @@ class Uploader { let xhr = this.xhr = new XMLHttpRequest(); xhr.open("POST", "upload", true); - // send file id, so that the server knows which file to resume + // 发送文件 id,以便服务器知道要恢复哪个文件 xhr.setRequestHeader('X-File-Id', this.fileId); - // send the byte we're resuming from, so the server knows we're resuming + // 发送我们要从哪个字节开始恢复,因此服务器知道我们正在恢复 xhr.setRequestHeader('X-Start-Byte', this.startByte); xhr.upload.onprogress = (e) => { @@ -44,9 +44,9 @@ class Uploader { xhr.send(this.file.slice(this.startByte)); // return - // true if upload was successful, - // false if aborted - // throw in case of an error + // true —— 如果上传成功, + // false —— 如果被中止 + // 出现 error 时将其抛出 return await new Promise((resolve, reject) => { xhr.onload = xhr.onerror = () => { @@ -59,7 +59,7 @@ class Uploader { } }; - // onabort triggers only when xhr.abort() is called + // onabort 仅在 xhr.abort() 被调用时触发 xhr.onabort = () => resolve(false); }); diff --git a/5-network/10-long-polling/article.md b/5-network/10-long-polling/article.md index 54852c3d3d..235886ce6f 100644 --- a/5-network/10-long-polling/article.md +++ b/5-network/10-long-polling/article.md @@ -1,63 +1,63 @@ -# 长轮询(long polling) +# 长轮询(Long polling) -长轮询是与服务器建立持久连接的最简单方法,它不使用任何特定协议,比如 WebSocket 或者服务端事件(Server Side Events)。 +长轮询是与服务器保持持久连接的最简单的方式,它不使用任何特定的协议,例如 WebSocket 或者 Server Sent Event。 它很容易实现,在很多场景下也很好用。 -## 普通轮询(regular Polling) +## 常规轮询 -最简单的从服务器获取新信息的方式就是轮询。 +从服务器获取新信息的最简单的方式是定期轮询。也就是说,定期向服务器发出请求:“你好,我在这儿,你有关于我的任何信息吗?”例如,每 10 秒一次。 -也就是说,定期向服务器发出请求:“Hello, I'm here, do you have any information for me?”。例如,10 秒发送一次。 +作为响应,服务器首先通知自己,客户端处于在线状态,然后 —— 发送目前为止的消息包。 -作为响应,服务器首先通知自己客户端在线,然后第二次 —— 发送直到那个时刻的消息包。 +这可行,但是也有些缺点: +1. 消息传递的延迟最多为 10 秒(两个请求之间)。 +2. 即使没有消息,服务器也会每隔 10 秒被请求轰炸一次,即使用户切换到其他地方或者处于休眠状态,也是如此。就性能而言,这是一个很大的负担。 -这很有效,但是也有些缺点: -1. 消息的传递时间长达 10 秒(每个请求之间)。 -2. 即使没有消息,服务器也会每隔 10 秒被请求轰炸一次。对于后端来说,出于性能的考量,这是一个非常大的负担。 +因此,如果我们讨论的是一个非常小的服务,那么这种方式可能可行,但总的来说,它需要改进。 -因此,如果我们讨论的是一个小型的服务,这种方法是可行的,但是一般来说,它需要一些改进。 +## 长轮询 -## 长轮询(long polling) +所谓“长轮询”是轮询服务器的一种更好的方式。 -所谓“长轮询”是一种更好的轮询服务器的方法。 - -它非常容易实现,并且可以无延迟地传递消息。 +它也很容易实现,并且可以无延迟地传递消息。 其流程为: -1. 发送请求到服务器。 +1. 请求发送到服务器。 2. 服务器在有消息之前不会关闭连接。 -3. 当消息出现 —— 服务器响应请求,并携带相应的数据。 -4. 浏览器马上创建一个新的请求。 +3. 当消息出现时 —— 服务器将对其请求作出响应。 +4. 浏览器立即发出一个新的请求。 -当浏览器发送一个请求并与服务器建立挂起(pending)连接的情况是此方法的标准。仅仅在传递消息时,才会重新建立连接。 +对于此方法,浏览器发出一个请求并与服务器之间建立起一个挂起的(pending)连接的情况是标准的。仅在有消息被传递时,才会重新建立连接。 ![](long-polling.svg) -如果连接丢失,可能是因为网络错误,浏览器立即发送一个新请求。 +如果连接丢失,可能是因为网络错误,浏览器会立即发送一个新请求。 -发出长请求的客户端 `subscribe` 函数的草图: +实现长轮询的客户端 `subscribe` 函数的示例代码: ```js async function subscribe() { let response = await fetch("/subscribe"); if (response.status == 502) { - // 连接超时错误, - // 当连接挂起太长可能会发生,远程服务器或者代理会关闭它 - // 重新连接 + // 状态 502 是连接超时错误, + // 连接挂起时间过长时可能会发生, + // 远程服务器或代理会关闭它 + // 让我们重新连接 await subscribe(); } else if (response.status != 200) { - // 显示错误 + // 一个 error —— 让我们显示它 showMessage(response.statusText); - // 1 秒后重连 + // 一秒后重新连接 await new Promise(resolve => setTimeout(resolve, 1000)); await subscribe(); } else { - // 得到消息 + // 获取并显示消息 let message = await response.text(); showMessage(message); + // 再次调用 subscribe() 以获取下一条消息 await subscribe(); } } @@ -65,30 +65,32 @@ async function subscribe() { subscribe(); ``` -你可以看到,`subscribe` 函数发起 fetch 请求,然后等待请求响应并处理它,然后再调用自己。 +正如你所看到的,`subscribe` 函数发起了一个 `fetch`,然后等待响应,处理它,并再次调用自身。 -```warn header="对于许多的挂起连接,服务器也应该能够很好的处理" -服务器架构必须能够处理许多挂起连接。 +```warn header="服务器应该可以处理许多挂起的连接" +服务器架构必须能够处理许多挂起的连接。 -某些服务器架构是每个连接对应一个进程。对于许多连接的情况,可能会有许多进程,每个进程占用很多内存。因此连接越多消耗也就越多。 +某些服务器架构是每个连接对应一个进程。对于许多连接的情况,将会有很多进程,并且每个进程占用大量内存。因此,过多的连接会消耗掉全部内存。 -这种情况通常是对于使用 PHP,Ruby 语言的后端,但是从技术上来说,它不是一种语言,而是实现的问题。 +使用 PHP,Ruby 语言编写的后端程序会经常遇到这个问题,但是从技术上讲,它不是语言问题,而是实现问题。大多数现代编程语言都允许实现适当的后端,但是其中一些语言比其他语言更容易实现。 使用 Node.js 写的后端通常不会出现这样的问题。 ``` -## Demo:chat +## 示例:聊天 -这是一个 demo: +这是一个聊天演示,你可以下载它并在本地运行(如果你熟悉 Node.js 并且可以安装模块): [codetabs src="longpoll" height=500] +浏览器代码在 `browser.js` 中。 + ## 使用场景 在消息很少的情况下,长轮询很有效。 -如果消息比较频繁,那么上面描绘的请求接收(requesting-receiving)消息的图表就会变成锯状(saw-like)。 +如果消息比较频繁,那么上面描绘的请求-接收(requesting-receiving)消息的图表就会变成锯状状(saw-like)。 -每条消息都是单独的请求,带有 headers,authentication 等开销。 +每个消息都是一个单独的请求,并带有 header,身份验证开销(authentication overhead)等。 -因此,在这种情况下,首选另一种方法,例如:[Websocket](info:websocket) 或者 [Server Sent Events](info:server-sent-events)。 +因此,在这种情况下,首选另一种方法,例如:[Websocket](info:websocket) 或 [Server Sent Events](info:server-sent-events)。