DELPHI盒子
!实时搜索: 盒子论坛 | 注册用户 | 修改信息 | 退出
检举帖 | 全文检索 | 关闭广告 | 捐赠
技术论坛
 用户名
 密  码
自动登陆(30天有效)
忘了密码
≡技术区≡
DELPHI技术
移动应用开发
Web应用开发
数据库专区
报表专区
网络通讯
开源项目
论坛精华贴
≡发布区≡
发布代码
发布控件
文档资料
经典工具
≡事务区≡
网站意见
盒子之家
招聘应聘
信息交换
论坛信息
最新加入: sql2003s
今日帖子: 0
在线用户: 1
导航: 论坛 -> DELPHI技术 斑竹:liumazi,sephil  
作者:
男 siaosa (siaosa) ★☆☆☆☆ -
盒子活跃会员
2022/9/12 15:15:32
标题:
TNetHttpClient上传大文件报错如何处理? 浏览:1059
加入我的收藏
楼主: TNetHttpClient上传大文件报错:Out of memory

procedure TfrmMain.Button1Click(Sender: TObject);
var
  cHttp: TNetHTTPClient;
  vData: TMultipartFormData;
  vRsp: TStringStream;
begin
  if OpenDialog1.Execute then
  begin
    cHttp := TNetHTTPClient.Create(nil);
    vData := TMultipartFormData.Create;
    vRsp := TStringStream.Create('', TEncoding.GetEncoding(65001));
    try
      vData.AddFile('fname', OpenDialog1.FileName);
      //如果上传1G以上的大文件报错:out of memory要怎么处理?
      with cHttp do
      begin
        ConnectionTimeout := 20000; 
        ResponseTimeout := 100000; // 100秒
        AcceptCharSet := 'utf-8';       
        ContentType := 'multipart/form-data;
        UserAgent := 'Embarcadero URI Client/1.0';
        try 
          Post('http://******/upfile', vData, vRsp);
          .......
 这代码如果上传1G以上的大文件报错:out of memory报错。看Delphi XE自带的TNetHTTPClient控件的源代码,应该是TNetHTTPClient执行AddFile方法时,一次性将文件读入内存导致。
  有其它的方法可以解决这个问题吗?
----------------------------------------------
-
作者:
男 pcplayer (pcplayer) ★☆☆☆☆ -
普通会员
2022/9/12 17:43:01
1楼: Http 方法可能无解?
----------------------------------------------
-
作者:
男 wk_knife (wk_knife) ★☆☆☆☆ -
盒子活跃会员
2022/9/12 21:58:16
2楼: 用IOCP,这个是服务端
此帖子包含附件:wk_knife_2022912215814.rar 大小:1.05M
----------------------------------------------
-
作者:
男 wk_knife (wk_knife) ★☆☆☆☆ -
盒子活跃会员
2022/9/12 22:00:35
3楼: 这个是对原客户端做了些修改,改了几处bug,把代码理得好看了一些,同时改了个lazars的源码,客户端支持linux和macOS
此帖子包含附件:wk_knife_20229122200.zip 大小:10.18M
----------------------------------------------
-
作者:
男 wk_knife (wk_knife) ★☆☆☆☆ -
盒子活跃会员
2022/9/12 22:03:23
4楼: 其中delphi的客户端代码是写的COM,为了能让C#调用。
----------------------------------------------
-
作者:
男 siaosa (siaosa) ★☆☆☆☆ -
盒子活跃会员
2022/9/13 9:06:18
5楼: To:pcplayer
Http方式应该是没问题的,用Chrome,IE,Edge等浏览器用HTTP协议上传1G的文件不会报这样的错误,应该是Delphi Xe的TNetHTTPClient控件的BUG导致的, 哪有上传上文件一次全读入TMemoryStream这种做法。

To:wk_knife
服务端没办法更改。不能用IOCP。

各位老大帮着看看,Http上传大文件还有其它什么方法没有?
----------------------------------------------
-
作者:
男 csm55 (鹰扬天下) ★☆☆☆☆ -
盒子活跃会员
2022/9/13 10:26:17
6楼: 这应该不是tnethttpclient自身问题,问题是delphi读取本地文件时,一次性加载全部文件,造成内存不够用,你可以试下用这个类库把你的文件使用流加载,然后再放到tnethttpclient中https://17slon.com/gp/gp/gphugefile.htm
正常上传那么大的文件,要专门写一个上传文件接口,大文件要拆分成小文件一个个上传,到服务器再合并
----------------------------------------------
——做人,为什么要过于执著?! ——做人,干嘛为难自己?! ——做人,先要相信自己。——做人,依靠自己!——做人,量力而行。——做人,记得反省自己。——做人,何妨放手一搏。——做人,要活在当下。
作者:
男 keymark (嬲) ▲▲△△△ -
注册会员
2022/9/13 10:40:11
7楼: 6.05: 2012-04-18
添加了对 TGp 过期文件 [流] 的日志记录。
https://17slon.com/gp/gp/gphugefile.htm


     6.15a: 2021-05-26
       - Fixed read prefetch for 64-bit.
     6.15: 2020-11-12
       - Added TGpHugeFileStream.CreateEx which does not raise exceptions.
     6.13: 2019-01-30
       - Asynchronous decriptors are added to a list to keep track of the owner,
         which is set to nil in the Close method so that a freed TGpHugeFile is not called
     6.12c: 2018-12-29
       - Enabling prefetcher must not start read operation if target block is
         already in the cache.
       - Seek must update prefetcher current position even if prefetcher is
         currently disabled.
     6.12b: 2018-11-29
       - TGpHugeFile.FileSize was cached even for WRITE access; only allowed for READ and SHARE_READ
     6.12: 2018-10-30
       - TGpHugeFile.FileSize returns a cached value if the file is open in exclusive read mode
     6.11b: 2017-05-22
       - Fixed range check error on negative diskLockTimeout.
     6.11a: 2015-09-29
       - Added empty parameter to Format in Win32Check because SOSError in XE5
         has one more %s than the XE2 version.
     6.11: 2015-01-28
       - When reraising exception, previous exception is wrapped as an inner exception.
     6.10c: 2014-03-27
       - Fixed windows error check in TGpHugeFile.AccessFile which could produce range
         check errors.
       - DSiTimeGetTime64 is used instead of GetTickCount in TGpHugeFile.AccessFile.
     6.10b: 2013-09-17
       - Removed top-level try..except in ResetEx/RewriteEx. It could only cause harm.
     6.10a: 2013-09-10
       - Fixed race condition in prefetcher handling that caused reader to loop endlessly.
     6.10: 2013-05-09
       - Available data is read from the prefetcher cache even when the prefetcher is
         disabled.
     6.09b: 2013-04-23
       - Fixed prefetcher enabling/disabling mechanism.
     6.09a: 2013-04-19
       - Prefetcher thread is paused when DisablePrefetcher is set to True.
       - Fixed bug when wrong data was returned while reading near the end of file and
         prefetcher was enabled.
     6.09: 2012-12-21
       - Added property DisablePrefetcher to TGpHugeFile and TGpHugeFileStream.
     6.08a: 2012-10-09
       - Works correctly when caller tries reading past the end of file and prefetcher
         is enabled.
     6.08: 2012-10-04
       - Better way to read cache blocks from the prefetcher.
       - Double reads are now prevented - main thread will wait for prefetcher to get
         the block and then use this block.
     6.07a: 2012-10-03
       - Queries to the prefetcher are always buffer-aligned.
     6.07: 2012-06-28
       - Better prefetch algorithm; produces constant network load instead of spikes.
     6.06b: 2012-05-16
       - Fixed memory leak in prefetcher.
     6.06a: 2012-05-08
       - Added parameter numPrefetchBeforeBuffers to TGpHugeFileStream.CreateW
     6.06: 2012-05-07
       - Added parameter numPrefetchBeforeBuffers to TGpHugeFile.ResetEx and
         TGpHugeFileStream.Create. If set, it specifies a number of prefetched buffers to
         keep before the current Seek point. Total number of buffers is still managed
         by the numPrefetchBuffers parameter - numPrefetchBeforeBuffers are used to store
         information before the Seek position and (numPrefetchBuffers - numPrefetchBeforeBuffers)
         are used to store information after the Seek position.
       - Not all buffers were used for prefetch.
       - Seek in prefetcher could sometimes be ignored.
       - Overlapped structures in prefetcher are used in a safer manner.
       - TCriticalSection is used instead of TSpinLock.
       - Added logging to the THFPrefetchCache.
     6.05a: 2012-04-24
       - Fixed invalid test in TGpHugeFileStream.Seek.

这里有更新 https://github.com/gabr42/GpDelphiUnits
----------------------------------------------
[alias]  co = clone --recurse-submodules  up = submodule update --init --recursiveupd = pullinfo = statusrest = reset --hard懒鬼提速http://qalculate.github.io/downloads.htmlhttps://www.cctry.com/
作者:
男 wk_knife (wk_knife) ★☆☆☆☆ -
盒子活跃会员
2022/9/13 10:48:29
8楼: 楼主都说了服务器不能动,那就是悟空已经给唐僧画好圈圈了,只能在里面跳舞。
----------------------------------------------
-
作者:
男 redhan (晓寒) ★☆☆☆☆ -
盒子活跃会员
2022/9/13 10:53:35
9楼: 把 TMultipartFormData 代码复制一份,自己改造下就可以,把 FStream: TMemoryStream; 改成 FileStream;

增加 MaxStreamSize 属性,用于设置 TMemoryStream 最大阈值,超出则自动使用 TFileStream 存储,未测试,有问题请自行修改。

核心代码:
procedure TMultipartFormData.AddFile(const AFieldName, AFilePath: string);
var
  LFileStream: TFileStream;
  LTempStream: TFileStream;
begin
  AdjustLastBoundary;
  WriteStringLn('--' + FBoundary);
  WriteStringLn('Content-Disposition: form-data; name="' + AFieldName + '"; filename="' + ExtractFileName(AFilePath) + '"'); // do not localize
  WriteStringLn('Content-Type: ' + GetFileMIMEType(AFilePath) + #13#10); // We need 2 line break here   // do not localize

  LFileStream := TFileStream.Create(AFilePath, fmOpenRead);
  try
    if (FMaxStreamSize>0) and ((FStream.Size+LFileStream.Size)>FMaxStreamSize) then
    begin
      // 如果内容大于阈值,则使用临时文件
      FTempFileName := TPath.GetTempFileName;
      LTempStream := TFile.Create(FTempFileName);
      LTempStream.CopyFrom(FStream, 0);
      FStream.Free;
      FStream := LTempStream;
    end;

    FStream.CopyFrom(LFileStream, 0);
  finally
    LFileStream.Free;
  end;
  WriteStringLn('');
end;
此帖子包含附件:redhan_202291311921.zip 大小:10.4K
----------------------------------------------
-
作者:
男 redhan (晓寒) ★☆☆☆☆ -
盒子活跃会员
2022/9/13 11:15:44
10楼: 官方为什么不用TFileStream,估计是因为大多数服务器,都会有上传文件大小限制,一般情况下不会有问题。
----------------------------------------------
-
作者:
男 tuesdays (Tuesday) ▲▲▲▲△ -
注册会员
2022/9/13 12:04:48
11楼: 这跟delphi没关系, java, python, php都无法上传1GB的文件.
----------------------------------------------
delphi界写python最强, python界写delphi最强. 写自己的代码, 让别人去运行.
作者:
男 letianwuji (大器晚成) ▲▲▲▲△ -
注册会员
2022/9/13 12:14:49
12楼: 直接用流传,不怕断点...
----------------------------------------------
相信自己,若自己都不相信,那还有谁可信。
作者:
男 keymark (嬲) ▲▲△△△ -
注册会员
2022/9/13 12:18:19
13楼:   while dataStream.Position <= dataStream.Size-1 do
  begin
    curDataLen := dataStream.Read(sBuffer,1024);
    send(s,sBuffer,curDataLen,0);
  end;
http://www.2ccc.com/btdown.asp?articleid=2327
这代码循环传 1024

ftp 是个好东西。

我觉得首先要确认 
服务端能接受nGB吗?

asp的时候好像有 插件用来支持上传文件?

如果单纯的传给http server 

https://www.cnblogs.com/doit8791/p/9108639.html (这协议能保证n个GB上传不出问题吗?)
HTTP协议1.1版本中消息体长度字段Content-Length的类型规定为16个字节的Decimal类型【它能表示的最大值大到没朋友】




~~~
一般来讲,上传文件需要在请求头中带上content-length,以告知服务端需要接受的数据包大小。

但HTTP协议中提供了另一种方式,来约定上传边界。就是以分块编码形式来发送数据。
数据发送方式在headers.Transfer-Encoding中表现。



作者:平仄_pingze
链接:https://www.jianshu.com/p/fcdb91fe5476
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。


切片上传java
https://blog.csdn.net/qq_36704478/article/details/126360711
----------------------------------------------
[alias]  co = clone --recurse-submodules  up = submodule update --init --recursiveupd = pullinfo = statusrest = reset --hard懒鬼提速http://qalculate.github.io/downloads.htmlhttps://www.cctry.com/
作者:
男 siaosa (siaosa) ★☆☆☆☆ -
盒子活跃会员
2022/9/13 15:08:16
14楼: 谢谢,晓寒,按您的方法已经搞定,功能OK。唯一不足的就是上传过程中要生成一个很大的临时文件。
----------------------------------------------
-
作者:
男 pcplayer (pcplayer) ★☆☆☆☆ -
普通会员
2022/9/13 16:52:02
15楼: vData := TMultipartFormData.Create;

说明你的文件是以 HTTP 协议里面的 TMultipartFormData 来上传的。

你的服务器端如果能够接受那么大的数据,那么,你可以考虑用任何的 TCP 控件比如 IdTCPClient 控件,自己拼 HTTP 字符串的方式来上传。这样做的话,那个 TMultipartFormData 你就可以加载一部分发送一部分。比如你可以把文件按照 1M 来加载并传输。
----------------------------------------------
-
作者:
男 pcplayer (pcplayer) ★☆☆☆☆ -
普通会员
2022/9/13 16:54:36
16楼: TNetHttpClient 应该是系统带的控件,你修改它的代码,相当于修改了 Delphi 自己的源代码,这种方式我不赞成,除非实在没办法的情况下。因为你的程序哪天会在新版的 Delphi 底下使用的,你又得把新安装的 Delphi 的代码找出来改。
----------------------------------------------
-
作者:
男 sxqwhxq (步惊云) ★☆☆☆☆ -
普通会员
2022/9/13 17:06:14
17楼: 我用datasnap的流上传文件,不受文件大小限制。
----------------------------------------------
-
作者:
男 pcplayer (pcplayer) ★☆☆☆☆ -
普通会员
2022/9/13 20:35:36
18楼: 楼上,这个给你用什么没关系,服务器端是 HTTP 决定了客户端是 HTTP
----------------------------------------------
-
作者:
男 siaosa (siaosa) ★☆☆☆☆ -
盒子活跃会员
2022/9/13 22:20:29
19楼: 非常感谢各位大佬的指点。

To:步惊云
服务端用的是http,且不能更改web页面。

To:pcplayer
如果有相关技术资料的话,我也想用IdTCPClient把一个文件分割后一部分一部分的POST到服务器。这是最理想的方法。

现在http/https协议已经很可靠了,未来会替代老旧的FTP协议,传送几个G的大文件是没什么问题的,我试过用浏览器上传三十多个G的大文件,没任何问题。百度云、阿里云等大一些大型的云盘都是用https协议传送文件,放弃了FTP协议。
  TMultipartFormData易波龙在设计时可能没考虑HTTP POST大文件,用了最简单的直接把大文件一次全读入内存的方法,导致报错:out of memory这个错误。如果易波龙能把这个BUG问题修订就最好。
----------------------------------------------
-
作者:
男 pcplayer (pcplayer) ★☆☆☆☆ -
普通会员
2022/9/13 23:12:53
20楼: 楼上,HTTP 传文件的 TMultipartFormData,你可以理解为:

1. HTTP 是一个 TCP;
2. 标准的 HTTP 是一个文本。
3. TMultipartFormData 是 HTTP 文本的一部分。

所以,Delphi 的实现应该是标准的实现方法,也就是先在内存里面把这个文本凑齐,然后通过 TCP 发过去。

你的问题是,TMultipartFormData 部分的数据太大,一次性凑齐,内存占用太多。

既然是 TCP,那么,即便是一个完整的文本,你也可以循环分次发送。实际上,你可以一个字节一个字节地发送。

因此,把【构造一个文本,然后发送】,改为:

构造文本的一部分,发送,再构造一部分,再发送,循环一直到结束。

所有代码能够操作的数据,那是一定会加入到内存里面的。但你的代码每次只操作一点点数据,多次循环操作,也是可以的。

采用 IndyTCPClient 发送 HTTP 的文件上传,我有博客文章

https://blog.csdn.net/pcplayer/article/details/86763311

当然,这篇文章里面,对于文件本身,也是构造好数据后一起传输的。但这里的代码你如果看明白了,就知道,文件的数据,可以一次加载一部分,用一个循环,分多次传输,最后完成 HTTP 的整个上传。

简单再多重复一句:
1. TCP 客户端发送数据给服务器端,你可以用一个循环,一个字节一个字节地发送。当然,一个循环发送一个字节还是发送 1M 字节,你自己根据情况确定。

2. HTTP 就是一堆文本。HTTP 头部分就是一堆 Name: Value 的对这个文本的描述。

3. TMultipartFormData 文件内容部分,可能是 MIME 编码(一般用 BASE64)后的文本,也可能是直接的 2 进制数据。这个看服务器端的要求。不同的情况,HTTP 头里面也会有相关的描述。
----------------------------------------------
-
作者:
男 wk_knife (wk_knife) ★☆☆☆☆ -
盒子活跃会员
2022/9/14 10:58:27
21楼: 速度一定很感人吧,如果局域X还好些。其他X络连接上行基本都比下行慢好多。
----------------------------------------------
-
作者:
男 siaosa (siaosa) ★☆☆☆☆ -
盒子活跃会员
2022/9/14 21:48:12
22楼: To:pcplayer
  非常感谢您热心的指点。看了您的博客,有一个不清楚的地方:用TCPClient拆分文件的方式传输,HTTP头如何描述这是文件的第一个块,第二个块,最后一块发送时怎样在HTTP头里告诉服务器文件发送完毕? 这些HTTP头里的内容具体应该怎么填写?
----------------------------------------------
-
作者:
男 pcplayer (pcplayer) ★☆☆☆☆ -
普通会员
2022/9/14 22:18:29
23楼: 楼上,对于服务器端来说,文件其实没有分块!

我写个伪代码:

TCP.Send(HTTP_Head);

var F: FileStream;

F := TFileStream.Create('your file');
F.Position := 0;

var ABuffer: TBytes;
var i: Integer;

SetLength(ABuffer, 100); //100个字节一次

while True do
begin
  i := F.Read(ABuffer[0], 100);
  TCP.Send(ABuffer);
  if i < 100 then Break;
end;

你在客户端,这样 100 个字节发送一次,也就是每次读取 100 个字节到内存。这样循环持续发送,对于服务器端来说,是一个块,而不是多个块!
----------------------------------------------
-
作者:
男 pcplayer (pcplayer) ★☆☆☆☆ -
普通会员
2022/9/14 22:22:17
24楼: 要理解:
1. HTTP 是在 TCP 基础上的。本质上一个 TCP 传输;
2. TCP 的传输,本质是字节流,你每次发多少个字节都没关系,对于接收端,它收到的就是字节流,它也不知道你在客户端一个发送指令发了几个字节。

也就是说,你一个发送指令发送 100 个字节,然后紧跟着再来个发送指令发送 200 个字节,对于接收端来说,它就是能够收到 300 个字节。它根本不知道是一个 100 和一个 200字节。

所以,对于 HTTP 来说,才需要在 HTTP 头里面,写上文件的字节数。否则服务器端不知道收完没。
----------------------------------------------
-
作者:
男 bdl1 (bdl1) ▲▲▲▲△ -
注册会员
2022/9/16 7:54:34
25楼: @21楼:
我现在上传一个2M的文件,要21秒,是不是这个原因?
局域网内要3秒。
----------------------------------------------
-我的博客
作者:
男 keymark (嬲) ▲▲△△△ -
注册会员
2022/9/16 11:41:08
26楼: 超过MTU的报文如何进行分片?
以太网~~~~~~~~~~缺省MTU=1500字节,这是以太网~~~~~~~~~~接口对IP层的约束,如果IP层有<=1500字节需要发送,只需要一个IP包就可以完成发送任务;如果IP层有>1500字节数据需要发送,需要分片才能完成发送。

以主机发送一个数据载荷长度为2000字节的报文为例说明其分片的过程(假设出接口的MTU值为1500)。在网~~~~~~~~~~络层会对报文进行封装,其结构组成:IP头部20字节+数据载荷长度2000字节,报文封装后,整个报文长度为2020字节。在出接口进行转发的时候,发现IP报文的长度超过了MTU的值1500,因此要进行分片处理,详情见下图。

https://info.support.huawei.com/info-finder/encyclopedia/zh/MTU.html
https://developer.aliyun.com/article/222535
https://product.pconline.com.cn/itbk/wlbg/wlsbjc/1706/9337932.html
MTU会影响网~~~~~~~速??????
----------------------------------------------
[alias]  co = clone --recurse-submodules  up = submodule update --init --recursiveupd = pullinfo = statusrest = reset --hard懒鬼提速http://qalculate.github.io/downloads.htmlhttps://www.cctry.com/
作者:
男 pcplayer (pcplayer) ★☆☆☆☆ -
普通会员
2022/9/16 20:29:15
27楼: 26 楼,网 -- 那个络通讯的 7 层结构,为什么要分层?

原理就是,每层只负责自己的业务逻辑,不用管底下那一层是如何干的。

这个所谓的以太网分片,是 MAC 层的东西,和 IP 层没关系。IP 层上面的 TCP 层,也不用去关心 IP 包的大小。使用 TCP 的应用层,只需要知道 TCP 是如何工作的就行了。

至于你的应用层代码,一个 Send 命令发出去多少个字节,到了底下,究竟是被分成多少个数据包,完全是底下的协议栈 TCP/IP 那一层搞定的。而 TCP/IP 那一层,具体到以太网的网卡那一层,又把你的几百兆数据如何分片的,是 MAC 层的事情,不关你上面应用层代码的事。

这里稍微特殊一点的事 UDP 包,因为 UDP 就是包,而不是 TCP 的流,所以,UDP 如果你发一个大包,有可能被分片后,因为某个片发丢了,导致整个 UDP 包丢掉。所以发送 UDP 数据可能需要考虑一下一次发出去的包是多大。我测试过,如果仅仅是局域网内部,你发一个超大的 UDP 包,也是能发送成功的。
----------------------------------------------
-
作者:
男 siaosa (siaosa) ★☆☆☆☆ -
盒子活跃会员
2022/9/19 9:06:21
28楼:
To:redhan (晓寒)
用您推荐的方法,把MemoryStream改成TFileStream后,NetHttpClient POST发送4G以内的文件没问题,也不报错。
但是NetHttpClient POST发送4G以上的文件报错(用浏览器发送4G以上的没问题):
Error sending data: (87) 参数错误。
而且是很有规律的报错,发送大于4G的文件,发送1分钟准时报错这个错误。
发送小于4G的文件即使传送时间大于10分钟都不会报错。
----------------------------------------------
-
作者:
男 earthsbest (全能中间件) ▲▲▲△△ -
注册会员
2022/9/20 15:43:25
29楼: IdHttp可以用表单方式传大文件,但是中文名会乱码,NetHttpClient文件名不乱码,但是不支持大文件。可以考虑其他第三方的Http控件,比如ics
----------------------------------------------
Delphi4Linux Delphi三层/FireDAC 技术群:734515869 http://www.cnblogs.com/rtcmw
作者:
男 nevergrief (孤独骑士) ★☆☆☆☆ -
盒子活跃会员
2022/9/21 3:18:16
30楼: to siaosa
你要不把你的客户端工程(简化版)附上来,服务端也把简化代码贴上来,我帮你到官方论坛去提问。
----------------------------------------------
只有偏执狂才能生存!
作者:
男 siaosa (siaosa) ★☆☆☆☆ -
盒子活跃会员
2022/9/21 9:32:53
31楼:
----------------------------------------------
-
作者:
男 siaosa (siaosa) ★☆☆☆☆ -
盒子活跃会员
2022/9/21 9:42:06
32楼:
TO: nevergrief (孤独骑士)
麻烦您帮我问下,附件是问题的详细描述, 帖子放上来是空的
此帖子包含附件:siaosa_202292194426.txt 大小:2,544B
----------------------------------------------
-
信息
登陆以后才能回复
Copyright © 2CCC.Com 盒子论坛 v2.1 版权所有 页面执行199.2188毫秒 RSS