让我们回到 smtp/pop3 等网络命令上来. 前面的文章已经说过了大多数的网络命令都是基于网络命令行的,我们就先来研究一行命令本身.

读取一行命令,在前面的 java 语言示例中实现很简单:

String s = br.readLine();

也就是说 java 中直接实现了读取一行的功能. 这个实现其实也没初学者想象的那么简单,甚至是网络编程中一个很易错的地方.换到直接操作 socket 的大多数环境中来,我们仍然以 C 语言为示例.我们以前面代码中的 RecvBuf() 来读取一行,这在真实的环境中是不正确的.如果大家测试过我们之前的示例一定会发现一个奇怪的现象:怎么有时候会一次收到好几行的内容呢?

这就涉及到一个很重要的概念:字节流. 在网络中传输的数据并不是我们发送多少对方就能接收到多少,更不是我们发送一次就对应对方的一次接收,而是我们发送的内容作为一个整体的字节流在网络中传输,如果我们发送了两次后对方才开始接收,那么对方就会一次性收到两次发出的命令.字节流的情况下还有一个更严重的问题:发送一次命令,对方有可能会花很多次接收调用后才能收取完! 很多程序员是不懂得这一点的,甚至很多公司的代码里长期存在这种缺陷的代码! 这种现象在现在这种网络环境超级好的中国国内是比较难重现的,在我们以前的拨号环境中就很常见,估计这也是现在的程序员不了解这种情况的原因之一吧. 既然比较难给出一个实例让大家去测试这种情况,那么让我们从原理上来解释吧.

我们先来看看 C 语言示例中的 RecvBuf() 函数实现.代码如下:

lstring * RecvBuf(SOCKET so, struct MemPool * pool)
{char buf[1024+1];int r = 0;lstring * s = NewString("", pool);memset(&buf, 0, sizeof(buf));r = _recv(so, buf, sizeof(buf)-1, 0); //留下一个 #0 结尾if (r > 0){LString_AppendCString(s, buf, r);}if (r == 0) //一般都是断开了
  {MessageBox(0, "recv error.[socket close]", "", 0);return s;}return s;}//

抛开字符串内存池的相关代码,实际上主要调用的是系统的 recv() socket 函数,而这个函数的原型为

int recv(SOCKET s, char *buf, int len, int flags);

其中 flags 一般是 0 可以不理会,而 s 就是网络连接也可以不用理会. buf 是接收到的数据要存放的缓冲区, len 则是缓冲区的大小,接收到的数据是不会大于 len 的(C语言的程序员都知道,那样就内存溢出了). 好了,让我们来模拟一下一个命令要接收两次的情况吧: 我们发送一个 4K 长的命令的话,我们 RecvBuf 一下, 眼尖的程序员一定看出来了,缓冲区只有 1024 啊. 很显然要接收多次才行嘛! 有的读者马上就会说了,你这不对嘛,谁让你缓冲区这么小的,来个 4K 的缓冲区! 这里有几个问题:你怎么知道 4K 就一定能接收完一个命令? 如果我是 1M 长的命令行呢? 那就开 1M 的 ... 如果我发送了一个 G 的文件过来呢? 先不考虑系统能不能开出这么大的缓冲区,先想一想 1G 长的文件或者命令要在网络中传送多久,那么这个 recv 函数是要等这一个 G 的数据都传输完了才返回吗? 很显然不是,要不我们就不会看到下载文件时的进度条了. 所以 recv 是在有数据到达时就返回了的,而不管收取到了多少,如果返回时收取了两个命令的数据那就会象我们示例中的那样显示出两个命令的内容.如果只收取到半个命令,就会出现过去拨号环境下的那种要多次收取才能得到一行命令的情况. 实际上数据在互联网中传输要经过很多设备,设备给每个连接的缓冲都是有限的,不可能指望对方发送的内容我们一次性就能收取. 所以在所有正规的 socket 的封装中都是要先用 recv 把数据先收取到程序中的一个缓冲区,一般来说会把每个连接设计成一个类,然后开一个 buf ,系统有空闲时就不停的调用 recv 直到 buf 收满为止,然后当程序要 readLine 一行时就在缓冲区取出一行的内容给它. 这就是 java 或者类似环境的 readLine 函数的实现原理和原因.

但是假如我们在 C 语言的测试中也这样做的话就比较困难,一是 C 语言没有类,二是 C 语言的内存管理不那么好做,要把这两个问题都解决好了再进行测试的话就太繁琐了,而且代码量太多的话也不好重用和维护. 所以我们可以先做一个简单的实现:每次读取一个字节,拼到字符串中,读取到行结束符时返回就可以了.这个代码很容易实现,但在实际的环境中效率是比较低的,可以在测试完成后实现一个更好一点的版本:每次读取一个缓冲区的内容,函数返回当前的命令和余下的内容,下次收取时再将余下的内容与新收取的内容合并就可以了.

根据以上思想就可以得到这样收取一行的函数:

//收取一行,可再优化
lstring * RecvLine(SOCKET so, struct MemPool * pool, lstring ** _buf)
{int i = 0;int index = -1;lstring * r = NULL;lstring * s = NULL;lstring * buf = *_buf;for (i=0;i<10;i++) //安全起见,不用 while ,用 for 一定次数就可以了
    {//index = pos("\n", buf);index = pos(NewString("\r\n", pool), buf);if (index>-1) break;s = RecvBuf(so, pool);buf->Append(buf, s);}if (index <0 ) return NewString("", pool);r = substring(buf, 0, index);buf = substring(buf, index + 2, Length(buf));*_buf = buf;return r;
}

因为这个示例中我已经实现了一个简单的字符串内存池,所以按第二种先读取到缓冲区的方法实现了这个读取一行的命令.如果大家在 c++ 环境里可以自己用 std::string 来实现第一种一次读取一个字节的实现方法(对于客户端接收命令来说,其实效率也没那么差,因为一般客户端就几个连接在工作嘛).

对比我们之前的结果,可以看到二者的区别.如图:

有经验并且眼尖的读者一定看到了 RecvLine 里的读取循环是 for 了一定的次数而不是用的 while 到成功后再跳出,这是因为长期的服务端开发我发现因为开发周期紧或者开发人员经验不足或者考虑不周等情况,在 while 里出现问题的话很容易造成死循环导致服务器不响应.所以我习惯在需要循环或者递归的地方设置一定的次数,如果超出这些次数就认为是出错了强制跳出.如果大家的服务器也不太稳定可以考虑也加入这种机制,还是很有效的.

大家可能对我读取一行的方法可能有疑虑,那我们来看看现有的语言是怎样实现的吧,java 的源码不是太方便看,我还是先用下 delphi 的吧:

function TIdTCPConnection.ReadLn(ATerminator: string = LF;const ATimeout: Integer = IdTimeoutDefault; AMaxLineLength: Integer = -1): string;
varLInputBufferSize: Integer;LSize: Integer;LTermPos: Integer;
beginif AMaxLineLength = -1 then beginAMaxLineLength := MaxLineLength;end;// User may pass '' if they need to pass arguments beyond the first.if Length(ATerminator) = 0 then beginATerminator := LF;end;FReadLnSplit := False;FReadLnTimedOut := False;LTermPos := 0;LSize := 0;repeatLInputBufferSize := InputBuffer.Size;if LInputBufferSize > 0 then beginLTermPos :=MemoryPos(ATerminator, PChar(InputBuffer.Memory) + LSize, LInputBufferSize - LSize);if LTermPos > 0 then beginLTermPos := LTermPos + LSize;end;LSize := LInputBufferSize;end;//ifif (LTermPos - 1 > AMaxLineLength) and (AMaxLineLength <> 0) then beginif MaxLineAction = maException then beginraise EIdReadLnMaxLineLengthExceeded.Create(RSReadLnMaxLineLengthExceeded);end else beginFReadLnSplit := True;Result := InputBuffer.Extract(AMaxLineLength);Exit;end;// ReadFromStack blocks - do not call unless we need toend else if LTermPos = 0 then beginif (LSize > AMaxLineLength) and (AMaxLineLength <> 0) then beginif MaxLineAction = maException then beginraise EIdReadLnMaxLineLengthExceeded.Create(RSReadLnMaxLineLengthExceeded);end else beginFReadLnSplit := True;Result := InputBuffer.Extract(AMaxLineLength);Exit;end;end;// ReadLn needs to call this as data may exist in the buffer, but no EOL yet disconnected
      CheckForDisconnect(True, True);// Can only return 0 if error or timeoutFReadLnTimedOut := ReadFromStack(True, ATimeout, ATimeout = IdTimeoutDefault) = 0;if ReadLnTimedout then beginResult := '';Exit;end;end;until LTermPos > 0;// Extract actual dataResult := InputBuffer.Extract(LTermPos + Length(ATerminator) - 1);// Strip terminatorsLTermPos := Length(Result) - Length(ATerminator);if (ATerminator = LF) and (LTermPos > 0) and (Result[LTermPos] = CR) then beginSetLength(Result, LTermPos - 1);end else beginSetLength(Result, LTermPos);end;
end;//ReadLn

这是 delphi 中著名的 indy 组件的实现,虽然代码比较长,大家对 delphi 语法可能也不熟,不过还是可以比较清楚的看到它也是先保存到一个缓冲区的.

改进后的完整代码如下(相关的依赖文件见文章末尾处):

#include <stdio.h>
#include <windows.h>
#include <time.h>
#include <winsock.h>#include "lstring.c"
#include "socketplus.c"
#include "lstring_functions.c"//vc 下要有可能要加 lib
//#pragma comment (lib,"*.lib")
//#pragma comment (lib,"libwsock32.a")
//#pragma comment (lib,"libwsock32.a")//SOCKET gSo = 0;
SOCKET gSo = -1;//收取一行,可再优化
lstring * RecvLine(SOCKET so, struct MemPool * pool, lstring ** _buf)
{int i = 0;int index = -1;lstring * r = NULL;lstring * s = NULL;lstring * buf = *_buf;for (i=0;i<10;i++) //安全起见,不用 while ,用 for 一定次数就可以了
    {//index = pos("\n", buf);index = pos(NewString("\r\n", pool), buf);if (index>-1) break;s = RecvBuf(so, pool);buf->Append(buf, s);}if (index <0 ) return NewString("", pool);r = substring(buf, 0, index);buf = substring(buf, index + 2, Length(buf));*_buf = buf;return r;
}void main()
{int r;mempool mem, * m;lstring * s;lstring * rs;lstring * buf;//--------------------------------------------------
mem = makemem(); m = &mem; //内存池,重要
    buf = NewString("", m);//--------------------------------------------------//直接装载各个 dll 函数
    LoadFunctions_Socket();InitWinSocket(); //初始化 socket, windows 下一定要有
gSo = CreateTcpClient();r = ConnectHost(gSo, "newbt.net", 25);if (r == 1) printf("连接成功!\r\n");s = NewString("EHLO\r\n", m);SendBuf(gSo, s->str, s->len);printf(s->str);s->Append(s, s);printf(s->str);s->AppendConst(s, "中文\r\n");printf(s->str);//--------------------------------------------------rs = RecvLine(gSo, m, &buf); //只收取一行
printf("\r\nRecvLine:");printf(rs->str); printf("\r\n");rs = RecvLine(gSo, m, &buf); //只收取一行
printf("\r\nRecvLine:");printf(rs->str); printf("\r\n");rs = RecvLine(gSo, m, &buf); //只收取一行
printf("\r\nRecvLine:");printf(rs->str); printf("\r\n");//--------------------------------------------------//    rs = RecvBuf(gSo, m); //注意这个并不只是收取一行
//
//    printf("\r\nRecvBuf:\r\n");
//    printf(rs->str);
//
//    rs = RecvBuf(gSo, m); //注意这个并不只是收取一行
//    printf("\r\nRecvBuf:\r\n");
//    printf(rs->str);//--------------------------------------------------
    Pool_Free(&mem); //释放内存池
    printf("gMallocCount:%d \r\n", gMallocCount); //看看有没有内存泄漏//简单的检测而已  //--------------------------------------------------
getch(); //getch().不过在VC中好象要用getch(),必须在头文件中加上<conio.h>

}

不知不觉这篇内容又占了很大的篇幅,这也是没办法,因为感觉确实有这么多要讲的,生怕哪个地方没说清楚又上大家走了弯路.如果大家看着觉得啰嗦,那就请多见谅吧!

--------------------------------------------------
本想上传依赖的相关文件到 github,到自己的账号里一看原来那个字符串的类已经传过了.所以补充了 socketplus.c 就好了. 大家可以到以下网址下载:

https://github.com/clqsrc/c_lib_lstring
 
也可以到之前同系列的文章中去复制,不过两者内容略有差异. 用 github 上的较好,因为以后有可能更新.

本系列文章已授权百家号 "clq的程序员学前班" . 文章编排上略有差异.

一步一步从原理跟我学邮件收取及发送 7.读取一行命令的实现相关推荐

  1. 一步一步手绘Spring MVC运行时序图(Spring MVC原理)

    相关内容: 架构师系列内容:架构师学习笔记(持续更新) 一步一步手绘Spring IOC运行时序图一(Spring 核心容器 IOC初始化过程) 一步一步手绘Spring IOC运行时序图二(基于XM ...

  2. Google账户两步验证的工作原理【转】

    最近在考虑一些用户登录验证的问题,现阶段在涉及到一些交易时,基本上都使用的是短信验证码验证,但有朋友说,有些时候短信验证码会出现延时,不及时.于是就看了一下Google Authenticator(c ...

  3. javascript 函数 add(1)(2)(3)(4)实现无限极累加 —— 一步一步原理解析

    问题:我们有一个需求,用js 实现一个无限极累加的函数, 形如 add(1) //=> 1; add(1)(2)  //=> 2; add(1)(2)(3) //=>  6; add ...

  4. 调试JDK源码-一步一步看HashMap怎么Hash和扩容

    调试JDK源码-一步一步看HashMap怎么Hash和扩容 调试JDK源码-ConcurrentHashMap实现原理 调试JDK源码-HashSet实现原理 调试JDK源码-调试JDK源码-Hash ...

  5. 【深度学习基础】一步一步讲解卷积神经网络

    点击上方"小白学视觉",选择加"星标"或"置顶" 重磅干货,第一时间送 本文转自:一步一步讲解卷积神经网络 卷积神经网络(Convoluti ...

  6. 基于postfix一步一步构建Mailserver,支持虚拟用户,支持WebMail

    我们来一步一步来构建MailServer,支持虚拟用户.虚拟域,支持Webmail,支持Mysql.这个实验化了两天的时间完成的,其中各种崎岖,认真的照着做,问题不大.不过新手还是不要做这个了,需要整 ...

  7. 手挽手带你学React:四档(上)一步一步学会react-redux (自己写个Redux)

    手挽手带你学React入门四档,用人话教你react-redux,理解redux架构,以及运用在react中.学完这一章,你就可以开始自己的react项目了. 之前在思否看到过某个大神的redux搭建 ...

  8. Verilog设计实例(2)一步一步实现一个多功能通用计数器

    博文目录 写在前面 正文 普通的二进制计数器 电路设计 行为仿真 普通的格雷码计数器 电路设计 行为仿真 LFSR 电路设计 行为仿真 多功能计数器 电路设计 行为仿真 生成语句实现方式 电路设计 行 ...

  9. 教你一步一步用C语言实现sift算法、上

    原文:http://blog.csdn.net/v_july_v/article/details/6245939 引言:     在我写的关于sift算法的前倆篇文章里头,已经对sift算法有了初步的 ...

  10. 【Linux】一步一步学Linux——Linux版本(03)

    目录 00. 目录 01. Linux内核版本 02. Linux内核官方网站 03. Linux发行版本 04. Linux发行版本介绍 4.1 Ubuntu 4.2 RedHat 4.3 Debi ...

最新文章

  1. VTK:PolyData之PointInsideObject
  2. .net学习笔记----WebConfig常用配置节点介绍
  3. Codeforces Round #666 (Div. 2) A. Juggling Letters
  4. 查询MySQL中某个数据库中有多少张表
  5. [5-21]绿色精品软件每天更新[uc23整理]
  6. 安卓分屏神器_【实用工具】一款鲜为人知的电脑神器,内置300多…找了很久了!...
  7. java支付宝app支付代码
  8. win10开机自启动在哪里设置(Win10设置开机自启动)
  9. 【概率论】事件的独立与事件的互斥(或互不相容)、以及它们之间的关系
  10. Centos7加入AD域并通过域账号登录
  11. Stm32f407zgt6 143引脚PDR_ON 的注意事项
  12. 一篇不错的关于VSS的入门介绍
  13. 七彩虹主板进BIOS设置和打开启动项菜单快捷键
  14. 费马小定理证明及应用
  15. 微信支付调用接口退款,返回SSL certificate not found: 是什么问题?
  16. mysql不停機添加節點_MySQL 5.7主从不停机添加新从库
  17. tomcat如何编译java_tomcat怎么编译java
  18. 17.6:迪瑞克斯啦算法
  19. 《canvas》之第8章 像素操作
  20. 高斯数学——看动画学奥数

热门文章

  1. X-NVR2000视频存储及安防管理一体机
  2. 基于区块链Baas平台的跨链实践
  3. 电磁干扰类型以及--电感和磁珠
  4. 坐火车硬座20小时是怎样的体验?
  5. 绕过iframe busting
  6. 20180310华为面试
  7. HZNU-1480-The Gougu Theorem【勾股数】
  8. 2013-2015阿里双十一技术网络文章总结
  9. 性能测试——jmeter性能测试——重点—核心——线程组、Ramp-Up Period、Loop Count 次采样...
  10. 移动硬盘安装操作系统以win7为例子