SSL上的JavaMail IMAP非常慢 - 批量获取多条消息
Posted
技术标签:
【中文标题】SSL上的JavaMail IMAP非常慢 - 批量获取多条消息【英文标题】:JavaMail IMAP over SSL quite slow - Bulk fetching multiple messages 【发布时间】:2012-01-09 12:07:51 【问题描述】:我目前正在尝试使用 JavaMail 从 IMAP 服务器(Gmail 和其他服务器)获取电子邮件。基本上,我的代码有效:我确实可以获得标题、正文内容等。我的问题如下:在 IMAP 服务器(无 SSL)上工作时,处理一条消息基本上需要 1-2 毫秒。当我使用 IMAPS 服务器(因此使用 SSL,例如 Gmail)时,我达到大约 250m/条消息。我只测量处理消息的时间(不考虑连接、握手等)。
我知道因为这是 SSL,所以数据是加密的。不过,解密的时间应该没那么重要吧?
我尝试设置更高的 ServerCacheSize 值、更高的 connectionpoolsize,但我的想法严重不足。有人遇到过这个问题吗?希望解决它?
我担心 JavaMail API 每次从 IMAPS 服务器获取邮件时使用不同的连接(涉及握手的开销......)。如果是这样,有没有办法覆盖这种行为?
这是我从 Main() 类调用的代码(虽然很标准):
public static int connectTest(String SSL, String user, String pwd, String host) throws IOException,
ProtocolException,
GeneralSecurityException
Properties props = System.getProperties();
props.setProperty("mail.store.protocol", SSL);
props.setProperty("mail.imaps.ssl.trust", host);
props.setProperty("mail.imaps.connectionpoolsize", "10");
try
Session session = Session.getDefaultInstance(props, null);
// session.setDebug(true);
Store store = session.getStore(SSL);
store.connect(host, user, pwd);
Folder inbox = store.getFolder("INBOX");
inbox.open(Folder.READ_ONLY);
int numMess = inbox.getMessageCount();
Message[] messages = inbox.getMessages();
for (Message m : messages)
m.getAllHeaders();
m.getContent();
inbox.close(false);
store.close();
return numMess;
catch (MessagingException e)
e.printStackTrace();
System.exit(2);
return 0;
提前致谢。
【问题讨论】:
注意:字符串 SSL 是“imap”或“imaps”。另外,我已经阅读了***.com/questions/2538481/javamail-performance 的问题,但在不是 Gmail 的 IMAPS 服务器上进行了尝试,并且仍然得到相同的结果。 同一 IMAP/IMAPS 服务器上的其他客户端(Thunderbird、Outlook、what-have-you)是否也会发生这种情况?在这种情况下,这不是你的代码的错,而是服务器的问题。 如何测量 Thunderbird 导入消息所需的时间? (我们在ms区...)。它在 20 秒内对所有文件夹收费(但我不知道它是否只获得了一些信息,当我点击消息时获得其余信息)。 嗯,这是有点麻烦,是的。我相信它有某种操作日志(默认关闭),但分辨率最多以秒为单位。您可以配置 TB 下载完整的邮件(默认只获取邮件头),然后测量整个收件箱;这至少应该告诉你它需要 你的底层操作系统是什么? 【参考方案1】:总时间包括加密操作所需的时间。加密操作需要一个随机播种机。有不同的随机播种实现提供用于加密的随机位。默认情况下,Java 使用 /dev/urandom,这在您的 java.security 中指定如下:
securerandom.source=file:/dev/urandom
在 Windows 上,java 使用通常没有问题的 Microsoft CryptoAPI 种子功能。但是,在 unix 和 linux 上,Java 默认使用 /dev/random 进行随机播种。 /dev/random 上的读取操作有时会阻塞并且需要很长时间才能完成。如果您使用的是 *nix 平台,则在此花费的时间将计入总时间。
因为,我不知道您使用的是什么平台,所以我不能肯定地说这可能是您的问题。但是,如果您是,那么这可能是您的操作需要很长时间的原因之一。解决此问题的方法之一是使用 /dev/urandom 而不是 /dev/random 作为随机播种器,它不会阻塞。这可以使用系统属性“java.security.egd”来指定。例如,
-Djava.security.egd=file:/dev/urandom
指定此系统属性将覆盖 java.security 文件中的 securerandom.source 设置。你可以试一试。希望对您有所帮助。
【讨论】:
我确实在 Ubuntu 11.04 上运行,我会尝试您的建议并及时发布。 不幸的是,我的 java.security 文件已经包含以下行:securerandom.source=file:/dev/urandom 尝试将以下属性(带有 3 ///)显式添加到您的 JVM -Djava.security.egd=file:///dev/urandom。无法识别上面的语法(带 1 /)。【参考方案2】:在遍历邮件之前,您需要将 FetchProfile 添加到收件箱。 消息是一个延迟加载对象,它会为每条消息和每个 默认配置文件未提供的字段。 例如
for (Message message: messages)
message.getSubject(); //-> goes to the imap server to fetch the subject line
如果您想像收件箱列表一样显示发件人、主题、已发送、附件等。您可以使用类似以下的内容
inbox.open(Folder.READ_ONLY);
Message[] messages = inbox.getMessages(start + 1, total);
FetchProfile fp = new FetchProfile();
fp.add(FetchProfile.Item.ENVELOPE);
fp.add(FetchProfileItem.FLAGS);
fp.add(FetchProfileItem.CONTENT_INFO);
fp.add("X-mailer");
inbox.fetch(messages, fp); // Load the profile of the messages in 1 fetch.
for (Message message: messages)
message.getSubject(); //Subject is already local, no additional fetch required
希望对您有所帮助。
【讨论】:
FetchProfile 有点帮助,谢谢!但是,我现在正在尝试批量获取多条消息的消息内容(直接使用 JavaMail API 时这是不可能的)。鉴于已修复大小限制以避免内存不足错误,这将带来更大的性能提升。 使用 FetchProfile 接收 25 条消息需要多长时间?对我来说,这需要将近 4 到 5 秒。【参考方案3】:经过大量工作以及 JavaMail 人员的帮助,这种“缓慢”的根源在于 API 中的 FETCH 行为。事实上,正如 pjaol 所说,每次我们需要消息的信息(标题或消息内容)时,我们都会返回服务器。
如果 FetchProfile 允许我们批量获取多条消息的标头信息或标志,则无法直接获取多条消息的内容。
幸运的是,我们可以编写自己的 IMAP 命令来避免这种“限制”(这样做是为了避免内存不足错误:在一个命令中获取内存中的每封邮件可能会非常繁重)。
这是我的代码:
import com.sun.mail.iap.Argument;
import com.sun.mail.iap.ProtocolException;
import com.sun.mail.iap.Response;
import com.sun.mail.imap.IMAPFolder;
import com.sun.mail.imap.protocol.BODY;
import com.sun.mail.imap.protocol.FetchResponse;
import com.sun.mail.imap.protocol.IMAPProtocol;
import com.sun.mail.imap.protocol.UID;
public class CustomProtocolCommand implements IMAPFolder.ProtocolCommand
/** Index on server of first mail to fetch **/
int start;
/** Index on server of last mail to fetch **/
int end;
public CustomProtocolCommand(int start, int end)
this.start = start;
this.end = end;
@Override
public Object doCommand(IMAPProtocol protocol) throws ProtocolException
Argument args = new Argument();
args.writeString(Integer.toString(start) + ":" + Integer.toString(end));
args.writeString("BODY[]");
Response[] r = protocol.command("FETCH", args);
Response response = r[r.length - 1];
if (response.isOK())
Properties props = new Properties();
props.setProperty("mail.store.protocol", "imap");
props.setProperty("mail.mime.base64.ignoreerrors", "true");
props.setProperty("mail.imap.partialfetch", "false");
props.setProperty("mail.imaps.partialfetch", "false");
Session session = Session.getInstance(props, null);
FetchResponse fetch;
BODY body;
MimeMessage mm;
ByteArrayInputStream is = null;
// last response is only result summary: not contents
for (int i = 0; i < r.length - 1; i++)
if (r[i] instanceof IMAPResponse)
fetch = (FetchResponse) r[i];
body = (BODY) fetch.getItem(0);
is = body.getByteArrayInputStream();
try
mm = new MimeMessage(session, is);
Contents.getContents(mm, i);
catch (MessagingException e)
e.printStackTrace();
// dispatch remaining untagged responses
protocol.notifyResponseHandlers(r);
protocol.handleResult(response);
return "" + (r.length - 1);
getContents(MimeMessage mm, int i) 函数是一个经典函数,它将消息的内容递归地打印到文件中(网上有很多示例)。
为了避免内存不足错误,我简单地设置了一个 maxDocs 和 maxSize 限制(这是任意完成的,并且可能可以改进!),如下所示:
public int efficientGetContents(IMAPFolder inbox, Message[] messages)
throws MessagingException
FetchProfile fp = new FetchProfile();
fp.add(FetchProfile.Item.FLAGS);
fp.add(FetchProfile.Item.ENVELOPE);
inbox.fetch(messages, fp);
int index = 0;
int nbMessages = messages.length;
final int maxDoc = 5000;
final long maxSize = 100000000; // 100Mo
// Message numbers limit to fetch
int start;
int end;
while (index < nbMessages)
start = messages[index].getMessageNumber();
int docs = 0;
int totalSize = 0;
boolean noskip = true; // There are no jumps in the message numbers
// list
boolean notend = true;
// Until we reach one of the limits
while (docs < maxDoc && totalSize < maxSize && noskip && notend)
docs++;
totalSize += messages[index].getSize();
index++;
if (notend = (index < nbMessages))
noskip = (messages[index - 1].getMessageNumber() + 1 == messages[index]
.getMessageNumber());
end = messages[index - 1].getMessageNumber();
inbox.doCommand(new CustomProtocolCommand(start, end));
System.out.println("Fetching contents for " + start + ":" + end);
System.out.println("Size fetched = " + (totalSize / 1000000)
+ " Mo");
return nbMessages;
不要说我在这里使用的是不稳定的消息编号(如果从服务器上删除消息,这些编号会发生变化)。更好的方法是使用 UID!然后将命令从 FETCH 更改为 UID FETCH。
希望这会有所帮助!
【讨论】:
我在使用 UID FETCH 时遇到了一些问题,它只是没有获取所有邮件。 @Justmaker:这个很好的答案是(IMO)或多或少正是我需要解决我的抓取问题的解决方案:***.com/questions/28166182/…。但是,我有一个问题:我看到您使用 `args.writeString("BODY[]");' 来获取每条消息的整个正文部分。例如,如果我想拥有 uid 为 16 的消息的正文部分 1.3 加上 uid 为 17 的消息的正文部分 2.1,那么论点应该是什么样子...... 在您的 CustomProtocolCommand 类中,doCommand 您正在使用方法 Contents.getContents(mm, i); ,我找不到,要导入什么库才能使此方法起作用,请将必需的导入添加到您的类中,谢谢。 我还注意到 body = (BODY) fetch.getItem(0);可能会导致转换异常错误,因为似乎在某些服务器中 getItem(0) 是 UID,尝试将 body = (BODY) fetch.getItem(BODY.class);相反,对我有用,如果你想要 UID,那么 fetch.getItem(UID.class); 我已经添加了必要的导入,它们应该在 JavaMail 库中。但是 Contents.getContents 实际上是您需要实现的方法。网上有很多例子,大多数是测试类型知道如何处理内容:***.com/questions/11240368/…。是的,你对 getItem 的看法是对的,我没有更新我现在有点旧的答案;)以上是关于SSL上的JavaMail IMAP非常慢 - 批量获取多条消息的主要内容,如果未能解决你的问题,请参考以下文章