前几篇的文章发表后,有网友留言说没有涉及到阻塞的问题吗?在 socket 的编程当中,这确实是个很重要的问题。结合目前我们文章的内容进度,我们来看看为什么说阻塞概念很重要。
接着上篇的内容,当我们发送了 ehlo 命令之后就要接收服务器的返回了。这个地方是一个很容易出错的位置,一般的网络命令都是发送一条命令接收一条回复,这很容易让初学好者以为每个命令都是一行内容,进而在代码中进行了错误的处理。而实际上无论是命令还是对命令的应答都是有多行的情况,如果对 socket 机制不了解,那就会说:那就读取完所有的行呗。但在实际情况中“读取所有的行”是不可能完成的任务,因为我们前面已经说过了 socket 实际上是字节流,并没有一个行结束或者一个数据包结束了的概念(当然底层实现会有 ip 包)。所以在网络编程中有一个重要的事情,那就是怎样定义一个数据包算是结束了?这是每个通讯协议都要解决的问题(我个人认为是每个协议中最为重要的内容),在每个通讯协议中做法都不同,而且方法那是五花八门,用现在的话来说成是脑洞大开都不为过。我印象最深的是前几个月写的一个公司的专有 http 包转发服务器时意外发现的一个 http 包的结束表示方法,很惭愧地说,我接触 http 协议很多年了,甚至写过好几个真正能用的 http 服务器实现,却不知道这个方法 ... 这也不能怪我,加上这个方法我都数不清 http 到底有多少种表示一个包结束的方式了(是 http 中的 Transfer-Encoding chunked,以后有机会再给大家详细介绍)。
回到 smtp 协议上来,前面的文章中其实我们已经提到过 ehlo 命令的响应是怎样处理的。它的回应类似于这样:
250-Eemail server 250 AUTH LOGIN
在 rfc 文档中就有说明,读取到有 250 而且没跟的 "-" 符号时就可以了。如果我们没有正确处理一直读取下去,那么就会触发 socket 中一个著名的问题:阻塞。就是程序整个不动弹了,除了把它的进程杀死以外没有别的任何办法。可以用以下 java 代码模拟(基于上一篇的代码):
//发送一个命令 //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 中只需要在连接后多加一个函数调用:
socket = new Socket(host, port); socket.setSoTimeout(10000);//设置超时,单位为毫秒
以原始 socket 方式处理的话,传统上则有好几种做法:
1.是设置 socket 的超时; 2.接收前使用 select 函数判断是否可以收发数据; 3.使用非阻塞的 socket; 4.使用线程。
其中第一种方法最简单,连接后简单的调用一下相关函数就一了百了(上面的 java 代码就是如此),不过有些简化版本的 socket 环境不一定支持;而 select 函数则最传统,可以在决大多数环境下使用;前两种都要配合线程使用才好,而非阻塞 socket 的方式则完全不会阻塞主线程,不过编程的复杂度会直线上升级,不适合初学者。所以我们这里简单地使用 select 函数来完成超时判断,实现代码如下:
//是否可读取,时间//超时返回,单位为秒 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 方法则类似于这样:
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 = '