一步一步从原理跟我学邮件收取及发送 9.多行结果与socket的阻塞

时间:2022-01-23 09:16:31

前几篇的文章发表后,有网友留言说没有涉及到阻塞的问题吗?在 socket 的编程当中,这确实是个很重要的问题。结合目前我们文章的内容进度,我们来看看为什么说阻塞概念很重要。

接着上篇的内容,当我们发送了 ehlo 命令之后就要接收服务器的返回了。这个地方是一个很容易出错的位置,一般的网络命令都是发送一条命令接收一条回复,这很容易让初学好者以为每个命令都是一行内容,进而在代码中进行了错误的处理。而实际上无论是命令还是对命令的应答都是有多行的情况,如果对 socket 机制不了解,那就会说:那就读取完所有的行呗。但在实际情况中“读取所有的行”是不可能完成的任务,因为我们前面已经说过了 socket 实际上是字节流,并没有一个行结束或者一个数据包结束了的概念(当然底层实现会有 ip 包)。所以在网络编程中有一个重要的事情,那就是怎样定义一个数据包算是结束了?这是每个通讯协议都要解决的问题(我个人认为是每个协议中最为重要的内容),在每个通讯协议中做法都不同,而且方法那是五花八门,用现在的话来说成是脑洞大开都不为过。我印象最深的是前几个月写的一个公司的专有 http 包转发服务器时意外发现的一个 http 包的结束表示方法,很惭愧地说,我接触 http 协议很多年了,甚至写过好几个真正能用的 http 服务器实现,却不知道这个方法 ... 这也不能怪我,加上这个方法我都数不清 http 到底有多少种表示一个包结束的方式了(是 http 中的 Transfer-Encoding chunked,以后有机会再给大家详细介绍)。

回到 smtp 协议上来,前面的文章中其实我们已经提到过 ehlo 命令的响应是怎样处理的。它的回应类似于这样:

-Eemail server
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 = ; FD_ZERO( &fd_read );
FD_SET(so, &fd_read ); //个数受限于 FD_SETSIZE //timeout.tv_sec = 0; //秒
timeout.tv_sec = sec; //秒 //linux 第一个参数一定要赋值
if (_select( so+, &fd_read, NULL, NULL, &timeout ) > ) //至少有1个等待Accept的connection
Result = ; 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 = ;
int index = ;
int count = ;
lstring * rs;
char c4 = '\0'; //判断第4个字符 lstring * mline = NewString("", pool); for (i=; i<; 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<) break; //长度要足够
c4 = rs->str[-]; //第4个字符
//if ('\x20' == c4) break; //"\xhh" 任意字符 二位十六进制//其实现在的转义符已经扩展得相当复杂,不建议用这个表示空格
if (' ' == c4) break; //第4个字符是空格就表示读取完了//也可以判断 "250[空格]" }// return mline;
}//

另外 RecvLine 函数中也多了几行内容:

        canread = SelectRead_Timeout(so, );//是否可读取,时间//超时返回,单位为秒
if ( == canread) break;

具体代码有点多,仿照才惯例,请大家到以下 github 地址下载吧:
https://github.com/clqsrc/c_lib_lstring/tree/master/email_book/book_9

另外,虽然这个系列的文章说的是邮件发送和收取,不过其中涉及到的知识都会应用于其他的网络通讯协议,了解了邮件相关的,象什么 ftp、http 等协议其实也基本上贯通了。其实我个人也是打算将自己所了解的网络编程相关的知识都放到这系列的文章中来,因为象邮件涉及到的 base64、mime 编码这样的内容其实都是在其他协议中广泛使用的。大家看完这系列的文章后写个 http 程序也完全不是问题。所以请大家多多关注吧!

有了前面这几篇的文章和代码,大家其实已经可以用程序写出完整的邮件发送代码了.这和真实的邮件客户闻风而动发送过程也差不多了(还差的主要是两点: base64 编码和 mime 过程,我们会在后面的文章详细说明).
--------------------------------------------------

版权声明:

本系列文章已授权百家号 "clq的程序员学前班" . 文章编排上略有差异. 百家号目前对文章中的代码转换得很厉害,因此推荐大家在博客园这边查看原始的代码.

一步一步从原理跟我学邮件收取及发送 9.多行结果与socket的阻塞的更多相关文章

  1. 一步一步从原理跟我学邮件收取及发送 4&period;不同平台下的socket

    既然是面向程序员的文章那当然不能只说说原理,一定要有实际动手的操作.    其实作为我个人的经历来说,对于网络编程,这是最重要的一章! 作为一位混迹业内近20年的快退休的程序员,我学习过很多的开发语言 ...

  2. 一步一步从原理跟我学邮件收取及发送 2&period;邮箱的登录和绕不开的base64

    一步一步从原理跟我学邮件收取及发送 2.邮箱的登录和绕不开的base64 好了,经过本系列上一篇文章 "1.网络命令的发送",假设大家已经掌握了 email 电子邮件的命令发送的方 ...

  3. 一步一步从原理跟我学邮件收取及发送 5&period;C语言的socket示例

    说到 C 语言版本的程序,首先要解决的问题就是兼容性. 作为 20 年开发有 10 多年是在服务端的程序员,我深刻地感受到服务端平台的两极分化之严重,linux 派对 windows 那是超级的不屑一 ...

  4. 一步一步从原理跟我学邮件收取及发送 3&period;telnet命令行发一封信

    首先要感谢博客园管理员的及时回复,本系列的第二篇文章得以恢复到首页,这是对作者的莫大鼓励.说实在的本来我真的挺受打击的.好在管理员说只是排版上有些问题,要用代码块修饰下相关的信息.说来惭愧因为常年编码 ...

  5. 一步一步从原理跟我学邮件收取及发送 8&period;EHLO 命令详解

    我们在上一篇中解决了接收一行命令的问题后,就可以来具体的分析邮件发送过程中涉及到的 SMTP 协议内容了. 首先来看通讯过程中的第一个内容:服务器在客户端连接上来后会主动发送一个问好的信息,所以这第一 ...

  6. 一步一步从原理跟我学邮件收取及发送 10&period;四句代码说清base64

    经过前几篇的文章,大家应该都能预感到一定要讲解 base64 函数的内容了.是的,马上要到程序登录的代码,base64 是必须要实现的. base64 很早以前我就接触了,在项目中也很喜欢用.但每换一 ...

  7. 一步一步从原理跟我学邮件收取及发送 11&period;完整的发送示例与go语言

    经过了这个系列的前几篇文章的学习,现在要写出一个完整的 smtp 邮件发送过程简直易如反掌.    例如我们可以轻松地写出以下的纯 C 语言代码(引用的其他C语言文件请看文末的 github 地址): ...

  8. 一步一步从原理跟我学邮件收取及发送 12&period;telnet命令行收一封信pop3

    本系列上一篇文章中我们就说到了,这一次我们要说 pop3 收信了.虽然我觉得应该先说完 mime 格式,不过估计大家已经不耐烦了 -- 怎么老在说发送啊?我们要看收取!    好吧,来啦,来啦!收取邮 ...

  9. 一步一步从原理跟我学邮件收取及发送 13&period;mime格式与常见字符编码

    在前面的本系列文章中我们已经学会了邮件的发送和收取.但在收取中我们看到的是一串串的乱码,回忆前面的发送过程,我们会奇怪:我们前面的邮件是明文啊.为什么明文的邮件明明也可以正常工作,还要弄乱码似的字符串 ...

随机推荐

  1. javascript超过容器后显示省略号效果&lpar;兼容一行或者多行&rpar;

    javascript超过容器后显示省略号效果       在实际的项目中,由于文字内容的长度不确定性和页面布局的固定性,难免会出现文字内容超过div(或其他标签,下同)区域的情况,此时比较好的做法就是 ...

  2. &period;NET (四)委托第四讲:内置委托Comparison

    // 摘要: // 表示比较同一类型的两个对象的方法. // // 参数: // x: // 要比较的第一个对象. // // y: // 要比较的第二个对象. // // 类型参数: // T: / ...

  3. HDU 2852 &lpar;树状数组&plus;无序第K小&rpar;

    题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=2852 题目大意:操作①:往盒子里放一个数.操作②:从盒子里扔掉一个数.操作③:查询盒子里大于a的第K小 ...

  4. WIFI驱动的移植 realtek 8188

    一般我们拿到的android源代码中wifi应用层部分是好的, 主要是wifi芯片的驱动要移植并添加进去. wifi驱动的移植, 以realtek的8188etv为例到官网下载相应的驱动, 解压后可以 ...

  5. 建立自己的Yum源

    转自http://kicklinux.com/setup-yum-repos-server/ 命令 reposync 可以直接同步yum源 如/etc/yum.repos.d/cloudera-cdh ...

  6. C&num;采用Winform实现类似Android的Listener

    本文实例讲述了C#采用Winform实现类似Android下Listener的方法.非常实用的一个技巧.具体实现方法如下: 首先是一个ICallBackListener接口,里面要写上你需要Liste ...

  7. 关于 HIVE Beeline 问题

    1  启动 hiveserver2 服务,启动 beeline -u jdbc:hive2:// 正常 ,启动 beeline -u jdbc:hive2://127.0.0.1:10000 包如下错 ...

  8. 【深度学习】批归一化(Batch Normalization)

    BN是由Google于2015年提出,这是一个深度神经网络训练的技巧,它不仅可以加快了模型的收敛速度,而且更重要的是在一定程度缓解了深层网络中"梯度弥散"的问题,从而使得训练深层网 ...

  9. Frame报文

    链路层帧常用的帧格式有两种:Ethernet II   与   IEEE802.3 Ethernet II 格式多用于终端设备的通信 IEEE802.3  格式多用于网络设备的通信 如何区分这两种报文 ...

  10. JS 事件冒泡、捕获。学习记录

    作为一个转行刚到公司的新人,任务不多,这一周任务全部消灭,闲暇的一天也别闲着,悄悄的看起了书.今天写一下JS的事件冒泡.捕获. 也是今天看的内容有点多了,有些消化不了,就随手记录一下.纯属自我理解,如 ...