前几篇的文章发表后,有网友留言说没有涉及到阻塞的问题吗?在 socket 的编程当中,这确实是个很重要的问题。结合目前咱们文章的内容进度,咱们来看看为何说阻塞概念很重要。
接着上篇的内容,当咱们发送了 ehlo 命令以后就要接收服务器的返回了。这个地方是一个很容易出错的位置,通常的网络命令都是发送一条命令接收一条回复,这很容易让初学好者觉得每一个命令都是一行内容,进而在代码中进行了错误的处理。而实际上不管是命令仍是对命令的应答都是有多行的状况,若是对 socket 机制不了解,那就会说:那就读取完全部的行呗。但在实际状况中“读取全部的行”是不可能完成的任务,由于咱们前面已经说过了 socket 其实是字节流,并无一个行结束或者一个数据包结束了的概念(固然底层实现会有 ip 包)。因此在网络编程中有一个重要的事情,那就是怎样定义一个数据包算是结束了?这是每一个通信协议都要解决的问题(我我的认为是每一个协议中最为重要的内容),在每一个通信协议中作法都不一样,并且方法那是五花八门,用如今的话来讲成是脑洞大开都不为过。我印象最深的是前几个月写的一个公司的专有 http 包转发服务器时意外发现的一个 http 包的结束表示方法,很惭愧地说,我接触 http 协议不少年了,甚至写过好几个真正能用的 http 服务器实现,殊不知道这个方法 ... 这也不能怪我,加上这个方法我都数不清 http 到底有多少种表示一个包结束的方式了(是 http 中的 Transfer-Encoding chunked,之后有机会再给你们详细介绍)。
回到 smtp 协议上来,前面的文章中其实咱们已经提到过 ehlo 命令的响应是怎样处理的。它的回应相似于这样:
java
250-Eemail server 250 AUTH LOGIN
在 rfc 文档中就有说明,读取到有 250 并且没跟的 "-" 符号时就能够了。若是咱们没有正确处理一直读取下去,那么就会触发 socket 中一个著名的问题:阻塞。就是程序整个不动弹了,除了把它的进程杀死之外没有别的任何办法。能够用如下 java 代码模拟(基于上一篇的代码):linux
//发送一个命令 //SendLine("EHLO"); //163 这样是不行的,必定要有 domain //SendLine("EHLO" + " " + domain); //domain 要求其实来自 HELO 命令//HELO <SP> <domain> <CRLF> //收取一行 line = RecvLine(); System.out.println("recv:" + line); //收取一行 line = RecvLine(); System.out.println("recv:" + line);
这里咱们设想,先尝试读取 100 行数据,当没有行内容的状况下就提早跳出,想是服务器的响应内容读取完了。这个思想是没问题的,惋惜现实下是行不通的。缘由就是 socket 的读取函数默认状况下会一直等待,一直到有数据为止,若是一直没有数据呢?那就一直在等,整个程序就中止响应了,除非对方主动把链接给断开了,或者是网络断线了。这就是为何安卓程序如今不容许在主线程中直接调用 socket 的最主要缘由:由于不少初学者处理很差这个问题,经常会让程序卡死,那干脆就强制不让他们放在主线程了。
要解决这个问题,java 中只须要在链接后多加一个函数调用:
git
socket = new Socket(host, port); socket.setSoTimeout(10000);//设置超时,单位为毫秒
以原始 socket 方式处理的话,传统上则有好几种作法:程序员
1.是设置 socket 的超时; 2.接收前使用 select 函数判断是否能够收发数据; 3.使用非阻塞的 socket; 4.使用线程。
其中第一种方法最简单,链接后简单的调用一下相关函数就一了百了(上面的 java 代码就是如此),不过有些简化版本的 socket 环境不必定支持;而 select 函数则最传统,能够在决大多数环境下使用;前两种都要配合线程使用才好,而非阻塞 socket 的方式则彻底不会阻塞主线程,不过编程的复杂度会直线上升级,不适合初学者。因此咱们这里简单地使用 select 函数来完成超时判断,实现代码以下:github
//是否可读取,时间//超时返回,单位为秒 int SelectRead_Timeout(SOCKET so, int sec) { fd_set fd_read; //fd_read:TFDSet; struct timeval timeout; // : TTimeVal; int Result = 0; FD_ZERO( &fd_read ); FD_SET(so, &fd_read ); //个数受限于 FD_SETSIZE //timeout.tv_sec = 0; //秒 timeout.tv_sec = sec; //秒 //linux 第一个参数必定要赋值 if (_select( so+1, &fd_read, NULL, NULL, &timeout ) > 0) //至少有1个等待Accept的connection Result = 1; return Result; }//
这里要注意的是 windows 的写法和 linux 的写法是小有差别,你们必定要当心。
顺便介绍一下其余几种方法的实现吧。
前面 java 代码的超时本质就是用 setsockopt 来实现的,对于 C 语言来讲相似于这样:
面试
//设置发送超时 setsockopt(socket,SOL_SOCKET,SO_SNDTIMEO, (char *)&timeout,sizeof(struct timeval)); //设置接收超时 setsockopt(socket,SOL_SOCKET,SO_RCVTIMEO, (char *)&timeout,sizeof(struct timeval));
其 jdk 实现代码为:编程
/** * Enable/disable SO_TIMEOUT with the specified timeout, in * milliseconds. With this option set to a non-zero timeout, * a read() call on the InputStream associated with this Socket * will block for only this amount of time. If the timeout expires, * a <B>java.net.SocketTimeoutException</B> is raised, though the * Socket is still valid. The option <B>must</B> be enabled * prior to entering the blocking operation to have effect. The * timeout must be > 0. * A timeout of zero is interpreted as an infinite timeout. * @param timeout the specified timeout, in milliseconds. * @exception SocketException if there is an error * in the underlying protocol, such as a TCP error. * @since JDK 1.1 * @see #getSoTimeout() */ public synchronized void setSoTimeout(int timeout) throws SocketException { if (isClosed()) throw new SocketException("Socket is closed"); if (timeout < 0) throw new IllegalArgumentException("timeout can't be negative"); getImpl().setOption(SocketOptions.SO_TIMEOUT, new Integer(timeout)); }
多线程的相关文章汗牛充栋,咱们就不重复了。
而非阻塞的 socket 方法则相似于这样:windows
ioctlsocket(so, FIONBIO, &arg);
我又不得不说,我很惭愧非阻塞的 socket 概念我是工做好几年之后才据说的。准确的说是毕业不久后就知道了,不过一直觉得只是 windows 下的一种扩展,由于 windows 对 socket 的扩展不少因此也并无多在乎。后来到了一家公司面试,说他们主要用非阻塞的 socket 时才知道还能实用...... 在之后的工做当中渐渐的发现,有些工做环境下没有非阻塞 socket 还真很差实现。因此如今非阻塞的 socket 基本上也是各个平台都支持了的。不过非阻塞的实现难度基本上是直接上升,咱们这里暂时就不给出示例了。这种方法的特色是 socket 被设置为非阻塞后,全部的接收和发送都会当即返回,不论是否成功。
根据以上思想修改后的 C 语言代码多了1个函数:服务器
//读取多行结果 lstring * RecvMCmd(SOCKET so, struct MemPool * pool, lstring ** _buf) { int i = 0; int index = 0; int count = 0; lstring * rs; char c4 = '\0'; //判断第4个字符 lstring * mline = NewString("", pool); for (i=0; i<50; i++) { rs = RecvLine(so, pool, _buf); //只收取一行 mline->Append(mline, rs); LString_AppendConst(mline, "\r\n"); //printf("\r\nRecvMCmd:%s\r\n", rs->str); if (rs->len<4) break; //长度要足够 c4 = rs->str[4-1]; //第4个字符 //if ('\x20' == c4) break; //"\xhh" 任意字符 二位十六进制//其实如今的转义符已经扩展得至关复杂,不建议用这个表示空格 if (' ' == c4) break; //第4个字符是空格就表示读取完了//也能够判断 "250[空格]" }// return mline; }//
另外 RecvLine 函数中也多了几行内容:网络
canread = SelectRead_Timeout(so, 3);//是否可读取,时间//超时返回,单位为秒 if (0 == canread) break;
具体代码有点多,仿照才惯例,请你们到如下 github 地址下载吧:
https://github.com/clqsrc/c_lib_lstring/tree/master/email_book/book_9
另外,虽然这个系列的文章说的是邮件发送和收取,不过其中涉及到的知识都会应用于其余的网络通信协议,了解了邮件相关的,象什么 ftp、http 等协议其实也基本上贯通了。其实我我的也是打算将本身所了解的网络编程相关的知识都放到这系列的文章中来,由于象邮件涉及到的 base6四、mime 编码这样的内容其实都是在其余协议中普遍使用的。你们看完这系列的文章后写个 http 程序也彻底不是问题。因此请你们多多关注吧!
有了前面这几篇的文章和代码,你们其实已经能够用程序写出完整的邮件发送代码了.这和真实的邮件客户闻风而动发送过程也差很少了(还差的主要是两点: base64 编码和 mime 过程,咱们会在后面的文章详细说明).
--------------------------------------------------
版权声明:
本系列文章已受权百家号 "clq的程序员学前班" . 文章编排上略有差别. 百家号目前对文章中的代码转换得很厉害,所以推荐你们在博客园这边查看原始的代码.