咱們在上一篇中解決了接收一行命令的問題後,就能夠來具體的分析郵件發送過程當中涉及到的 SMTP 協議內容了。
首先來看通信過程當中的第一個內容:服務器在客戶端鏈接上來後會主動發送一個問好的信息,因此這第一行的內容是服務器發送的,這時候客戶端要回答的內容其實並不肯定。緣由是根據不一樣的客戶端意圖,客戶端要發送的內容是有些差別的。以咱們示例中要發送一封信來講,要回復的第一句話就是 "EHLO" 命令,而看過咱們前面文章都知道,要鏈接 163 郵箱這樣的服務器光有這個命令仍是不夠的,還要帶上 "163.com"。那麼就有兩個問題,一是誰規定要加這個和呢?二是若是是其實的郵箱應該加什麼呢?
這個答案就要到 RFC 文檔中去找了,smtp 的 rfc 文檔爲 RFC821 能夠在如下網址中看到(內容仍是比較多的,其實你們能夠先別急着看,咱們文章會講解其中的內容)
http://man.chinaunix.net/develop/rfc/RFC821.txt
或者備用網址
http://newbt.net/ms/vdisk/show_bbs.php?id=8001DA8441F9AE9DC7AFF0709A875279&pid=160
其實若是你們真的仔細看了 rfc 的文檔必定會以爲奇怪,通篇文章都沒有 "EHLO" 命令啊,這就涉及到 rfc 文檔的一個特色了,那就是一種通信協議一般是記載在多個文檔當中的,形成這種狀況的緣由一是通信協議在發展,加入廢棄了一些內容,而 rfc 寫上去後通常就是不修改了的,通常是另外再寫一個文檔補充;另一種常見的就是某個通信協議是將以前的好幾種組合在一塊兒造成的,因此就確定要引用別的 rfc 文檔了。回到 "EHLO" 命令上來,這個命令其實記述在另一個文檔 RFC1869 當中,能夠在如下網址中查看:
http://man.chinaunix.net/develop/rfc/RFC1869.txt
或者備用網址
http://newbt.net/ms/vdisk/show_bbs.php?id=9D31E50F0BEE16B48703FDF4234A332E&pid=160
我又要說你們先別急着查看,由於文檔中的說明太複雜了,單就 "EHLO" 命令後面帶的內容來講,實際上是來自第一個文檔的 "HELO" 命令的表述部分(HELO 就是英文 hello 的意思,而 EHLO 是擴展的 hello 的意思)。文檔記載爲 "HELO <SP> <domain> <CRLF>"。文檔中的表述其實仍然有很強的誤導性,命令中的空格是千萬不要放到命令中的,那只是文檔爲了分隔語句而已,你會說命令中確實有空格啊!沒錯,看到那個 "<SP>" 沒有,那個才表示的是空格 ... 若是有同窗一上來就看命令的必定會很鬱悶<SP>究竟是什麼意思,固然文檔中是有解釋的,只不過是在文檔的最後,若是你老老實實的從頭讀到尾的話 ... 基本上都要破口大罵吧。總的來講閱讀 rfc 文檔是件很辛苦的事,但有時候又很必要,並且以上的都是中文版本,實在上因爲翻譯和中英文差別的問題,有些細節操做時還得去查看英文原版的。
在命令中的 "<domain>" 是關鍵,它就是前述命令中的 "163.com",而每一個服務器的 domain 是不能亂寫的,實際上要來自服務器風鏈接上的第一條響應命令行。例如 "220 newbt.net ESMTP eEmail-Server 2.0" 或者 "220 163.com Anti-spam GT for Coremail System (163com[20141201])",這個命令是由空格分隔的多個參數組成的,在實際的開發中實際上只須要按空格分隔字符串,而後取第二個參數就好了,這個就是 EHLO 命令後面要帶上的東西。
代碼實現:
知道了原理,其實用 java 語言來實現很是的簡單:
php
//解碼一行命令,這裏比較簡單就是按空格進行分隔就好了 public static String[] DecodeCmd(String line, String sp) { //String[] aa = "aaa|bbb|ccc".split("|"); String[] tmp = line.split(sp); //用空格分開//「.」和「|」都是轉義字符,必須得加"\\";//不必定是空格也有多是其餘的 String[] cmds = {"", "", "", "", ""}; //先定義多幾個,以面後面使用時產生異常 for(int i=0;i<tmp.length;i++) { if (i >= cmds.length) break; cmds[i] = tmp[i]; } return cmds; }//
結合咱們前面的例子就能夠有:
java
//解碼一下,這樣後面的 EHLO 纔能有正確的第二個參數 String cmds[] = DecodeCmd(line, " "); String domain = cmds[1]; //要從對方的應答中取出域名//空格分開的各個命令參數中的第二個 //發送一個命令 //SendLine("EHLO"); //163 這樣是不行的,必定要有 domain SendLine("EHLO" + " " + domain); //domain 要求其實來自 HELO 命令//HELO <SP> <domain> <CRLF>
完整代碼你們能夠手工加入以前文章的代碼中去,也能夠到 github 地址去下載:
https://github.com/clqsrc/c_lib_lstring
另外這系列文章的 java 示例我只放了一個源碼,就不象 C 語言系列那樣給出每一篇演變的代碼了,由於 java 的源碼相對比較簡單,你們應該都看得懂。
就不用象 C 語言的那樣分得那麼清楚了。
C 語言要實現的話,有了前面的基礎,要實現其實也不復雜。值得一提的是 C 語言的分隔字符串,要說的是 C 語言中分隔字符串時有種很常見的作法,就是利用 C 語言字符串的特色,直接在原字符串上打上字符串結束符號,這樣的代碼對於不少剛從學校 C 語言書本中走出來的初學者來講是個巨大的挑戰,可是由於這種方法沒有從新分配內存,運行效率是很是的高(之後有機會我再給你們詳細講解程序優化中不從新分配內存能讓程序效率提升到什麼程度,能夠提早說下,服務端大量鏈接的狀況下提升100倍都不止 -- 就是能有這麼多)。
根據以上思想能夠簡單的寫出一個版本的實現爲:git
//解碼一行命令,這裏比較簡單就是按空格進行分隔就好了 //這是用可怕的指針運算的版本 void DecodeCmd(lstring * line, char sp, char ** cmds, int cmds_count) { int i = 0; int index = 0; int count = 0; cmds[index] = line->str; for (i=0; i<line->len; i++) { if (sp == line->str[i]) { index++; line->str[i] = '\0'; //直接修改成字符串結束符號,若是是隻讀的字符串這樣作實際上是不對的,不過效率很高 cmds[index] = line->str + i; //指針向後移動 if (i >= line->len - 1) break;//若是是最後必定字符了就要退出,若是不是指針還要再移動一位 cmds[index] = line->str + i + 1; count++; if (count >= cmds_count) break; //不要大於緩衝區 } }// }//
調用前先要聲明命令參數的緩衝區,以下:程序員
char * cmds[5] = {NULL}; int cmds_count = 5; rs = RecvLine(gSo, m, &buf); //只收取一行 printf("\r\nRecvLine:"); printf(rs->str); printf("\r\n"); DecodeCmd(rs, ' ', cmds, cmds_count); printf("\r\ndomain:%s\r\n", cmds[1]); domain = NewString(cmds[1], m); s = NewString("EHLO", m); LString_AppendConst(s," "); s->Append(s, domain); //去掉這一行試試,163 郵箱就會返回錯誤了 LString_AppendConst(s,"\r\n"); SendBuf(gSo, s->str, s->len);
完整代碼就多了些,貼上來你們也難看清楚,能夠到如下 github 地址下載或查看:
https://github.com/clqsrc/c_lib_lstring/tree/master/email_book/book_8
github
--------------------------------------------------數組
版權聲明:服務器
本系列文章已受權百家號 "clq的程序員學前班" .dom